Завершённая загрузка ложно «воскресала» из 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>
3.3 KiB
3.3 KiB
1. Статус file_link и стор
- 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проходит.