## Why Раскладка хардлинками устроена как «claim-then-side-effect»: сначала задача переводится в `linking` (claim владения шагом), затем создаются хардлинки и пишется их учёт (`file_link`). Ревью жизненного цикла (Fable, 2026-07-08) нашло две связанные дыры устойчивости этого пути. - **MINOR-7:** `worker.transition` при ошибке записи состояния логировал её, но НЕ возвращал вызывающему. На путях `Apply` и авто-раскладки в `finishRecognition` выполнение продолжалось к побочным эффектам: хардлинки создавались, пока claim перехода в `linking` не закоммичен. Финальный переход `linking → done` оценивался графом как `review → done` (нелегальное ребро) и отклонялся — задача застревала в `review` со stale-планом, скан/уведомление не срабатывали. - **MAJOR-4:** задача может осиротеть в `linking`: (A) без краха — хардлинки созданы, но `CreateFileLinks` упал транзиентно (SQLite busy) → голый `return` оставлял задачу в `linking`, а файлы на диске — без строк `file_link`; (B) краш процесса между переходом в `linking` и финальным переходом → на рестарте `linking` не листит НИКТО (поллинг листит `downloading`, распознавание — `completed`/`recognizing`, сверка — `done`/`target_missing`/`orphaned`, восстановление — `failed`/`stuck`). Задача сидит в `linking` вечно; выход — только ручной Cancel/Defer (недискаверабельно). Нарушен инвариант «у каждого нетерминального состояния есть владелец» (`recognizing` уже лечится рестартом через `recognizePending`, `linking` — нет). ## What Changes - `transition` разделяется на fire-and-forget обёртку (прежнее имя, прежнее поведение для reconcile/таймаутов/финальных переходов) и `transitionErr`, которая ВОЗВРАЩАЕТ ошибку записи. На claim-then-side-effect путях (`Apply`, авто-раскладка в `finishRecognition`) провал claim перехода в `linking` теперь прерывает выполнение ДО хардлинков. - В `linkPlan` провал `CreateFileLinks` больше не оставляет задачу в `linking`: задача уходит в `review` с кодом `persist` и причиной; повторный `Apply` идемпотентен (хардлинки уже на диске → `StatusExists`, учёт дописывается). - Новый шаг поллинга `sweepLinking`: на каждом тике и на старте задачи в `linking` возвращаются в `review` с кодом `interrupted` и причиной «прерванная раскладка, повтори применение». Любая `linking`, видимая под `w.mu`, устарела по построению (активная раскладка держит `w.mu` весь свой срок), значит осталась после краха. ## Capabilities ### Modified Capabilities - `file-layout`: раскладка становится устойчивой к сбою записи claim/учёта — хардлинки не создаются при незакоммиченном claim, а сбой записи учёта не стрэндит задачу в `linking`. - `state-reconciliation`: у нетерминального `linking` появляется владелец на рестарте/тике — sweep осиротевших `linking` в `review`. ## Impact - **Код:** `internal/worker/worker.go` (`transition`/`transitionErr`, `pollOnce`, `sweepLinking`), `internal/worker/review.go` (`Apply`, `finishRecognition`, `linkPlan`). Граф переходов (`internal/store/download.go`) правки не требует — ребро `linking → review` уже объявлено. - **Тесты:** провал claim прерывает до хардлинков; провал `CreateFileLinks` уводит в `review` (файлы на диске); sweep осиротевшего `linking` → `review`. - **БД/схема:** без изменений.