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

117 lines
8.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.
## ADDED Requirements
### Requirement: Владение целевым путём — один путь, один владелец
Целевой путь раскладки (`file_link.dst_path`) SHALL принадлежать не более
чем одной загрузке одновременно. При успешной раскладке загрузки на путь,
который ранее заняла **другая** загрузка, владение SHALL переходить к новой
загрузке: ссылки прежней загрузки на тот же `dst_path` система SHALL
помечать вышедшими из обращения (статус, не относящийся к разложенной цели),
после чего они перестают считаться целью прежней загрузки при сверке.
Присутствие цели при сверке SHALL определяться по **владению**, а не по
факту существования пути: цель загрузки считается присутствующей, только
если существующие на ФС файлы по её путям — это ссылки, всё ещё
принадлежащие этой загрузке (не вышедшие из обращения). Файл, лежащий по
тому же пути, но созданный другой загрузкой, целью первой загрузки
считаться SHALL NOT.
Переход владения возможен лишь когда путь к моменту раскладки **свободен**
(прежний файл уже удалён): занятый реальным файлом путь по-прежнему даёт
коллизию и уходит в review (новая раскладка не перезаписывает чужой файл).
#### Scenario: Повторная закачка забирает освободившийся путь
- **GIVEN** загрузка A разложена по пути P, но её файл по P удалён вручную
- **WHEN** загрузка B успешно раскладывается по тому же пути P
- **THEN** ссылки A на P помечаются вышедшими из обращения
- **AND** при сверке цель A по пути P считается отсутствующей
#### Scenario: Чужой файл по пути не считается своей целью
- **GIVEN** по пути P лежит файл, созданный загрузкой B
- **WHEN** сверка проверяет присутствие цели загрузки A, чьи ссылки на P
вышли из обращения
- **THEN** цель A считается отсутствующей, несмотря на существование файла
по P
#### Scenario: Занятый путь даёт коллизию, а не переход владения
- **GIVEN** файл загрузки A по пути P всё ещё существует
- **WHEN** загрузка B пытается разложиться по тому же пути P
- **THEN** возникает коллизия и B уходит в review
- **AND** владение путём P за A не отбирается
## MODIFIED Requirements
### Requirement: Периодическая сверка состояния с реальностью
`worker` SHALL периодически (на тике поллинга) сверять задачи, для которых
ожидаются разложенные файлы, с фактом на файловой системе и в qBittorrent, и
выводить состояние задачи из двух независимых признаков: присутствия
**источника** (раздача с `download.infohash` в выдаче qBittorrent) и
присутствия **цели** (см. требование о владении целевым путём: существуют все
ссылки последнего батча со статусом раскладки, всё ещё принадлежащие этой
загрузке).
Сверке SHALL подвергаться только состояния `done`, `target_missing`,
`orphaned`. Состояние `deleted` сверка трогать SHALL NOT — оно терминально.
Активные (`downloading`/`recognizing`/`review`/`deferred`/`linking`) и
пользовательски-терминальные (`reverted`/`cancelled`/`failed`/`stuck`)
состояния сверка трогать SHALL NOT.
Состояние SHALL переписываться только при его изменении (без записи и логов,
когда выведенное состояние совпадает с текущим).
#### Scenario: Источник и цель на месте — состояние не меняется
- **WHEN** для задачи в `done` раздача присутствует в qBittorrent и все её
разложенные хардлинки существуют
- **THEN** задача остаётся в `done`
- **AND** запись состояния и лог перехода не выполняются
#### Scenario: Частичная пропажа цели считается отсутствием
- **WHEN** часть разложенных хардлинков задачи удалена, а источник на месте
- **THEN** цель считается отсутствующей и задача переходит в `target_missing`
#### Scenario: Задача в deleted сверкой не переоценивается
- **WHEN** задача находится в `deleted`
- **THEN** сверка её не рассматривает и состояние не меняет, даже если по её
бывшему пути появился файл другой загрузки
### Requirement: Состояние deleted при пропаже источника и цели
Когда отсутствуют и источник (с учётом дебаунса), и цель, система SHALL
переводить задачу в состояние `deleted`. `deleted` терминально: действий над
задачей больше нет, и сверка её больше не переоценивает (источник к
терминальной задаче не возвращается из-за идемпотентности, а цель отбирается
переходом владения путём к другой загрузке).
#### Scenario: Источник и цель удалены
- **WHEN** сверка устойчиво не находит раздачу в qBittorrent и разложенных
хардлинков задачи на ФС больше нет
- **THEN** задача переходит в `deleted`
#### Scenario: deleted не воскресает при переиспользовании пути
- **GIVEN** задача A в `deleted`
- **WHEN** другая задача раскладывается по бывшему пути A
- **THEN** задача A остаётся в `deleted` (не переходит в `orphaned`)
### Requirement: Самовосстановление состояния при возврате реальности
Система SHALL возвращать задачу в согласованное состояние, когда реальность
восстановилась (состояние выводится из текущей матрицы «источник × цель»):
при возврате источника и/или цели задача SHALL переходить из
`orphaned`/`target_missing` обратно (в т.ч. в `done`, когда присутствуют
оба). Из терминального `deleted` самовосстановления SHALL NOT быть.
#### Scenario: Источник вернулся
- **WHEN** для задачи в `orphaned` раздача снова появилась в qBittorrent, а
цель по-прежнему на месте
- **THEN** задача возвращается в `done`