- требование «Ручное закрытие загрузки» приведено к коду: два пути закрытия, error_code на каждом, раскладка поверхностей — описательно, а не SHALL - уборка своего торрента после отмены названа исключением по состоянию, а не по команде; убрана ложная гарантия «данных пользователя не касается» - в docs/review.md записан проскочивший дефект гарда окна после add и новый вопрос проходу adversary про асимметрию признака владения
34 KiB
Триаж чекпоинта 2 — change cancel-dismiss-split-wording
Дата: 2026-08-06. Профиль: standard, режим: по графу.
База диффа: 01e64d60de7495900d9a4c8f5ce9bf811ee7b7e6.
Сводка
Гейт: зелёный. Кодовые шаги (build/vet/lint/gofmt/test/flaky/race/
diff-coverage/migrations/govulncheck) — SKIP с причиной «кода в диффе нет»;
canon — OK; gitleaks — OK.
Проходы поимённо:
| Проход | Исход |
|---|---|
review-gate |
зелёный (кодовые шаги SKIP — кода в диффе нет) |
review-specs |
отработал, 4 находки + 4 строки границ |
review-code |
отработал, 1 находка + подтверждение A3 + блок «нарушений нет» |
review-adversary |
отработал, 5 находок (4 воспроизводящих теста), 3 свойства без пути |
review-reimpl |
не запускался — стадия 3, только deep; вне профиля standard |
review-architecture |
не запускался на этом чекпоинте — стадия 4 (wide/deep); отработал на чекпоинте 1 в профиле design, находки закрыты правками предложения (design.md, D1–D3, D6–D8) |
review-ops |
отработал, 3 находки + ответы на обязательные вопросы |
Состав запущенного сверен с профилем standard — расхождений нет.
Счёт находок: на входе 13 (S1–S4, C2, A1, A2, A3adv, A4, A5, O1–O3; C1 — подтверждение приёмки, не находка). После дедупа — 10 (S1=A3adv, C2=A4, S4=A5). Разложено: 3 блокируют, 4 стоит исправить сейчас, 0 гипотез, 6 в урожай, 1 promote-кандидат.
Состояние change: файлы staged, но не закоммичены (HEAD == база; дифф
worktree против базы — 5 файлов, 514 вставок, все под
openspec/changes/cancel-dismiss-split-wording/). Перед архивацией нужен
коммит — это состояние прогона, не находка.
Вердикт по архивации: change годится к архивации после инлайн-правок секций 1–2. Находки, требующей остановки или переработки предложения, нет: все семь пунктов — правки текста дельты, локальные и однозначные, кроме одной развилки (п. 7, выбор регистра нормы о подтверждении). Дефекты в коде, вскрытые тестами adversary, — пред-существующие, изменением не введены и не задеты (критерий A3 требует пустой кодовый дифф); они уходят урожаем.
Приёмка A1–A3 (доклад приёмщику, не отметка «принято»)
- A1 — требование различает
Cancel/Dismissпо состояниям (не-терминальные против «любое кромеdeleted», конфликт против no-op) и называетerror_codeобоих путей (user_dismissпротив пустого). Оракул:openspec validate --strict cancel-dismiss-split-wording→Change … is valid, exit 0 (прогнано мной). Выполнен, с оговоркой: нормативная фраза «SHALL называть путь закрытия» внутренне противоречит пустому кодуCancel— п. 4 ниже. - A2 — сценарий «Закрытие не-терминальной загрузки из веб-UI идёт отменой»
(дельта, строки 112–119): исход
cancelled,error_codeназван (пустой). Оракул: тот же прогон + чтение сценария. Выполнен. - A3 —
git diff --stat <база> -- internal web cmd— пусто (прогнано мной по worktree; полный дифф — 5 файлов только подopenspec/changes/). ГейтDismissableи danger-zone шаблона не тронуты. Выполнен.
Блокирует мердж (архивацию)
Все три — корзина (а): дефект введён текстом этого изменения. Все три —
правки одного-двух абзацев дельты
openspec/changes/cancel-dismiss-split-wording/specs/state-reconciliation/spec.md.
1. Дельта гарантирует «Dismiss SHALL NOT вызывать qBittorrent» — гарантия ложна: Dismiss из catched в окне после add даёт torrents/delete с deleteFiles=true
- Файл: дельта, строки 36–47; код
internal/worker/worker.go:520–556(уборка по условиюafter.State != store.StateCatched, безразлична к команде),internal/httpapi/review.go:384–391(маршрутdismissпо состоянию не гейтится — путь достижим сегодня прямым POST) - Severity: major, Confidence: high. Корзина: (а) — сам механизм уборки
пред-существует и нормирован командо-нейтрально в
download-tracking(строки 216–234); ложна только атрибуция исключения одной командеCancel. - Оракул (прогнан мной):
go test ./tmp/adv/ -run TestDismissInAddWindow -v→ «ADV-3 воспроизведён: закрытие путём Dismiss (error_code="user_dismiss") вызвало torrents/delete deleteFiles=[true]». Плюсinternal/worker/catched_test.go:244–268— штатный тест уборки ставитcancelledнапрямую, не черезCancel. - Дедуп: S1 (
specs) + A3adv (adversary) — одна причина: исключение в коде привязано к состоянию/окну, а не к команде. - Действие: инлайн. Переформулировать исключение по условию, а не по
команде: «закрытие любым путём, уведшее задачу из
catchedв окне между нашимaddи записью перехода» — как это уже сформулировано вdownload-tracking. Тем же правом убрать из абзаца атрибуцию «Cancelтем же ограничен, с одним исключением» — исключение общее для обоих путей. - В ту же правку входит C2/A4 (дедуп: одна ссылка, одно предложение):
markdown-ссылка
../../../specs/download-tracking/spec.md(строка 44) не резолвится ни из позиции дельты (нормализуется вopenspec/changes/specs/download-tracking/spec.md, файла нет; верно было бы 4 уровня), ни после архивации (изopenspec/specs/state-reconciliation/нормализуется вspecs/download-tracking/spec.md; верно — 1 уровень). Единого относительного пути, верного в обеих позициях, не существует. Проверено арифметикой пути (os.path.normpath+exists, прогнано мной). Фикс: дать ссылку бэктиком —openspec/specs/download-tracking/spec.md— как во всех соседних кросс-ссылках capability (прецедент:openspec/specs/state-reconciliation/spec.md:92); markdown-ссылок вopenspec/specs/нет ни одной (grep пуст), иdocs.py checkэтот каталог не проверяет — битую ссылку не покрасил бы никто.
2. Дельта заявляет, что уборка в окне после add «данных пользователя не касается» — безусловное утверждение сильнее того, что гарантирует код: пред-существующие файлы под paths.downloads уничтожаются
- Файл: дельта, строки 45–46; код
internal/worker/worker.go:495–556(гард «подтверждённое отсутствие» проверяет отсутствие торрента, не отсутствие данных) - Severity: major (для текста; кодовый дефект — кандидат
critical, см. урожай У-1), Confidence: high для текстовой части. Корзина: (а) для текста / (в) для кода — дыра в гарде пред-существует, изменением не задета; этим change введена только ложная гарантия. - Оракул (прогнан мной):
go test ./tmp/adv/ -run TestCancelInAddWindow -v→ «ADV-1 воспроизведён: worker вызвал torrents/delete deleteFiles=[true]; данные …/downloads/Show/e1.mkv существовали под paths.downloads ДО add (гард проверял отсутствие торрента, а не отсутствие данных)». - Почему текст — major, а не перенос
criticalс кода: текст сам данных не уничтожает; но, будучи влит в нормативный дом, он узаконивает дыру — следующий аудит поверит «данных не касается» и закроет расследование. Severity кода на severity текста автоматически не переносится; кодовый дефект едет урожаем со своим весом. - Действие: инлайн. Убрать безусловное «и данных пользователя не касается».
Right-size замена — отослать к дому нормы без собственной гарантии
(«нормирована в
download-tracking»), не расширяя дельту признанием дыры: дыра — предмет задачи У-1, а не этого требования.
3. Обещание «библиотечные ссылки без штатной команды снятия до повторной привязки» ложно: перепривязка их не снимает никогда — Undo/Delete работают только с последним батчем
- Файл: дельта, строки 62–66 и сценарий 93–101; код
internal/worker/review.go:332(каждыйApply— новый батч),:557–572(Undo— толькоLatestBatchID),:625–641(Delete— толькоLatestBatchID) - Severity: major, Confidence: high. Корзина: (а) для формулировки / (в) для кода (поведение батчей пред-существует, изменением не задето).
- Оракул (прогнан мной):
go test ./tmp/adv/ -run 'TestDismissRelink|TestStrandedLink' -v→ «ADV-2: в библиотеке осталась неуправляемая ссылка (батч 1), которую не снимает ни Undo, ни Delete»; «ADV-2b: запись в deleted, раздача снесена с файлами, но в библиотеке осталась неуправляемая ссылка с живыми данными — место не освобождено». - Последствие текста: единственный маршрут к брошенной ссылке — ровно та связка
Dismissизdone→ relink, которую дельта нормирует; «до повторной привязки» называет relink лекарством, которым он не является, а после него «полное удаление» перестаёт освобождать место — danger-зона обещает обратное. - Действие: инлайн. Заменить «до повторной привязки» честным: «…без штатной
команды снятия; последующая перепривязка их не снимает —
UndoиDeleteработают только с последним батчем раскладки». РасширениеUndo/Deleteна все батчи — урожай У-2.
Стоит исправить сейчас
4. Нормативное «error_code SHALL называть путь закрытия» нарушено пустым кодом Cancel и опровергнуто тем же документом тремя абзацами ниже
- Файл: дельта, строка 49; код
internal/worker/worker.go:985(SetDownloadState(ctx, id, StateCancelled, "", "")— проверено чтением) - Severity: major, Confidence: high. Корзина: (а) — фраза введена этим изменением.
- Оракул: код (пустой код ничего не «называет») + сам документ: строки 56–58
(«пустота кода на пути
Cancel… не закреплена нормативно») и 73–82 («известное ограничение»).SHALL, который документ сам объявляет неудовлетворённым, — приглашение следующему аудиту завести ту же задачу. - Действие: инлайн. Перевести фразу в согласованный регистр: например,
«
error_codeперехода SHALL различать пути закрытия, а не унифицировать их» (пустой уCancelпротивuser_dismissуDismiss— различает), либо сузить нормативность доDismiss(егоSHALL user_dismissуже стоит строкой ниже). Остальной абзац и «известное ограничение» уже согласованы.
5. «Прежняя диагностика остаётся только в логе перехода» — фактически неверно: лог-строки перехода Cancel/Dismiss прежних error_code/error_msg не содержат
- Файл: дельта, строки 53–58 и сценарий 103–110; код
internal/worker/worker.go:985–991, 1016–1022— проверено чтением:Cancelлогирует толькоfrom/to,Dismiss—from/to/codeнового перехода; контракт поляcode—docs/conventions/logging.md:26–30. - Severity: major, Confidence: high. Корзина: (а) для формулировки / (б) для наблюдаемости (лог и раньше не нёс прежней диагностики — урожай У-3).
- Последствие: причина падения восстановима только по более ранней, никак не
связанной по смыслу лог-строке, чья доступность зависит от неописанной
ретенции логов (
docs/architecture.md— только «логи в stdout контейнера»). - Действие: инлайн. Поправить формулировку: прежние код и текст «в записи
не сохраняются; лог-строка перехода их не дублирует — они остаются лишь в
более ранних строках лога состояния». Добавление
prev_error_code/prev_error_msgв лог — кодовая правка, запрещённая критерием A3, → урожай У-3.
6. Описанная раскладка Telegram врёт в трёх клетках построенной по ней таблицы «состояние × поверхность»
- Файл: дельта, строки 30–34; код
internal/tgbot/bot.go:452–461,internal/tgbot/render.go:44–50, 281–290— проверено чтением: уведомлениеEventDone— текст без клавиатуры;EventTargetMissing/EventOrphaned— только ссылка в веб (webOnly, и таnilпри пустомweb_base_url);StateLinking— клавиатуры нет вовсе (не «отсылает в веб»). «Закрыть» появляется на карточке (renderCard/refresh), а не в уведомлении. - Severity: minor, Confidence: high. Корзина: (а) — перечень введён этим изменением (текст описателен: «сегодня раскладка такова», но неверен).
- Последствие: реальный пробел — уведомление о рассинхроне приходит без стоп-крана — текстом замаскирован.
- Действие: инлайн. Либо уточнить: «Telegram даёт „Закрыть“ на карточке
загрузки в
failed/stuck/done/orphaned/target_missing; уведомления оdoneи рассинхроне кнопок закрытия не несут», либо снять перечень Telegram целиком, оставив нормативную гарантию только за страницей загрузки (раскладка по поверхностям всё равно объявлена ненормативной).
7. Новый SHALL об «отдельном подтверждающем шаге» въезжает уже нарушенным: без JS диалога нет, в Telegram нет ни danger-зоны, ни подтверждения
- Файл: дельта, строки 68–71; код
web/templates/partials/download_main.html:95–98— проверено чтением, комментарий дословно: «без JS гейт — только раскрытие details и явный submit (диалога нет)»;internal/tgbot/render.go:266–290— «Закрыть» в общем ряду кнопок.openspec/specs/web-ui/spec.mdнормирует «без JavaScript не ломается». - Severity: minor, Confidence: medium. Корзина: (а) — прежняя редакция обязательства не содержала (уточнение adversary), норма введена этим изменением; тот же изъян в пред-существующем требовании «Полное удаление загрузки пользователем» — корзина (в), урожай У-6.
- Дедуп: S4 (
specs) + A5 (adversary) — одна причина. - Действие: развилка — выбор регистра нормы, а не правка формулировки:
- Сузить норму до фактического гейта: «„Закрыть“ SHALL размещаться в обособленной danger-зоне, скрытой по умолчанию (раскрытие — сознательный шаг); с JS дополнительно SHALL показываться диалог подтверждения» + назвать известным ограничением, что Telegram danger-зоны не даёт. Цена: слабее звучит R7, зато спека не лжёт и код не требуется.
- Оставить
SHALLкак цель и завести урожаем задачу: no-JS-подтверждение (например, промежуточная страница-подтверждение) и danger-гейт в Telegram. Цена: до выполнения задачи нормативный дом содержит невыполненныйSHALL(ровно то, за что бьёт п. 4). Рекомендация триажа — вариант 1: он согласуется с R12 («спека фиксирует наблюдаемое») и с решением п. 4.
Гипотезы без доказательства
Пусто. Все выжившие находки имеют прогнанный оракул (тест, чтение названных
строк кода, арифметика пути, вывод команды). Понижений за отсутствие оракула
не было; critical кода из A1 не понижен, а отделён от текстовой находки и
уехал урожаем со своим весом (У-1) — его единственный непрогнанный шаг назван
там честно.
Отдельно: совпадение S1/A3adv и S4/A5 в разных проходах учтено как рост приоритета, не как рост Confidence — под всеми проходами одна модель.
Promote candidates
- Проверка ссылок в
openspec/— шагcanon(docs.py check) обходит толькоdocs/иCLAUDE.md; битую markdown-ссылку в спеке не красит никто (п. 1/C2 прожил бы до архивации молча). Кандидат: расширить проверку битых ссылок наopenspec/specs/иopenspec/changes/, либо записать конвенцией «кросс-ссылки в спеках — только бэктиком» (сегодня это фактическая, но нигде не записанная практика: markdown-ссылок вopenspec/specs/ноль). Провенанс:review-codeC2 +review-adversaryA4.
Урожай (пред-существующие дефекты — владельцу задач, не в этот change)
- У-1 (кандидат
critical, Confidence: medium). Отмена/закрытие в окне послеaddсносит пред-существующие файлы пользователя подpaths.downloads: гард «подтверждённое отсутствие» (worker.go:495–509) проверяет отсутствие торрента в qBittorrent, но не отсутствие данных на save_path;qbt.Addподхватывает лежащие там файлы, уборка бьётtorrents/deleteсdeleteFiles=true. Путь: пользователь снял раздачу, файлы оставив (это предписывает самdownload-tracking:169–174как восстановление) → подал тот же торрент → отменил в окне листинг+add (сотни мс — секунды). Инвариант «источник неприкосновенен»: исключение (2) покрывает «собственный торрент», но не пред-существующие данные; UI при этом обещает «файлы и раздача останутся нетронутыми» (download_main.html:100–111). Оракул:tmp/adv/adv_test.go,TestCancelInAddWindowDeletesPreExistingData— прогнан, воспроизводит (каталог gitignored — при заведении задачи тест перенести в её материалы). Единственный непрогнанный шаг — семантика боевого qBittorrentdeleteFiles=trueнад пред-существующими файлами: в бой ходить запрещено, отсюда medium. Провенанс:review-adversaryA1, чекпоинт 2 changecancel-dismiss-split-wording. - У-2 (major, Confidence: high).
Undo/Deleteработают только с последним батчем (review.go:557–572, 625–641), каждыйApplyсоздаёт новый (review.go:332): связка «закрытьdone→ relink» бросает ссылку батча 1 навсегда, а «полное удаление» после этого не освобождает место (инод жив за счёт брошенной ссылки). Оракул:TestDismissRelinkStrandsLibraryLinks,TestStrandedLinkDefeatsDelete— прогнаны, воспроизводят. Провенанс:review-adversaryA2. - У-3 (minor, наблюдаемость). Лог-строки переходов
Cancel/Dismissне несут прежнихerror_code/error_msg, хотяdих держит в момент записи (worker.go:985–991, 1016–1022): «почему задача упала» после закрытия не восстановить одной строкой. Кандидат:prev_error_code/prev_error_msgв лог перехода. Оракул: чтение кода + контрактlogging.md:26–30. Провенанс:review-opsO1. - У-4 (minor). «Per-download блокировка» из
CLAUDE.md(инвариант «переходы состояний») иworker.go:11— фактически один глобальныйsync.Mutex(worker.go:239), удерживаемыйApplyна весь файловый ввод-вывод: долгая раскладка блокирует команды по другим загрузкам и поллинг. Минимум — поправить формулировку в двух местах; по существу — per-id блокировка либо вынос I/O из-под замка. Оракул: чтение кода (одно поле, не карта). Провенанс:review-opsO2. - У-5 (minor, эффект — гипотеза при сегодняшнем профиле «единицы/день»).
Дефолтный список —
SCAN download(негативныйstate NOT INмимоidx_download_state, измереноEXPLAIN QUERY PLANна копии схемы), аcancelledкопятся без ретенции, и требование под ревью расширяет канал их производства. Частичный индекс или позитивный список состояний; ретеншен — задача в беклоге уже есть. Провенанс:review-opsO3. - У-6 (minor). Пред-существующее требование «Полное удаление загрузки
пользователем» (
openspec/specs/state-reconciliation/spec.md:463–465) несёт ту же формулировку о подтверждающем шаге с той же no-JS-дырой, что п. 7. Судьба зависит от развилки п. 7 — править согласованно. Провенанс:review-specsS4 (смягчающее наблюдение).
Уже назначено урожаем ранее (не дублирую задачей): R10 из рубрики чекпоинта 1 —
«допустимость команды переоценивается на момент исполнения, а не по снимку
рендера» (tasks.md); туда же примыкает граница specs про /review/{id},
открытый для любого состояния.
Границы покрытия
Запускалось: профиль standard, режим «по графу», чекпоинт 2 (после
apply). review-gate (кодовые шаги SKIP — кода в диффе нет; canon, gitleaks —
OK), review-specs, review-code, review-adversary, review-ops.
Не запускалось: review-reimpl — стадия 3, только в deep;
review-architecture — стадия 4 (wide/deep); оба вне профиля standard.
review-architecture отработал на чекпоинте 1 (профиль design) вместе с
review-specs и review-rubric; его находки закрыты правками предложения
(design.md, решения D1–D3, D6–D8) и на этом чекпоинте не перепроверялись.
Что запущенные проходы не могли проверить в принципе (из charter'ов):
specs: поведение живых qBittorrent/Telegram — клавиатуры Telegram не покрыты тестами вовсе, сверка была чтением кода; плюс его заявленные границы: владение целевым путём после закрытияdone(ушло в У-2),/review/{id}без гейта состояния (примыкает к R10), «активный список» как три разных множества (store/list.go,store/download.go).code: только записанные конвенции; конвенции о форме OpenSpec-спек вdocs/conventions/нет — норму задавалиopenspec/config.yamlиCLAUDE.md.adversary: семантика боевого qBittorrent (deleteFiles=trueнад пред-существующими файлами) — в бой ходить запрещено; это единственный непрогнанный шаг У-1. Свойства без пути: снятие последней копии последовательностью команд не строится; секретов вerror_msg/логах не найдено; негейченный маршрутdismissв сегодняшнем периметре (docs/security.md) — операторское действие, не дефект (совпадает с «Типовым ложноположительным» про отсутствие авторизации — корректно не выведен находкой).ops:-raceна этом диффе не гонялся (кода нет — SKIP); гонки суждены рассуждением. Ретенция логов stdout нигде не описана — доступность «более ранней лог-строки» из У-3 неизвестна.- триаж (я): нового не ищу по определению — работаю с чужими выводами; пропуск
любого прохода — мой пропуск тоже. Тесты
tmp/adv/прогнаны и воспроизводят, но их фикстуры — фейковый qbt и локальная ФС: они доказывают поведение нашего кода, не связки с боем.
Осталось целиком на человеке (из docs/review.md, два списка раздельно):
Не проверит ни один проход:
- история инцидентов на umbar и что уже ломалось в проде;
- поведение SQLite под реальным объёмом и профилем нагрузки;
- завязка внешних потребителей (Jellyfin, закладки, чужие ссылки) на текущее поведение;
- качество распознавания (корпус решено не собирать, REJECTED 2026-08-06);
- суждение «этой функциональности не должно существовать».
Перестали проверять сознательно:
- идиоматичность Go — с 2026-08-04, вместе с упразднением прохода
idiomпри переезде на плагин; различение «идиоматично против распространено» не спрашивает никто; пересмотр — задачаquality-review-agents.
Каких документов/фактов не хватило (поразрядно, с причиной):
docs/conventions/— конвенции о форме OpenSpec-спек нет: правило «ссылки бэктиком, не markdown» пришлось выводить grep'ом по фактике (review-code);docs/architecture.md— ретенция логов не описана (только «stdout контейнера»): судьба «прежней диагностики в логе» из У-3 неопределима (review-ops);docs/review.md, журнал дефектов — пуст (заведён 2026-07-23): оракулов «такое здесь уже воспроизводилось» не было ни для одной находки, все подтверждения добывались тестами и чтением кода (триаж);CLAUDE.md, инварианты — присутствуют и использованы (ранжирование У-1 по «необратимо» и границе исключения (2)); «Типовые ложноположительные» и оба подраздела «Недоступно проверке» — присутствуют и использованы. Здесь пробелов нет.