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

6.6 KiB
Raw Blame History

1. Хранилище и состояния

  • 1.1 Добавить значения состояний target_missing, orphaned, deleted в internal/store (константы State*) и в перечень допустимых состояний.
  • 1.2 Миграция goose 0003_*: ALTER TABLE download ADD COLUMN source_miss_count INTEGER NOT NULL DEFAULT 0; обновить модель Download.
  • 1.3 Запросы в store: выборка desync-кандидатов (done/target_missing/orphaned/deleted), чтение/сброс/инкремент source_miss_count, чтение file_link (status = linked) по задаче.

2. Конфигурация

  • 2.1 Добавить [worker].source_missing_threshold (int, дефолт 3) в internal/config с валидацией (>= 1); пробросить в worker.Config.
  • 2.2 Отразить ключ в примере конфига и docs/conventions/config.md/ README, если там перечислены ключи [worker].

3. Сверка с реальностью (worker)

  • 3.1 Выделить общий помощник probe(download) → (sourcePresent, targetPresent) (источник: infohash в qBit; цель: Lstat всех file_link.dst_path со status = linked, частичная пропажа = цель отсутствует) и deriveState(src, tgt) → State — единая точка правды для фона и preflight.
  • 3.2 В Poll для desync-кандидатов вызвать probe и реализовать дебаунс источника: инкремент source_miss_count при отсутствии, сброс при обнаружении; «источник удалён» только при source_miss_count >= threshold.
  • 3.3 Применить deriveState и переходить только при изменении: done/target_missing/orphaned/deleted + healing обратно в done. Переиспользовать transition.
  • 3.4 Не трогать фоновой сверкой активные и пользовательски-терминальные состояния (reverted/cancelled/failed/stuck и активные).

3a. Синхронный preflight перед действием

  • 3a.1 Перед командами, требующими источника/цели (relink, «Распознать заново», «Уточнить», Apply, Undo), вызывать probe без дебаунса (единичная немедленная проба), не доверяя state в БД.
  • 3a.2 При неуспехе предусловия: действие не выполнять, прогнать deriveState (привести state к реальности, напр. done → orphaned), вернуть пользователю причину; при недоступности qBittorrent — отказ «источник недоступен».

4. Безопасный Undo (layout + worker)

  • 4.1 internal/layout Undo: для каждой ссылки Lstat(dst) (нет — пропустить идемпотентно), иначе проверить nlink <= 1 и наличие src_path; при «последней копии» — отказ с типизированной ошибкой, без unlink.
  • 4.2 worker.Undo: отклонять команду для задачи в orphaned сразу с понятным сообщением; при отказе layout.Undo — не переводить в reverted, пробросить причину пользователю.
  • 4.3 Разрешить переход target_missing → recognizing в команде «Привязать заново» (наряду с reverted/cancelled).

5. Уведомления

  • 5.1 Добавить события EventOrphaned/EventTargetMissing и слать уведомление автору в transition при входе в эти состояния (как для review/done), неблокирующе и вне w.mu.

6. Транспорты (httpapi + web-UI)

  • 6.1 Отобразить новые состояния в списке/карточке загрузки (метки, пояснения «разложено, но файлов нет» / «источник удалён — последняя копия»).
  • 6.2 Скрыть/заблокировать Undo для orphaned; показать команду «Привязать заново» для target_missing.

7. Тесты

  • 7.1 Таблица переходов сверки: все четыре ячейки матрицы + healing, частичная пропажа цели → target_missing.
  • 7.2 Дебаунс: пропажа < порога не помечает; >= порога помечает; возврат сбрасывает счётчик.
  • 7.3 layout.Undo: отказ при nlink <= 1/отсутствии src_path; снятие лишнего хардлинка при живом источнике; пропуск отсутствующей цели.
  • 7.4 worker.Undo отклоняется для orphaned; relink из target_missing ведёт в recognizing.
  • 7.5 Preflight: команда с устаревшим state = done, но удалённым источником немедленно отказывает и приводит состояние к orphaned/ deleted (не дожидаясь фоновой сверки).

8. Документация и спеки

  • 8.1 Обновить docs/specs/workflow.md: граф состояний (+target_missing/ orphaned/deleted, переходы, healing) и описания.
  • 8.2 Обновить docs/specs/jellyfin-layout.md: инвариант безопасного Undo (не снимать последнюю копию).
  • 8.2a Обновить ER-схему docs/specs/database.md: столбец download.source_miss_count + отметка миграции 0003 (конвенция: схема едет вместе с миграцией).
  • 8.3 Снять пункт «Рассинхрон состояния с реальностью» (часть про detection/marking и undo-guard) из docs/todo.md или сузить до path 2.
  • 8.4 openspec validate --strict; ревью кода; затем opsx:archive (влить дельту state-reconciliation в openspec/specs/).