sprint: набран спринт 2026-08-06 под цель распознавания

- в наборе шесть задач: локаль TVDB и confidence-гейт под цель, плюс баги и
  техдолг помимо неё
- взятым дописаны разделы своего типа: «Затрагивает», критерии с оракулами,
  воспроизведение
- решены развилки: оба расхождения код↔спека правятся спекой (и потому стали
  chore), опрос qBittorrent тормозится бэкоффом до минутного потолка
This commit is contained in:
av
2026-08-06 13:57:33 +03:00
parent d2d386945e
commit f42db0a275
8 changed files with 197 additions and 65 deletions
-6
View File
@@ -12,14 +12,10 @@
## Ядро продукта ## Ядро продукта
- [✨ Пересчитывать абсолютную нумерацию аниме в SxxEyy](items/anime-absolute-numbering.md) — аниме со сквозной нумерацией (#137) не раскладывается в SxxEyy, который ждёт Jellyfin — нужен пересчёт абсолютной нумерации - [✨ Пересчитывать абсолютную нумерацию аниме в 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) сейчас разбирается пофайлово вместо раскладки каталога целиком — редкий, но реальный случай - [✨ Раскладывать раздачу-копию диска (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-гард) - [✨ Принимать ссылку на .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-раскладкой, не трогая существующие ссылки — блокирует типовой сценарий свежих сериалов - [✨ Докладывать недостающие эпизоды merge-раскладкой при повторной заливке](items/merge-incremental-redownload.md) — повторная заливка сериала целиком должна доложить недостающие эпизоды merge-раскладкой, не трогая существующие ссылки — блокирует типовой сценарий свежих сериалов
- [🐞 Связать v1/v2-хеши одного торрента и не доверять паре xt из магнета (F4, F5)](items/infohash-identity-integrity.md) — split v1/v2 идентичность и крафт-магнет отравляют владение инфохэшами _(ревью 2026-07-08)_ - [🐞 Связать 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 убрал бы то и другое (поллинг работает, поэтому улучшение, не блокер) - [✨ Слать живые обновления через SSE вместо htmx-поллинга](items/sse-live-updates.md) — живые обновления на htmx-поллинге дают задержку и холостые запросы — SSE убрал бы то и другое (поллинг работает, поэтому улучшение, не блокер)
- [✨ Проверять свободное место перед copy-fallback](items/free-space-check-copy-fallback.md) — copy-fallback при невозможности хардлинка может упереться в переполненный диск посреди раскладки — нет проверки места до копирования - [✨ Проверять свободное место перед copy-fallback](items/free-space-check-copy-fallback.md) — copy-fallback при невозможности хардлинка может упереться в переполненный диск посреди раскладки — нет проверки места до копирования
- [✨ Слать уведомления автору загрузки в его транспорт (мульти-бот)](items/notification-source-binding.md) — пинги и ревью должен получать автор загрузки в своём транспорте — нет привязки загрузки к источнику/отправителю (нужно для мульти-бота) - [✨ Слать уведомления автору загрузки в его транспорт (мульти-бот)](items/notification-source-binding.md) — пинги и ревью должен получать автор загрузки в своём транспорте — нет привязки загрузки к источнику/отправителю (нужно для мульти-бота)
@@ -28,7 +24,6 @@
- [✨ Переделать веб-UI в устанавливаемое PWA](items/web-ui-pwa.md) — текущий server-rendered UI функционален — PWA (устанавливаемое, удобное с телефона) это улучшение большого объёма, не блокер - [✨ Переделать веб-UI в устанавливаемое PWA](items/web-ui-pwa.md) — текущий server-rendered UI функционален — PWA (устанавливаемое, удобное с телефона) это улучшение большого объёма, не блокер
- [✨ Править на ревью маппинг «файл → серия» и раскладывать вручную при провале LLM](items/review-mapping-editor.md) — правка S·E, «нумеровать подряд» и ручной режим при полном провале LLM были запланированы объёмом Ф5 и не заведены задачей — в ревью сегодня можно только подсказать текстом - [✨ Править на ревью маппинг «файл → серия» и раскладывать вручную при провале LLM](items/review-mapping-editor.md) — правка S·E, «нумеровать подряд» и ручной режим при полном провале LLM были запланированы объёмом Ф5 и не заведены задачей — в ревью сегодня можно только подсказать текстом
- [✨ Заказать спекой крайние случаи именования: многофайловый фильм, редакции, двойная серия](items/naming-edge-cases.md) — стэкинг частей (part1/cd1), редакции [edition-…] и двойная серия SxxEyy-Eyy описаны нарративом, но в file-layout не заказаны — раскладка таких раздач не определена - [✨ Заказать спекой крайние случаи именования: многофайловый фильм, редакции, двойная серия](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 подтягивает не те подписи серий, если канон файлов ≠ дефолтный порядок провайдера тега - [🔬 Канон нумерации серий и порядок у провайдера тега](items/episode-numbering-canon.md) — Косметика/редкость: порядок просмотра ок, но у тайтлов со спорным порядком (Бибоп) Jellyfin подтягивает не те подписи серий, если канон файлов ≠ дефолтный порядок провайдера тега
- [🔬 Тексты и формат уведомлений в Telegram](items/telegram-messages-audit.md) — зонтичный проход по всем текстам бота: полнота карточек, единый язык, оформление; порождает под-задачи - [🔬 Тексты и формат уведомлений в Telegram](items/telegram-messages-audit.md) — зонтичный проход по всем текстам бота: полнота карточек, единый язык, оформление; порождает под-задачи
- [🔬 guessit как сервис-спутник](items/guessit-sidecar.md) — go-ptn слабее питоновского guessit — если точности пред-парса не хватит, завернуть guessit в сервис-спутник рядом с бинарём - [🔬 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 - [✨ Проверять в healthcheck доступность qBittorrent, LLM и метабаз и показывать её в UI](items/deep-healthcheck-dependencies.md) — /healthz проверяет только сам сервис — недоступность qBittorrent/LLM/метабазы видна лишь по застрявшим задачам, нет readiness и бейджа в UI
- [✨ Хранить историю переходов загрузки отдельной таблицей](items/download-transition-history.md) — хранится только текущий статус загрузки — разбор «как сюда попали» идёт по логам сервера, нет таблицы истории переходов - [✨ Хранить историю переходов загрузки отдельной таблицей](items/download-transition-history.md) — хранится только текущий статус загрузки — разбор «как сюда попали» идёт по логам сервера, нет таблицы истории переходов
- [🧹 Кэшировать ответы метабаз с TTL (и опционально LLM)](items/metadata-cache.md) — повторные и ретраящиеся прогоны бьют TMDB/TVDB/TVMaze одним запросом — кэш с TTL сэкономил бы лимиты и ускорил «Распознать заново» - [🧹 Кэшировать ответы метабаз с 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 копятся вечно — без авточистки список загрузок и БД деградируют по мере эксплуатации - [✨ Чистить БД от терминальных задач и сырых ответов LLM старше срока хранения](items/db-retention-cleanup.md) — терминальные задачи и сырые ответы LLM копятся вечно — без авточистки список загрузок и БД деградируют по мере эксплуатации
- [🧹 Свести термины домена в словарь единого языка](items/ubiquitous-language-glossary.md) — наименования домена расходятся между спеками, UI и кодом — нет единого глоссария (на нём же стоит агент-ревьювер наименований) - [🧹 Свести термины домена в словарь единого языка](items/ubiquitous-language-glossary.md) — наименования домена расходятся между спеками, UI и кодом — нет единого глоссария (на нём же стоит агент-ревьювер наименований)
- [🔬 Потолок нагрузки: 100 одновременных загрузок, план-максимум 1000](items/scale-100-downloads.md) — Зафиксировать в НФТ ориентир 100/1000 загрузок + аудит узких мест (SQLite, воркер, поллинг) - [🔬 Потолок нагрузки: 100 одновременных загрузок, план-максимум 1000](items/scale-100-downloads.md) — Зафиксировать в НФТ ориентир 100/1000 загрузок + аудит узких мест (SQLite, воркер, поллинг)
+12 -2
View File
@@ -1,6 +1,16 @@
# Спринт # Спринт
Спринта нет. Цель называет человек, набор собирает агент: - **Цель:** [🎯 Раздача узнаётся верно без подсказок человека](items/recognition-accuracy.md)
`tasks.py sprint start --goal <слаг>`. - **Начат:** 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)_
@@ -49,6 +49,31 @@
которому порог можно было бы подобрать заранее, решено не собирать которому порог можно было бы подобрать заранее, решено не собирать
(`../REJECTED.md`, 2026-08-06), так что первое значение остаётся оценкой. (`../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` + правки Оформить как OpenSpec-change (дельта `recognition` + правки
`validate.go`/`recognize.go`/`config`). `validate.go`/`recognize.go`/`config`).
+59 -26
View File
@@ -1,14 +1,13 @@
# 🐞 Не штормить ERROR при недоступном qBittorrent и эскалировать устойчивый сбой тика # 🐞 Тормозить опрос qBittorrent бэкоффом при недоступности и эскалировать устойчивый сбой
- **Тип:** fix - **Тип:** fix
- **Категория:** Инфраструктура - **Категория:** Инфраструктура
- **Зачем:** Остаток задачи логирования: ext.* ERROR-шторм при недоступном qBittorrent + эскалация устойчивого сбоя тика _(ревью Fable)_ - **Зачем:** недоступный qBittorrent опрашивается каждые 5 с и даёт WARN на каждом тике: нужен экспоненциальный бэкофф до минутного потолка со сбросом по первому успеху и ERROR на устойчивой деградации
- **Теги:** goal:operational-resilience
Остаток от задачи «классификация доменных ошибок + конвенции логирования» Остаток от задачи «классификация доменных ошибок + конвенции логирования»
(основное реализовано, см. ниже). Здесь — два смежных пункта про уровень (основное реализовано, см. ниже) плюс бэкофф опроса, заказанный 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` вариант с пониженным уровнем для рутинно-частых **Эскалация уровня.** Сейчас сбой тика — **всегда WARN**, сколько бы тиков
вызовов (симметрично `SuccessDebug`) — поллинг-вызовы (`torrents/info`) на подряд он ни падал. `docs/conventions/logging.md` требует иного: устойчивый сбой
транзиентном сбое пишут WARN, не ERROR; N тиков подряд — это реальная деградация, и она пишется ERROR.
- (б) Дедуп/circuit-breaker: первый ERROR, дальше тишина до восстановления;
- (в) Оставить как есть, признав `ext.*` ERROR легитимным сигналом «зависимость
лежит» (тогда шум гасить уровнем сбора, а не кодом).
### Эскалация устойчивого сбоя тика ## Воспроизведение
Сейчас транзиентный сбой тика = WARN всегда. Договорённость на будущее 1. Остановить qBittorrent (локально, не на umbar).
(`logging.md`): устойчивый сбой N тиков подряд эскалировать в ERROR (реальная 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`).
+37 -18
View File
@@ -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)_ - **Зачем:** спека требует перечитывать source_type непосредственно перед добавлением, код перечитывает после тик-снимка — узкое namer-окно самоисцеляется через Retry и признано допустимым
- **Теги:** goal:state-integrity
Найдено аудитом capability **download-tracking** (сверка код↔спека после пачки Найдено аудитом capability **download-tracking** (сверка код↔спека после пачки
lifecycle-задач). Пред-существующее, вне scope задачи F3/cancel-cleanup — T4 lifecycle-задач). Пред-существующее, вне scope задачи F3/cancel-cleanup — T4
@@ -35,22 +34,42 @@ lifecycle-задач). Пред-существующее, вне scope зада
«источник неприкосновенен» не задет. Окно узкое (апгрейд должен лечь ровно в «источник неприкосновенен» не задет. Окно узкое (апгрейд должен лечь ровно в
LLM-вызов по тому же infohash). Поэтому средний, не высокий. LLM-вызов по тому же infohash). Поэтому средний, не высокий.
## Развилка (решить до кода) ## Решение (2026-08-06): B — привести спеку к коду
- **A — ужесточить код (соответствие букве спеки, закрыть окно):** после re-read Рациональ требования — «не полагаться на снимок, снятый ранее вне блокировки» —
`before` под замком (`:484`) пересобирать `addReq`/`hint` из `before`, если уже выполнен первым re-read под замком на `:447`. Требование переформулируется
`source_type` изменился. Нюанс: `sourceAddParts` читает байты `.torrent` — это как «перечитывать `source_type` под блокировкой после тик-снимка», а узкое
тяжёлый вызов, держать под замком нельзя (спека: тяжёлое — вне блокировки), плюс namer-окно принимается явно: оно самоисцеляется через `magnet_timeout`
подсказка имени для `.torrent` иная (метаданные раздачи vs имя из magnet), т.е. `failed``Retry`.
при апгрейде корректно был бы и повторный namer. Не однострочник.
- **B — смягчить спеку (принять реальность):** признать, что рациональ («не
полагаться на снимок, снятый ранее вне блокировки») уже выполнен первым re-read
под замком на `:447`, и переформулировать требование как «перечитывать
`source_type` под блокировкой после тик-снимка», явно приняв узкое namer-окно
как самоисцеляемое через `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`
в отчёте ревью).
## Ссылки ## Ссылки
+31 -12
View File
@@ -1,9 +1,8 @@
# 🐞 Не терять маркер `user_dismiss` при закрытии не-терминальной загрузки из веб-UI # 🧹 Зафиксировать в спеке разделение Cancel и Dismiss по состояниям
- **Тип:** fix - **Тип:** chore
- **Категория:** Ядро продукта - **Категория:** Ядро продукта
- **Зачем:** Функционально ок (Cancel даёт cancelled), но маркер user_dismiss в error_code теряется; расхождение с буквой спеки _(аудит 2026-07-17)_ - **Зачем:** спека обещает dismiss из любого состояния, код осознанно даёт Cancel для активных и Dismiss для терминальных — расходится буква, а не поведение
- **Теги:** goal:state-integrity
Найдено аудитом capability **state-reconciliation** (сверка код↔спека). Найдено аудитом capability **state-reconciliation** (сверка код↔спека).
Пред-существующее, вне scope пачки lifecycle-задач. Пред-существующее, вне scope пачки lifecycle-задач.
@@ -31,16 +30,36 @@
наблюдаемости (в аналитике/логах не отличить «пользователь закрыл активную» от наблюдаемости (в аналитике/логах не отличить «пользователь закрыл активную» от
«пользователь отменил»). Отсюда низкий приоритет. «пользователь отменил»). Отсюда низкий приоритет.
## Развилка (решить до кода) ## Решение (2026-08-06): B — привести спеку к коду
- **A — привести код к спеке:** веб-UI на не-терминальных тоже зовёт `Dismiss` Разделение осознанное: `Cancel` — стоп-кран для активных состояний, `Dismiss`
ради единого маркера `user_dismiss`; либо `Cancel` пишет `user_dismiss`. закрытие терминальных. Требование «Ручное закрытие» уточняется: на
- **B — привести спеку к коду:** зафиксировать осознанное разделение (`Cancel` не-терминальных состояниях закрытие из интерфейса реализуется `Cancel`'ом, и
для активных, `Dismiss` для терминальных) — уточнить требование, что стоп-кран называется, какой `error_code` при этом ожидается.
на не-терминальных реализуется `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/` нет).
## Ссылки ## Ссылки
+23 -1
View File
@@ -3,7 +3,6 @@
- **Тип:** chore - **Тип:** chore
- **Категория:** Ядро продукта - **Категория:** Ядро продукта
- **Зачем:** косметика приёма: NoName в контексте, устаревшие комментарии, лог, bencode-аллокации _(ревью 2026-07-08)_ - **Зачем:** косметика приёма: NoName в контексте, устаревшие комментарии, лог, bencode-аллокации _(ревью 2026-07-08)_
- **Теги:** goal:state-integrity
Ревью Fable 2026-07-08 (приём). Косметические нити. Ревью 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-путей там не найдено.) 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`, шаг канона).
+10
View File
@@ -32,6 +32,16 @@
([metadata-match](../../../openspec/specs/metadata-match/spec.md)) написано ([metadata-match](../../../openspec/specs/metadata-match/spec.md)) написано
только под TMDB — либо обобщается на провайдеров, либо получает соседа. только под 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` - Запрос поиска TVDB содержит параметр языка, выведенный из `[general].language`