Беклог + чистка: две находки аудита в беклог, поправлен устаревший комментарий

Аудит 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) <noreply@anthropic.com>
This commit is contained in:
av
2026-07-18 08:44:36 +03:00
co-authored by Claude Opus 4.8
parent 659fa5ec44
commit 50f29b56aa
4 changed files with 110 additions and 2 deletions
+2
View File
@@ -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)_
@@ -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'а.
@@ -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` — требование «Ручное закрытие»
+3 -2
View File
@@ -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)