From 50f29b56aa9069e372b82e8cb4c76ca50fe12c71 Mon Sep 17 00:00:00 2001 From: Anton Vakhrushev Date: Sat, 18 Jul 2026 08:44:36 +0300 Subject: [PATCH] =?UTF-8?q?=D0=91=D0=B5=D0=BA=D0=BB=D0=BE=D0=B3=20+=20?= =?UTF-8?q?=D1=87=D0=B8=D1=81=D1=82=D0=BA=D0=B0:=20=D0=B4=D0=B2=D0=B5=20?= =?UTF-8?q?=D0=BD=D0=B0=D1=85=D0=BE=D0=B4=D0=BA=D0=B8=20=D0=B0=D1=83=D0=B4?= =?UTF-8?q?=D0=B8=D1=82=D0=B0=20=D0=B2=20=D0=B1=D0=B5=D0=BA=D0=BB=D0=BE?= =?UTF-8?q?=D0=B3,=20=D0=BF=D0=BE=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=20=D1=83=D1=81=D1=82=D0=B0=D1=80=D0=B5=D0=B2=D1=88=D0=B8?= =?UTF-8?q?=D0=B9=20=D0=BA=D0=BE=D0=BC=D0=BC=D0=B5=D0=BD=D1=82=D0=B0=D1=80?= =?UTF-8?q?=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Аудит capability после пачки lifecycle-задач вскрыл две пред-существующие находки (вне scope самих задач) — заведены в беклог: - catched-source-type-namer-okno (средний): addReq не пересобирается из свежего source_type в окне namer'а; самоисцеляется через magnet_timeout→Retry. - dismiss-cancel-user-dismiss-marker (низкий): веб-UI зовёт Cancel вместо Dismiss на не-терминальных, теряется маркер user_dismiss. Инлайн: finishRecognition — комментарий врал про «Ф3, авто-раскладки нет»; фактически авто-раскладка идёт при Decision.Auto. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/backlog/README.md | 2 + .../backlog/catched-source-type-namer-okno.md | 58 +++++++++++++++++++ .../dismiss-cancel-user-dismiss-marker.md | 47 +++++++++++++++ internal/worker/review.go | 5 +- 4 files changed, 110 insertions(+), 2 deletions(-) create mode 100644 docs/backlog/catched-source-type-namer-okno.md create mode 100644 docs/backlog/dismiss-cancel-user-dismiss-marker.md diff --git a/docs/backlog/README.md b/docs/backlog/README.md index 991421d..807c431 100644 --- a/docs/backlog/README.md +++ b/docs/backlog/README.md @@ -35,6 +35,7 @@ Tududi (проект `jellybit`) больше **не** держит беклог - [Обучение на правках человека (few-shot из прошлых ревью)](obuchenie-na-pravkah.md) — Когда человек поправил матч, тип или нумерацию — сохранять это как пример и подмешивать… - [Confidence-гейт авто-раскладки: узаконить в спеке + сделать выключаемым (дефолт 0.7)](gate-confidence-spec-vs-code.md) — Решено (B): гейт оставляем как доп. проверку на ревью — выключаемый порог, дефолт 0.85→0.7, записать в спеку - [Внешние субтитры: пары VobSub и языковой суффикс](vneshnie-subtitry.md) — Привязка субтитр→серия уже работает; остались пары VobSub .idx+.sub и потеря Lang/Flags +- [`addReq` не пересобирается из свежего `source_type` перед Add (окно namer'а)](catched-source-type-namer-okno.md) — При апгрейде magnet→.torrent в окне namer'а добавится magnet из устаревшего снимка; самоисцеляется через magnet_timeout→failed→Retry _(аудит 2026-07-17)_ ## Низкий @@ -56,3 +57,4 @@ Tududi (проект `jellybit`) больше **не** держит беклог - [Современный Web-UI как PWA](web-ui-pwa.md) — Переделать веб-интерфейс в современное PWA-приложение (устанавливаемое, отзывчивое… - [Идентичность инфохэшей: split v1/v2 одного торрента + крафт-магнет отравляет владение (F4, F5)](review-f4-f5-infohash-identity.md) — split v1/v2 идентичность и крафт-магнет отравляют владение инфохэшами _(ревью 2026-07-08)_ - [Нити приёма: NoName в контексте, устаревшие комментарии, лог без причины, bencode-аллокации (N1, N3, N4, N5)](review-ingest-nits.md) — косметика приёма: NoName в контексте, устаревшие комментарии, лог, bencode-аллокации _(ревью 2026-07-08)_ +- [Веб-UI зовёт Cancel вместо Dismiss на не-терминальных → теряется `user_dismiss`](dismiss-cancel-user-dismiss-marker.md) — Функционально ок (Cancel даёт cancelled), но маркер user_dismiss в error_code теряется; расхождение с буквой спеки _(аудит 2026-07-17)_ diff --git a/docs/backlog/catched-source-type-namer-okno.md b/docs/backlog/catched-source-type-namer-okno.md new file mode 100644 index 0000000..09df76f --- /dev/null +++ b/docs/backlog/catched-source-type-namer-okno.md @@ -0,0 +1,58 @@ +# `addReq` не пересобирается из свежего `source_type` перед `Add` (окно namer'а) + +**Приоритет:** средний · **Теги:** review-2026-07-17, lifecycle + +Найдено аудитом capability **download-tracking** (сверка код↔спека после пачки +lifecycle-задач). Пред-существующее, вне scope задачи F3/cancel-cleanup — T4 +осознанно вынес это за рамки и задокументировал в своём design.md. + +## Суть + +`processCatched` (`internal/worker/worker.go:442-517`) строит `addReq` из записи +`cur`, перечитанной под замком на `:447-455`, **до** вызова namer'а (LLM, секунды, +вне замка, `:463-478`). Затем на `:484-491` под замком перечитывается `before`, +но `addReq` из него **не пересобирается** — проверяется только `state == catched`. + +Если апгрейд пойманной magnet-задачи до `.torrent` +(`UpgradeCatchedMagnetToTorrent`, `internal/store/download.go:392`) отработает +именно в окне namer'а (приём принял `.torrent` с тем же infohash, `state` +остаётся `catched`), воркер добавит **magnet-ссылку из устаревшего снимка**, хотя +в БД уже `source_type=torrent`. + +Спека (`openspec/specs/download-tracking/spec.md`, раздел про добавление по +`source_type`) требует перечитывать `source_type` **под блокировкой переходов +непосредственно перед добавлением** — сейчас это требование в окне namer'а +нарушается. + +## Насколько больно + +Ограниченно и самоисцеляемо: на закрытом трекере magnet без метаданных зависнет +в `metaDL` → предохранитель `magnet_timeout` → `failed`; ручной `Retry` +перечитает актуальный `source_type` и добьёт. Данные не страдают, инвариант +«источник неприкосновенен» не задет. Окно узкое (апгрейд должен лечь ровно в +LLM-вызов по тому же infohash). Поэтому средний, не высокий. + +## Развилка (решить до кода) + +- **A — ужесточить код (соответствие букве спеки, закрыть окно):** после re-read + `before` под замком (`:484`) пересобирать `addReq`/`hint` из `before`, если + `source_type` изменился. Нюанс: `sourceAddParts` читает байты `.torrent` — это + тяжёлый вызов, держать под замком нельзя (спека: тяжёлое — вне блокировки), плюс + подсказка имени для `.torrent` иная (метаданные раздачи vs имя из magnet), т.е. + при апгрейде корректно был бы и повторный namer. Не однострочник. +- **B — смягчить спеку (принять реальность):** признать, что рациональ («не + полагаться на снимок, снятый ранее вне блокировки») уже выполнен первым re-read + под замком на `:447`, и переформулировать требование как «перечитывать + `source_type` под блокировкой после тик-снимка», явно приняв узкое namer-окно + как самоисцеляемое через `Retry`. + +Рекомендация — начать с B (дёшево, отражает фактическое осознанное поведение), A +завести только если узкое окно окажется реальной болью в эксплуатации. + +## Ссылки + +- `internal/worker/worker.go:442-517` — `processCatched` +- `internal/store/download.go:392` — `UpgradeCatchedMagnetToTorrent` +- `openspec/specs/download-tracking/spec.md` — требование про `source_type` +- Тест `TestProcessCatchedReReadsSourceTypeUnderLock` покрывает апгрейд между + тик-снимком и re-read, но **не** окно namer'а. diff --git a/docs/backlog/dismiss-cancel-user-dismiss-marker.md b/docs/backlog/dismiss-cancel-user-dismiss-marker.md new file mode 100644 index 0000000..5a191fa --- /dev/null +++ b/docs/backlog/dismiss-cancel-user-dismiss-marker.md @@ -0,0 +1,47 @@ +# Веб-UI зовёт Cancel вместо Dismiss на не-терминальных → теряется `user_dismiss` + +**Приоритет:** низкий · **Теги:** review-2026-07-17, state-reconciliation + +Найдено аудитом 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` — расхождение с буквой спеки и небольшая потеря +наблюдаемости (в аналитике/логах не отличить «пользователь закрыл активную» от +«пользователь отменил»). Отсюда низкий приоритет. + +## Развилка (решить до кода) + +- **A — привести код к спеке:** веб-UI на не-терминальных тоже зовёт `Dismiss` + ради единого маркера `user_dismiss`; либо `Cancel` пишет `user_dismiss`. +- **B — привести спеку к коду:** зафиксировать осознанное разделение (`Cancel` + для активных, `Dismiss` для терминальных) — уточнить требование, что стоп-кран + на не-терминальных реализуется `Cancel`'ом, и определить, какой `error_code` + ожидается. + +Сначала решить, осознанно ли разделение Cancel/Dismiss; если да — вероятно B. + +## Ссылки + +- `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` — требование «Ручное закрытие» diff --git a/internal/worker/review.go b/internal/worker/review.go index d32b754..2032e05 100644 --- a/internal/worker/review.go +++ b/internal/worker/review.go @@ -136,8 +136,9 @@ func (w *Worker) runRecognize(ctx context.Context, d store.Download) (recognize. return res, savePath, nil } -// finishRecognition сохраняет попытку распознавания и двигает задачу. В Ф3 -// метабазы выключены → авто-раскладки не делаем, всегда уходим в review. +// finishRecognition сохраняет попытку распознавания и двигает задачу: при +// уверенном матче (Decision.Auto) и чистой валидации — авто-раскладка, иначе — +// в review (см. ветвление ниже). func (w *Worker) finishRecognition(ctx context.Context, id, claim string, res recognize.Result, savePath string) { log := logctx.From(ctx) planJSON, err := json.Marshal(res.Plan)