Владение целевым путём при повторной раскладке (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:
@@ -0,0 +1,54 @@
|
||||
## 1. Статус file_link и стор
|
||||
|
||||
- [x] 1.1 Добавить `StatusSuperseded LinkStatus = "superseded"` в
|
||||
`internal/layout/layout.go` (вокабуляр статусов `file_link`).
|
||||
- [x] 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. Переход владения при раскладке
|
||||
|
||||
- [x] 2.1 В `linkPlan` (`internal/worker/review.go`) после успешного
|
||||
`CreateFileLinks` собрать `dst_path` фактически разложенных ссылок
|
||||
(статус из `isLaidOut`: `linked`/`copied`/`exists`) и вызвать
|
||||
`SupersedeForeignLinks(ctx, d.ID, paths)` до перехода в `done`.
|
||||
- [x] 2.2 Убедиться, что покрыты обе воронки раскладки (авто-апплай и ручной
|
||||
`Apply`) — обе идут через `linkPlan`.
|
||||
|
||||
## 3. deleted вне сверки
|
||||
|
||||
- [x] 3.1 Убрать `store.StateDeleted` из `desyncStates`
|
||||
(`internal/worker/reconcile.go`). `terminalStates`/`IsTerminal`
|
||||
(`internal/store/download.go`) не трогаем — `deleted` там уже есть.
|
||||
|
||||
## 4. Аудит потребителей статуса
|
||||
|
||||
- [x] 4.1 Проверить места, читающие `file_link.status` (undo в
|
||||
`internal/worker/review.go`/`internal/layout`, листинги UI в
|
||||
`internal/httpapi`): `superseded`-ссылки не должны считаться активной
|
||||
целью и не должны попадать в undo как «снимаемые». Поправить при
|
||||
необходимости.
|
||||
|
||||
## 5. Тесты
|
||||
|
||||
- [x] 5.1 Тест сверки: задача в `deleted` не переоценивается, даже если по
|
||||
её бывшему пути появился файл (нет перехода `deleted → orphaned`).
|
||||
- [x] 5.2 Тест раскладки: повторная раскладка по освободившемуся чужому пути
|
||||
помечает прежние ссылки `superseded`; `targetPresent` прежней загрузки →
|
||||
`false`.
|
||||
- [x] 5.3 Тест: занятый реальным файлом путь даёт коллизию → review,
|
||||
владение не отбирается.
|
||||
- [x] 5.4 Тест: загрузка не «суперсидит» сама себя (`download_id != self`).
|
||||
|
||||
## 6. Документация
|
||||
|
||||
- [x] 6.1 `docs/specs/workflow.md` («Сверка с реальностью») — `deleted`
|
||||
терминален, без самовосстановления; согласовать с графом `deleted --> [*]`.
|
||||
- [x] 6.2 `docs/specs/jellyfin-layout.md` — добавить переход владения целевым
|
||||
путём при повторной раскладке на освободившийся путь.
|
||||
|
||||
## 7. Проверки
|
||||
|
||||
- [x] 7.1 `task test` и `task lint` зелёные.
|
||||
- [x] 7.2 `openspec validate target-path-ownership --strict` проходит.
|
||||
Reference in New Issue
Block a user