Распознаём ручное удаление источника (раздача в 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>
13 KiB
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 система отправляет автору загрузки уведомление о потере источника