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

При подтверждённом матче база папки (имя+год) наследуется от живой
папки-якоря того же (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,63 @@
## Why
Второй/третий сезон одного сериала должен ложиться в ТУ ЖЕ папку, а не заводить
рядом почти одинаковую. Сейчас имя папки печатается заново из выхода LLM при
каждой раскладке (`layout.BuildLinks` из полей плана), и совпадение
`(provider, provider_id)` не гарантирует совпадение строки папки: LLM может дать
«Fargo» и «Фарго», год сезона вместо года сериала — и верно заматченный второй
сезон уедет в соседнюю папку. Это ядро «проблемы второго сезона»: она **не про
группировку, а про сходимость папки** (см. `docs/drafts/logical-title-model.md`
§3, §5.2).
## What Changes
- Вводим **правило сходимости папки** при построении плана раскладки: при
подтверждённом матче (`provider` и `provider_id` заданы) база папки
(`Название (Год)`) **наследуется** от живых `file_link` других загрузок того
же тайтла, а выход LLM для папки игнорируется.
- Якорь берём из **существующего `dst_path`** живых ссылок (`status IN
linked/copied/exists`) загрузок, чей current recognition имеет тот же
`(provider, provider_id)` — живые пути — истина (переименовали папку → сверка
уводит ссылки в `target_missing`, якорь исчезает, следующая загрузка печатает
заново).
- Унаследованная база применяется **и к папке, и к именам файлов внутри**
(episode/movie stem), чтобы `Fargo (2014) S02E01` совпадало с
`Fargo (2014) S01E01`. Провайдер-тег `[tvdbid-…]` по-прежнему берётся из
текущего `(provider, provider_id)`.
- **Рассинхрон** (несколько РАЗНЫХ живых папок с одним `(provider, provider_id)`,
случившийся до внедрения правила) → загрузка уходит в **review** с явной
причиной, якорь молча не выбираем.
- Отдельная сущность «тайтл» **не вводится**: это join по существующим таблицам
(`file_link → download → recognition(is_current)`), без новых
сущностей/таблиц. Схема БД не меняется.
## Capabilities
### New Capabilities
<!-- нет -->
### Modified Capabilities
- `file-layout`: добавляется правило сходимости базы папки при построении
целевого пути — при подтверждённом матче база наследуется от живых ссылок того
же `(provider, provider_id)`, а не печатается из выхода распознавания; несколько
расходящихся живых папок → уход в review с причиной «рассинхрон» вместо тихого
выбора (по прецеденту коллизии, которая тоже уводит в review из file-layout).
## Impact
- **Код:** `internal/layout` (возможность построить план с унаследованной базой
папки), `internal/worker` (`linkPlan`/построение плана раскладки: lookup якоря,
ветка рассинхрон→review), `internal/store` (новый read-метод: живые целевые
папки по `(provider, provider_id)`, исключая текущую загрузку).
- **Инварианты безопасности данных:** не трогаем. `download` остаётся мостом,
выход распознавания недоверенный, безопасность — на санитизации и проверке
пути под библиотекой. Никаких новых прав на источник.
- **Схема БД:** без изменений (только новый SELECT-join).
- **Границы:** нет якоря → папка из распознавания (как сейчас); нет матча
(`provider=none`) → авто-раскладки нет, всё через review (инвариант); смена
провайдера между сезонами якорь не склеит — редкий случай, штатно review
(осознанная цена, черновик §6.1).
- **Совместимость:** поведение первой загрузки тайтла не меняется; правило
влияет только на последующие вклады при наличии живого якоря.