Устойчивость раскладки и переходов 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>
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
## 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`.
|
||||
- **БД/схема:** без изменений.
|
||||
Reference in New Issue
Block a user