Files
jellybit/openspec/changes/archive/2026-06-29-reconcile-removed-source-target/proposal.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

88 lines
7.0 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.
## Why
Пришло время удалять просмотренные фильмы/сериалы, чтобы освобождать место
под новые. Удаляют их **вручную** — из qBittorrent (источник) или из
Jellyfin (целевые хардлинки). Сейчас jellybit этого не замечает: `worker`
поллит только задачи в `downloading`, терминальные задачи (`done`) с
реальностью не сверяет. Состояние в БД молча расходится с диском:
- **Источник пропал, цель осталась.** qBittorrent стирает скачанные файлы
при удалении раздачи. Хардлинк в библиотеке становится **последней**
ссылкой на inode, а обычный `Undo` (`unlink` цели) сотрёт единственную
копию — прямая потеря данных. Инвариант «источник неприкосновенен»
молчаливо перестаёт держаться: источника уже нет.
- **Цель пропала, источник остался.** Файлы убрали из библиотеки, а
jellybit по-прежнему числит загрузку `done` — состояние врёт.
Цель этого change — **path 1**: научить jellybit корректно **распознавать**
ручное удаление и **отражать** его в состоянии, **не предпринимая
автоматических действий**. Удаление средствами самого jellybit («единое
окно», path 2) — отдельная будущая работа.
## What Changes
- **Фоновая сверка с реальностью.** `worker` расширяет периодический поллинг:
помимо `downloading` сверяет терминальные/desync-задачи с фактом на ФС —
присутствие раздачи в qBittorrent (источник) и существование разложенных
хардлинков `file_link.dst_path` (цель).
- **Новые состояния FSM** для рассинхрона (двумерная матрица «источник × цель»):
- `target_missing` — источник на месте, цель удалена: доступен
пользовательский флоу повторной привязки (relink); авто-действий нет.
- `orphaned` — источник пропал, цель на месте: «осиротевшая» раздача,
библиотечный хардлинк — единственная копия.
- `deleted` — пропали и источник, и цель: терминально, действий больше нет.
- Реальность «лечится» сама: если источник/цель снова появились, сверка
возвращает задачу в согласованное состояние.
- **Дебаунс пропажи источника.** Раздача считается удалённой только после
`N` подряд тиков без неё (qBittorrent мог рестартовать / API мигнул);
любое появление сбрасывает счётчик. Порог — в конфиге.
- **Защита `Undo` от потери данных.** `Undo`/`layout.Undo` отказывается
снимать хардлинк, если он **последняя копия** (`nlink == 1`) или исходный
путь не существует — откат снимает лишний хардлинк, а не единственный
файл; причина отказа сообщается явно.
- **Уведомление** автору загрузки при переходе в `orphaned`/`target_missing`
(через существующий `notifier`), чтобы рассинхрон не оставался незаметным.
Не входит в объём (Non-goals):
- Удаление раздач/файлов средствами самого jellybit (path 2, «единое окно»).
- Автоматический повторный прогон распознавания/раскладки при рассинхроне —
только пометка состояния; relink инициирует человек.
- Сверка содержимого источника (manual delete файлов **внутри** живой
раздачи → это `error`/`missingFiles` qBittorrent, отдельная тема).
- Ретеншн/авточистка терминальных задач (отдельная задача в TODO).
## Capabilities
### New Capabilities
- `state-reconciliation`: периодическая сверка записанного состояния
загрузки с фактом на ФС (раздача в qBittorrent, разложенные хардлинки),
состояния рассинхрона (`target_missing`/`orphaned`/`deleted`), их переходы
и дебаунс; а также инвариант безопасного `Undo` (не снимать последнюю
копию).
### Modified Capabilities
<!-- Граф состояний и Undo живут в docs/specs/workflow.md и
docs/specs/jellyfin-layout.md и ещё не перенесены в OpenSpec; меняем их
напрямую как живые спеки (см. Impact). Capability-дельт для них нет. -->
## Impact
- **Спеки:** новая `openspec/specs/state-reconciliation/`; правки живых
`docs/specs/workflow.md` (граф состояний: +`target_missing`/`orphaned`/
`deleted`, переходы) и `docs/specs/jellyfin-layout.md` (инвариант
безопасного `Undo`).
- **Код:** `internal/worker` (расширение `Poll`/`reconcile` на терминальные
задачи, дебаунс, новые переходы, уведомления), `internal/store` (новые
значения `state`, столбец счётчика промахов, миграция, запросы выборки
desync-задач), `internal/layout` (`Undo` с проверкой `nlink`/наличия
источника), `internal/qbt` (присутствие infohash — уже листаем все
торренты), `internal/httpapi` + web-UI (отображение новых состояний,
блокировка `Undo` для `orphaned`), `internal/config` (`[worker]` порог
дебаунса).
- **Конфиг:** новый ключ в `[worker]` (порог пропусков источника).
- **Совместимость:** новые значения `state` — аддитивно; миграция goose для
столбца счётчика.