Files
avandClaude Opus 4.8 0c9421f4c1 Имя: восстановление display_name после распознавания + гард пустого входа
Голый magnet без dn/контекста заставлял namer звать LLM на пустом входе,
и модель галлюцинировала мусорное имя («Unknown»), которое писалось и в
display_name, и в rename qBittorrent, а заодно ломало UI-фолбэк на
распознанное название. Верное каноническое имя, вычисляемое позже при
распознавании, никуда не переливалось.

- naming: гард пустого входа в DeriveName (нет контекста и подсказки → ""
  без вызова LLM) + детерминированный форматтер FormatTitleYear.
- qbt: операция RenameTorrent (переименование существующей раздачи).
- store: SetDisplayName — обновление имени постфактум без гарда состояния.
- worker: refreshDisplayNameLocked/RefreshDisplayName — перелив канонического
  имени (эффективный план) в display_name + best-effort rename раздачи по
  реальному t.Hash; авто-триггер при подтверждении матча (choose/manual add).
- web-ui: кнопка «Обновить имя» на странице загрузки (htmx-своп заголовка,
  деградация без JS), видимая при наличии распознавания (вкл. done/orphaned).

Спека: дельты ingest/review/web-ui влиты в openspec/specs; change
refresh-display-name заархивирован.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 16:51:04 +03:00

41 lines
5.5 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 1. Гард пустого входа в naming (корень «Unknown»)
- [x] 1.1 В `internal/naming/naming.go` `DeriveName`: если `strings.TrimSpace(contextText)` и `strings.TrimSpace(hint)` оба пусты — вернуть `""` до вызова LLM и фолбека
- [x] 1.2 Тест: пустой контекст + пустой hint → `""`, провайдер LLM не вызван (фейковый provider фиксирует отсутствие вызова)
- [x] 1.3 Тест регрессии: непустой hint (или контекст) при пустом другом — прежнее поведение сохранено
## 2. Операция переименования раздачи в qBittorrent
- [x] 2.1 В `internal/qbt` добавить `Client.RenameTorrent(ctx, hash, name)``POST /api/v2/torrents/rename` (form `hash`, `name`), логирование внешнего вызова как `ext.service=qbittorrent`, `ext.operation=torrents/rename`
- [x] 2.2 Тест клиента qBittorrent на форму запроса и разбор ответа/ошибки (по образцу существующих тестов `Add`/`Delete`)
## 3. Store: обновление display_name пост-фактум
- [x] 3.1 В `internal/store` добавить `SetDisplayName(ctx, id, name)` — UPDATE `display_name`, `updated_at`, без гарда состояния (не двигает FSM)
- [x] 3.2 Тест store: имя обновляется в `review`/`done`; `updated_at` продвинут
## 4. Форматтер и помощник обновления имени
- [x] 4.1 В пакете `naming`: экспортировать `sanitize`/`truncate` (или добавить публичную обёртку) и добавить детерминированный форматтер `FormatTitleYear(title string, year int) string` (`Title (Year)`, год опционален; очистка/обрезка) — без LLM. НЕ унифицировать с `render()` (у refresh нет режиссёра/сезона)
- [x] 4.2 Добавить в интерфейсы worker методы `RenameTorrent` (QBittorrent) и `SetDisplayName` (Store)
- [x] 4.3 Реализовать `refreshDisplayNameLocked(ctx, id)` (вызывается ПОД `w.mu`): прочитать загрузку и эффективное распознанное название (пины `title`/`year` поверх `recognition.title`/`year`), собрать имя через `naming.FormatTitleYear`, при пустом — no-op; записать `display_name` через `SetDisplayName`; затем резолвить раздачу через `torrentByInfohash` и best-effort `RenameTorrent` по её реальному `t.Hash` (не по сырому primary infohash) — сбой/отсутствие раздачи логировать `WARN`, операцию не проваливать
- [x] 4.4 Публичная обёртка `RefreshDisplayName(ctx, id)`: берёт `w.mu` и вызывает `refreshDisplayNameLocked` (для внешнего вызова из httpapi)
- [x] 4.5 Тесты worker: перелив имени в `display_name` + вызов `RenameTorrent` с реальным `t.Hash`; недоступность/отсутствие раздачи qBittorrent → `display_name` обновлён, ошибки нет; отсутствие распознавания → no-op
## 5. Авто-триггер при подтверждённом матче (review)
- [x] 5.1 В `chooseCandidateLocked` после успешного `SetCandidateChosen` вызвать `refreshDisplayNameLocked(ctx, id)` (общий путь покрывает и `ChooseCandidate`, и `AddManualSource`)
- [x] 5.2 Тесты: выбор кандидата (`ChooseCandidate`) и ручное добавление источника (`AddManualSource`) обновляют `display_name` каноническим именем и переименовывают раздачу; ошибка qBittorrent не ломает команду
## 6. Ручное действие «Обновить имя» (web-ui)
- [x] 6.1 Эндпоинт `POST /ui/downloads/{id}/refresh-name` в `internal/httpapi`: вызвать `RefreshDisplayName` (обёртка с `w.mu`), вернуть htmx-партиал заголовка (ветвление по `isHTMX`), деградация без JS — редирект/страница загрузки с новым заголовком
- [x] 6.2 Кнопка «Обновить имя» в шаблоне страницы загрузки, видимая при наличии распознавания (`rd.Recognition != nil`/наличие плана), включая `done`/`orphaned` — НЕ гейтить по `Reviewable`; htmx-обновление фрагмента заголовка на месте
- [x] 6.3 Тест httpapi: POST на загрузку с распознаванием возвращает партиал с новым заголовком; идемпотентность (повтор даёт то же имя); загрузка без распознавания → без изменений; кнопка видна на `done`
## 7. Финализация
- [x] 7.1 `task test` и `task lint` зелёные
- [x] 7.2 `openspec validate refresh-display-name --strict` проходит
- [x] 7.3 Ревью кода (чекпоинт перед archive): сверка со спеками + инварианты/конвенции