Files
jellybit/openspec/specs/state-reconciliation/spec.md
T
avandClaude Opus 4.8 cc7e51b3a4 Обработка рассинхрона состояния с реальностью (state-reconciliation)
Распознаём ручное удаление источника (раздача в qBittorrent) и/или цели
(разложенные хардлинки) и отражаем его в состоянии задачи, без автодействий.

- Новая capability state-reconciliation (OpenSpec): фоновая сверка по матрице
  «источник × цель» → состояния target_missing/orphaned/deleted, переходы и
  самовосстановление (healing).
- worker: reconcileDesync в Poll (только разложенные/desync-задачи), дебаунс
  пропажи источника (порог [worker].source_missing_threshold) и синхронный
  preflight перед действиями (relink/recognize/apply/undo) — не доверяем
  state в БД.
- layout.Undo: отказ снять последнюю копию (nlink<=1 или нет источника),
  отказ всего батча без частичного отката (ErrLastCopy).
- store: единый список terminalStates для IsTerminal и FindActiveByInfohash
  (иначе семантика «активности» разъезжается), столбец source_miss_count,
  миграция 0003.
- httpapi/web и Telegram: показ новых состояний и уведомления о рассинхроне.
- Доки: workflow.md, jellyfin-layout.md, database.md (+0003), config.

Change заархивирован в openspec/changes/archive, дельта влита в openspec/specs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 11:07:09 +03:00

202 lines
13 KiB
Markdown
Raw 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.
# 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** система отправляет автору загрузки уведомление о потере источника