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

55 lines
3.3 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 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` проходит.