Files
jellybit/openspec/changes/archive/2026-06-29-target-path-ownership/tasks.md
T
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

3.3 KiB
Raw Blame History

  • 1.1 Добавить StatusSuperseded LinkStatus = "superseded" в internal/layout/layout.go (вокабуляр статусов file_link).
  • 1.2 Добавить метод стора SupersedeForeignLinks(ctx, downloadID int64, dstPaths []string) error в internal/store/recognition.go: UPDATE file_link SET status='superseded' WHERE dst_path IN (...) AND download_id != ? AND status IN ('linked','copied','exists'). Пустой dstPaths — no-op. Объявить метод в интерфейсе стора в internal/worker/worker.go.

2. Переход владения при раскладке

  • 2.1 В linkPlan (internal/worker/review.go) после успешного CreateFileLinks собрать dst_path фактически разложенных ссылок (статус из isLaidOut: linked/copied/exists) и вызвать SupersedeForeignLinks(ctx, d.ID, paths) до перехода в done.
  • 2.2 Убедиться, что покрыты обе воронки раскладки (авто-апплай и ручной Apply) — обе идут через linkPlan.

3. deleted вне сверки

  • 3.1 Убрать store.StateDeleted из desyncStates (internal/worker/reconcile.go). terminalStates/IsTerminal (internal/store/download.go) не трогаем — deleted там уже есть.

4. Аудит потребителей статуса

  • 4.1 Проверить места, читающие file_link.status (undo в internal/worker/review.go/internal/layout, листинги UI в internal/httpapi): superseded-ссылки не должны считаться активной целью и не должны попадать в undo как «снимаемые». Поправить при необходимости.

5. Тесты

  • 5.1 Тест сверки: задача в deleted не переоценивается, даже если по её бывшему пути появился файл (нет перехода deleted → orphaned).
  • 5.2 Тест раскладки: повторная раскладка по освободившемуся чужому пути помечает прежние ссылки superseded; targetPresent прежней загрузки → false.
  • 5.3 Тест: занятый реальным файлом путь даёт коллизию → review, владение не отбирается.
  • 5.4 Тест: загрузка не «суперсидит» сама себя (download_id != self).

6. Документация

  • 6.1 docs/specs/workflow.md («Сверка с реальностью») — deleted терминален, без самовосстановления; согласовать с графом deleted --> [*].
  • 6.2 docs/specs/jellyfin-layout.md — добавить переход владения целевым путём при повторной раскладке на освободившийся путь.

7. Проверки

  • 7.1 task test и task lint зелёные.
  • 7.2 openspec validate target-path-ownership --strict проходит.