web-ui: карточка и страница обновляются, пока задачу может двигать фон

- условие самообновления — доменный предикат store.State.IsObservable() вместо
  фазы catched; один поллер на поверхность, интервалы 5 с и 15 с
- отказ тика отвечает 200 и самозавершающимся фрагментом с корневым id цели
  вместо 404/500, который htmx не свопит
- заведён ADR-2026-08-10-observability-is-not-terminality, переписан раздел
  «Живой поллинг» в конвенции веб-UI
This commit is contained in:
av
2026-08-10 14:02:38 +03:00
parent 969926fae3
commit a5d873b62d
29 changed files with 1575 additions and 99 deletions
+20
View File
@@ -64,6 +64,26 @@ func (s State) IsTerminal() bool {
return slices.Contains(terminalStates, s)
}
// selfHealingStates — терминальные состояния, которые фон возвращает в поток
// САМ, без человека: failed (по восстановимым кодам — см. ListRecoverable) и
// состояния рассинхрона, которые сверка переоценивает по реальности (см.
// reconcileDesync). done в перечень не входит сознательно: его переоценка
// означает удаление файлов мимо сервиса — событие редкое, а разложенных задач в
// списке больше всех, и наблюдать за каждой дороже, чем показать новое
// состояние при следующем заходе.
var selfHealingStates = []State{
StateFailed, StateTargetMissing, StateOrphaned,
}
// IsObservable сообщает, может ли состояние задачи измениться без участия
// человека: любое нетерминальное плюс терминальные из selfHealingStates. На
// этом предикате стоит самообновление веб-UI: поверхность обновляет себя, пока
// задача наблюдаема, и замолкает, когда двигать её может только человек (см.
// openspec/specs/web-ui, «Самообновление живой задачи»).
func (s State) IsObservable() bool {
return !s.IsTerminal() || slices.Contains(selfHealingStates, s)
}
// allowedTransitions — декларативный граф легальных переходов машины состояний
// (from → множество допустимых to). Единственный источник истины о легальности
// рёбер: покрывает все переходы, которые worker выполняет по всем capability