From f42db0a27588259e0498947e6459b1d21155bb0f Mon Sep 17 00:00:00 2001 From: Anton Vakhrushev Date: Thu, 6 Aug 2026 13:57:33 +0300 Subject: [PATCH] =?UTF-8?q?sprint:=20=D0=BD=D0=B0=D0=B1=D1=80=D0=B0=D0=BD?= =?UTF-8?q?=20=D1=81=D0=BF=D1=80=D0=B8=D0=BD=D1=82=202026-08-06=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=B4=20=D1=86=D0=B5=D0=BB=D1=8C=20=D1=80=D0=B0=D1=81?= =?UTF-8?q?=D0=BF=D0=BE=D0=B7=D0=BD=D0=B0=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - в наборе шесть задач: локаль TVDB и confidence-гейт под цель, плюс баги и техдолг помимо неё - взятым дописаны разделы своего типа: «Затрагивает», критерии с оракулами, воспроизведение - решены развилки: оба расхождения код↔спека правятся спекой (и потому стали chore), опрос qBittorrent тормозится бэкоффом до минутного потолка --- docs/tasks/BACKLOG.md | 6 -- docs/tasks/SPRINT.md | 14 ++- docs/tasks/items/auto-link-confidence-gate.md | 25 ++++++ docs/tasks/items/background-error-noise.md | 85 +++++++++++++------ .../items/catched-source-type-refresh.md | 55 ++++++++---- docs/tasks/items/dismiss-marker-lost.md | 43 +++++++--- docs/tasks/items/ingest-nits.md | 24 +++++- docs/tasks/items/tvdb-title-locale.md | 10 +++ 8 files changed, 197 insertions(+), 65 deletions(-) diff --git a/docs/tasks/BACKLOG.md b/docs/tasks/BACKLOG.md index 53c1a00..0797870 100644 --- a/docs/tasks/BACKLOG.md +++ b/docs/tasks/BACKLOG.md @@ -12,14 +12,10 @@ ## Ядро продукта - [✨ Пересчитывать абсолютную нумерацию аниме в SxxEyy](items/anime-absolute-numbering.md) — аниме со сквозной нумерацией (#137) не раскладывается в SxxEyy, который ждёт Jellyfin — нужен пересчёт абсолютной нумерации -- [🐞 Пересобирать `addReq` из свежего `source_type` перед `Add` (окно namer'а)](items/catched-source-type-refresh.md) — При апгрейде magnet→.torrent в окне namer'а добавится magnet из устаревшего снимка; самоисцеляется через magnet_timeout→failed→Retry _(аудит 2026-07-17)_ - [✨ Раскладывать раздачу-копию диска (VIDEO_TS/BDMV) каталогом целиком](items/disc-image-releases.md) — раздача-образ диска (VIDEO_TS/BDMV) сейчас разбирается пофайлово вместо раскладки каталога целиком — редкий, но реальный случай -- [🐞 Не терять маркер `user_dismiss` при закрытии не-терминальной загрузки из веб-UI](items/dismiss-marker-lost.md) — Функционально ок (Cancel даёт cancelled), но маркер user_dismiss в error_code теряется; расхождение с буквой спеки _(аудит 2026-07-17)_ - [✨ Принимать ссылку на .torrent и скачивать файл самим (нужен SSRF-гард)](items/torrent-url-fetch.md) — magnet и .torrent-файл приняты; остался фетч .torrent по URL (нужен SSRF-гард) -- [✨ Узаконить confidence-гейт авто-раскладки в спеке и сделать его выключаемым (дефолт 0.7)](items/auto-link-confidence-gate.md) — Решено (B): гейт оставляем как доп. проверку на ревью — выключаемый порог, дефолт 0.85→0.7, записать в спеку - [✨ Докладывать недостающие эпизоды merge-раскладкой при повторной заливке](items/merge-incremental-redownload.md) — повторная заливка сериала целиком должна доложить недостающие эпизоды merge-раскладкой, не трогая существующие ссылки — блокирует типовой сценарий свежих сериалов - [🐞 Связать v1/v2-хеши одного торрента и не доверять паре xt из магнета (F4, F5)](items/infohash-identity-integrity.md) — split v1/v2 идентичность и крафт-магнет отравляют владение инфохэшами _(ревью 2026-07-08)_ -- [🧹 Закрыть мелочи приёма: NoName в контексте, устаревшие комментарии, лог без причины, bencode-аллокации (N1, N3, N4, N5)](items/ingest-nits.md) — косметика приёма: NoName в контексте, устаревшие комментарии, лог, bencode-аллокации _(ревью 2026-07-08)_ - [✨ Слать живые обновления через SSE вместо htmx-поллинга](items/sse-live-updates.md) — живые обновления на htmx-поллинге дают задержку и холостые запросы — SSE убрал бы то и другое (поллинг работает, поэтому улучшение, не блокер) - [✨ Проверять свободное место перед copy-fallback](items/free-space-check-copy-fallback.md) — copy-fallback при невозможности хардлинка может упереться в переполненный диск посреди раскладки — нет проверки места до копирования - [✨ Слать уведомления автору загрузки в его транспорт (мульти-бот)](items/notification-source-binding.md) — пинги и ревью должен получать автор загрузки в своём транспорте — нет привязки загрузки к источнику/отправителю (нужно для мульти-бота) @@ -28,7 +24,6 @@ - [✨ Переделать веб-UI в устанавливаемое PWA](items/web-ui-pwa.md) — текущий server-rendered UI функционален — PWA (устанавливаемое, удобное с телефона) это улучшение большого объёма, не блокер - [✨ Править на ревью маппинг «файл → серия» и раскладывать вручную при провале LLM](items/review-mapping-editor.md) — правка S·E, «нумеровать подряд» и ручной режим при полном провале LLM были запланированы объёмом Ф5 и не заведены задачей — в ревью сегодня можно только подсказать текстом - [✨ Заказать спекой крайние случаи именования: многофайловый фильм, редакции, двойная серия](items/naming-edge-cases.md) — стэкинг частей (part1/cd1), редакции [edition-…] и двойная серия SxxEyy-Eyy описаны нарративом, но в file-layout не заказаны — раскладка таких раздач не определена -- [✨ Брать у TVDB название на языке настройки и оригинальное название](items/tvdb-title-locale.md) — [general].language правит только TMDB и промпт LLM — TVDB отдаёт primary name, и при language=ru в карточку ревью и имя папки попадает 哪吒之魔童降世 вместо «Нэчжа» - [🔬 Канон нумерации серий и порядок у провайдера тега](items/episode-numbering-canon.md) — Косметика/редкость: порядок просмотра ок, но у тайтлов со спорным порядком (Бибоп) Jellyfin подтягивает не те подписи серий, если канон файлов ≠ дефолтный порядок провайдера тега - [🔬 Тексты и формат уведомлений в Telegram](items/telegram-messages-audit.md) — зонтичный проход по всем текстам бота: полнота карточек, единый язык, оформление; порождает под-задачи - [🔬 guessit как сервис-спутник](items/guessit-sidecar.md) — go-ptn слабее питоновского guessit — если точности пред-парса не хватит, завернуть guessit в сервис-спутник рядом с бинарём @@ -44,7 +39,6 @@ - [✨ Проверять в healthcheck доступность qBittorrent, LLM и метабаз и показывать её в UI](items/deep-healthcheck-dependencies.md) — /healthz проверяет только сам сервис — недоступность qBittorrent/LLM/метабазы видна лишь по застрявшим задачам, нет readiness и бейджа в UI - [✨ Хранить историю переходов загрузки отдельной таблицей](items/download-transition-history.md) — хранится только текущий статус загрузки — разбор «как сюда попали» идёт по логам сервера, нет таблицы истории переходов - [🧹 Кэшировать ответы метабаз с TTL (и опционально LLM)](items/metadata-cache.md) — повторные и ретраящиеся прогоны бьют TMDB/TVDB/TVMaze одним запросом — кэш с TTL сэкономил бы лимиты и ускорил «Распознать заново» -- [🐞 Не штормить ERROR при недоступном qBittorrent и эскалировать устойчивый сбой тика](items/background-error-noise.md) — Остаток задачи логирования: ext.* ERROR-шторм при недоступном qBittorrent + эскалация устойчивого сбоя тика _(ревью Fable)_ - [✨ Чистить БД от терминальных задач и сырых ответов LLM старше срока хранения](items/db-retention-cleanup.md) — терминальные задачи и сырые ответы LLM копятся вечно — без авточистки список загрузок и БД деградируют по мере эксплуатации - [🧹 Свести термины домена в словарь единого языка](items/ubiquitous-language-glossary.md) — наименования домена расходятся между спеками, UI и кодом — нет единого глоссария (на нём же стоит агент-ревьювер наименований) - [🔬 Потолок нагрузки: 100 одновременных загрузок, план-максимум 1000](items/scale-100-downloads.md) — Зафиксировать в НФТ ориентир 100/1000 загрузок + аудит узких мест (SQLite, воркер, поллинг) diff --git a/docs/tasks/SPRINT.md b/docs/tasks/SPRINT.md index 3638c73..95ffea5 100644 --- a/docs/tasks/SPRINT.md +++ b/docs/tasks/SPRINT.md @@ -1,6 +1,16 @@ # Спринт -Спринта нет. Цель называет человек, набор собирает агент: -`tasks.py sprint start --goal <слаг>`. +- **Цель:** [🎯 Раздача узнаётся верно без подсказок человека](items/recognition-accuracy.md) +- **Начат:** 2026-08-06 +- **Спринт:** `2026-08-06` + +Урожай спринта поднимается `tasks.py list --tag sprint:2026-08-06` — это первая порция переоценки на сессии. ## Набор + +- [✨ Брать у TVDB название на языке настройки и оригинальное название](items/tvdb-title-locale.md) — [general].language правит только TMDB и промпт LLM — TVDB отдаёт primary name, и при language=ru в карточку ревью и имя папки попадает 哪吒之魔童降世 вместо «Нэчжа» +- [✨ Узаконить confidence-гейт авто-раскладки в спеке и сделать его выключаемым (дефолт 0.7)](items/auto-link-confidence-gate.md) — Решено (B): гейт оставляем как доп. проверку на ревью — выключаемый порог, дефолт 0.85→0.7, записать в спеку +- [🧹 Уточнить в спеке требование о re-read `source_type` перед `Add`](items/catched-source-type-refresh.md) — спека требует перечитывать source_type непосредственно перед добавлением, код перечитывает после тик-снимка — узкое namer-окно самоисцеляется через Retry и признано допустимым +- [🧹 Зафиксировать в спеке разделение Cancel и Dismiss по состояниям](items/dismiss-marker-lost.md) — спека обещает dismiss из любого состояния, код осознанно даёт Cancel для активных и Dismiss для терминальных — расходится буква, а не поведение +- [🐞 Тормозить опрос qBittorrent бэкоффом при недоступности и эскалировать устойчивый сбой](items/background-error-noise.md) — недоступный qBittorrent опрашивается каждые 5 с и даёт WARN на каждом тике: нужен экспоненциальный бэкофф до минутного потолка со сбросом по первому успеху и ERROR на устойчивой деградации +- [🧹 Закрыть мелочи приёма: NoName в контексте, устаревшие комментарии, лог без причины, bencode-аллокации (N1, N3, N4, N5)](items/ingest-nits.md) — косметика приёма: NoName в контексте, устаревшие комментарии, лог, bencode-аллокации _(ревью 2026-07-08)_ diff --git a/docs/tasks/items/auto-link-confidence-gate.md b/docs/tasks/items/auto-link-confidence-gate.md index d2112b6..db19f60 100644 --- a/docs/tasks/items/auto-link-confidence-gate.md +++ b/docs/tasks/items/auto-link-confidence-gate.md @@ -49,6 +49,31 @@ которому порог можно было бы подобрать заранее, решено не собирать (`../REJECTED.md`, 2026-08-06), так что первое значение остаётся оценкой. +## Затрагивает + +- `internal/recognize` — `decide`/`validate.go` (четвёртое условие) и + `recognize.go` (пере-применение дефолта, из-за которого `0` не выключает + гейт); +- секция `[recognition]` конфига, `config.example.toml` и значение по умолчанию + в `internal/config`; +- `openspec/specs/recognition/spec.md` — требование «Модель уверенности и + решение auto/review»; +- `docs/database.md` (дом числа) и `docs/conventions/config.md` (описание ключа). + +## Критерии приёмки + +- `auto_confidence_threshold = 0` выключает гейт: при чистых условиях 1–3 + раздача уходит в авто независимо от `confidence` (оракул: тест `decide` с + нулевым порогом). +- При `confidence` ниже порога и выполненных условиях 1–3 раздача уходит в + review (оракул: табличный тест на границе порога — ниже, равно, выше). +- Умолчание равно 0.7 в одном месте — загрузке конфига; `recognize.go` своего + дефолта не применяет (оракул: тест, что пустой конфиг даёт 0.7, плюс + отсутствие `defaultAutoThreshold` в диффе). +- Спека называет `confidence` четвёртым конфигурируемым блокирующим условием и + несёт сценарий «матч чист, но уверенность ниже порога → review» (оракул: + `openspec validate --strict`). + Оформить как OpenSpec-change (дельта `recognition` + правки `validate.go`/`recognize.go`/`config`). diff --git a/docs/tasks/items/background-error-noise.md b/docs/tasks/items/background-error-noise.md index bb275f3..7bc8252 100644 --- a/docs/tasks/items/background-error-noise.md +++ b/docs/tasks/items/background-error-noise.md @@ -1,14 +1,13 @@ -# 🐞 Не штормить ERROR при недоступном qBittorrent и эскалировать устойчивый сбой тика +# 🐞 Тормозить опрос qBittorrent бэкоффом при недоступности и эскалировать устойчивый сбой - **Тип:** fix - **Категория:** Инфраструктура -- **Зачем:** Остаток задачи логирования: ext.* ERROR-шторм при недоступном qBittorrent + эскалация устойчивого сбоя тика _(ревью Fable)_ -- **Теги:** goal:operational-resilience +- **Зачем:** недоступный qBittorrent опрашивается каждые 5 с и даёт WARN на каждом тике: нужен экспоненциальный бэкофф до минутного потолка со сбросом по первому успеху и ERROR на устойчивой деградации Остаток от задачи «классификация доменных ошибок + конвенции логирования» -(основное реализовано, см. ниже). Здесь — два смежных пункта про уровень -повторяющихся сбоев фоновых циклов, каждый требует небольшого решения, а не -только правки. +(основное реализовано, см. ниже) плюс бэкофф опроса, заказанный 2026-08-06. +Речь о поведении фонового цикла, пока зависимость лежит: с какой частотой он её +дёргает и каким уровнем об этом пишет. ## Что уже сделано (не переоткрывать) @@ -29,29 +28,63 @@ ## Остаток -### ERROR-шторм при недоступном qBittorrent +**Шум `ext.*` ERROR решено оставить как есть (2026-08-06).** Запись +«зависимость недоступна» на каждом тике — легитимный сигнал транспортного слоя, +и гасится он уровнем сбора логов, а не кодом. Варианты с пониженным уровнем у +`logging.ExtCall` и с дедупом отклонены: первый заводит второе правило уровня +для того же класса вызовов, второй даёт транспортному логгеру память о +состоянии. -Клиент `qbt` логирует `ext.*` `Failure` → **ERROR** на каждом тике поллинга -(`torrents/info`, `internal/qbt/qbt.go`), пока qBittorrent недоступен (рестарт -демона, сеть). Домен уже пишет `poll failed` = WARN (по новой конвенции), но -транспортная `ext.*`-запись остаётся ERROR по правилу ext-конвенции («сервис -недоступен → ERROR»). При частом поллинге это шумит. +Остаются две вещи, и обе стоят на одном счётчике подряд-идущих сбоев тика. -Развилка (решить до правки): +**Бэкофф опроса (решение 2026-08-06).** Пока qBittorrent недоступен, цикл +продолжает дёргать его каждые `poll_interval` (5 с) — недоступную зависимость +незачем опрашивать с рабочей частотой. Интервал растёт экспоненциально от +`poll_interval` до потолка порядка минуты; первый успешный ответ возвращает +рабочий интервал сразу, без ступенчатого спуска. Бэкофф заодно снимает и остроту +шума: записей становится столько же на событие, но событий — единицы в минуту. -- (а) Ввести у `logging.ExtCall` вариант с пониженным уровнем для рутинно-частых - вызовов (симметрично `SuccessDebug`) — поллинг-вызовы (`torrents/info`) на - транзиентном сбое пишут WARN, не ERROR; -- (б) Дедуп/circuit-breaker: первый ERROR, дальше тишина до восстановления; -- (в) Оставить как есть, признав `ext.*` ERROR легитимным сигналом «зависимость - лежит» (тогда шум гасить уровнем сбора, а не кодом). +**Эскалация уровня.** Сейчас сбой тика — **всегда WARN**, сколько бы тиков +подряд он ни падал. `docs/conventions/logging.md` требует иного: устойчивый сбой +N тиков подряд — это реальная деградация, и она пишется ERROR. -### Эскалация устойчивого сбоя тика +## Воспроизведение -Сейчас транзиентный сбой тика = WARN всегда. Договорённость на будущее -(`logging.md`): устойчивый сбой N тиков подряд эскалировать в ERROR (реальная -деградация, а не разовый промах). Не реализовано — нужен счётчик подряд-сбоев по -циклу и порог в конфиге. +1. Остановить qBittorrent (локально, не на umbar). +2. Смотреть лог воркера в течение нескольких минут поллинга. +3. Наблюдается: запрос к qBittorrent уходит каждые 5 секунд всё время + недоступности, а доменная запись `poll failed` идёт WARN на каждом тике и + остаётся WARN бесконечно. +4. Ожидается: интервал опроса растёт до минутного потолка, а после N + подряд-идущих неудачных тиков уровень поднимается до ERROR — деградация + отличается от разового промаха. +5. Поднять qBittorrent обратно: опрос возвращается к `poll_interval` с первого + успешного ответа. -Вердикт: мелкая надёжностная полировка, не блокер. Делать вместе (обе про -уровень сбоев фоновых циклов) или отдельной строкой. +## Затрагивает + +- цикл поллинга воркера (`internal/worker`) — счётчик подряд-идущих сбоев, + текущий интервал тика и его сброс по успеху; +- секция `[worker]` конфига и `config.example.toml` — потолок бэкоффа и порог + эскалации; +- `docs/database.md`, таблица «Настройки с числовым значением» — дом обоих + чисел; +- `docs/architecture.md`, «Характер потока» — там сказано, что фон непрерывный с + периодом поллинга; переменный интервал это уточняет; +- `docs/conventions/logging.md` — правило эскалации уже записано, меняться не + должно; задача приводит код к нему. + +## Критерии приёмки + +- При подряд-идущих сбоях интервал опроса растёт экспоненциально от + `poll_interval` и упирается в потолок из конфига, дальше не растёт (оракул: + тест цикла с подставным клиентом и управляемыми часами — проверяет + последовательность интервалов). +- Первый успешный ответ возвращает `poll_interval` немедленно (оракул: тот же + тест, сценарий «серия сбоев, успех, сбой» — после успеха интервал рабочий). +- Сбой тика ниже порога пишется WARN, начиная с N-го подряд — ERROR, а успешный + тик сбрасывает счётчик (оракул: тест, считающий уровни записей на сценарии + «сбой, сбой, успех, сбой»). +- Потолок бэкоффа и порог эскалации читаются из конфига и описаны в + `config.example.toml` с единицами и диапазоном (оракул: `task gate`, шаг + канона — сверка с `database.md`). diff --git a/docs/tasks/items/catched-source-type-refresh.md b/docs/tasks/items/catched-source-type-refresh.md index b897a09..500db1a 100644 --- a/docs/tasks/items/catched-source-type-refresh.md +++ b/docs/tasks/items/catched-source-type-refresh.md @@ -1,9 +1,8 @@ -# 🐞 Пересобирать `addReq` из свежего `source_type` перед `Add` (окно namer'а) +# 🧹 Уточнить в спеке требование о re-read `source_type` перед `Add` -- **Тип:** fix +- **Тип:** chore - **Категория:** Ядро продукта -- **Зачем:** При апгрейде magnet→.torrent в окне namer'а добавится magnet из устаревшего снимка; самоисцеляется через magnet_timeout→failed→Retry _(аудит 2026-07-17)_ -- **Теги:** goal:state-integrity +- **Зачем:** спека требует перечитывать source_type непосредственно перед добавлением, код перечитывает после тик-снимка — узкое namer-окно самоисцеляется через Retry и признано допустимым Найдено аудитом capability **download-tracking** (сверка код↔спека после пачки lifecycle-задач). Пред-существующее, вне scope задачи F3/cancel-cleanup — T4 @@ -35,22 +34,42 @@ lifecycle-задач). Пред-существующее, вне scope зада «источник неприкосновенен» не задет. Окно узкое (апгрейд должен лечь ровно в LLM-вызов по тому же infohash). Поэтому средний, не высокий. -## Развилка (решить до кода) +## Решение (2026-08-06): B — привести спеку к коду -- **A — ужесточить код (соответствие букве спеки, закрыть окно):** после re-read - `before` под замком (`:484`) пересобирать `addReq`/`hint` из `before`, если - `source_type` изменился. Нюанс: `sourceAddParts` читает байты `.torrent` — это - тяжёлый вызов, держать под замком нельзя (спека: тяжёлое — вне блокировки), плюс - подсказка имени для `.torrent` иная (метаданные раздачи vs имя из magnet), т.е. - при апгрейде корректно был бы и повторный namer. Не однострочник. -- **B — смягчить спеку (принять реальность):** признать, что рациональ («не - полагаться на снимок, снятый ранее вне блокировки») уже выполнен первым re-read - под замком на `:447`, и переформулировать требование как «перечитывать - `source_type` под блокировкой после тик-снимка», явно приняв узкое namer-окно - как самоисцеляемое через `Retry`. +Рациональ требования — «не полагаться на снимок, снятый ранее вне блокировки» — +уже выполнен первым re-read под замком на `:447`. Требование переформулируется +как «перечитывать `source_type` под блокировкой после тик-снимка», а узкое +namer-окно принимается явно: оно самоисцеляется через `magnet_timeout` → +`failed` → `Retry`. -Рекомендация — начать с B (дёшево, отражает фактическое осознанное поведение), A -завести только если узкое окно окажется реальной болью в эксплуатации. +Вариант A (пересобирать `addReq` из `before` под замком) отклонён: не +однострочник — `sourceAddParts` читает байты `.torrent`, держать это под +блокировкой нельзя, а при апгрейде корректно был бы и повторный вызов namer'а. +Заводить его отдельной задачей, только если узкое окно окажется реальной болью в +эксплуатации. + +Кода задача не трогает: наблюдаемое поведение остаётся прежним, меняется +заявленное. + +## Затрагивает + +- `openspec/specs/download-tracking/spec.md` — требование про re-read + `source_type` перед добавлением, его формулировка и сценарии; +- дельта-спека change'а — новых сценариев с namer-окном может потребоваться два + (апгрейд до тик-снимка и апгрейд в окне namer'а); +- `internal/worker/worker.go` — только чтение, правок не предполагается. + +## Критерии приёмки + +- Требование спеки описывает фактическое поведение: re-read `source_type` под + блокировкой после тик-снимка, апгрейд в окне namer'а назван допустимым и + самоисцеляемым (оракул: `openspec validate --strict`). +- В спеке есть сценарий, покрывающий апгрейд в окне namer'а с исходом «magnet из + снимка, дальше `magnet_timeout` → `failed` → `Retry`» (оракул: тот же прогон + плюс существующий `TestProcessCatchedReReadsSourceTypeUnderLock` продолжает + проходить без правок). +- Ни один файл под `internal/` в диффе не изменён (оракул: `git diff --stat` + в отчёте ревью). ## Ссылки diff --git a/docs/tasks/items/dismiss-marker-lost.md b/docs/tasks/items/dismiss-marker-lost.md index be2b4ea..5d51272 100644 --- a/docs/tasks/items/dismiss-marker-lost.md +++ b/docs/tasks/items/dismiss-marker-lost.md @@ -1,9 +1,8 @@ -# 🐞 Не терять маркер `user_dismiss` при закрытии не-терминальной загрузки из веб-UI +# 🧹 Зафиксировать в спеке разделение Cancel и Dismiss по состояниям -- **Тип:** fix +- **Тип:** chore - **Категория:** Ядро продукта -- **Зачем:** Функционально ок (Cancel даёт cancelled), но маркер user_dismiss в error_code теряется; расхождение с буквой спеки _(аудит 2026-07-17)_ -- **Теги:** goal:state-integrity +- **Зачем:** спека обещает dismiss из любого состояния, код осознанно даёт Cancel для активных и Dismiss для терминальных — расходится буква, а не поведение Найдено аудитом capability **state-reconciliation** (сверка код↔спека). Пред-существующее, вне scope пачки lifecycle-задач. @@ -31,16 +30,36 @@ наблюдаемости (в аналитике/логах не отличить «пользователь закрыл активную» от «пользователь отменил»). Отсюда низкий приоритет. -## Развилка (решить до кода) +## Решение (2026-08-06): B — привести спеку к коду -- **A — привести код к спеке:** веб-UI на не-терминальных тоже зовёт `Dismiss` - ради единого маркера `user_dismiss`; либо `Cancel` пишет `user_dismiss`. -- **B — привести спеку к коду:** зафиксировать осознанное разделение (`Cancel` - для активных, `Dismiss` для терминальных) — уточнить требование, что стоп-кран - на не-терминальных реализуется `Cancel`'ом, и определить, какой `error_code` - ожидается. +Разделение осознанное: `Cancel` — стоп-кран для активных состояний, `Dismiss` — +закрытие терминальных. Требование «Ручное закрытие» уточняется: на +не-терминальных состояниях закрытие из интерфейса реализуется `Cancel`'ом, и +называется, какой `error_code` при этом ожидается. -Сначала решить, осознанно ли разделение Cancel/Dismiss; если да — вероятно B. +Вариант 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/` нет). ## Ссылки diff --git a/docs/tasks/items/ingest-nits.md b/docs/tasks/items/ingest-nits.md index c19b988..b8862bd 100644 --- a/docs/tasks/items/ingest-nits.md +++ b/docs/tasks/items/ingest-nits.md @@ -3,7 +3,6 @@ - **Тип:** chore - **Категория:** Ядро продукта - **Зачем:** косметика приёма: NoName в контексте, устаревшие комментарии, лог, bencode-аллокации _(ревью 2026-07-08)_ -- **Теги:** goal:state-integrity Ревью Fable 2026-07-08 (приём). Косметические нити. @@ -16,3 +15,26 @@ N4 — qbt.go:246 логирует «Fails.» со счётчиками, но qB N5 — anacrolix bencode (v1.61.0, bencode/decode.go:17,250) аллоцирует до MaxStrLen (~128MiB) на объявленную строку до чтения — крафт-8MiB-торрент может форсить транзиентные ~128MiB аллокации при metainfo.Load. Ограничено и завершается ошибкой; на umbar приемлемо, но знать стоит. (files()-panic-guard torrent.go НЕ покрывает Load/UnmarshalInfo/HashBytes, но panic-путей там не найдено.) Вердикт: простые фиксы/принять. + +## Затрагивает + +- `internal/torrent/torrent.go` — `Context()` и фильтр NoName-сентинела «-»; +- `internal/httpapi/httpapi.go` и `internal/tgbot/bot.go` — устаревшие + комментарии про непустой `DownloadID` на пути ошибки; +- `internal/qbt/qbt.go` — лог `Fails.` без причины; +- N5 (аллокации bencode в `anacrolix/torrent`) — граница чужой библиотеки, + правке не подлежит: исход пункта — запись наблюдения, а не код. + +## Критерии приёмки + +- Для безымянного торрента `Context()` не отдаёт «-» как название — поле пустое + (оракул: тест разбора на фикстуре безымянного торрента в + `internal/torrent`). +- Комментарии в `httpapi` и `tgbot` описывают фактическое поведение `Ingest`: + на любом пути ошибки возвращается пустой `Result`, корреляция идёт по + `request_id` (оракул: чтение диффа на ревью — механического оракула нет). +- Лог неудачного добавления в qBittorrent несёт инфохэш для корреляции (оракул: + тест клиента с подставным сервером, проверяющий поля записи). +- Наблюдение про аллокации bencode до `MaxStrLen` записано в + `docs/research/` с провенансом либо явно отклонено строкой в теле задачи + (оракул: `task gate`, шаг канона). diff --git a/docs/tasks/items/tvdb-title-locale.md b/docs/tasks/items/tvdb-title-locale.md index 10ee466..9816067 100644 --- a/docs/tasks/items/tvdb-title-locale.md +++ b/docs/tasks/items/tvdb-title-locale.md @@ -32,6 +32,16 @@ ([metadata-match](../../../openspec/specs/metadata-match/spec.md)) написано только под TMDB — либо обобщается на провайдеров, либо получает соседа. +## Затрагивает + +- `internal/metadata/tvdb.go` — `TVDBConfig` (поле языка), строка запроса + `Search`, разбор переводов и `OriginalTitle` в кандидате; +- `cmd/jellybit/serve.go` — сборка провайдера TVDB из конфига; +- `openspec/specs/metadata-match/spec.md` — требование «Локаль запроса к TMDB»: + обобщается на провайдеров либо получает соседа под TVDB; +- внешний контракт: поиск TVDB (`/search`) и его блок переводов — формат + сверяется живым прогоном под `TVDB_API_KEY`, наугад не пишется. + ## Критерии приёмки - Запрос поиска TVDB содержит параметр языка, выведенный из `[general].language`