Files
jellybit/openspec/changes/linking-transition-robustness/proposal.md
T
avandClaude Opus 4.8 9bab7dc402 Устойчивость раскладки и переходов linking (MAJOR-4, MINOR-7)
Закрывает две связанные дыры «claim-then-side-effect» в раскладке хардлинками.

MINOR-7: transition глотал ошибку записи состояния — на путях Apply и
авто-раскладки выполнение продолжалось к хардлинкам при незакоммиченном claim
перехода в linking, а финальный linking→done отклонялся графом (задача застревала
со stale-планом). Выделен transitionErr, возвращающий ошибку; Apply и
finishRecognition прерываются ДО linkPlan при провале claim. Обёртка transition
(void) сохранена для fire-and-forget переходов — соседние функции воркера не
тронуты.

MAJOR-4: (A) провал CreateFileLinks после создания хардлинков больше не оставляет
задачу в linking голым return — уводим в review (код persist), повтор Apply
идемпотентен. (B) новый шаг pollOnce sweepLinking возвращает осиротевшие после
краха linking-задачи в review (код interrupted) на тике и старте; любая linking
под w.mu устарела по построению. Восстановлен инвариант «у каждого нетерминального
состояния есть владелец».

Граф переходов не тронут (ребро linking→review уже объявлено). Тесты: провал claim
не создаёт хардлинков; провал учёта уводит в review; sweep осиротевшего linking.

OpenSpec-change linking-transition-robustness (дельты file-layout,
state-reconciliation).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 17:18:28 +03:00

4.8 KiB
Raw Blame History

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 осиротевшего linkingreview.
  • БД/схема: без изменений.