Рефакторинг границ 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
+108 -5
View File
@@ -2,12 +2,13 @@
## Purpose
Приём загрузки: использование контекста для отображаемого имени торрента в
qBittorrent. Capability описывает вывод человекочитаемого имени из контекста
(через LLM или алгоритмический фолбек) и его передачу в qBittorrent.
Приём загрузки — единый use-case для всех транспортов (HTTP, Telegram, CLI):
парс источника (Ф1 — magnet), извлечение инфохэшей (`download_infohash`),
дедупликация по активной задаче, атомарное заведение `download` и отдача
источника в qBittorrent, а также вывод человекочитаемого отображаемого имени из
контекста (через LLM или алгоритмический фолбек). Держит инвариант «не более
одной активной загрузки на infohash» (атомарный возврат в активное состояние).
## Requirements
### Requirement: Отображаемое имя торрента из контекста
При добавлении загрузки в qBittorrent система SHALL выводить из контекста
@@ -107,3 +108,105 @@ JSON-вывод), извлекая из контекста тип (movie/series)
- **WHEN** ни LLM, ни алгоритмический фолбек не дали непустого имени
- **THEN** система добавляет загрузку без параметра `rename`
### Requirement: Приём источника и заведение загрузки
Приём SHALL быть единым use-case, общим для всех транспортов (HTTP, Telegram,
CLI): по источнику (Ф1 — magnet) и текстовому контексту система SHALL извлечь
инфохэши, дедуплицировать по активной задаче, при отсутствии дубля завести
загрузку (`download` в состоянии `downloading` + записи `download_infohash`) и
отдать источник в qBittorrent (категория `qbittorrent.category`, savepath). Если
добавление в qBittorrent не удалось, система SHALL перевести уже заведённую
загрузку в `failed` (`error_code` `qbit_add`) и уведомить автора. Заведение
загрузки и запись её хешей SHALL выполняться атомарно (см. «Атомарность возврата
загрузки в активное состояние»).
#### Scenario: Успешный приём magnet
- **GIVEN** валидная magnet-ссылка и контекст
- **WHEN** вызывается приём
- **THEN** создаётся `download` в `downloading` с записями `download_infohash`
- **AND** источник отдан в qBittorrent с нашей категорией
#### Scenario: Падение добавления в qBittorrent
- **GIVEN** заведённую загрузку не удалось добавить в qBittorrent
- **WHEN** обрабатывается ошибка добавления
- **THEN** загрузка переходит в `failed` с `error_code` `qbit_add`
- **AND** автор загрузки уведомляется
### Requirement: Множество инфохэшей загрузки
Загрузка SHALL иметь одну или более записей инфохэша (`download_infohash`:
`infohash` lowercase hex, `kind``v1`|`v2`). При приёме magnet-ссылки
SHALL записываться ВСЕ известные из неё хеши — гибридный magnet несёт и
btih (v1), и btmh (v2); `kind` определяется по длине hex (40 — `v1`, 64 —
`v2`). Когда qBittorrent сообщает для раздачи оба хеша (`infohash_v1`,
`infohash_v2`), система SHALL дописывать недостающие записи загрузке;
усечённый хеш v2-only раздачи (поле `hash` qBittorrent, 40 hex от v2)
записываться SHALL NOT. Сопоставление раздачи qBittorrent с загрузкой
(поллинг, discover) SHALL выполняться по любому из известных хешей. Один и
тот же infohash MAY принадлежать нескольким загрузкам во времени (повторный
приём после терминального состояния), но активной из них MUST быть не более
одной.
#### Scenario: Гибридный торрент раскрывает оба хеша
- **GIVEN** загрузка принята по magnet с v1-хешем
- **WHEN** qBittorrent отдаёт раздачу с заполненными `infohash_v1` и
`infohash_v2`
- **THEN** у загрузки появляются обе записи (`kind` = `v1` и `v2`)
#### Scenario: Сопоставление по v2-хешу
- **GIVEN** загрузка с записями v1- и v2-хешей
- **WHEN** поллинг находит раздачу, совпавшую только по v2-хешу
- **THEN** раздача сопоставляется с этой загрузкой
### Requirement: Дедупликация приёма по любому из хешей
При приёме система SHALL искать **активную** (нетерминальную) загрузку по
любому из известных хешей и, найдя, SHALL возвращать её вместо создания
новой. Проверка активности и вставка новой загрузки с её хешами SHALL
выполняться атомарно (в одной write-транзакции), поддерживая инвариант «не
более одной активной загрузки на infohash». Отдельного снимаемого/
восстанавливаемого ключа идемпотентности в схеме быть SHALL NOT — активность
выводится только из `state`.
#### Scenario: Повторный приём при активной загрузке
- **GIVEN** активная загрузка с infohash `h`
- **WHEN** принимается magnet с тем же `h`
- **THEN** новая загрузка не создаётся, возвращается существующая
#### Scenario: Повторный приём после завершения
- **GIVEN** загрузка с infohash `h` в терминальном состоянии (`done`)
- **WHEN** принимается magnet с тем же `h`
- **THEN** создаётся новая загрузка со своим ULID и записью `h`
### Requirement: Атомарность возврата загрузки в активное состояние
Система SHALL атомарно (в одной write-транзакции) проверять на каждом пути,
возвращающем загрузку из терминального состояния в активное (ручной retry,
воскрешение фоновой сверкой, повторная раскладка/relink) или создающем её
(приём, adopt чужой раздачи), что никакая другая активная загрузка не
владеет любым из хешей этой, и при владении SHALL отказывать в переходе,
сохраняя инвариант «не более одной активной загрузки на infohash».
Отказ SHALL происходить до побочных эффектов во внешних системах
(повторного добавления торрента в qBittorrent).
Та же проверка SHALL применяться к дозаписи хешей загрузке (раскрытие
гибридного торрента): хеш, которым владеет другая активная загрузка,
дописан быть SHALL NOT. Прямой перевод терминальной загрузки в активное
состояние в обход этой проверки SHALL отклоняться хранилищем (механический
бэкстоп вместо удалённого unique-индекса).
#### Scenario: Retry при занятом хеше
- **GIVEN** загрузка #1 в `failed` с хешем `h`, и другая активная загрузка
#2 с тем же `h`
- **WHEN** пользователь вызывает retry для #1
- **THEN** переход отклоняется с пояснением, #1 остаётся в `failed`
- **AND** активной по `h` остаётся #2