chore(release): 0.1.4 #35

Merged
codex-bot merged 19 commits from release/0.1.4 into main 2026-10-13 07:43:17 +03:00
11 changed files with 849 additions and 0 deletions
Showing only changes of commit e8e68a7ed6 - Show all commits
@@ -0,0 +1,2 @@
schema: wealth-ledger-sdd-v2
created: 2026-10-12
@@ -0,0 +1,220 @@
# Engineering Design
## Context and Constraints
- Один backend-процесс; durable fence — reservation попытки в SQLite; таймер
процесса только предлагает работу (существующий инвариант, комментарии в
`tbank-hourly.scheduler.ts`).
- Итог попытки пишется CAS-обновлением только из состояния `running`:
устаревшая публикация после появления преемника уже отклоняется существующим
fence (`settleHourly` where `state: 'running'`; `read()` при несовпадении
`progress.attempts` помечает шаг `cancelled`).
- Сегодня итог вычисляется по in-memory списку ошибок walk (пуст при
исключении — ложный `succeeded`), ошибка `settleHourly` подавляется
`.catch(() => undefined)`, `settledWorks` поллит без deadline, а id строк
шагов не персистятся на hourly-попытке (`progress: { sections: [] }`).
- Финализаторы enrichment/operations могут очистить память без durable finish —
шаг остаётся `running`.
- Топология не меняется: без lease и мультреплик; CAS-ограничения
`attempt`/`revision`/`source` не ослабляются.
## Technical Goals / Non-Goals
**Goals**
- Итог попытки выводится из durable-состояния шагов; ложный `succeeded`
невозможен ни на одном error-пути.
- Каждая попытка достигает терминального итога в ограниченное время
(ограничение ожидания на каждый шаг).
- Попытка с утраченным выполнением закрывается диагностируемо (`INTERRUPTED`)
и не блокирует следующий обход дольше обычного часового срока.
- Поздняя публикация шага текущей попытки сохраняется до фиксации итога;
после появления преемника отклоняется существующим fence.
**Non-Goals**
- Lease, очередь, вторая реплика, параллельный scheduler.
- Отмена уже начатых обращений к T-Bank.
- Новый публичный статус попытки и изменения HTTP contract.
- Graceful drain — только если тест контролируемой остановки докажет
необходимость.
## Implementation Impact Map
### main
`N/A` — workspace/dev/release orchestration не меняется.
### frontend
`N/A` — публичный словарь состояний и форма `meta` не меняются.
### backend
`affected` — координатор `tbank-hourly.scheduler.ts` (walk/settle/
settledWorks/launch), `tbank-sync.repository.ts` (`reserveHourly`/
`settleHourly`), персист id шагов на попытке, durable finish в финализаторах
`catalog-enrichment.service.ts` и `brokerage-operations-sync.service.ts`,
изолированные SQLite-тесты reservation/coordinator.
### qa
`N/A` — QA repository не существует; системная верификация остаётся ручной по
QA Analysis.
## Decisions
### backend
1. **Итог выводится из durable-состояния шагов (корневой фикс).**
`settleHourly` в одной транзакции читает строки шагов попытки и вычисляет
итог: `succeeded` — только когда каждый обязательный шаг успешен и закрытие
инициировано нормальным завершением walk; `partial` — успешны некоторые;
`failed` — ни один. Вызывающие больше не передают результат и список
ошибок; walk передаёт максимум `cause: 'INTERRUPTED'` (закрытие до
нормального завершения), который капит итог до `partial`. Исключение —
обязательные секции, чей dispatch не создал durable-строку шага: walk
передаёт их как дополнительные problem-записи (наблюдаемый факт
диспатча, не результат), иначе попытка могла бы получить ложный успех.
Альтернатива
«прокидывать настоящий failures-map в catch» отклонена: не закрывает
lost-settle и краш-пути — каждый error-путь требовал бы своего guard.
Отдельный janitor-сервис отклонён: issue требует чинить существующих
владельцев, не вводить параллельный scheduler.
2. **Id шагов персистятся на попытке в момент dispatch.** `progress`
hourly-попытки переиспользует существующую форму `children: string[]` +
`attempts: { [id]: attemptId }` (паттерн `ScreenProgress`/`progress()`).
Walk записывает id каждой строки шага сразу после dispatch, до ожидания —
иначе recovery после краша не знает множество шагов попытки, а шаг, упавший
в dispatch, исчезает из множества.
3. **Ограниченное ожидание на каждый шаг.** `settledWorks` получает per-id
deadline: константа `WORK_WAIT_BOUND_MS = 15 * 60 * 1000`; истёкший id
возвращается как проблемный шаг с причиной `DEADLINE`. Тем же bound покрыты
и ожидания walk до появления durable-строк шагов (review-замечание,
follow-up WealthLedger/backend#78): `membership.request`/`member.done`,
`access.local` и dispatch `portfolio`/`operations`/`events.refresh` гоняются
с `WORK_WAIT_BOUND_MS` через race; истечение трактуется как проблемная
секция (как failed dispatch), для `access.local` — как закрытие
`INTERRUPTED`. Вызов не отменяется; поздняя durable-строка fenced по
`attemptId`, как любая поздняя публикация. Управляемые часы в
тестах — jest fake timers, инъекция clock в production-код не вводится.
RPC не отменяется; поздний результат шага сохраняется его собственной
строкой — учтён ли он в итоге, решает fence и момент фиксации.
4. **In-memory активные обходы + recovery на существующем tick.**
`TbankHourlyScheduler` держит `active: Set<string>` (attemptId): добавление
в начале `walk`, удаление в `finally`. `launch()` перед `reserveHourly`
вызывает `recoverAbandoned(userId)`: если строка hourly `running`,
`processRunId` свой и attemptId не в `active` — попытка потеряла settle и
закрывается через `settleHourly` с `cause: 'INTERRUPTED'` (итог из детей,
максимум `partial`). Это же закрывает застрявшие legacy-строки `running`.
Новых таймеров и санитаров нет — восстановление едет на существующем
минутном tick.
5. **Restart-ветка `reserveHourly` закрывает попытку итогом.** Существующая
ветка «running + чужой `processRunId`» вместо голой записи
`state: 'cancelled'` выполняет тот же derived settle с
`cause: 'INTERRUPTED'` внутри транзакции reservation; далее прежний
`due`-гейт: новый обход — не ранее следующего часового срока от начала
попытки. `cancelled` для hourly-строк перестаёт писаться; существующие
маппинги `cancelled` в `hourly()`/`hourlyAttempt()` остаются толерантным
чтением и не удаляются.
6. **Причина прерывания — в существующем поле `errorCode`.** `INTERRUPTED` —
уровень попытки (закрытие до нормального завершения); `DEADLINE` — уровень
шага (истечение ожидания), попадает в `errorCode` попытки как первая ошибка
секции по существующему precedence. Коды ошибок шагов остаются в
`progress.sections[].errorCode`. Поле — открытое строковое: форма ответа и
enum `TbankErrorCode` не расширяются (маркеры жизненного цикла — не ошибки
T-Bank).
7. **Durable finish в дочерних финализаторах.** Внешний catch финализаторов
(enrichment, operations drain/step) обязан перед возвратом или пробросом
записать терминальный неуспешный результат своей строки шага существующим
путём записи ошибки, а не только очистить память. Иначе шаг остаётся
`running`, и попытка не проходит terminal barrier.
## Cross-cutting Implementation
- Подавление ошибки `settleHourly` (`.catch(() => undefined)`) допустимо только
как пара к решению 4: `walk` удаляет attemptId из `active` в `finally`, и
следующий tick закрывает строку recovery. Ошибка фиксируется существующим
структурным логом.
- Новых фоновых таймеров, очередей и таблиц нет; concurrency не меняется
(один walk на подключение).
## Data / Storage Changes
- Миграции нет: `children`/`attempts` живут в существующем JSON `progress`
строки `tbankSyncWork`; поле `errorCode` уже в схеме
(`20260924170000_persist_tbank_data/migration.sql:177`).
- Legacy-застрявшие `running` строки закрываются первым recovery-проходом
(итог из детей + `INTERRUPTED`); чтение уже закрытых строк не меняется.
## Contract Implementation
- HTTP contract не меняется: форма `meta` и словарь состояний
(`running`/`succeeded`/`partial`/`failed`) прежние; `cancelled` остаётся в
enum DTO как толерантное чтение.
- До закрытия change: backend-generated OpenAPI проверяется на семантическую
совместимость с текущим контрактом — изменения формы не ожидаются; новые
значения существуют только внутри открытого строкового поля диагностики и
в hourly-read наружу не попадают.
## Developer Verification Strategy
- Изолированные SQLite-тесты reservation/coordinator (не stub
`upcoming-events`): ветки `first`, `not-due`, `running`, `restart`,
`partial`; итог проверяется после terminal barrier.
- Инъекция ошибок отдельно в дочерние шаги и в родительский settle;
восстановление после ошибки сохранения; защита от overlap и от поздней
публикации; controlled stop/restart — нет бесконечного drain, после
остановки новые reservations не создаются.
- Управляемые часы — jest fake timers.
- Сохранить существующие наборы auth/money/publication; полный
`backend verify` на финальном diff.
- Graceful drain не добавляется, пока тест остановки не покажет
необходимость.
## UI/UX Package
`N/A` — UI не меняется; новый mockup не требуется.
## Risks / Trade-offs
- Derivation зависит от полноты `children`: пропуск записи id при dispatch
делает шаг невидимым для итога. Митигируется решением 2 (запись до await) и
тестом инъекции ошибки в dispatch.
- Per-step deadline может закрыть попытку как `partial` при медленном, но
живом шаге; поздний результат сохраняется строкой шага, данные не теряются.
- In-memory `active` не переживает рестарт — этот случай закрывает
restart-ветка reservation (решение 5).
- Константа 15 минут без конфигурации: потолок опыта одного процесса
(`ponytail:` вынести в конфиг, если медленные шаги станут регулярными).
## Migration / Rollout / Rollback
- Rollout: один PR в backend; миграций и флагов нет; следующий tick после
деплоя закрывает застрявшие строки.
- Rollback contract: `N/A` — нет миграции, security или public contract
изменения; откат — revert кода, уже закрытые recovery строки остаются в
корректном терминальном виде.
## Solution Assessment
Обязателен (concurrency/recovery). Варианты:
- (a) Root-cause: settle выводит итог из durable-детей; recovery на
существующем tick/reservation; один guard в общей точке.
- (b) Минимальный патч: настоящий failures-map в catch плюс guard в каждом
вызывающем.
- (c) Отдельный janitor/очередь с lease.
Выбор — (a): закрывает все error-пути одной точкой (exception, lost settle,
краш, deadline), переиспользует существующие fence и `progress`-паттерн, без
новых компонентов. (b) оставляет открытыми lost-settle и краш — каждый новый
error-путь снова требует своего guard, что и есть текущий баг. (c) отклонён:
issue явно запрещает параллельный scheduler, lease не даёт выгоды в
одно-процессной топологии, а стоимость нового компонента выше фикса.
@@ -0,0 +1,43 @@
# Documentation Impact
## Summary
Durable docs update нужен ограниченно: наблюдаемая семантика итога часовой
попытки уже описана в AS-IS, но правила вывода итога, ограниченного ожидания и
восстановления после сбоя сохранения/перезапуска в docs отсутствуют. Переносится
только это знание; migration history остаётся в archived change.
## Documentation Changes
| Action | Target | Category | Resulting AS-IS | Source | Verification |
|---|---|---|---|---|---|
| update | `docs/architecture/cross-cutting/data-freshness.md` | cross-cutting | Рядом со словарём `lastAttempt` (`none`, `running`, `succeeded`, `partial`, `failed`): итог попытки выводится из фактических результатов её шагов; попытка, не завершившая обязательный шаг, не обозначается полным успехом; ожидание шага ограничено, зависший шаг учитывается как проблемный; попытка, потерявшая выполнение (сбой сохранения, остановка процесса), закрывается частичным успехом либо неуспехом и не блокирует следующий обход дольше обычного срока | System Analysis — Data and State Semantics, Error and Recovery Semantics; Design 1–5 | Страница не противоречит canonical spec `hourly-investment-sync` и фактическому поведению `meta` |
| update | `docs/integrations/tbank.md` | integration | В разделе «Состояние и ошибки»: диагностические причины жизненного цикла попытки (`INTERRUPTED` — прервана до нормального завершения, `DEADLINE` — истечение ожидания шага) — внутренняя диагностика, не ошибки T-Bank; в hourly-read наружу не попадают, начатые обращения к T-Bank не отменяются | System Analysis — Observability Semantics; Design 3, 6 | Раздел различает ошибки подключения/external-коды и внутренние причины жизненного цикла |
PR реализации: WealthLedger/docs#37 (open) — обе страницы обновлены, diff
соответствует таблице выше (задачи 6.1 и 6.2 верифицируемы по нему).
## Diagram Promotion
Не переносить. Sequence-диаграммы (`hourly-attempt-terminal-barrier.mmd`,
`hourly-bounded-wait.mmd`, `hourly-recovery.mmd`) — planning evidence этого
change; AS-IS достаточно текстового описания семантики в `data-freshness.md`.
## ADR Decision
`N/A` — архитектурно значимого решения с новыми alternatives/consequences нет:
чинятся существующие владельцы строк в прежней одно-процессной топологии,
без новых компонентов и контрактов (Solution Assessment остаётся в archived
change).
## Glossary / Navigation Impact
Нет: новые термины (`terminal barrier`, преемник попытки) — внутренние;
публичный словарь состояний не расширяется, navigation не меняется.
## No-Change Rationale
`docs/architecture/containers.md` не меняется: раздел «Сохранение и
восстановление данных» описывает backup/restore уровня базы, а не восстановление
попытки приложением. Остальные AS-IS страницы не затрагиваются: HTTP contract
и словарь состояний не изменились.
@@ -0,0 +1,114 @@
# Proposal
## Why
Почасовая синхронизация T-Bank может завершиться в неконсистентном состоянии
после ошибки сохранения: незавершённая попытка записывается как полный успех,
ошибка финализации либо подавленная ошибка закрытия оставляет попытку в
состоянии выполнения навсегда, а локальная reservation не позволяет запустить
следующий обход в том же процессе. Пользователь видит ложный успех и перестаёт
получать обновления до перезапуска backend, а неудавшееся ожидание дочерних
шагов может ждать без ограничения. Механизм подтверждён статически (BE-002,
BE-015, RISK-001, RISK-002, TQ-003); частота проявления в production пока не
установлена.
## What Changes
- Итог попытки определяется фактическим результатом каждого обязательного шага:
невыполненный шаг даёт частичный успех либо неуспех, но не полный успех, а
уже полученные успехи отдельных счетов и разделов сохраняются.
- Прерванная попытка (ошибка, сбой сохранения, остановка процесса, истечение
ожидания) обозначается частичным успехом либо неуспехом и не получает нового
публичного состояния; причина прерывания фиксируется существующим
диагностическим полём.
- Попытка достигает итога в ограниченное время: ожидание дочернего шага
ограничено, а зависшая попытка не остаётся в выполнении бесконечно.
- Состояние в SQLite восстанавливается после сбоя сохранения: брошенная попытка
закрывается диагностируемым итогом, ожидание завершается, следующая
допустимая попытка может стартовать без перезапуска процесса.
- Восстановление не создаёт перекрытия обходов и не принимает устаревшие
результаты: поздняя публикация относительно актуального преемника
отклоняется.
## Scope
### In Scope
- Итоговый статус попытки часового обхода и обработка ошибок в дочерних
финализаторах.
- Ограниченное ожидание дочерних шагов и восстановление после ошибок
завершения шага, публикации результата и закрытия попытки.
- Защита от перекрытия обходов и от поздней публикации устаревших данных при
восстановлении.
- Контролируемая остановка и перезапуск процесса и их влияние на незавершённую
попытку, подтверждённые тестами.
- Наблюдаемый результат попытки: время начала, состояние, безопасный перечень
проблемных счетов и разделов.
### Out of Scope / Non-Goals
- Распределённая очередь, lease, поддержка нескольких реплик и изменение
топологии deployment.
- Удаление данных.
- Ослабление существующих ограничений CAS для попытки, ревизии подключения и
источника данных.
- Новый публичный статус попытки и изменения HTTP contract.
- Новый параллельный scheduler или замена владельцев строк синхронизации.
- Проверка часовой cadence и cleanup workflow — независимые задачи.
## Capabilities
### New Capabilities
- Нет.
### Modified Capabilities
- `hourly-investment-sync`: итог попытки из фактических результатов шагов,
ограниченное ожидание шага, диагностируемое закрытие брошенной попытки,
отсутствие перекрытий и устаревшей публикации.
## Impact
- **Пользователь**: достоверный статус последнего обхода вместо ложного успеха
и бесконечного «выполняется» после сбоя сохранения; обновления продолжаются
без перезапуска процесса.
- **Backend**: координатор часового обхода, дочерние финализаторы обновления
данных и reservation попытки в SQLite.
- **Frontend**: не затрагивается — наблюдаемый словарь состояний и форма
ответов не меняются.
- **Внешняя система T-Bank**: contract не меняется; начатые запросы к T-Bank
могут завершаться поздно и должны корректно отклоняться как устаревшие.
- **Процессы**: диагностика причин прерывания попыток и восстановление после
ошибок сохранения.
## Success Criteria
- Попытка с невыполненным обязательным шагом никогда не наблюдается как полный
успех; частичные успехи счетов и разделов остаются видимыми.
- После однократной ошибки завершения шага, публикации результата либо закрытия
попытки и последующего восстановления ожидание завершается, а следующая
допустимая попытка стартует.
- Устаревшая попытка наблюдается относительно актуального преемника; поздняя
публикация старого процесса отклоняется и не меняет данные.
- Координатор различает состояния `first`, `not-due`, `running`, `restart` и
`partial`; итог проверяется после terminal barrier.
- Сценарий остановки и перезапуска доказан тестами: нет бесконечного ожидания,
после остановки новые попытки не создаются.
- Сохранены существующие наборы проверок auth/money/publication, полный
`backend verify` на финальном изменении зелёный.
## Assumptions / Dependencies
- Топология остаётся одно-процессной; межпроцессный lease и мультреплики не
требуются.
- Истечение ожидания не отменяет уже начатый запрос к T-Bank; защита
опирается на существующие ограничения CAS и правило срока следующего обхода.
- Причина прерывания фиксируется существующим диагностическим полём, поэтому
HTTP contract и публичный словарь состояний не меняются; упомянутое в задаче
состояние `interrupted` реализуется как частичный успех либо неуспех.
- Graceful drain добавляется только если тест контролируемой остановки докажет
такую необходимость.
- Правило устаревшей публикации относится к итогу попытки и данным её шагов;
существующие требования `tbank-data-sync` уже запрещают выдавать неполный или
продолжающийся результат за полный, поэтому их изменение не планируется.
@@ -0,0 +1,90 @@
# QA Analysis
## Verification Scope
### In Scope
- Дельта `hourly-investment-sync`: «Последний результат обхода доступен только
для наблюдения» (MODIFIED) и ADDED «Каждая попытка обхода достигает итога в
ограниченное время», «Восстановление после сбоя сохранения не блокирует
последующие обходы», «Устаревшая публикация не влияет на актуальную попытку».
- Системные проверки через resource GET `meta` и durable-состояние попытки
после контролируемых действий backend (инъекции сбоев, остановка/запуск
процесса).
### Out of Scope
- Внутренние unit/component тесты — developer-owned, определены Design.
- Неизменяемые поведения `tbank-data-sync`/`tbank-connection` — только
regression.
- Проверка часовой cadence (T03) и cleanup workflow — независимые задачи.
## Coverage Matrix
| Requirement / Scenario | Системная проверка |
| --- | --- |
| Последний результат… — Сбой после частичного обновления | Инъекция сбоя шага после успешного обновления Portfolio одного счёта: GET обозначает частичный успех с проблемным шагом, данные первого счёта остаются обновлёнными |
| Последний результат… — Сбой до первого обновления | Инъекция сбоя до завершения любого шага: GET обозначает неуспех, не полный успех |
| Последний результат… — Частичный успех / Наблюдение не запускает обход / Старая граница подключения | Regression существующего поведения `meta` |
| Каждая попытка… — Шаг не завершается | Зависший шаг с управляемыми часами: попытка достигает терминального итога в ограниченное время, шаг учтён как проблемный |
| Каждая попытка… — Наблюдение не ждёт без ограничения | Наблюдение результата получает итоговое состояние, а не бесконечное «выполняется» |
| Восстановление… — Потерянное закрытие попытки | Ошибка закрытия при живом процессе: следующий проход закрывает попытку диагностируемо, следующая допустимая попытка стартует |
| Восстановление… — Перезапуск процесса | Остановка процесса во время обхода и повторный запуск: брошенная попытка закрыта, новый обход не ранее следующего часового срока, перекрытия нет |
| Устаревшая публикация… — После появления преемника | Позднее завершение шага старой попытки после запуска следующей: данные не изменились, итог актуальной попытки не изменился |
| Устаревшая публикация… — До фиксации итога | Позднее завершение шага до фиксации итога попытки: результат сохранён и учтён в итоге |
## Cross-system Verification
- T-Bank upstream не отменяется при истечении ожидания; поздний ответ после
появления преемника не изменяет данные (проверяется на симулированном
upstream в контролируемой среде backend-тестов).
- Внешние ошибки шагов отображаются только безопасным перечнем проблемных
счетов/разделов; причины `INTERRUPTED`/`DEADLINE` не раскрывают наружу
ничего сверх существующей формы `meta`.
## Risk-based Priorities
- **Critical**: ложный полный успех (целостность данных), бесконечное
«выполняется», перекрытие обходов, порча данных устаревшей публикацией.
- **Compatibility**: публичный словарь состояний `meta` неизменен;
`cancelled` в enum остаётся толерантным значением.
- **Money/data integrity**: успешные результаты шагов не теряются при сбоях
остальных.
## Test Data and Environment Needs
- Изолированный backend на SQLite с управляемыми часами и симулированным
upstream T-Bank (контролируемая тестовая среда; внутренние mocks — на
усмотрение разработчиков по Design).
- Подключение с не менее чем двумя доступными счетами для сценариев частичного
успеха.
## Manual / Exploratory Scope
- Ручная проверка отображения результата последнего обхода на инвестиционных
экранах после симулированного сбоя сохранения.
- Exploratory: повторные остановки процесса в разных фазах обхода; наблюдение
результата сразу после старта процесса.
## Automation Candidates
- Black-box API-кандидаты (GET `meta` после инъекций сбоев, контролируемая
остановка) — `deferred until QA repo exists`.
## Observability Verification
- Причины `INTERRUPTED`/`DEADLINE` наблюдаются в durable-диагностике попытки,
а не в публичном API; подавленная ошибка закрытия фиксируется структурным
логом backend.
## Regression Impact
- Существующие наборы auth/money/publication и полный `backend verify`.
- Happy path часового обхода, первый обход после подключения, скрытие
результата прежней границы подключения.
## Gaps / Blockers
- Gaps нет. Graceful drain остаётся условным: добавляется только если
controlled-stop тест докажет необходимость (routing: пересмотр Design/QA
после теста).
@@ -0,0 +1,59 @@
## MODIFIED Requirements
### Requirement: Последний результат обхода доступен только для наблюдения
Система SHALL предоставлять авторизованному User результат последнего обхода только в границе его текущего подключения: время начала, состояние выполнения, полного успеха, частичного успеха либо неуспеха и безопасный перечень проблемных счетов или разделов. Это состояние MUST быть доступно в `meta` каждого затронутого resource read вместе с freshness, полнотой и доступностью его `data`. Успех одного счёта или раздела MUST сохраняться независимо от ошибки другого; ошибка MUST NOT заменять его последнее успешное состояние или время. Итог попытки MUST определяться фактическим результатом каждого обязательного шага обхода: попытка, не завершившая обязательный шаг, MUST обозначаться частичным успехом либо неуспехом и MUST NOT обозначаться полным успехом. Отсутствие состоявшейся попытки MUST отличаться от успешной попытки. Наблюдение результата MUST NOT предоставлять способ начать личную синхронизацию.
#### Scenario: Частичный успех
- **WHEN** Portfolio одного доступного счёта обновился, а другого не обновился
- **THEN** новый Portfolio первого сохраняется, прежний успешный Portfolio второго остаётся доступным, а результат обхода обозначает частичный успех и проблемный счёт
#### Scenario: Наблюдение не запускает обход
- **WHEN** User запрашивает результат последнего обхода
- **THEN** система возвращает доступный сохранённый результат без нового обращения к T-Bank
#### Scenario: Старая граница подключения
- **WHEN** User заменил либо удалил подключение и запрашивает результат прежнего обхода
- **THEN** система не раскрывает результат прежней границы подключения
#### Scenario: Сбой после частичного обновления
- **WHEN** попытка прервана ошибкой либо сбоем сохранения после обновления Portfolio одного доступного счёта
- **THEN** результат попытки обозначает частичный успех с проблемными шагами, а не полный успех, а обновлённый Portfolio сохраняется
#### Scenario: Сбой до первого обновления
- **WHEN** попытка прервана до обновления любого обязательного шага
- **THEN** результат попытки обозначает неуспех, а не полный успех
## ADDED Requirements
### Requirement: Каждая попытка обхода достигает итога в ограниченное время
Каждая начатая попытка часового обхода MUST достигать итогового состояния в ограниченное время. Ожидание завершения каждого обязательного шага MUST быть ограничено; шаг, не завершившийся в это ограничение, MUST быть учтён как проблемный, а попытка MUST быть закрыта частичным успехом либо неуспехом после того, как каждый обязательный шаг достиг терминального результата либо был учтён как проблемный. Истечение ограничения MUST NOT отменять уже начатые шаги.
#### Scenario: Шаг не завершается
- **WHEN** обязательный шаг не покидает состояние выполнения дольше ограничения ожидания
- **THEN** шаг учитывается как проблемный, попытка закрывается частичным успехом либо неуспехом, а её состояние перестаёт быть выполняющимся
#### Scenario: Наблюдение не ждёт без ограничения
- **WHEN** попытка не может завершиться из-за незавершённых шагов
- **THEN** наблюдение результата получает итоговое состояние в ограниченное время и не ожидает завершения бесконечно
### Requirement: Восстановление после сбоя сохранения не блокирует последующие обходы
Если попытка осталась в состоянии выполнения после сбоя завершения шага, публикации результата либо закрытия попытки, система MUST привести её к диагностируемому итоговому состоянию: частичный успех либо неуспех с сохранёнными результатами шагов, а не полный успех. Последующий обход MUST становиться допустимым по обычному правилу срока, отсчитанному от начала этой попытки. Восстановление MUST NOT создавать перекрывающийся обход и MUST NOT повторять уже полученные результаты шагов.
#### Scenario: Потерянное закрытие попытки
- **WHEN** закрытие попытки не сохранилось, а процесс продолжает работать
- **THEN** система закрывает попытку итогом из фактических результатов шагов, наблюдение получает итоговое состояние, а следующая допустимая попытка может стартовать
#### Scenario: Перезапуск процесса
- **WHEN** процесс остановился после начала попытки и запускается снова
- **THEN** брошенная попытка обозначается частичным успехом либо неуспехом с сохранёнными результатами шагов, а новый обход начинается не ранее следующего часового срока от её начала
### Requirement: Устаревшая публикация не влияет на актуальную попытку
Результат шага попытки, у которой появился преемник, MUST отклоняться как устаревший: он MUST NOT обновлять данные и MUST NOT учитываться в итоге актуальной попытки. Шаг устаревшей попытки MUST наблюдаться как устаревший относительно актуального преемника. Результат шага текущей попытки, полученный до фиксации её итога, MUST сохраняться и учитываться в этом итоге независимо от того, наступил ли он до или после истечения ограничения ожидания.
#### Scenario: Поздняя публикация после появления преемника
- **WHEN** шаг старой попытки завершается после запуска следующей попытки
- **THEN** его результат отклоняется как устаревший, данные не обновляются, а итог актуальной попытки не меняется
#### Scenario: Поздняя публикация до фиксации итога
- **WHEN** шаг завершился после истечения ограничения ожидания, но итог его попытки ещё не зафиксирован
- **THEN** его результат сохраняется и учитывается при выводе итога этой попытки
@@ -0,0 +1,140 @@
# System Analysis
## Requirement Coverage
| Requirement / Scenario | Flow | Contract / Semantics |
| --- | --- | --- |
| Последний результат обхода… — Сбой после частичного обновления | `hourly-attempt-terminal-barrier.mmd` | Outcome derivation, terminal barrier |
| Последний результат обхода… — Сбой до первого обновления | `hourly-attempt-terminal-barrier.mmd` | Outcome derivation |
| Последний результат обхода… — Частичный успех | `hourly-attempt-terminal-barrier.mmd` | Step results preserved |
| Каждая попытка обхода достигает итога в ограниченное время — Шаг не завершается | `hourly-bounded-wait.mmd` | Bounded wait per required step |
| Каждая попытка обхода достигает итога в ограниченное время — Наблюдение не ждёт без ограничения | `hourly-bounded-wait.mmd` | Bounded terminal outcome |
| Восстановление после сбоя сохранения… — Потерянное закрытие попытки | `hourly-recovery.mmd` | Recovery of lost settle |
| Восстановление после сбоя сохранения… — Перезапуск процесса | `hourly-recovery.mmd` | Recovery of abandoned attempt |
| Устаревшая публикация… — Поздняя публикация после появления преемника | `hourly-recovery.mmd` | Successor fencing |
| Устаревшая публикация… — Поздняя публикация до фиксации итога | `hourly-bounded-wait.mmd` | Late publication counts before outcome is recorded |
## Systems and Responsibilities
- **Hourly walk coordinator** (backend): резервирует попытку часового обхода для
текущего подключения, запускает обязательные шаги, ждёт их терминального
результата с ограничением, выводит итог попытки и закрывает её.
- **Durable attempt status** (SQLite): единственная durable-запись попытки
(состояние, диагностическая причина, результаты шагов) и источник итога для
наблюдения и восстановления.
- **Child step owners**: шаги membership, Portfolio, операций и будущих событий
публикуют свой полный результат в durable-хранилище и обозначают свой
терминальный результат; финализатор шага обязан фиксировать результат даже
при ошибке.
- **T-Bank и публичные источники**: внешние данные; начатые обращения не
отменяются по внутреннему ограничению ожидания.
- **Resource GET observer**: читает сохранённые данные и `meta` с состоянием
последней попытки; наблюдение не запускает обход.
## Interaction Flows
- `sequences/hourly-attempt-terminal-barrier.mmd` — попытка, шаги, terminal
barrier и вывод итога из фактических результатов шагов.
- `sequences/hourly-bounded-wait.mmd` — ограничение ожидания шага, закрытие
попытки по его истечении и учёт позднего результата до появления преемника.
- `sequences/hourly-recovery.mmd` — восстановление после сбоя сохранения и
после перезапуска процесса, отклонение устаревшей публикации.
## Contracts
- Новых и изменённых межсистемных HTTP/event contracts нет. Публичный словарь
состояний последней попытки (`выполнение`, `полный успех`, `частичный успех`,
`неуспех`) и форма `{ data, meta }` не меняются.
- Машинно-читаемый target contract в `system-analysis/contracts/` не создаётся:
изменения ограничены уточнением значений внутри существующих полей.
- Диагностическое поле причины остаётся открытым строковым полем; значения
причин прерывания (`INTERRUPTED` — ошибка, сбой сохранения, остановка
процесса; `DEADLINE` — истечение ограничения ожидания шага) дополняют
существующие коды ошибок шагов и не требуют изменения формы ответа.
- Compatibility check до закрытия change: backend-generated OpenAPI остаётся
семантически совместимым с текущим контрактом (ожидаются только дополнительные
значения открытого строкового поля диагностики).
## Data and State Semantics
- **Попытка**: `running` → терминальный итог (`succeeded` | `partial` | `failed`).
Попытка, не завершившая обязательный шаг, не может получить `succeeded`.
- **Terminal barrier**: итог попытки выводится только после того, как каждый
обязательный шаг достиг терминального результата либо учтён как истёкший по
ограничению ожидания. Итог не вычисляется по неполному списку шагов.
- **Вывод итога**: `succeeded` — только когда все обязательные шаги успешны и
координатор сам зафиксировал закрытие попытки; `partial` — успешны некоторые
шаги; `failed` — не успешен ни один. Успешные результаты шагов сохраняются
независимо от итога.
- **Причина прерывания**: попытка, закрытая до нормального завершения всех
шагов, сохраняет причину прерывания в диагностическом поле и не превышает
`partial`.
- **Internal coordinator decisions** `first`, `not-due`, `running`, `restart`,
`partial`: внутренние метки решения о запуске и проверки итога после terminal
barrier; в HTTP contract не входят и публично не наблюдаемы.
- **Шаги**: каждый шаг владеет своей durable-строкой результата; строка шага
устаревшей попытки помечается устаревшей относительно преемника.
## Error and Recovery Semantics
- **Сбой шага** (внешняя ошибка T-Bank): шаг фиксирует терминальный неуспех со
своим кодом ошибки; остальные шаги продолжаются; итог — `partial` или `failed`.
- **Сбой сохранения** (завершение шага, публикация результата, закрытие
попытки): ошибка не подавляется молча в durable-состоянии; попытка, оставшаяся
`running`, закрывается восстановлением по фактическим результатам шагов.
- **Остановка процесса**: начатая попытка остаётся `running` в SQLite;
следующий запуск процесса закрывает её как прерванную (`partial` или `failed`
с сохранёнными результатами шагов) и не создаёт перекрывающийся обход.
- **Ограничение ожидания**: истечение ограничивает только ожидание шага; уже
начатое обращение к T-Bank не отменяется. Его поздний результат сохраняется и
учитывается при выводе итога своей попытки, если итог ещё не зафиксирован, и
отклоняется как устаревший после появления преемника.
- **Повтор**: неуспешная попытка не повторяется ранее следующего часового срока,
отсчитанного от начала этой попытки; восстановление не сокращает и не
продлевает этот срок.
- **Overlapping**: следующая попытка не резервируется, пока предыдущая не
терминальна; восстановление обязано сначала закрыть предыдущую.
## Observability Semantics
- Наблюдаемый итог попытки и безопасный перечень проблемных счетов/разделов —
существующее обязательство; значения итога не расширяются.
- Семантические коды причин: `INTERRUPTED` — попытка прервана до нормального
завершения (ошибка, сбой сохранения, остановка процесса); `DEADLINE` — шаг не
завершился в ограничение ожидания. Коды сохраняются в диагностическом поле
попытки и/или шага и дополняют коды ошибок шагов без изменения формы `meta`.
- SDK/exporter/logger — вне границ этого изменения; используется существующий
структурный лог backend.
## Security and NFR at System Boundaries
- Граница наблюдения попытки остаётся границей текущего подключения: результат
прежней границы не раскрывается (существующее обязательство).
- Ограниченное время достижения итога попытки — наблюдаемое обязательство
надёжности: ожидание шага и ожидание наблюдателя ограничены.
- Новых trust boundaries, authn/authz требований нет.
## Compatibility / Migration
- Схема данных и HTTP contract не меняются; миграция не требуется.
- Уже застрявшие строки попыток в состоянии выполнения закрываются первым
проходом восстановления по правилам этого изменения: итог из фактических
результатов шагов, причина прерывания, без полного успеха. Это ожидаемое
разовое приведение legacy-состояния к диагностируемому виду.
- Совместимость с одно-процессной топологией сохраняется; межпроцессный lease
не требуется.
## Durable Documentation Candidates
- `docs/integrations/tbank.md` (раздел «Состояние и ошибки»): итог часовой
попытки, причины прерывания и правило ограниченного ожидания как часть
наблюдаемого состояния интеграции.
- `docs/architecture/containers.md` (раздел «Сохранение и восстановление
данных»): восстановление попытки после сбоя сохранения и после перезапуска
процесса как durable-свойство системы.
- Итоговое решение о переносе принимает `docs-impact`.
## Open Questions
- Нет блокирующих вопросов. Частота проявления механизма в production остаётся
не установленной и не влияет на contract.
@@ -0,0 +1,23 @@
sequenceDiagram
participant Coordinator
participant DB as Durable attempt status
participant Steps as Child step owners
participant TB as T-Bank and public sources
participant API as Resource GET
Coordinator->>DB: Reserve attempt for current connection
DB-->>Coordinator: Attempt with connection revision
Coordinator->>Steps: Run required steps (membership, portfolios, operations, events)
Steps->>TB: Fetch missing data
Steps->>DB: Publish complete successful step result
Steps->>DB: Record terminal step result (success or failure with code)
Coordinator->>DB: Read step results after terminal barrier
alt All required steps succeeded
Coordinator->>DB: Settle succeeded
else Some required steps succeeded
Coordinator->>DB: Settle partial with safe problem sections
else No required step succeeded
Coordinator->>DB: Settle failed with safe problem sections
end
API->>DB: Read last attempt result
DB-->>API: Time, outcome, safe problem list
@@ -0,0 +1,24 @@
sequenceDiagram
participant Coordinator
participant DB as Durable attempt status
participant Steps as Child step owners
participant TB as T-Bank and public sources
Coordinator->>Steps: Run required steps
Steps->>TB: Start upstream call
Coordinator->>Coordinator: Wait for step result within bound
alt Step reaches terminal result in time
Steps->>DB: Publish complete result
Coordinator->>DB: Settle attempt after terminal barrier
else Wait bound expires
Note over Coordinator,TB: Upstream call is not cancelled
Coordinator->>DB: Record step problem cause DEADLINE
Coordinator->>DB: Settle partial or failed after terminal barrier
Steps->>TB: Upstream call completes later
Steps->>DB: Publish late result for the same attempt
alt Outcome not recorded yet and no successor
DB-->>Steps: Late result is preserved and counts at outcome derivation
else Successor attempt already started
DB-->>Steps: Publication rejected as stale
end
end
@@ -0,0 +1,23 @@
sequenceDiagram
participant Coordinator
participant DB as Durable attempt status
participant Steps as Child step owners
Note over Coordinator,DB: Attempt is still running but its walk no longer executes
Coordinator->>DB: Detect running attempt without active walk
alt Process still alive after lost settle
Coordinator->>DB: Read actual step results
Coordinator->>DB: Close attempt as partial or failed with cause INTERRUPTED
else Process restarted after stop
Coordinator->>DB: Read actual step results
Coordinator->>DB: Close abandoned attempt as partial or failed with cause INTERRUPTED
end
DB-->>Coordinator: Attempt is terminal, no overlap possible
Coordinator->>DB: Reserve next attempt at next hourly term from attempt start
alt Next term reached
DB-->>Coordinator: Successor attempt starts
Steps->>DB: Late publication from previous attempt
DB-->>Steps: Publication rejected as stale relative to successor
else Next term not reached
DB-->>Coordinator: No work (not-due)
end
@@ -0,0 +1,111 @@
# 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
- [x] 2.4 [backend] ожидания walk до durable-строк шагов
(`membership.request`/`member.done`, `access.local`, dispatch
`portfolio`/`operations`/`events.refresh`, post-wait `events.get`)
ограничены тем же `WORK_WAIT_BOUND_MS` через race; истечение — проблемная
секция либо `INTERRUPTED`-закрытие без отмены вызова
(WealthLedger/backend#78)
- **Covers:** `hourly-investment-sync` — «Каждая попытка обхода достигает итога в ограниченное время»
- **Source:** Design — Decision 3; review specs#33 (CRITICAL)
- **Verification:** SQLite-тесты с никогда не завершающимися dispatch/membership/`done`/`get`: попытка терминальна в ограниченное время, поздние вызовы не отменяются
- **Depends on:** 2.1
## 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