Раскладка: сходимость папки сериала (второй сезон в ту же папку)

При подтверждённом матче база папки (имя+год) наследуется от живой
папки-якоря того же (provider, provider_id) вместо печати заново из выхода
LLM — так второй/последующий сезон ложится в ТУ ЖЕ папку, а не заводит
рядом почти одинаковую. Отдельная сущность «тайтл» не вводится.

- layout: Plan.FolderBase перекрывает базу в папке и именах файлов;
  TitleFolder разбирает dst_path в папку тайтла и базу (снятие тега).
- store: LiveTitleFolders — dst_path живых ссылок того же матча.
- worker: resolveFolderBase (живость якоря — по наличию папки на диске,
  os.Lstat, а не по статусу ссылки в БД) в linkPlan и в превью ревью
  (превью=применение); рассинхрон нескольких живых папок → review из
  linking (deferred→review в графе нет).

Схема БД не менялась. Change series-folder-convergence заархивирован,
требования влиты в openspec/specs/file-layout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
av
2026-07-10 13:50:49 +03:00
co-authored by Claude Opus 4.8
parent 3f1a928000
commit 5c3ef79496
20 changed files with 1031 additions and 56 deletions
+84
View File
@@ -144,3 +144,87 @@ SHALL перевести её в `review` с причиной. Повторно
- **AND** созданные хардлинки остаются на диске
- **AND** повторное «Применить» идемпотентно дописывает учёт и доводит до `done`
### Requirement: Сходимость базы папки при подтверждённом матче
Система SHALL при построении плана раскладки для загрузки с подтверждённым матчем (заданы `provider` и `provider_id`) наследовать **базу имени** (`Название (Год)` — строку без provider-тега) от существующей на диске папки-якоря того же тайтла, а НЕ печатать её заново из выхода распознавания. Кандидаты
в якорь — целевые пути (`dst_path`) ссылок со статусом `linked`/`copied`/`exists`
**других** загрузок (не текущей), чей current recognition имеет тот же
`(provider, provider_id)`.
Истина — папка на диске, а не запись в БД: кандидат SHALL считаться живым
якорем, только если его папка тайтла реально существует на файловой системе.
Статус ссылки в БД недостаточен — при ручном/Jellyfin-переименовании папки
запись `file_link` какое-то время остаётся `linked` (загрузка лишь позже уходит
в `target_missing` по сверке), а `dst_path` указывает на уже несуществующий путь.
Поэтому кандидат, чья папка тайтла отсутствует на диске, в якоря НЕ берётся; если
живых якорей не осталось, следующая загрузка снова печатает базу из распознавания.
Унаследованная база SHALL применяться **и к папке сериала/фильма, и к именам
файлов внутри** (episode/movie stem), чтобы серии разных сезонов совпадали по
базе (`Fargo (2014) S02E01` рядом с `Fargo (2014) S01E01`). Provider-тег на папке
(`[tvdbid-…]`) по-прежнему SHALL строиться из текущего `(provider, provider_id)`.
База тайтла извлекается из папки-якоря снятием хвостового provider-тега
` [...]`; извлечённая база прогоняется через ту же санитизацию, что и печатаемая.
Если папку-якорь нельзя разобрать (сегмент не под корнем библиотеки, база пуста),
кандидат в якоря НЕ берётся (безопасный fallback на печать из распознавания), а
не даёт искажённую базу.
Наследование SHALL происходить только при подтверждённом матче. Нет живого якоря
(первая загрузка тайтла) → база печатается из распознавания, как прежде. Нет
матча (`provider` пуст / `none`) → авто-раскладки нет (инвариант), база не
наследуется — папку на ревью выбирает человек. Правило не вводит новых сущностей
и не меняет схему БД: это выборка по существующим `file_link → download →
recognition(is_current)` плюс проверка существования папки на диске. Безопасность
по-прежнему держится на санитизации и проверке пути под библиотекой, а не на
доверии к выходу распознавания.
Разрешение базы SHALL быть единым для авто-раскладки и ручного «Применить», а
также для **предпросмотра** раскладки на ревью — чтобы превью показывало ту же
папку/имена, что даст применение (инвариант «превью = применение»). В
предпросмотре разрешение выполняется без побочных эффектов (в review из-за
рассинхрона переводит только применение, не показ).
#### Scenario: Второй сезон ложится в папку первого
- **GIVEN** первый сезон уже разложен в `series/Фарго (2014) [tvdbid-269613]/Season 01/…` (ссылки живые)
- **AND** новая загрузка со вторым сезоном имеет матч TVDB `269613`, но распознавание дало название «Fargo» и год `2017`
- **WHEN** строится план раскладки второго сезона
- **THEN** база наследуется от живого якоря: папка = `series/Фарго (2014) [tvdbid-269613]/`
- **AND** серия ложится как `Season 02/Фарго (2014) S02E01.mkv` (база в имени файла — унаследованная, а не из выхода LLM)
#### Scenario: Нет живого якоря — печатаем из распознавания
- **GIVEN** ни у одной загрузки нет живых ссылок с тем же `(provider, provider_id)`
- **WHEN** строится план раскладки при подтверждённом матче
- **THEN** база берётся из распознавания (название+год), как прежде — первая загрузка «печатает» имя папки
#### Scenario: Нет матча — сходимость не применяется
- **GIVEN** у загрузки нет подтверждённого матча (`provider` пуст / `none`)
- **WHEN** обрабатывается раскладка
- **THEN** авто-наследования базы не происходит, загрузка идёт через review (папку выбирает человек)
#### Scenario: Папка-якорь переименована на диске — печатаем заново
- **GIVEN** у загрузки-кандидата статус ссылок ещё `linked`, но её папка тайтла на диске переименована/удалена (по `dst_path` папки нет)
- **WHEN** строится план раскладки новой загрузки с тем же матчем
- **THEN** отсутствующая на диске папка в якоря не берётся
- **AND** при отсутствии других живых якорей база печатается из распознавания
#### Scenario: Превью на ревью совпадает с применением
- **GIVEN** есть живой якорь тайтла, а распознавание текущей загрузки дало иную базу
- **WHEN** на ревью открывается предпросмотр целевой раскладки
- **THEN** превью показывает папку/имена с унаследованной базой якоря — те же, что даст «Применить»
### Requirement: Рассинхрон живых папок тайтла уходит в review
Система MUST NOT молча выбирать якорь при обнаружении **нескольких РАЗНЫХ** живых целевых папок с одним `(provider, provider_id)` (рассинхрон, случившийся до внедрения правила сходимости): такая загрузка SHALL уходить в `review` с явной причиной «рассинхрон папок тайтла», чтобы человек выбрал/свёл папку вручную (по прецеденту коллизии, которая тоже уводит в review из раскладки).
#### Scenario: Две живые папки одного матча → review
- **GIVEN** для матча TVDB `269613` существуют две разные живые папки (`Фарго (2014) …` и `Fargo (2017) …`)
- **WHEN** строится план раскладки новой загрузки с этим матчем
- **THEN** раскладка не выполняется, задача переходит в `review` с причиной рассинхрона папок тайтла