# Отдельную сущность «тайтл» не вводим - **Дата:** 2026-07-02 - **Источник:** черновик `docs/drafts/logical-title-model.md` (разбор от 2026-07-01, переработан 2026-07-02; удалён при переводе проекта на канон, полный текст — в истории git). Первый производный change — [openspec/changes/archive/2026-07-02-ulid-identity/design.md](../../openspec/changes/archive/2026-07-02-ulid-identity/design.md). ## Решение Логический тайтл (фильм или сериал, складывающийся из нескольких загрузок во времени) остаётся **вычисляемой группой**, а не хранимой сущностью: доменная идентичность — `download` (ULID + множество инфохэшей), связь с диском — `file_link` с владением целевым путём, а «второй сезон в ту же папку» решается **правилом сходимости папки** при построении плана раскладки. ## Почему Разбор шёл от операций, и у тайтла их не нашлось: > У `title` при разборе **не нашлось ни одной собственной операции**: сходимость > папки — правило при построении плана; merge докачивания — per-path логика; > удаление целиком — цикл по вычисляемой группе. Сущность без собственных > операций — это линза, а линзу достаточно вычислять, не хранить. Второй аргумент — у папки уже есть дом, и вычисляемый якорь **корректнее** хранимого: > «Папка — это title-уровневое состояние, ей нужен дом» разбивается о то, что > дом у папки уже есть — файловая система и `dst_path` живых `file_link`'ов. > Реестр дублировал бы то, что и так записано в БД в N экземплярах. Причём > вычисляемый якорь корректнее хранимого: если все файлы сериала снесли, живых > ссылок нет — и новая загрузка честно создаёт свежую папку; хранимый > `title.folder` указывал бы в пустоту. Третий — отказ **устраняет**, а не решает хвост развилок: жизненный цикл тайтла (рождение, смерть, пустой тайтл), слияние тайтлов, ad-hoc тайтл без провайдера, обратная миграция существующих строк, отдельный title-лог. ## Рассмотренные варианты - **L2 — `title` с ключом `(provider, provider_id)`.** Отвергнут: привязывает долгоживущую сущность к провайдеру, который может смениться. - **L2-min — `title` со своим ULID + `title_external_id`** (провайдерные id множеством-атрибутом, симметрично `download_infohash`). Схема красивая и решает смену провайдера, ad-hoc тайтлы и слияние. Отвергнут именно по аргументу выше: собственных операций нет, а сущность тянет жизненный цикл, миграцию и четыре развилки. - **L3 — title-центричная медиатека (модель sonarr).** Отвергнут осознанно: мы не ходим в индексеры, не мониторим тайтлы и не ведём профили качества — контент приносит пользователь. Это граница домена, [passport.md](../passport.md) → «Что целью не является». ## Последствия - `+` Идентичность осталась одноуровневой: `download` — мост между раздачей в qBittorrent и файлами на диске, и каждая сущность цепочки отвечает на свои операции. - `+` Главная боль («второй сезон должен лечь в ту же папку») закрыта дешёвым правилом при построении плана — реализовано change'ем `2026-07-10-series-folder-convergence`, требования влиты в [openspec/specs/file-layout](../../openspec/specs/file-layout/spec.md). - `+` Устранён, а не отложен, хвост развилок вокруг жизненного цикла тайтла. - `−` Группировка тайтла в UI и «удалить тайтл целиком» придётся каждый раз **вычислять** по `(provider, provider_id)` и общей папке; дешёвого хранимого ключа для этого нет. - `−` Рассинхрон «несколько живых папок с одним `(provider, provider_id)`» разрешается только уходом в review — in-app лечения нет, чинится руками на диске. - `−` Слияние загрузок при перезаливе «той же вещи» осталось открытым: когда несколько инфохэшей считать одной загрузкой, а когда разными, — вопрос переехал в задачу про merge-раскладку. ## Триггер пересмотра Записан отдельно, чтобы не гонять этот круг заново: > Сущность `title` возвращается в обсуждение, только когда появится **операция > или состояние, которому реально негде жить** в `download` + `file_link` — > например, «переименовать сериал целиком с переносом ссылок» как регулярное > действие или заметки уровня группы. До того — вычисляем.