Завершённая загрузка ложно «воскресала» из deleted в orphaned, когда её целевой путь переиспользовала другая загрузка (повторная закачка того же фильма в другом качестве): сверка проверяла лишь существование пути, не проверяя, что файл по нему — наша раскладка. Вводим инвариант «один целевой путь — один владелец»: - при успешной раскладке на освободившийся чужой путь владение переходит к новой загрузке — прежние file_link на этот путь помечаются статусом superseded и перестают считаться целью при сверке; - deleted исключён из desyncStates — терминальное состояние больше не переоценивается (источник к нему не вернётся из-за идемпотентности, цель отбирается переходом владения); - Undo снимает только реально свои разложенные ссылки (superseded пропускает — файл по пути теперь чужой хардлинк); - ошибку перехода владения трактуем как некритичную (WARN-and-continue): файлы уже разложены, рассинхрон чужих задач исправит следующий тик. Без миграции схемы (status — TEXT). Дельта влита в основную спеку, обновлены workflow.md и jellyfin-layout.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
5.4 KiB
Why
Фоновая сверка может ложно «воскресить» завершённую загрузку, если её
целевой путь переиспользовала другая загрузка. Реальный инцидент: фильм
скачали в 4K (download A), затем удалили его из qBittorrent и Jellyfin —
сверка увела A в терминальный deleted (нет ни источника, ни цели). После
этого тот же фильм скачали в 1080p (download B); он распознался как тот же
фильм и сделал хардлинк по тому же dst_path. На следующем тике сверка
для A увидела, что файл по пути снова существует (хотя это файл B), и
вывела deleted → orphaned с ложным уведомлением «источник потерян».
Корень: targetPresent считает цель присутствующей по факту существования
пути, не проверяя, что лежащая там раскладка принадлежит этой
загрузке. Один dst_path может оказаться «своим» сразу для двух загрузок.
What Changes
- Вводим инвариант «один целевой путь — один владелец»: цель загрузки
считается присутствующей, только если разложенные по её путям ссылки всё
ещё принадлежат именно ей. Когда новая раскладка ложится на путь, ранее
занятый другой загрузкой (путь к тому моменту свободен — иначе была бы
коллизия → review), владение переходит к новой загрузке, а ссылки прежней
на этот путь помечаются вышедшими из обращения (новый статус
file_link—superseded). - Делаем
deletedдействительно терминальным для сверки: исключаем его из набора сверяемых состояний — завершённую начисто задачу больше не переоценивают (источник к ней не вернётся из-за идемпотентности терминальных задач, а цель отбирается переходом владения). - Приводим спеку в соответствие с графом состояний: убираем из требования
«самовосстановление» возврат из
deleted(он противоречил диаграммеdeleted --> [*]вworkflow.mdи под новым инвариантом нереализуем). - Коллизия остаётся как есть: если файл прежней загрузки всё ещё на месте, новая раскладка не перезаписывает его, а уходит в review.
Capabilities
New Capabilities
(нет)
Modified Capabilities
state-reconciliation: присутствие цели определяется по владению, а не по факту существования пути; вводится переход владения путём при повторной раскладке;deletedисключается из сверки и из самовосстановления.
Impact
- Затрагиваемый код:
internal/worker/reconcile.go— убратьStateDeletedизdesyncStates;targetPresent/isLaidOutуже считают целью толькоlinked/copied/exists, новый статусsupersededотсекается автоматически.internal/worker/review.go(linkPlan) — после фиксации своих ссылок пометить чужиеfile_linkна тех жеdst_pathкакsuperseded; покрывает и авто-раскладку, и ручнойApply(обе идут черезlinkPlan).internal/store/recognition.go— новый метод стора (supersede чужих ссылок по спискуdst_path).internal/layout/layout.go— константа статусаStatusSuperseded.
- Без миграции схемы:
file_link.status—TEXT, новое значение enum-а не меняет таблицу. ER-схемаdocs/specs/database.mdне меняется. - Документация:
docs/specs/workflow.md(раздел «Сверка с реальностью» —deletedтерминален) иdocs/specs/jellyfin-layout.md(переход владения путём при повторной раскладке) —file-layoutещё не перенесён в OpenSpec, источник истины по нему —docs/specs/. - Поведение пользователя: исчезают ложные уведомления
orphaned/target_missingпо уже удалённым загрузкам; повторная закачка того же фильма в другом качестве больше не «трогает» прежнюю задачу.