18 Commits
Author SHA1 Message Date
ksv741 aae23d86f0 ci: validate release PR commits independently (WealthLedger/main#30)
Specs security / security (push) Successful in 7s
Specs verify / verify (push) Successful in 9s
Conventional commits / commits (pull_request) Successful in 10s
Specs security / security (pull_request) Successful in 6s
Specs verify / verify (pull_request) Successful in 10s
2026-10-12 23:39:30 +03:00
ksv741 ebc9ca872c chore(release): 0.1.4
Conventional commits / commits (pull_request) Failing after 11s
Specs security / security (pull_request) Successful in 7s
Specs verify / verify (pull_request) Successful in 10s
2026-10-12 23:19:12 +03:00
ksv741 fe295aa73a spec: archive harden-hourly-sync-lifecycle change (WealthLedger/backend#56)
Conventional commits / commits (pull_request) Successful in 10s
Specs security / security (pull_request) Successful in 8s
Specs verify / verify (pull_request) Successful in 9s
Specs security / security (push) Successful in 7s
Specs verify / verify (push) Successful in 11s
2026-10-12 22:52:44 +03:00
ksv741 e8e68a7ed6 Merge pull request 'spec: add harden-hourly-sync-lifecycle change (WealthLedger/backend#56)' (#33) from fix/56-harden-hourly-sync-lifecycle into develop
Specs security / security (push) Successful in 7s
Specs verify / verify (push) Successful in 10s
Reviewed-on: #33
2026-10-12 22:29:16 +03:00
ksv741 77b211186c spec: address hourly-sync review findings (WealthLedger/backend#56)
Conventional commits / commits (pull_request) Successful in 14s
Specs security / security (pull_request) Successful in 9s
Specs verify / verify (pull_request) Successful in 11s
2026-10-12 22:19:03 +03:00
ksv741 14174a7bb3 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
2026-10-12 20:28:58 +03:00
codex-bot 78f01066d2 Merge pull request 'chore: align specs engineering workflow (WealthLedger/main#27)' (#32) from chore/27-engineering-workflow into develop
Specs security / security (push) Successful in 7s
Specs verify / verify (push) Successful in 10s
2026-10-12 11:00:37 +03:00
ksv741 7f12327c37 docs: pin specs workflow reference (WealthLedger/main#27)
Conventional commits / commits (pull_request) Successful in 10s
Specs security / security (pull_request) Successful in 8s
Specs verify / verify (pull_request) Successful in 10s
2026-10-12 10:22:13 +03:00
ksv741 a29cbf6aae fix: align specs commitlint workflow (WealthLedger/main#27)
Conventional commits / commits (pull_request) Canceled after 0s
Specs security / security (pull_request) Canceled after 0s
Specs verify / verify (pull_request) Canceled after 0s
2026-10-12 10:21:32 +03:00
ksv741 a441b08378 chore: align specs engineering workflow (WealthLedger/main#27)
Conventional commits / commits (pull_request) Successful in 11s
Specs security / security (pull_request) Successful in 7s
Specs verify / verify (pull_request) Successful in 9s
2026-10-12 09:21:12 +03:00
codex-bot ea7beea863 Merge pull request 'docs: clarify specs Node bootstrap' (#31) from codex/issue-24-process into develop
Specs security / security (push) Successful in 6s
Specs verify / verify (push) Successful in 9s
2026-10-11 23:23:10 +03:00
ksv741 18518e0d2d docs: clarify specs Node bootstrap
Conventional commits / commits (pull_request) Successful in 11s
Specs security / security (pull_request) Successful in 8s
Specs verify / verify (pull_request) Successful in 11s
2026-10-11 23:06:51 +03:00
codex-bot 0b23fc3013 spec: add stock screener
Specs security / security (push) Successful in 7s
Specs verify / verify (push) Successful in 11s
2026-10-11 11:45:09 +03:00
ksv741 fcdc805f6c ci: skip checks on PR metadata edits
Conventional commits / commits (pull_request) Successful in 11s
Specs security / security (pull_request) Successful in 6s
Specs verify / verify (pull_request) Successful in 8s
2026-10-11 11:36:51 +03:00
ksv741 c132770fbb ci: avoid duplicate PR checks
Specs security / security (pull_request) Successful in 7s
Specs verify / verify (pull_request) Successful in 8s
Conventional commits / commits (pull_request) Successful in 9s
2026-10-11 11:24:57 +03:00
ksv741 951f1a9a20 spec: add stock screener
Specs security / security (push) Successful in 7s
Specs verify / verify (push) Successful in 9s
Conventional commits / commits (pull_request) Successful in 12s
Specs security / security (pull_request) Successful in 7s
Specs verify / verify (pull_request) Successful in 9s
2026-10-11 11:18:21 +03:00
ksv741 329d66e767 spec: finalize wealth-ledger SDD migration
Specs security / security (push) Successful in 8s
Specs verify / verify (push) Successful in 11s
2026-10-09 21:14:22 +03:00
codex-bot 90edab6e33 Merge pull request 'spec: define hourly portfolio sync history' (#25) from release/0.1.3 into develop
Specs security / security (push) Successful in 7s
Specs verify / verify (push) Successful in 9s
Merge hourly portfolio sync history specs
2026-10-09 09:19:38 +03:00
26 changed files with 1027 additions and 18 deletions
+12
View File
@@ -0,0 +1,12 @@
---
name: Bug Report
about: Неверное поведение
---
## Проблема и шаги воспроизведения
## Факт / ожидание
## Acceptance Criteria
- [ ]
## Окружение / ссылки (если применимо)
+10
View File
@@ -0,0 +1,10 @@
---
name: Engineering Task
about: CI, тесты, документация, процесс и прочее
---
## Контекст и результат
## Scope / связанные Issues (если есть)
## Acceptance Criteria
- [ ]
+12
View File
@@ -0,0 +1,12 @@
---
name: Feature Request
about: Новое наблюдаемое поведение
---
## Зачем и какой результат
## Ожидаемое поведение
## Acceptance Criteria
- [ ]
## Зависимости / связанные Issues (если есть)
+12
View File
@@ -0,0 +1,12 @@
---
name: Refactoring / Technical Debt
about: Улучшение без нового поведения
---
## Проблема и результат
## Сохраняемые инварианты / границы
## Acceptance Criteria
- [ ]
## Зависимости / ссылки (если есть)
+11
View File
@@ -0,0 +1,11 @@
## Summary
## Linked Issue
Refs
## Changes
## Verification
- Команда / результат / ограничения:
## Breaking Changes (если есть)
+10 -2
View File
@@ -2,7 +2,11 @@ name: Conventional commits
on:
pull_request:
types: [opened, synchronize, reopened, edited]
types: [opened, synchronize, reopened]
concurrency:
group: specs-commits-${{ gitea.event.pull_request.number }}
cancel-in-progress: true
jobs:
commits:
@@ -23,4 +27,8 @@ jobs:
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
printf '%s\n' "$PR_TITLE" | npm run commitlint -- --strict
npm run commitlint -- --strict --from "$BASE_SHA" --to "$HEAD_SHA"
if [[ "$PR_TITLE" =~ ^chore\(release\):\ [0-9]+\.[0-9]+\.[0-9]+$ ]]; then
npm run commitlint -- --strict --from "$HEAD_SHA^" --to "$HEAD_SHA"
else
npm run commitlint -- --strict --from "$BASE_SHA" --to "$HEAD_SHA"
fi
+6 -2
View File
@@ -2,12 +2,16 @@ name: Specs security
on:
push:
branches:
- '**'
branches: [develop, main]
pull_request:
types: [opened, synchronize, reopened]
schedule:
- cron: '17 5 * * *'
concurrency:
group: specs-security-${{ gitea.event_name }}-${{ gitea.event.pull_request.number || gitea.ref }}
cancel-in-progress: true
jobs:
security:
runs-on: ubuntu-latest
+6 -1
View File
@@ -2,8 +2,13 @@ name: Specs verify
on:
push:
branches: ['**']
branches: [develop, main]
pull_request:
types: [opened, synchronize, reopened]
concurrency:
group: specs-verify-${{ gitea.event_name }}-${{ gitea.event.pull_request.number || gitea.ref }}
cancel-in-progress: true
jobs:
verify:
+11 -5
View File
@@ -2,8 +2,11 @@
Репозиторий содержит существующие behavioral specifications и SDD history.
Для обычной разработки не создавай и не обновляй planning artifacts ради
процесса. Требование пользователя определяет работу; OpenSpec применяется
только по явному запросу.
процесса. До edits найди/создай подходящую Gitea Issue, проверь AC и ветку;
чтение допускается без Issue. Выбор lightweight/SDD — по
[Engineering Workflow](https://git.personal.ksv741.ru/WealthLedger/main/src/commit/03b3b59e6ff2ba391f9dfc42a039a96d0d84c1c9/.ai/conventions/engineering-workflow.md)
(main#28; после merge — актуальная версия `develop`);
OpenSpec lifecycle начинается только после явного решения Change Owner.
Если нужна существующая behavioral spec, выбери точный target через
[AI_INDEX](AI_INDEX.md). Specs не являются вторым editable HTTP OpenAPI:
@@ -16,11 +19,14 @@ anchors и `git diff --check`; application gates принадлежат affected
Если явно запрошена работа через OpenSpec, следуй
[локальным инструкциям OpenSpec](README.md#начало-работы)
и используй store `wealth-ledger-specs`. Перед Node-командами активируй
местную `.nvmrc` через `nvm use --silent`.
местную `.nvmrc`: если `nvm` не загружен в non-interactive shell, загрузи
установленный `$HOME/.nvm/nvm.sh`, затем выполни `nvm use --silent` из specs.
Если `nvm.sh` недоступен, остановись; не используй системный Node.
## Ветки и релизы
Работа — в отдельной ветке от `develop`, затем PR в `develop`; в `main` напрямую
Работа — в issue-linked ветке от `develop`, затем PR в `develop`; в `main` напрямую
не пушить. Release/hotfix и синхронизация веток — по [README](README.md#релизы).
Commits и заголовки PR — Conventional Commits; для спецификаций допустим `spec:`.
Commits и заголовки PR — Conventional Commits со ссылкой на Issue (кроме release);
для спецификаций допустим `spec:`.
Перед подготовкой релиза прочитай местный README; версии вручную не повышать.
+29
View File
@@ -1,5 +1,34 @@
# Changelog
## [0.1.4](https://git.personal.ksv741.ru/WealthLedger/specs/compare/v0.1.3...v0.1.4) (2026-10-12)
### Bug Fixes
* align specs commitlint workflow (WealthLedger/main[#27](https://git.personal.ksv741.ru/WealthLedger/specs/issues/27)) ([a29cbf6](https://git.personal.ksv741.ru/WealthLedger/specs/commit/a29cbf6aae7ebe48a51ae1979ff3f30b9fa8e305))
### Documentation
* clarify specs Node bootstrap ([18518e0](https://git.personal.ksv741.ru/WealthLedger/specs/commit/18518e0d2daecb843fea1b4d55d1c7d2cabf24f6))
* pin specs workflow reference (WealthLedger/main[#27](https://git.personal.ksv741.ru/WealthLedger/specs/issues/27)) ([7f12327](https://git.personal.ksv741.ru/WealthLedger/specs/commit/7f12327c37055ba903f962b1220138da9c7edd06))
### Specifications
* add harden-hourly-sync-lifecycle change (WealthLedger/backend[#56](https://git.personal.ksv741.ru/WealthLedger/specs/issues/56)) ([14174a7](https://git.personal.ksv741.ru/WealthLedger/specs/commit/14174a7bb335152c8cc76d9a6bbaefaaaa58b22f))
* add stock screener ([951f1a9](https://git.personal.ksv741.ru/WealthLedger/specs/commit/951f1a9a20a09695e2ff85d1ee96632e98a202d8))
* address hourly-sync review findings (WealthLedger/backend[#56](https://git.personal.ksv741.ru/WealthLedger/specs/issues/56)) ([77b2111](https://git.personal.ksv741.ru/WealthLedger/specs/commit/77b211186c297df4187e044804786ffc9096ce19))
* archive harden-hourly-sync-lifecycle change (WealthLedger/backend[#56](https://git.personal.ksv741.ru/WealthLedger/specs/issues/56)) ([fe295aa](https://git.personal.ksv741.ru/WealthLedger/specs/commit/fe295aa73ab5bc9d826067892930b7161fb4f1b9))
* define hourly portfolio sync history ([b24a686](https://git.personal.ksv741.ru/WealthLedger/specs/commit/b24a6861887e831ef2d73a97f36cc213be2be0b8))
* finalize wealth-ledger SDD migration ([329d66e](https://git.personal.ksv741.ru/WealthLedger/specs/commit/329d66e767f7e89a40b96bb8e5b499f39f67feb6))
### CI
* avoid duplicate PR checks ([c132770](https://git.personal.ksv741.ru/WealthLedger/specs/commit/c132770fbb4a4e48cae0a4ceca7407bb6a768ded))
* skip checks on PR metadata edits ([fcdc805](https://git.personal.ksv741.ru/WealthLedger/specs/commit/fcdc805f6c1b3be6a20a7e858e3e80cbd83a9121))
### Maintenance
* align specs engineering workflow (WealthLedger/main[#27](https://git.personal.ksv741.ru/WealthLedger/specs/issues/27)) ([a441b08](https://git.personal.ksv741.ru/WealthLedger/specs/commit/a441b083785d8f50c42dea543e684b23f27732a2))
## [0.1.3](https://git.personal.ksv741.ru/WealthLedger/specs/compare/v0.1.2...v0.1.3) (2026-10-07)
### Bug Fixes
+5 -3
View File
@@ -4,7 +4,7 @@
## Начало работы
OpenSpec применяется по явному запросу согласно [AGENTS](AGENTS.md). Для команд, читающих или изменяющих change, явно передавайте `--store wealth-ledger-specs`. Структурная готовность CLI не означает approval: перед каждым downstream artifact нужно явное решение Change Owner.
OpenSpec применяется после явного решения Change Owner согласно [AGENTS](AGENTS.md). Для команд, читающих или изменяющих change, явно передавайте `--store wealth-ledger-specs`. Структурная готовность CLI не означает approval: перед каждым downstream artifact нужно явное решение Change Owner.
| Change | Lifecycle |
| --- | --- |
@@ -37,9 +37,11 @@ Node задаётся собственной `.nvmrc`; после `nvm use --sil
## Релизы
Репозиторий выпускается независимо. `develop` — интеграция, `main` — принятые
релизы. Работа: `codex/<task>` от актуального `develop`, PR в `develop`.
релизы. Обычная работа: issue-linked ветка от актуального `develop`, PR в
`develop`; общие правила — в Engineering Workflow по ссылке из AGENTS.
Обычные PR допускают squash; release и обратная синхронизация сохраняют историю.
Commits и PR titles — Conventional Commits, включая `spec:`. Версии вручную не повышай.
Commits и PR titles — Conventional Commits со ссылкой на Issue (кроме release),
включая `spec:`. Версии вручную не повышай.
1. Получи `main`, `develop` и теги: `git fetch origin --tags`. Убедись, что
`develop` содержит последний релиз. Создай `release/<name>` от проверенного
+9 -1
View File
@@ -1,4 +1,12 @@
module.exports = {
extends: ["@commitlint/config-conventional"],
rules: { "type-enum": [2, "always", ["feat", "fix", "perf", "docs", "spec", "refactor", "build", "ci", "test", "style", "chore", "revert"]] },
defaultIgnores: false,
ignores: [
message => /^chore\(release\): \d+\.\d+\.\d+$/.test(message.trim()),
message => /^Merge pull request ['"].+['"] \(#\d+\) from .+ into .+$/.test(message.trim()),
],
rules: {
"type-enum": [2, "always", ["feat", "fix", "perf", "docs", "spec", "refactor", "build", "ci", "test", "style", "chore", "revert"]],
"references-empty": [2, "never"],
},
};
@@ -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
+42 -1
View File
@@ -25,7 +25,7 @@
- **THEN** система не запускает обход в 11:00 и может начать следующий не ранее 12:00
### Requirement: Последний результат обхода доступен только для наблюдения
Система SHALL предоставлять авторизованному User результат последнего обхода только в границе его текущего подключения: время начала, состояние выполнения, полного успеха, частичного успеха либо неуспеха и безопасный перечень проблемных счетов или разделов. Это состояние MUST быть доступно в `meta` каждого затронутого resource read вместе с freshness, полнотой и доступностью его `data`. Успех одного счёта или раздела MUST сохраняться независимо от ошибки другого; ошибка MUST NOT заменять его последнее успешное состояние или время. Отсутствие состоявшейся попытки MUST отличаться от успешной попытки. Наблюдение результата MUST NOT предоставлять способ начать личную синхронизацию.
Система SHALL предоставлять авторизованному User результат последнего обхода только в границе его текущего подключения: время начала, состояние выполнения, полного успеха, частичного успеха либо неуспеха и безопасный перечень проблемных счетов или разделов. Это состояние MUST быть доступно в `meta` каждого затронутого resource read вместе с freshness, полнотой и доступностью его `data`. Успех одного счёта или раздела MUST сохраняться независимо от ошибки другого; ошибка MUST NOT заменять его последнее успешное состояние или время. Итог попытки MUST определяться фактическим результатом каждого обязательного шага обхода: попытка, не завершившая обязательный шаг, MUST обозначаться частичным успехом либо неуспехом и MUST NOT обозначаться полным успехом. Отсутствие состоявшейся попытки MUST отличаться от успешной попытки. Наблюдение результата MUST NOT предоставлять способ начать личную синхронизацию.
#### Scenario: Частичный успех
- **WHEN** Portfolio одного доступного счёта обновился, а другого не обновился
@@ -38,3 +38,44 @@
#### Scenario: Старая граница подключения
- **WHEN** User заменил либо удалил подключение и запрашивает результат прежнего обхода
- **THEN** система не раскрывает результат прежней границы подключения
#### Scenario: Сбой после частичного обновления
- **WHEN** попытка прервана ошибкой либо сбоем сохранения после обновления Portfolio одного доступного счёта
- **THEN** результат попытки обозначает частичный успех с проблемными шагами, а не полный успех, а обновлённый Portfolio сохраняется
#### Scenario: Сбой до первого обновления
- **WHEN** попытка прервана до обновления любого обязательного шага
- **THEN** результат попытки обозначает неуспех, а не полный успех
### 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** его результат сохраняется и учитывается при выводе итога этой попытки
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "wealth-ledger-specs",
"version": "0.1.3",
"version": "0.1.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "wealth-ledger-specs",
"version": "0.1.3",
"version": "0.1.4",
"devDependencies": {
"@commitlint/cli": "21.2.2",
"@commitlint/config-conventional": "21.2.2",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "wealth-ledger-specs",
"version": "0.1.3",
"version": "0.1.4",
"private": true,
"engines": {
"node": "24.21.0"