Рефакторинг границ capabilities: цепочка загрузка→матч→ревью→раскладка (openspec)

Привёл набор capabilities в OpenSpec к цепочке обработки, чтобы имя capability
отвечало одному поведению. Чисто по спекам, код и поведение системы не меняются.

Change refactor-capability-boundaries (архивирован):
- recognition разделён на recognition (разбор LLM) + metadata-match (сверка с базами)
- review выделен из web-ui + мигрирован из docs/specs/review-ux.md
- новые capability из docs/specs: file-layout, download-tracking, notifications
- identity очищен до инфра-id; приём (инфохэши, дедуп, ядро приёма) — в ingest
- уведомление о рассинхроне перенесено из state-reconciliation в notifications
- дубль владения путём и безопасного undo оставлен в state-reconciliation

Итог: 11 capabilities, openspec validate --strict проходит (+37/−11 требований).
Источник истины по мигрированным темам переехал в openspec/specs (шапки в docs).
Снят пункт беклога «Пересмотр набора capabilities».

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
av
2026-07-03 21:17:51 +03:00
co-authored by Claude Opus 4.8
parent b3d7c08f4a
commit 512567c8ba
29 changed files with 1866 additions and 315 deletions
+93
View File
@@ -0,0 +1,93 @@
# download-tracking Specification
## Purpose
Отслеживание скачивания и прямой путь машины состояний загрузки: поллинг
qBittorrent и сопоставление его состояний (downloading → completed; готовность
только когда файлы на месте), таймауты-предохранители (`magnet_timeout`/
`stuck_after`), ошибка qBit → failed, усыновление раздач по категории/тегу и
переходы под per-download блокировкой. Сверка уже разложенного с реальностью —
в `state-reconciliation`.
## Requirements
### Requirement: Поллинг qBittorrent и сопоставление состояний
Worker SHALL периодически (`worker.poll_interval`, дефолт 5 с) опрашивать
qBittorrent и сопоставлять его состояния раздачи с состоянием загрузки в БД.
Готовые к раскладке состояния (`uploading`/`stalledUP`/`pausedUP`/`stoppedUP`/
`queuedUP`/`forcedUP`, с учётом различий имён между qBit v4 и v5) SHALL переводить
загрузку в `completed`. Ещё качающиеся состояния (`downloading`/`stalledDL`/
`metaDL`/…) SHALL оставлять её в `downloading`.
#### Scenario: Раздача завершилась
- **GIVEN** загрузка в `downloading`
- **WHEN** qBittorrent сообщает состояние `stalledUP` и файлы на месте
- **THEN** загрузка переходит в `completed`
### Requirement: Готовность только когда файлы на месте
Переходные состояния qBittorrent система SHALL трактовать как «ждём»
(`moving`/`checkingUP`/`checkingResumeData`/`allocating`): оставаться в
`downloading` и НЕ объявлять готовность, даже если выставлены флаги `UP`, пока
qBit не завершит перенос/проверку. Финальные пути файлов система SHALL брать из
API после завершения переноса.
#### Scenario: Ждём завершения переноса
- **GIVEN** загрузка, у которой qBittorrent в состоянии `moving`
- **WHEN** идёт тик поллинга
- **THEN** загрузка остаётся в `downloading`, готовность не объявляется
### Requirement: Таймауты-предохранители downloading
Система SHALL переводить `metaDL`/`forcedMetaDL` дольше `magnet_timeout` (дефолт
`24h`, редкий предохранитель) в `failed` (`error_code` `magnet_timeout`), а
`stalledDL` дольше `stuck_after` — в `stuck` (`error_code`
`stalled`). Возраст система SHALL считать от времени добавления в qBittorrent
(`added_on`), а не от создания задачи, чтобы базис переживал retry и усыновление.
Долгий `metaDL` система НЕ SHALL убивать агрессивно (медленные трекеры — норма).
#### Scenario: Завис на метаданных дольше таймаута
- **GIVEN** раздача в `metaDL` дольше `magnet_timeout` от `added_on`
- **WHEN** идёт тик поллинга
- **THEN** загрузка переходит в `failed` с `error_code` `magnet_timeout`
### Requirement: Ошибка qBittorrent переводит в failed
Состояния `error`/`missingFiles` система SHALL трактовать как настоящий провал и
переводить загрузку в `failed` (`error_code` `qbit_error`) — в отличие от
таймаутов-предохранителей, такой провал сверкой не воскрешается.
#### Scenario: qBit сообщает об ошибке
- **GIVEN** раздача в состоянии `missingFiles`
- **WHEN** идёт тик поллинга
- **THEN** загрузка переходит в `failed` с `error_code` `qbit_error`
### Requirement: Усыновление раздач по категории или тегу
Worker SHALL периодически сверять раздачи qBittorrent с БД и **усыновлять** те, у
которых наша категория (`qbittorrent.category`) ИЛИ тег (`qbittorrent.tag`), а
записи в БД ещё нет, заводя для них загрузку в состоянии `downloading`. Категория
ставится на добавляемые нами раздачи (push); тег позволяет подхватить уже
существующую раздачу (pull), не трогая её категорию и файлы.
#### Scenario: Подхват существующей раздачи по тегу
- **GIVEN** в qBittorrent есть раздача с тегом `qbittorrent.tag`, которой нет в БД
- **WHEN** worker сверяет qBittorrent с БД
- **THEN** для раздачи заводится загрузка в состоянии `downloading`
### Requirement: Переходы состояний под per-download блокировкой
Все переходы состояний загрузки SHALL проходить через worker под per-download
блокировкой, чтобы два транспорта не гонялись за одно состояние. Состояние SHALL
быть персистентным в SQLite; активность загрузки SHALL выводиться только из
`state`, без отдельного флага.
#### Scenario: Команды сериализуются
- **GIVEN** две одновременные команды к одной загрузке из разных транспортов
- **WHEN** они обрабатываются
- **THEN** переходы применяются последовательно под блокировкой, без гонки