Рефакторинг границ 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
@@ -0,0 +1,83 @@
## ADDED Requirements
### Requirement: Целевые имена фильмов
Фильм система SHALL раскладывать в папку и файл вида `Название (Год)`, помещённые
под `paths.movies`. При подтверждённом матче в базе имя папки SHALL нести
provider-id (`[tmdbid-…]`/`[tvdbid-…]`) — он снимает неоднозначность русских
названий для Jellyfin. Внешние субтитры SHALL именоваться `Имя.<lang>[.flag].srt`
(флаги `forced`/`sdh`/`default`/`hi`), с базой имени, совпадающей с именем
видеофайла; пары VobSub — `.idx` + `.sub`.
#### Scenario: Фильм с provider-id
- **GIVEN** распознанный фильм «Дюна Часть вторая» (2024) с матчем TMDB `693134`
- **WHEN** строится целевой путь
- **THEN** папка = `movies/Дюна Часть вторая (2024) [tmdbid-693134]/`
- **AND** видеофайл = `Дюна Часть вторая (2024).mkv`
### Requirement: Целевые имена сериалов
Сериал система SHALL раскладывать под `paths.series` в папку `Название (Год)` с
provider-id на папке сериала, сезонными подпапками `Season xx` и файлами вида
`Название (Год) SxxEyy`.
#### Scenario: Серия сезона
- **GIVEN** распознанный сериал «Фарго» (2024) с матчем TVDB `123456`, серия S01E02
- **WHEN** строится целевой путь
- **THEN** путь = `series/Фарго (2024) [tvdbid-123456]/Season 01/Фарго (2024) S01E02.mkv`
### Requirement: Сопоставление источник → цель хардлинками
Для каждого распознанного **файла** (не каталога) система SHALL создавать
**хардлинк** в `paths.movies`/`paths.series`; исходный путь берётся из
qBittorrent (`save_path` + относительное имя файла из `/torrents/files`, уже
включающее корневую папку многофайловой раздачи). Целевые каталоги SHALL
создаваться `mkdir` (0755, `1000:1000`). Исходный файл система НЕ SHALL трогать —
раздача продолжается, inode общий, диск не дублируется.
#### Scenario: Хардлинк не дублирует данные
- **GIVEN** видеофайл раздачи под `paths.downloads`
- **WHEN** файл раскладывается
- **THEN** в библиотеке создаётся хардлинк на тот же inode
- **AND** исходный файл остаётся на месте
### Requirement: Санитизация целевого пути и запрет выхода за библиотеку
Целевое имя система SHALL санитизировать (без разделителей пути, `..`,
управляющих символов), а финальный путь SHALL проверять на строгое нахождение под
`paths.movies`/`paths.series`. Путь, выходящий за пределы библиотеки, система НЕ
SHALL создавать. Безопасность SHALL держаться на валидации пути, а не на доверии к
выходу LLM.
#### Scenario: Traversal отклоняется
- **GIVEN** распознанное имя, содержащее `../`
- **WHEN** строится и проверяется целевой путь
- **THEN** путь отклоняется как выходящий за пределы библиотеки, хардлинк не создаётся
### Requirement: Существующую цель не перезаписываем
Существующий целевой файл система НЕ SHALL перезаписывать. Если по целевому пути
уже лежит тот же inode — операция идемпотентна (готово); если другой файл —
это коллизия, и задача SHALL уходить в `review`.
#### Scenario: Коллизия уходит в review
- **GIVEN** по целевому пути уже лежит другой файл
- **WHEN** выполняется раскладка
- **THEN** файл не перезаписывается, задача переходит в `review` с причиной коллизии
### Requirement: Copy-fallback при невозможности хардлинка
Система SHALL при невозможности хардлинка (разные ФС или ФС без поддержки жёстких
ссылок) НЕ падать, а копировать файл с предупреждением в лог, помечая ссылку
статусом `copied`.
#### Scenario: Разные ФС — копирование
- **GIVEN** целевой и исходный каталоги на разных ФС
- **WHEN** выполняется раскладка файла
- **THEN** файл копируется, ссылка получает статус `copied`, в лог пишется предупреждение