Рефакторинг границ 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:
@@ -0,0 +1,92 @@
|
||||
# file-layout Specification
|
||||
|
||||
## Purpose
|
||||
Раскладка распознанных файлов хардлинками под библиотеку Jellyfin: целевые
|
||||
имена фильмов и сериалов (папка/файл, provider-id, сезоны), сопоставление
|
||||
источник→цель, санитизация пути и запрет выхода за библиотеку, never-overwrite
|
||||
(коллизия → review) и copy-fallback при невозможности хардлинка. Владение
|
||||
целевым путём (`superseded`) и безопасный `Undo` (`nlink<=1`) —
|
||||
в `state-reconciliation`.
|
||||
## 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`, в лог пишется предупреждение
|
||||
|
||||
Reference in New Issue
Block a user