Files
avandClaude Opus 4.8 6b7c090ce4 Владение целевым путём при повторной раскладке (state-reconciliation)
Завершённая загрузка ложно «воскресала» из deleted в orphaned, когда её
целевой путь переиспользовала другая загрузка (повторная закачка того же
фильма в другом качестве): сверка проверяла лишь существование пути, не
проверяя, что файл по нему — наша раскладка.

Вводим инвариант «один целевой путь — один владелец»:

- при успешной раскладке на освободившийся чужой путь владение переходит
  к новой загрузке — прежние file_link на этот путь помечаются статусом
  superseded и перестают считаться целью при сверке;
- deleted исключён из desyncStates — терминальное состояние больше не
  переоценивается (источник к нему не вернётся из-за идемпотентности,
  цель отбирается переходом владения);
- Undo снимает только реально свои разложенные ссылки (superseded
  пропускает — файл по пути теперь чужой хардлинк);
- ошибку перехода владения трактуем как некритичную (WARN-and-continue):
  файлы уже разложены, рассинхрон чужих задач исправит следующий тик.

Без миграции схемы (status — TEXT). Дельта влита в основную спеку,
обновлены workflow.md и jellyfin-layout.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 18:10:05 +03:00

5.4 KiB

Why

Фоновая сверка может ложно «воскресить» завершённую загрузку, если её целевой путь переиспользовала другая загрузка. Реальный инцидент: фильм скачали в 4K (download A), затем удалили его из qBittorrent и Jellyfin — сверка увела A в терминальный deleted (нет ни источника, ни цели). После этого тот же фильм скачали в 1080p (download B); он распознался как тот же фильм и сделал хардлинк по тому же dst_path. На следующем тике сверка для A увидела, что файл по пути снова существует (хотя это файл B), и вывела deleted → orphaned с ложным уведомлением «источник потерян».

Корень: targetPresent считает цель присутствующей по факту существования пути, не проверяя, что лежащая там раскладка принадлежит этой загрузке. Один dst_path может оказаться «своим» сразу для двух загрузок.

What Changes

  • Вводим инвариант «один целевой путь — один владелец»: цель загрузки считается присутствующей, только если разложенные по её путям ссылки всё ещё принадлежат именно ей. Когда новая раскладка ложится на путь, ранее занятый другой загрузкой (путь к тому моменту свободен — иначе была бы коллизия → review), владение переходит к новой загрузке, а ссылки прежней на этот путь помечаются вышедшими из обращения (новый статус file_linksuperseded).
  • Делаем deleted действительно терминальным для сверки: исключаем его из набора сверяемых состояний — завершённую начисто задачу больше не переоценивают (источник к ней не вернётся из-за идемпотентности терминальных задач, а цель отбирается переходом владения).
  • Приводим спеку в соответствие с графом состояний: убираем из требования «самовосстановление» возврат из deleted (он противоречил диаграмме deleted --> [*] в workflow.md и под новым инвариантом нереализуем).
  • Коллизия остаётся как есть: если файл прежней загрузки всё ещё на месте, новая раскладка не перезаписывает его, а уходит в review.

Capabilities

New Capabilities

(нет)

Modified Capabilities

  • state-reconciliation: присутствие цели определяется по владению, а не по факту существования пути; вводится переход владения путём при повторной раскладке; deleted исключается из сверки и из самовосстановления.

Impact

  • Затрагиваемый код:
    • internal/worker/reconcile.go — убрать StateDeleted из desyncStates; targetPresent/isLaidOut уже считают целью только linked/copied/exists, новый статус superseded отсекается автоматически.
    • internal/worker/review.go (linkPlan) — после фиксации своих ссылок пометить чужие file_link на тех же dst_path как superseded; покрывает и авто-раскладку, и ручной Apply (обе идут через linkPlan).
    • internal/store/recognition.go — новый метод стора (supersede чужих ссылок по списку dst_path).
    • internal/layout/layout.go — константа статуса StatusSuperseded.
  • Без миграции схемы: file_link.statusTEXT, новое значение enum-а не меняет таблицу. ER-схема docs/specs/database.md не меняется.
  • Документация: docs/specs/workflow.md (раздел «Сверка с реальностью» — deleted терминален) и docs/specs/jellyfin-layout.md (переход владения путём при повторной раскладке) — file-layout ещё не перенесён в OpenSpec, источник истины по нему — docs/specs/.
  • Поведение пользователя: исчезают ложные уведомления orphaned/ target_missing по уже удалённым загрузкам; повторная закачка того же фильма в другом качестве больше не «трогает» прежнюю задачу.