## ADDED Requirements

### Requirement: Периодическая сверка состояния с реальностью

`worker` SHALL периодически (на тике поллинга) сверять задачи, для которых
ожидаются разложенные файлы, с фактом на файловой системе и в qBittorrent, и
выводить состояние задачи из двух независимых признаков: присутствия
**источника** (раздача с `download.infohash` в выдаче qBittorrent) и
присутствия **цели** (все `file_link` со `status = linked` существуют на ФС).

Сверке SHALL подвергаться только состояния `done`, `target_missing`,
`orphaned`, `deleted`. Активные (`downloading`/`recognizing`/`review`/
`deferred`/`linking`) и пользовательски-терминальные (`reverted`/`cancelled`/
`failed`/`stuck`) состояния сверка трогать SHALL NOT.

Состояние SHALL переписываться только при его изменении (без записи и логов,
когда выведенное состояние совпадает с текущим).

#### Scenario: Источник и цель на месте — состояние не меняется

- **WHEN** для задачи в `done` раздача присутствует в qBittorrent и все её
  разложенные хардлинки существуют
- **THEN** задача остаётся в `done`
- **AND** запись состояния и лог перехода не выполняются

#### Scenario: Частичная пропажа цели считается отсутствием

- **WHEN** часть разложенных хардлинков задачи удалена, а источник на месте
- **THEN** цель считается отсутствующей и задача переходит в `target_missing`

### Requirement: Принудительная проверка источника/цели перед действием

Команда workflow, требующая наличия источника или цели, SHALL синхронно
проверять их присутствие непосредственно перед выполнением действия и SHALL
NOT полагаться только на фоновую сверку `worker` (она отстаёт на интервал
поллинга и дебаунс). Проверка перед действием выполняется как **единичная
немедленная проба без дебаунса**: дебаунс применяется только к фоновому
авто-маркированию.

Под это требование подпадают как минимум: повторная привязка/распознавание
(`relink`, «Распознать заново», «Уточнить») и раскладка (`Apply`) — требуют
**источника**; `Undo` — требует **источника** (чтобы не снять последнюю
копию).

Если предусловие не выполнено, система SHALL NOT выполнять действие, SHALL
привести состояние задачи в соответствие с реальностью (вывести состояние из
матрицы «источник × цель», как при сверке) и SHALL сообщить причину
пользователю.

#### Scenario: Relink проверяет источник перед запуском

- **WHEN** пользователь даёт команду, требующую источника (например
  «Привязать заново»)
- **THEN** система синхронно проверяет наличие раздачи в qBittorrent перед
  запуском распознавания
- **AND** если источника нет — распознавание не запускается, задача
  приводится к `orphaned` либо `deleted` (по наличию цели), причина
  сообщается

#### Scenario: Проверка не ждёт фоновую сверку

- **WHEN** источник уже удалён, а фоновая сверка ещё не отметила это (в БД
  состояние, например, `done`)
- **THEN** команда, требующая источника, немедленно обнаруживает его
  отсутствие собственной проверкой и отказывает, не дожидаясь `worker`

### Requirement: Состояние target_missing и доступность повторной привязки

Когда у задачи источник присутствует, а цель отсутствует, система SHALL
переводить её в состояние `target_missing` и SHALL NOT предпринимать
автоматических действий (не запускать повторное распознавание/раскладку
самостоятельно).

Из `target_missing` система SHALL предоставлять пользовательскую команду
повторной привязки (relink) с переходом `target_missing → recognizing`, как
из `reverted`/`cancelled`; повторная привязка идёт через `review` с ручным
подтверждением.

#### Scenario: Цель удалена, источник на месте

- **WHEN** сверка обнаруживает, что разложенных хардлинков задачи `done`
  больше нет, но раздача в qBittorrent присутствует
- **THEN** задача переходит в `target_missing`
- **AND** система не запускает распознавание или раскладку автоматически

#### Scenario: Пользователь инициирует повторную привязку

- **WHEN** для задачи в `target_missing` пользователь даёт команду «Привязать
  заново»
- **THEN** задача переходит в `recognizing` и далее проходит через `review`
  с ручным подтверждением

### Requirement: Состояние orphaned при пропаже источника

Система SHALL переводить задачу в состояние `orphaned`, когда источник
отсутствует (с учётом дебаунса), а цель присутствует, отражая, что
библиотечный хардлинк остался единственной копией данных.

#### Scenario: Источник удалён, цель на месте

- **WHEN** сверка устойчиво (после дебаунса) не находит раздачу задачи в
  qBittorrent, а её разложенные хардлинки существуют
- **THEN** задача переходит в `orphaned`

### Requirement: Состояние deleted при пропаже источника и цели

Когда отсутствуют и источник (с учётом дебаунса), и цель, система SHALL
переводить задачу в состояние `deleted`. В `deleted` действий над задачей
больше нет.

#### Scenario: Источник и цель удалены

- **WHEN** сверка устойчиво не находит раздачу в qBittorrent и разложенных
  хардлинков задачи на ФС больше нет
- **THEN** задача переходит в `deleted`

### Requirement: Дебаунс пропажи источника

Система SHALL дебаунсить только **отсутствие источника**, чтобы временная
недоступность qBittorrent (рестарт демона, сбой API) не вызывала ложных
пометок: источник считается удалённым лишь после `N` подряд тиков сверки без
него, где `N = [worker].source_missing_threshold`. Любое обнаружение раздачи
SHALL сбрасывать счётчик пропусков.

Отсутствие цели дебаунсу подвергаться SHALL NOT (локальная проверка ФС
надёжна).

#### Scenario: Кратковременная пропажа источника не помечается

- **WHEN** раздача отсутствует в qBittorrent меньше `source_missing_threshold`
  тиков подряд
- **THEN** источник трактуется как присутствующий и состояние задачи не
  меняется

#### Scenario: Возврат источника сбрасывает счётчик

- **WHEN** раздача снова обнаружена в qBittorrent
- **THEN** счётчик пропусков источника сбрасывается в ноль

### Requirement: Самовосстановление состояния при возврате реальности

Система SHALL возвращать задачу в согласованное состояние, когда реальность
восстановилась (состояние выводится из текущей матрицы «источник × цель»):
при возврате источника и/или цели задача SHALL переходить из
`orphaned`/`target_missing`/`deleted` обратно (в т.ч. в `done`, когда
присутствуют оба).

#### Scenario: Источник вернулся

- **WHEN** для задачи в `orphaned` раздача снова появилась в qBittorrent, а
  цель по-прежнему на месте
- **THEN** задача возвращается в `done`

### Requirement: Безопасный Undo не снимает последнюю копию

`Undo` (снятие созданных хардлинков) SHALL отказываться удалять целевую
ссылку, если она является последней копией данных: целевой файл существует и
его счётчик ссылок `nlink <= 1`, либо исходный файл (`src_path`) не
существует. В этом случае система SHALL NOT выполнять `unlink` такого файла и
SHALL явно сообщать причину отказа; задача в `reverted` при отказе переходить
SHALL NOT.

Отсутствующую целевую ссылку (файла уже нет) `Undo` SHALL пропускать как
успешно снятую (идемпотентность). Команда `Undo` для задачи в `orphaned`
SHALL отклоняться сразу с пояснением, что источник удалён.

#### Scenario: Отказ снять единственную копию

- **WHEN** при `Undo` целевой хардлинк существует, но его `nlink <= 1` (или
  исходный файл отсутствует)
- **THEN** система не удаляет файл и сообщает, что это последняя копия
- **AND** задача остаётся в текущем состоянии (не `reverted`)

#### Scenario: Undo снимает лишний хардлинк при живом источнике

- **WHEN** при `Undo` целевой хардлинк существует, исходный файл на месте и
  `nlink > 1`
- **THEN** система снимает целевой хардлинк, оставляя исходный файл нетронутым

### Requirement: Уведомление о рассинхроне

При переходе задачи в `orphaned` или `target_missing` система SHALL
уведомлять автора загрузки через настроенный механизм уведомлений
(`notifier`), чтобы рассинхрон не оставался незамеченным.

#### Scenario: Уведомление при потере источника

- **WHEN** задача переходит в `orphaned`
- **THEN** система отправляет автору загрузки уведомление о потере источника
