# state-reconciliation Specification ## Purpose Сверка записанного состояния загрузки с фактом на файловой системе и в qBittorrent. Capability описывает периодическую и принудительную проверку присутствия **источника** (раздача в qBittorrent) и **цели** (разложенные хардлинки), вывод состояний рассинхрона (`target_missing`/`orphaned`/ `deleted`) из матрицы «источник × цель», их переходы и самовосстановление, дебаунс пропажи источника, инвариант безопасного `Undo` (не снимать последнюю копию) и уведомления о рассинхроне. ## 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** система отправляет автору загрузки уведомление о потере источника