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

При подтверждённом матче база папки (имя+год) наследуется от живой
папки-якоря того же (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
@@ -0,0 +1,23 @@
## 1. Layout: приём унаследованной базы и разбор пути
- [x] 1.1 Добавить в `layout.Plan` опциональное поле `FolderBase string` (пусто → как прежде)
- [x] 1.2 В `BuildLinks`: если `FolderBase != ""` — использовать её как `base` (через `sanitizeComponent`) вместо `titleYear(Title, Year)`, применяя и к папке, и к именам файлов; тег складывать как прежде
- [x] 1.3 Экспортировать хелпер(ы): извлечь папку тайтла из `dst_path` (первый сегмент под корнем `movies`/`series`; не под корнем → пусто) и снять базу из папки (убрать хвостовой ` [...]`; пустой результат → пусто)
- [x] 1.4 Юнит-тесты layout: `FolderBase` перекрывает базу в папке и в именах файлов (movie и series); извлечение папки/базы из пути — с тегом, без тега, с чужим/старым тегом, не под корнем
## 2. Store: lookup живых ссылок тайтла
- [x] 2.1 Добавить `LiveTitleFolders(ctx, provider, providerID, excludeDownloadID) ([]string, error)`: `dst_path` ссылок со `status IN linked/copied/exists` загрузок с тем же `(provider, provider_id)` current recognition, кроме `excludeDownloadID`; пустой provider/id → пусто
- [x] 2.2 Юнит-тест store: возврат путей по матчу; исключение текущей загрузки; superseded/collision/удалённые не попадают; пустой provider/id → пусто
## 3. Worker: правило сходимости, рассинхрон, превью
- [x] 3.1 Чистый хелпер `resolveFolderBase(ctx, dl, provider, providerID, mediaType) (base string, desync bool, err error)`: нет матча → пусто; иначе `LiveTitleFolders` → свести к различным папкам тайтла (хелпер 1.3), отбросить несуществующие на диске (`os.Lstat`) и неразбираемые; 0 → пусто, 1 → база, ≥2 → desync. Без побочных эффектов.
- [x] 3.2 В `linkPlan` (из состояния `linking`, рядом с обработкой коллизии — `deferred→review` в графе нет): вызвать `resolveFolderBase`; `desync``transition(review, "title_folder_desync")`, раскладку не выполнять; иначе прокинуть `base` в `toLayoutPlan`/`layout.Plan`
- [x] 3.3 Превью раскладки на ревью (review.go ~921/981) зовёт тот же `resolveFolderBase` и наследует базу (инвариант «превью=применение»); `desync` в превью → база из распознавания, без перевода в review
- [x] 3.4 Тесты worker/integration: сходимость второго сезона в ту же папку; отсутствие якоря → свежая папка; переименование папки на диске у кандидата → не якорь (печать заново); смена провайдера → не склеивается; рассинхрон (2 живые папки) → review с причиной; превью совпадает с применением
## 4. Спеки и проверка
- [x] 4.1 `openspec validate --strict series-folder-convergence`
- [x] 4.2 `task test` и `task lint` — зелёные