spec: add harden-hourly-sync-lifecycle change (WealthLedger/backend#56)
Conventional commits / commits (pull_request) Successful in 12s
Specs security / security (pull_request) Successful in 7s
Specs verify / verify (pull_request) Successful in 10s

This commit is contained in:
2026-10-12 20:28:58 +03:00
parent 78f01066d2
commit 14174a7bb3
11 changed files with 828 additions and 0 deletions
@@ -0,0 +1,100 @@
# Tasks
## 1. Данные попытки и вывод итога
- [x] 1.1 [backend] `settleHourly` выводит итог попытки из durable-строк шагов
в одной транзакции; `cause: 'INTERRUPTED'` капит итог до `partial`;
вызывающие не передают результат и список ошибок
- **Covers:** `hourly-investment-sync` — «Последний результат обхода доступен только для наблюдения»
- **Source:** Design — Decisions 1, 6; System Analysis — Data and State Semantics
- **Verification:** изолированный SQLite-тест: сбой после частичного обновления даёт `partial` с сохранённым успехом; сбой до первого шага даёт `failed`; ложный `succeeded` невозможен ни на одном error-пути
- **Depends on:** none
- [x] 1.2 [backend] id строк шагов персистятся в `progress` hourly-попытки
(`children` + `attempts`) в момент dispatch, до ожидания шага
- **Covers:** `hourly-investment-sync` — «Восстановление после сбоя сохранения не блокирует последующие обходы», «Устаревшая публикация не влияет на актуальную попытку»
- **Source:** Design — Decisions 2, 5
- **Verification:** SQLite-тест: после потери выполнения попытки множество шагов восстанавливается из `progress`, включая шаг, упавший в dispatch
- **Depends on:** 1.1
## 2. Ограниченное ожидание и восстановление
- [x] 2.1 [backend] ограничение ожидания на каждый шаг в `settledWorks`
(константа ~15 минут, per-id deadline); истёкший шаг учитывается проблемным
с причиной `DEADLINE`; начатые обращения к T-Bank не отменяются; часы в
тестах — jest fake timers без инъекции clock в production-код
- **Covers:** `hourly-investment-sync` — «Каждая попытка обхода достигает итога в ограниченное время»
- **Source:** Design — Decision 3; System Analysis — Error and Recovery Semantics
- **Verification:** SQLite-тест с зависшим шагом на fake timers: попытка терминальна в ограниченное время, наблюдение не ждёт бесконечно
- **Depends on:** 1.1
- [x] 2.2 [backend] in-memory `active`-реестр обходов + `recoverAbandoned`
перед reservation: running-попытка своего `processRunId` без активного walk
закрывается derived settle с `INTERRUPTED`; застрявшие legacy-строки
закрываются первым проходом
- **Covers:** `hourly-investment-sync` — «Восстановление после сбоя сохранения не блокирует последующие обходы» (Потерянное закрытие попытки)
- **Source:** Design — Decisions 4, 6; System Analysis — Error and Recovery Semantics
- **Verification:** SQLite-тест: подавленная ошибка закрытия при живом процессе — следующий проход закрывает попытку, следующая допустимая попытка стартует
- **Depends on:** 1.1, 1.2
- [x] 2.3 [backend] restart-ветка `reserveHourly` закрывает чужую running-попытку
derived settle с `INTERRUPTED` вместо голого `cancelled`; `due`-гейт и
защита от overlap прежние
- **Covers:** `hourly-investment-sync` — «Восстановление после сбоя сохранения не блокирует последующие обходы» (Перезапуск процесса)
- **Source:** Design — Decision 5
- **Verification:** SQLite-тест перезапуска процесса: брошенная попытка закрыта `partial`/`failed` с сохранёнными результатами шагов, новый обход не ранее следующего часового срока, перекрытия нет
- **Depends on:** 1.1, 1.2
## 3. Финализаторы шагов
- [x] 3.1 [backend] внешний catch финализаторов шагов (enrichment, operations
drain/step) записывает терминальный неуспешный результат своей строки шага
существующим путём записи ошибки перед возвратом или пробросом
- **Covers:** `hourly-investment-sync` — «Каждая попытка обхода достигает итога в ограниченное время» (шаг покидает running)
- **Source:** Design — Decision 7
- **Verification:** SQLite-тест с инъекцией ошибки финализатора: строка шага терминальна, попытка проходит terminal barrier
- **Depends on:** none
## 4. Поздняя публикация
- [x] 4.1 [backend] подтверждено fence-поведение публикации шага: результат до
фиксации итога попытки сохраняется и учитывается в итоге; после появления
преемника — отклоняется без изменения данных и итога актуальной попытки
- **Covers:** `hourly-investment-sync` — «Устаревшая публикация не влияет на актуальную попытку»
- **Source:** Design — Decisions 1–3; System Analysis — Error and Recovery Semantics
- **Verification:** SQLite-тесты поздней публикации: до фиксации итога — учтён; после successor — данные и итог актуальной попытки не изменились
- **Depends on:** 1.1, 1.2, 2.1
## 5. Системная верификация backend
- [x] 5.1 [backend] полный набор изолированных SQLite-тестов coordinator:
ветки `first`, `not-due`, `running`, `restart`, `partial`, итог после
terminal barrier; инъекции ошибок отдельно в дочерние шаги и родительский
coordinator; controlled stop/restart без бесконечного drain и без новых
reservations после остановки; scheduler stub `upcoming-events` не является
единственным доказательством
- **Covers:** `hourly-investment-sync` — все Requirements дельты
- **Source:** Design — Developer Verification Strategy; QA Analysis — Coverage Matrix, Risk-based Priorities
- **Verification:** полный `backend verify` зелёный на финальном diff; наборы auth/money/publication сохранены
- **Depends on:** 1.1, 1.2, 2.1, 2.2, 2.3, 3.1, 4.1
- [x] 5.2 [backend] backend-generated OpenAPI семантически совместим с
текущим контрактом: форма `meta` и словарь состояний не изменились, новые
значения только внутри открытого строкового поля диагностики
- **Covers:** `N/A` — защита контракта (изменений контракта нет)
- **Source:** Design — Contract Implementation; System Analysis — Contracts
- **Verification:** diff сгенерированного OpenAPI до/после изменения не содержит shape-изменений
- **Depends on:** 1.1, 1.2, 2.1, 2.2, 2.3, 3.1, 4.1
## 6. Canonical documentation
- [x] 6.1 [docs] обновить `data-freshness.md`: вывод итога из фактических
результатов шагов, ограниченное ожидание шага, закрытие брошенной попытки
без блокировки расписания — рядом со словарём `lastAttempt`
- **Covers:** `N/A` — docs promotion
- **Source:** docs-impact — Documentation Changes, строка 1
- **Verification:** страница не противоречит canonical spec `hourly-investment-sync` и фактическому поведению `meta`
- **Depends on:** 5.1
- [x] 6.2 [docs] обновить раздел «Состояние и ошибки» `tbank.md`: причины
жизненного цикла `INTERRUPTED`/`DEADLINE` — внутренняя диагностика, не
ошибки T-Bank; начатые обращения не отменяются
- **Covers:** `N/A` — docs promotion
- **Source:** docs-impact — Documentation Changes, строка 2
- **Verification:** раздел различает ошибки подключения/external-коды и внутренние причины жизненного цикла
- **Depends on:** 5.1