Владение целевым путём при повторной раскладке (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>
This commit is contained in:
av
2026-06-29 18:10:05 +03:00
co-authored by Claude Opus 4.8
parent 783664622c
commit 6b7c090ce4
17 changed files with 658 additions and 24 deletions
+5 -3
View File
@@ -14,14 +14,16 @@ import (
)
// desyncStates — состояния, которые ведёт сверка с реальностью: уже
// разложенные (done) и сами состояния рассинхрона. Активные и
// разложенные (done) и восстановимые состояния рассинхрона. deleted сюда не
// входит — оно терминально и сверкой не переоценивается (источник к
// терминальной задаче не вернётся из-за идемпотентности, а цель отбирается
// переходом владения путём; см. state-reconciliation). Активные и
// пользовательски-терминальные (reverted/cancelled/failed/stuck) сверка не
// трогает (см. state-reconciliation).
// трогает.
var desyncStates = []store.State{
store.StateDone,
store.StateTargetMissing,
store.StateOrphaned,
store.StateDeleted,
}
// deriveState выводит состояние задачи из двумерной матрицы «источник × цель»