Files
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

96 lines
6.6 KiB
Markdown
Raw Permalink 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.
## 1. Хранилище и состояния
- [x] 1.1 Добавить значения состояний `target_missing`, `orphaned`, `deleted`
в `internal/store` (константы `State*`) и в перечень допустимых состояний.
- [x] 1.2 Миграция goose `0003_*`: `ALTER TABLE download ADD COLUMN
source_miss_count INTEGER NOT NULL DEFAULT 0`; обновить модель `Download`.
- [x] 1.3 Запросы в `store`: выборка desync-кандидатов
(`done`/`target_missing`/`orphaned`/`deleted`), чтение/сброс/инкремент
`source_miss_count`, чтение `file_link` (`status = linked`) по задаче.
## 2. Конфигурация
- [x] 2.1 Добавить `[worker].source_missing_threshold` (int, дефолт `3`) в
`internal/config` с валидацией (`>= 1`); пробросить в `worker.Config`.
- [x] 2.2 Отразить ключ в примере конфига и `docs/conventions/config.md`/
README, если там перечислены ключи `[worker]`.
## 3. Сверка с реальностью (worker)
- [x] 3.1 Выделить общий помощник `probe(download) → (sourcePresent,
targetPresent)` (источник: infohash в qBit; цель: `Lstat` всех
`file_link.dst_path` со `status = linked`, частичная пропажа = цель
отсутствует) и `deriveState(src, tgt) → State` — единая точка правды для
фона и preflight.
- [x] 3.2 В `Poll` для desync-кандидатов вызвать `probe` и реализовать
дебаунс источника: инкремент `source_miss_count` при отсутствии, сброс при
обнаружении; «источник удалён» только при `source_miss_count >= threshold`.
- [x] 3.3 Применить `deriveState` и переходить только при изменении:
`done`/`target_missing`/`orphaned`/`deleted` + healing обратно в `done`.
Переиспользовать `transition`.
- [x] 3.4 Не трогать фоновой сверкой активные и пользовательски-терминальные
состояния (`reverted`/`cancelled`/`failed`/`stuck` и активные).
## 3a. Синхронный preflight перед действием
- [x] 3a.1 Перед командами, требующими источника/цели (relink, «Распознать
заново», «Уточнить», `Apply`, `Undo`), вызывать `probe` **без дебаунса**
(единичная немедленная проба), не доверяя `state` в БД.
- [x] 3a.2 При неуспехе предусловия: действие не выполнять, прогнать
`deriveState` (привести `state` к реальности, напр. `done → orphaned`),
вернуть пользователю причину; при недоступности qBittorrent — отказ
«источник недоступен».
## 4. Безопасный Undo (layout + worker)
- [x] 4.1 `internal/layout` `Undo`: для каждой ссылки `Lstat(dst)` (нет —
пропустить идемпотентно), иначе проверить `nlink <= 1` и наличие
`src_path`; при «последней копии» — отказ с типизированной ошибкой, без
`unlink`.
- [x] 4.2 `worker.Undo`: отклонять команду для задачи в `orphaned` сразу с
понятным сообщением; при отказе `layout.Undo` — не переводить в `reverted`,
пробросить причину пользователю.
- [x] 4.3 Разрешить переход `target_missing → recognizing` в команде
«Привязать заново» (наряду с `reverted`/`cancelled`).
## 5. Уведомления
- [x] 5.1 Добавить события `EventOrphaned`/`EventTargetMissing` и слать
уведомление автору в `transition` при входе в эти состояния (как для
`review`/`done`), неблокирующе и вне `w.mu`.
## 6. Транспорты (httpapi + web-UI)
- [x] 6.1 Отобразить новые состояния в списке/карточке загрузки (метки,
пояснения «разложено, но файлов нет» / «источник удалён — последняя копия»).
- [x] 6.2 Скрыть/заблокировать `Undo` для `orphaned`; показать команду
«Привязать заново» для `target_missing`.
## 7. Тесты
- [x] 7.1 Таблица переходов сверки: все четыре ячейки матрицы + healing,
частичная пропажа цели → `target_missing`.
- [x] 7.2 Дебаунс: пропажа < порога не помечает; >= порога помечает; возврат
сбрасывает счётчик.
- [x] 7.3 `layout.Undo`: отказ при `nlink <= 1`/отсутствии `src_path`;
снятие лишнего хардлинка при живом источнике; пропуск отсутствующей цели.
- [x] 7.4 `worker.Undo` отклоняется для `orphaned`; relink из
`target_missing` ведёт в `recognizing`.
- [x] 7.5 Preflight: команда с устаревшим `state = done`, но удалённым
источником немедленно отказывает и приводит состояние к `orphaned`/
`deleted` (не дожидаясь фоновой сверки).
## 8. Документация и спеки
- [x] 8.1 Обновить `docs/specs/workflow.md`: граф состояний (+`target_missing`/
`orphaned`/`deleted`, переходы, healing) и описания.
- [x] 8.2 Обновить `docs/specs/jellyfin-layout.md`: инвариант безопасного
`Undo` (не снимать последнюю копию).
- [x] 8.2a Обновить ER-схему `docs/specs/database.md`: столбец
`download.source_miss_count` + отметка миграции `0003` (конвенция:
схема едет вместе с миграцией).
- [x] 8.3 Снять пункт «Рассинхрон состояния с реальностью» (часть про
detection/marking и undo-guard) из `docs/todo.md` или сузить до path 2.
- [x] 8.4 `openspec validate --strict`; ревью кода; затем `opsx:archive`
(влить дельту `state-reconciliation` в `openspec/specs/`).