54 Commits
Author SHA1 Message Date
codex-bot a2d2df3b5a spec: archive group-account-rows-by-instrument and sync canonical specs (WealthLedger/main#47) (#65)
Specs security / security (push) Successful in 6s
Specs verify / verify (push) Successful in 7s
2026-10-30 07:35:30 +03:00
codex-bot f3a6b78461 spec: plan grouping of all-account rows by instrument (WealthLedger/main#47) (#64)
Specs security / security (push) Successful in 7s
Specs verify / verify (push) Successful in 9s
2026-10-30 07:28:31 +03:00
codex-bot 322ddf6a7f feat: add experimental wealth-ledger-sdd-lab OpenSpec schema (#61)
Specs security / security (push) Successful in 6s
Specs verify / verify (push) Successful in 7s
2026-10-29 05:58:29 +03:00
ksv741 093185d44d chore(release): 0.1.6
Conventional commits / commits (pull_request) Successful in 10s
Specs security / security (pull_request) Successful in 8s
Specs verify / verify (pull_request) Successful in 10s
Specs security / security (push) Successful in 7s
Specs verify / verify (push) Successful in 9s
2026-10-24 19:15:05 +03:00
renovate-bot 08646ff505 chore(deps): update non-major dependencies (WealthLedger/specs#56)
Specs security / security (push) Successful in 16s
Specs verify / verify (push) Successful in 11s
Co-authored-by: renovate-bot <renovate-bot@ksv741.ru>
2026-10-24 19:04:54 +03:00
codex-bot c5b9cadbbd chore: bump Node to 24.22.1 (WealthLedger/backend#122)
Specs security / security (push) Successful in 10s
Specs verify / verify (push) Successful in 13s
2026-10-23 16:20:01 +03:00
ksv741 ab24ccc32b chore(release): 0.1.5
Conventional commits / commits (pull_request) Successful in 10s
Specs security / security (pull_request) Successful in 8s
Specs verify / verify (pull_request) Successful in 10s
Specs security / security (push) Successful in 7s
Specs verify / verify (push) Successful in 11s
2026-10-18 16:24:55 +03:00
codex-bot f0478c6d6b spec: archive quiet-user-feedback and sync canonical specs (#50)
Specs security / security (push) Successful in 8s
Specs verify / verify (push) Successful in 10s
Native archive применил 9 added и 12 modified требований без удалений; все 10 сценариев сохранены. Новый canonical spec frontend-user-feedback внесён в AI_INDEX.
2026-10-17 22:18:27 +03:00
codex-bot 5a7e319caf spec: reconcile quiet-user-feedback with source-state requirements (#50)
Specs security / security (push) Successful in 9s
Specs verify / verify (push) Successful in 10s
Переносит change quiet-user-feedback в develop и согласует его delta-блоки с текущими canonical specs. Нормативное поведение: тихое чтение клиента и видимое состояние источника из meta.
2026-10-17 21:46:58 +03:00
codex-bot 06d08e2972 chore: adopt linear git model in specs instructions (#48)
Specs security / security (push) Successful in 7s
Specs verify / verify (push) Successful in 10s
2026-10-17 14:14:17 +03:00
codex-bot 1bd6181585 spec: align user-authentication password requirement with ASCII 8-72 policy (WealthLedger/specs#46) (#47)
Specs security / security (push) Successful in 8s
Specs verify / verify (push) Successful in 10s
2026-10-16 10:47:41 +03:00
codex-bot 0a1db52baf Merge pull request 'chore: synchronize release 0.1.4 into develop (WealthLedger/main#30)' (#45) from chore/30-sync-release into develop
Specs security / security (push) Successful in 6s
Specs verify / verify (push) Successful in 9s
2026-10-16 07:48:20 +03:00
codex-bot 5ca44d16b1 Merge pull request 'chore: bring develop into release sync branch (WealthLedger/main#30)' (#44) from develop into chore/30-sync-release
Conventional commits / commits (pull_request) Successful in 10s
Specs security / security (pull_request) Successful in 7s
Specs verify / verify (pull_request) Successful in 9s
2026-10-16 07:41:05 +03:00
codex-bot 7da2f90e0a chore: make OpenSpec toolchain explicit and diagnosable (WealthLedger/specs#4) (#43)
Specs security / security (push) Successful in 7s
Specs verify / verify (push) Successful in 11s
Conventional commits / commits (pull_request) Successful in 10s
Specs security / security (pull_request) Successful in 7s
Specs verify / verify (pull_request) Successful in 9s
2026-10-15 06:34:41 +03:00
codex-bot 7ebfc510af spec: clarify SDD applies only on explicit request (WealthLedger/specs#1)
Specs security / security (push) Successful in 8s
Specs verify / verify (push) Successful in 10s
2026-10-14 22:43:01 +03:00
codex-bot d5d5480eed spec: add five capability entries to AI_INDEX (WealthLedger/specs#28)
Specs security / security (push) Canceled after 0s
Specs verify / verify (push) Canceled after 0s
2026-10-14 22:43:00 +03:00
codex-bot 4b2806ead5 Merge pull request 'spec: archive three completed changes (main#33, frontend#63, screener)' (#40) from specs/archive-three-changes into develop
Specs security / security (push) Successful in 17s
Specs verify / verify (push) Successful in 10s
2026-10-14 21:46:55 +03:00
ksv741 6efb989ed9 spec: archive three completed changes (main#33, frontend#63, screener)
Conventional commits / commits (pull_request) Successful in 13s
Specs security / security (pull_request) Successful in 8s
Specs verify / verify (pull_request) Successful in 12s
2026-10-14 21:40:26 +03:00
codex-bot 0ed462d157 Merge pull request 'spec: fix upcoming events date filter default and picker bounds (WealthLedger/frontend#63)' (#39) from specs/63-upcoming-events-date-filter into develop
Specs security / security (push) Successful in 9s
Specs verify / verify (push) Successful in 12s
2026-10-14 21:35:02 +03:00
ksv741 b4c0589745 spec: fix upcoming events date filter default and picker bounds (WealthLedger/frontend#63)
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-14 21:24:13 +03:00
codex-bot 6c33c770db Merge pull request 'ci: rerun commits proof on PR title edit (WealthLedger/specs#30)' (#38) from fix/30-commits-workflow-edited into develop
Specs security / security (push) Successful in 34s
Specs verify / verify (push) Successful in 11s
2026-10-13 20:33:44 +03:00
ksv741 4e3855fc35 ci: rerun commits proof on PR title edit (WealthLedger/specs#30)
Conventional commits / commits (pull_request) Successful in 12s
Specs security / security (pull_request) Successful in 8s
Specs verify / verify (pull_request) Successful in 14s
2026-10-13 20:18:57 +03:00
codex-bot 6c858a8858 spec: unify income payment cards and drop period comparisons (WealthLedger/main#33) (#37)
Specs security / security (push) Successful in 9s
Specs verify / verify (push) Successful in 12s
spec: unify income payment cards and drop period comparisons (WealthLedger/main#33)
2026-10-13 18:16:27 +03:00
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
ksv741 b24a686188 spec: define hourly portfolio sync history
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 7s
Specs verify / verify (pull_request) Successful in 9s
2026-10-09 09:05:04 +03:00
ksv741 1ee31e3b49 chore(release): 0.1.3
Conventional commits / commits (pull_request) Successful in 11s
Specs security / security (pull_request) Successful in 8s
Specs verify / verify (pull_request) Successful in 9s
Specs security / security (push) Successful in 8s
Specs verify / verify (push) Successful in 11s
2026-10-07 18:13:58 +03:00
ksv741 a1b0e99bdd spec: add reduce-screen-sync-polling change
Specs security / security (push) Successful in 8s
Specs verify / verify (push) Successful in 10s
OpenSpec change: screen sync status polling and observe reads run
every five seconds during active sync; terminal state observes
resources immediately.
2026-10-07 14:25:16 +03:00
ksv741 0fd148ffbb chore: run commitlint in commit-msg hook
Conventional commits / commits (pull_request) Successful in 11s
Specs security / security (pull_request) Successful in 8s
Specs verify / verify (pull_request) Successful in 11s
Specs security / security (push) Successful in 8s
Specs verify / verify (push) Successful in 11s
Local commits now get the same commitlint --strict check as CI.
2026-10-07 14:18:14 +03:00
ksv741 33cc4f3e0c fix(deps): patch release tooling advisories
Conventional commits / commits (pull_request) Successful in 11s
Specs security / security (pull_request) Successful in 10s
Specs verify / verify (pull_request) Successful in 9s
Specs security / security (push) Successful in 9s
Specs verify / verify (push) Successful in 10s
Scope patched basic-ftp and undici overrides to release-it.

Keep the existing development-dependency security gate.
2026-10-03 19:47:30 +03:00
ksv741 d861de5ead spec: simplify runtime requirements
Specs security / security (push) Failing after 8s
Specs verify / verify (push) Successful in 10s
Conventional commits / commits (pull_request) Successful in 12s
Specs security / security (pull_request) Failing after 8s
Specs verify / verify (pull_request) Successful in 10s
Preserve identity isolation and financial integrity while accepting
cross-tab reauthentication and on-demand candle history.

Align portfolio freshness with persisted reads and keep income
consistency at the backend owner.
2026-10-03 11:38:08 +03:00
ksv741 4d580f145a chore(release): 0.1.2
Conventional commits / commits (pull_request) Successful in 11s
Specs security / security (pull_request) Successful in 6s
Specs verify / verify (pull_request) Successful in 10s
Specs security / security (push) Successful in 7s
Specs verify / verify (push) Successful in 9s
2026-09-26 22:10:11 +03:00
ksv741 2f5814abdd spec: complete persist-tbank-data implementation
Conventional commits / commits (pull_request) Successful in 12s
Specs security / security (pull_request) Successful in 7s
Specs verify / verify (pull_request) Successful in 11s
Specs security / security (push) Successful in 7s
Specs verify / verify (push) Successful in 9s
Record the approved proposal, delta specifications, design and 27 verified
implementation tasks. Keep the change active; archiving and main-spec synchronization
are deferred.
2026-09-26 21:29:24 +03:00
ksv741 ba76f6fccc spec: complete local frontend stack evolution
Specs security / security (pull_request) Successful in 7s
Specs verify / verify (pull_request) Successful in 11s
Conventional commits / commits (pull_request) Successful in 11s
Specs security / security (push) Successful in 7s
Specs verify / verify (push) Successful in 9s
OpenSpec change: evolve-frontend-stack
2026-09-22 22:11:05 +03:00
ksv741 7f40372c3f spec: define versioned deployment bundle
Conventional commits / commits (pull_request) Successful in 12s
Specs security / security (pull_request) Successful in 8s
Specs verify / verify (pull_request) Successful in 11s
Specs security / security (push) Successful in 7s
Specs verify / verify (push) Successful in 10s
2026-09-20 18:49:25 +03:00
ksv741 e7a3c30b5d chore(release): 0.1.1
Conventional commits / commits (pull_request) Successful in 13s
Specs security / security (pull_request) Successful in 7s
Specs verify / verify (pull_request) Successful in 9s
Specs security / security (push) Successful in 7s
Specs verify / verify (push) Successful in 10s
2026-09-16 11:35:57 +03:00
ksv741 b34f23a230 spec: define containerized application delivery #10
Specs security / security (pull_request) Successful in 6s
Specs verify / verify (pull_request) Successful in 8s
Conventional commits / commits (pull_request) Successful in 11s
Specs security / security (push) Successful in 6s
Specs verify / verify (push) Successful in 9s
2026-09-16 09:41:59 +03:00
ksv741 e4f10dedef docs: make specs instructions self-contained
Specs security / security (pull_request) Successful in 7s
Specs verify / verify (pull_request) Successful in 10s
Conventional commits / commits (pull_request) Successful in 10s
Specs security / security (push) Successful in 7s
Specs verify / verify (push) Successful in 10s
2026-09-15 20:45:41 +03:00
210 changed files with 17408 additions and 1007 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 (если есть)
+9 -1
View File
@@ -4,6 +4,10 @@ on:
pull_request: pull_request:
types: [opened, synchronize, reopened, edited] types: [opened, synchronize, reopened, edited]
concurrency:
group: specs-commits-${{ gitea.event.pull_request.number }}
cancel-in-progress: true
jobs: jobs:
commits: commits:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -23,4 +27,8 @@ jobs:
HEAD_SHA: ${{ github.event.pull_request.head.sha }} HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: | run: |
printf '%s\n' "$PR_TITLE" | npm run commitlint -- --strict 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: on:
push: push:
branches: branches: [develop, main]
- '**'
pull_request: pull_request:
types: [opened, synchronize, reopened]
schedule: schedule:
- cron: '17 5 * * *' - cron: '17 5 * * *'
concurrency:
group: specs-security-${{ gitea.event_name }}-${{ gitea.event.pull_request.number || gitea.ref }}
cancel-in-progress: true
jobs: jobs:
security: security:
runs-on: ubuntu-latest runs-on: ubuntu-latest
+6 -1
View File
@@ -2,8 +2,13 @@ name: Specs verify
on: on:
push: push:
branches: ['**'] branches: [develop, main]
pull_request: 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: jobs:
verify: verify:
+2
View File
@@ -0,0 +1,2 @@
#!/bin/sh
npx --no-install commitlint --edit "$1" --strict
+1 -1
View File
@@ -1 +1 @@
24.21.0 24.22.1
+22 -11
View File
@@ -2,25 +2,36 @@
Репозиторий содержит существующие behavioral specifications и SDD history. Репозиторий содержит существующие behavioral specifications и SDD history.
Для обычной разработки не создавай и не обновляй planning artifacts ради Для обычной разработки не создавай и не обновляй 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 через Если нужна существующая behavioral spec, выбери точный target через
[AI_INDEX](AI_INDEX.md). Specs не являются вторым editable HTTP OpenAPI: [AI_INDEX](AI_INDEX.md). Specs не являются вторым editable HTTP OpenAPI:
backend публикует контракт, а frontend доверяет предоставленному snapshot и HTTP-провайдер публикует контракт, а потребитель получает отдельный artifact
локально проверяет generated types; flow описан в и локально проверяет generated types. Specs фиксируют behavioral requirements;
[docs](../docs/architecture/http-api.md#источник-контракта). другие checkout не нужны для чтения и native validation этого store.
Архивы и чужие active changes сохраняй. Для правки индекса проверь targets, Архивы и чужие active changes сохраняй. Для правки индекса проверь targets,
anchors и `git diff --check`; application gates принадлежат affected repos. anchors и `git diff --check`; application gates принадлежат affected repos.
Если явно запрошена работа через OpenSpec, следуй Если явно запрошена работа через OpenSpec, следуй
[локальным инструкциям OpenSpec](README.md#начало-работы) [локальным инструкциям OpenSpec](README.md#начало-работы)
и используй store `wealth-ledger-specs`. Перед Node-командами выбери frontend и используй 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 и синхронизация веток — по [root README](../README.md#релизы-и-ветки). не пушить. PR сливается squash или rebase-merge; merge-коммит не используется.
Commits и заголовки PR — Conventional Commits; для спецификаций допустим `spec:`. `main` обновляется только fast-forward из `develop`, поэтому `origin/main`
Перед подготовкой релиза прочитай root README; версии вручную не повышать. всегда предок `origin/develop`:
`git merge-base --is-ancestor origin/main origin/develop`. Срочный fix —
обычная ветка `fix/*` от `develop`; пути hotfix от `main` нет.
Релиз — по [README](README.md#релизы).
Commits и заголовки PR — Conventional Commits со ссылкой на Issue (кроме release);
для спецификаций допустим `spec:`.
Перед подготовкой релиза прочитай местный README; версии вручную не повышать.
+10 -3
View File
@@ -3,20 +3,26 @@
Эти существующие specs — источник сохранённых требований по конкретной теме. Эти существующие specs — источник сохранённых требований по конкретной теме.
Читай нужный requirement/scenario, если задача зависит от него; не загружай всё Читай нужный requirement/scenario, если задача зависит от него; не загружай всё
дерево и не создавай OpenSpec change для обычной разработки. дерево и не создавай OpenSpec change для обычной разработки.
As-built факты и принятие решений — через [docs index](../docs/README.md). Этот индекс и основные specs читаются без соседних checkout. Внешние
сведения о реализации служат справочными свидетельствами, когда предоставлены.
При конфликте с текущим поведением установи расхождение; не копируй устаревшее При конфликте с текущим поведением установи расхождение; не копируй устаревшее
правило или случайный код молча. правило или случайный код молча.
HTTP OpenAPI имеет единственный editable source в backend, а не здесь. HTTP OpenAPI имеет единственный editable source в backend, а не здесь.
Frontend доверяет предоставленному snapshot и локально проверяет generated Потребитель доверяет предоставленному artifact и локально проверяет generated
types: [Источник HTTP-контракта](../docs/architecture/http-api.md#источник-контракта). types. CI каждого участника использует только его собственные входы.
| Тема / trigger | Existing spec | | Тема / trigger | Existing spec |
| --- | --- | | --- | --- |
| Auth/session/refresh/error contract | [User authentication](openspec/specs/user-authentication/spec.md) | | Auth/session/refresh/error contract | [User authentication](openspec/specs/user-authentication/spec.md) |
| T-Bank connection/security/cache generation | [T-Bank connection](openspec/specs/tbank-connection/spec.md) | | T-Bank connection/security/cache generation | [T-Bank connection](openspec/specs/tbank-connection/spec.md) |
| Владелец синхронизации T-Bank, scheduling/priority | [T-Bank data sync](openspec/specs/tbank-data-sync/spec.md) |
| Часовой обход, результат последней попытки | [Hourly investment sync](openspec/specs/hourly-investment-sync/spec.md) |
| Счета/сводки | [Brokerage accounts](openspec/specs/brokerage-accounts/spec.md) | | Счета/сводки | [Brokerage accounts](openspec/specs/brokerage-accounts/spec.md) |
| Portfolio positions/totals/stale | [Portfolio](openspec/specs/portfolio-view/spec.md) | | Portfolio positions/totals/stale | [Portfolio](openspec/specs/portfolio-view/spec.md) |
| История стоимости Portfolio | [Portfolio value history](openspec/specs/portfolio-value-history/spec.md) |
| Справочные сведения/поиск инструментов (online-only) | [Online instrument information](openspec/specs/online-instrument-information/spec.md) |
| Ручные дебетовые карты | [Manual bank cards](openspec/specs/manual-bank-cards/spec.md) |
| Операции/cursor/filters | [Account ledger](openspec/specs/brokerage-account-ledger/spec.md) | | Операции/cursor/filters | [Account ledger](openspec/specs/brokerage-account-ledger/spec.md) |
| Будущие выплаты | [Upcoming events](openspec/specs/brokerage-account-upcoming-events/spec.md) | | Будущие выплаты | [Upcoming events](openspec/specs/brokerage-account-upcoming-events/spec.md) |
| Доходы и периоды | [Income analytics](openspec/specs/brokerage-account-income-analytics/spec.md) | | Доходы и периоды | [Income analytics](openspec/specs/brokerage-account-income-analytics/spec.md) |
@@ -24,6 +30,7 @@ types: [Источник HTTP-контракта](../docs/architecture/http-api.
| Каталог компонентов | [UI catalog](openspec/specs/frontend-ui-catalog/spec.md) | | Каталог компонентов | [UI catalog](openspec/specs/frontend-ui-catalog/spec.md) |
| Loading/skeleton | [Loading presentation](openspec/specs/frontend-loading-presentation/spec.md) | | Loading/skeleton | [Loading presentation](openspec/specs/frontend-loading-presentation/spec.md) |
| UI error/recovery | [UI resilience](openspec/specs/frontend-ui-resilience/spec.md) | | UI error/recovery | [UI resilience](openspec/specs/frontend-ui-resilience/spec.md) |
| Тихое чтение и редкие уведомления | [User feedback](openspec/specs/frontend-user-feedback/spec.md) |
| Query coherence/cache | [Query coherence](openspec/specs/frontend-query-coherence/spec.md), [data resilience](openspec/specs/frontend-data-resilience/spec.md) | | Query coherence/cache | [Query coherence](openspec/specs/frontend-query-coherence/spec.md), [data resilience](openspec/specs/frontend-data-resilience/spec.md) |
| OpenAPI generation | [API contract generation](openspec/specs/api-contract-generation/spec.md) | | OpenAPI generation | [API contract generation](openspec/specs/api-contract-generation/spec.md) |
| Logging/request context | [Structured logging](openspec/specs/backend-structured-logging/spec.md) | | Logging/request context | [Structured logging](openspec/specs/backend-structured-logging/spec.md) |
+91
View File
@@ -1,5 +1,96 @@
# Changelog # Changelog
## [0.1.6](https://git.personal.ksv741.ru/WealthLedger/specs/compare/v0.1.5...v0.1.6) (2026-10-24)
### Maintenance
* bump Node to 24.22.1 (WealthLedger/backend[#122](https://git.personal.ksv741.ru/WealthLedger/specs/issues/122)) ([c5b9cad](https://git.personal.ksv741.ru/WealthLedger/specs/commit/c5b9cadbbd2dfbb6f139bc71851117d81c0e7c6d))
* **deps:** update non-major dependencies (WealthLedger/specs[#56](https://git.personal.ksv741.ru/WealthLedger/specs/issues/56)) ([08646ff](https://git.personal.ksv741.ru/WealthLedger/specs/commit/08646ff505eed501e54314c07ca4d1076a4c37dd))
## [0.1.5](https://git.personal.ksv741.ru/WealthLedger/specs/compare/v0.1.4...v0.1.5) (2026-10-18)
### Specifications
* add five capability entries to AI_INDEX (WealthLedger/specs[#28](https://git.personal.ksv741.ru/WealthLedger/specs/issues/28)) ([d5d5480](https://git.personal.ksv741.ru/WealthLedger/specs/commit/d5d5480eedbc25c77c52b3b477de37e373beb8d9))
* align user-authentication password requirement with ASCII 8-72 policy (WealthLedger/specs[#46](https://git.personal.ksv741.ru/WealthLedger/specs/issues/46)) ([#47](https://git.personal.ksv741.ru/WealthLedger/specs/issues/47)) ([1bd6181](https://git.personal.ksv741.ru/WealthLedger/specs/commit/1bd618158588871f1801bfc6bead82fba31ce0ae))
* archive quiet-user-feedback and sync canonical specs ([#50](https://git.personal.ksv741.ru/WealthLedger/specs/issues/50)) ([f0478c6](https://git.personal.ksv741.ru/WealthLedger/specs/commit/f0478c6d6b5a5f259ea766276295758b34d240b3))
* archive three completed changes (main[#33](https://git.personal.ksv741.ru/WealthLedger/specs/issues/33), frontend[#63](https://git.personal.ksv741.ru/WealthLedger/specs/issues/63), screener) ([6efb989](https://git.personal.ksv741.ru/WealthLedger/specs/commit/6efb989ed9051981a314c8eb5714dffa88195a80))
* clarify SDD applies only on explicit request (WealthLedger/specs[#1](https://git.personal.ksv741.ru/WealthLedger/specs/issues/1)) ([7ebfc51](https://git.personal.ksv741.ru/WealthLedger/specs/commit/7ebfc510afb7440f85ecf95e2891d84c1b8ad843))
* fix upcoming events date filter default and picker bounds (WealthLedger/frontend[#63](https://git.personal.ksv741.ru/WealthLedger/specs/issues/63)) ([b4c0589](https://git.personal.ksv741.ru/WealthLedger/specs/commit/b4c0589745e0d1fb6b482addbea82d06c4924dad))
* reconcile quiet-user-feedback with source-state requirements ([#50](https://git.personal.ksv741.ru/WealthLedger/specs/issues/50)) ([5a7e319](https://git.personal.ksv741.ru/WealthLedger/specs/commit/5a7e319cafd8d9b6861b5a6bab7085bb57b08576))
* unify income payment cards and drop period comparisons (WealthLedger/main[#33](https://git.personal.ksv741.ru/WealthLedger/specs/issues/33)) ([#37](https://git.personal.ksv741.ru/WealthLedger/specs/issues/37)) ([6c858a8](https://git.personal.ksv741.ru/WealthLedger/specs/commit/6c858a8858dfdfc5c9607b3f925de5900ef1c903))
### CI
* rerun commits proof on PR title edit (WealthLedger/specs[#30](https://git.personal.ksv741.ru/WealthLedger/specs/issues/30)) ([4e3855f](https://git.personal.ksv741.ru/WealthLedger/specs/commit/4e3855fc3526e2b0b7ba80fd23b996d9993edcf5))
### Maintenance
* adopt linear git model in specs instructions ([#48](https://git.personal.ksv741.ru/WealthLedger/specs/issues/48)) ([06d08e2](https://git.personal.ksv741.ru/WealthLedger/specs/commit/06d08e29724e6cbe95eeca5ee1e6af7ec365a559))
* make OpenSpec toolchain explicit and diagnosable (WealthLedger/specs[#4](https://git.personal.ksv741.ru/WealthLedger/specs/issues/4)) ([#43](https://git.personal.ksv741.ru/WealthLedger/specs/issues/43)) ([7da2f90](https://git.personal.ksv741.ru/WealthLedger/specs/commit/7da2f90e0a6a7e0e5cb40b371c965a772aaa5db2))
## [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
* **deps:** patch release tooling advisories ([33cc4f3](https://git.personal.ksv741.ru/WealthLedger/specs/commit/33cc4f3e0c20ed3d6f5814cdc609724ffb8564ac))
### Specifications
* add reduce-screen-sync-polling change ([a1b0e99](https://git.personal.ksv741.ru/WealthLedger/specs/commit/a1b0e99bddb91f7b7ca729ddf6b7d6c688fdca41))
* simplify runtime requirements ([d861de5](https://git.personal.ksv741.ru/WealthLedger/specs/commit/d861de5ead41b4977083b4aee08fc668c35e5b34))
### Maintenance
* run commitlint in commit-msg hook ([0fd148f](https://git.personal.ksv741.ru/WealthLedger/specs/commit/0fd148ffbb39ea883a7e7799aa1b87c93b31fa17))
## [0.1.2](https://git.personal.ksv741.ru/WealthLedger/specs/compare/v0.1.1...v0.1.2) (2026-09-26)
### Specifications
* complete local frontend stack evolution ([ba76f6f](https://git.personal.ksv741.ru/WealthLedger/specs/commit/ba76f6fccc63b315e9ebf9d05ce181b283a6c094))
* complete persist-tbank-data implementation ([2f5814a](https://git.personal.ksv741.ru/WealthLedger/specs/commit/2f5814abdd52984bf1bcbd1d09b27ff4df3d7a25))
* define versioned deployment bundle ([7f40372](https://git.personal.ksv741.ru/WealthLedger/specs/commit/7f40372c3f67bb67cc77feaad17b61573c65f41e))
## [0.1.1](https://git.personal.ksv741.ru/WealthLedger/specs/compare/v0.1.0...v0.1.1) (2026-09-16)
### Documentation
* make specs instructions self-contained ([e4f10de](https://git.personal.ksv741.ru/WealthLedger/specs/commit/e4f10dedefde06d5c710ea36ab99b199cf2c1f7f))
### Specifications
* define containerized application delivery [#10](https://git.personal.ksv741.ru/WealthLedger/specs/issues/10) ([b34f23a](https://git.personal.ksv741.ru/WealthLedger/specs/commit/b34f23a23034195a6ae0ff324c20182e664bb26d))
## 0.1.0 (2026-09-14) ## 0.1.0 (2026-09-14)
### Features ### Features
+78 -9
View File
@@ -1,29 +1,98 @@
# WealthLedger Specs # WealthLedger Specs
Этот репозиторий — единственный versioned SDD authority рабочей области. Canonical OpenSpec store `wealth-ledger-specs` расположен в `openspec/` (`specs/openspec` от workspace), новые changes используют project-local schema `wealth-ledger-sdd`. Этот репозиторий — единственный versioned SDD authority рабочей области. Canonical OpenSpec store `wealth-ledger-specs` расположен в `openspec/` (`specs/openspec` от workspace). Новые changes используют `wealth-ledger-sdd-v2`; существующие active changes с `schema: wealth-ledger-sdd` завершаются по legacy schema без автоматической миграции.
## Начало работы ## Начало работы
OpenSpec применяется по явному запросу согласно [AGENTS](AGENTS.md). Proposal, Delta Specs, Design и Tasks создаются последовательно по одному после approval Change Owner. Для любой команды, читающей или изменяющей change, явно передавайте `--store wealth-ledger-specs`. OpenSpec применяется после явного решения Change Owner согласно [AGENTS](AGENTS.md). Для команд, читающих или изменяющих change, явно передавайте `--store wealth-ledger-specs`. Структурная готовность CLI не означает approval: перед каждым downstream artifact нужно явное решение Change Owner.
Design должен оценить `frontend`, `backend`, `specs`, `docs` в Delivery Map и при обязательном trigger включить Solution Assessment. Docs описывает as-built state и не становится параллельным store требований. | Change | Lifecycle |
| --- | --- |
| Active `schema: wealth-ledger-sdd` | Proposal → Delta Specs → Design → Tasks → Apply; действующие approvals сохраняются. Legacy Design оценивает `frontend`, `backend`, `specs`, `docs` в Delivery Map. |
| New `schema: wealth-ledger-sdd-v2` | Proposal → Delta Specs → System Analysis → Design + QA Analysis → Docs Impact → Tasks → Apply. Design и QA Analysis готовы после System Analysis; Docs Impact ждёт обоих. |
Для changes без изменения observable behavior используется `skip_specs: true`. В v2 System Analysis/QA Analysis/Docs Impact всё равно фиксируют только применимое содержание и обоснованное N/A, а не фиктивные contracts или tests. `specs/openspec/specs/` хранит нормативное поведение, `docs/` — текущий AS-IS, backend-generated OpenAPI — текущий executable HTTP contract. Target contracts остаются в change до архива; docs обновляются только по согласованному Docs Impact через `[docs]` tasks. После завершения последнего legacy change эту переходную заметку можно удалить отдельным изменением.
## Проверка ## Проверка
Для native validation из этого репозитория: Для native validation из этого репозитория:
```bash ```bash
npm run check:openspec
openspec schema validate wealth-ledger-sdd --verbose openspec schema validate wealth-ledger-sdd --verbose
openspec schema validate wealth-ledger-sdd-v2 --verbose
openspec status --change <change> --store wealth-ledger-specs
openspec validate <change> --type change --strict --store wealth-ledger-specs openspec validate <change> --type change --strict --store wealth-ledger-specs
openspec validate --specs --strict --store wealth-ledger-specs openspec validate --specs --strict --store wealth-ledger-specs
``` ```
Structural validation не заменяет semantic verification и independent read-only review перед archive. OpenSpec CLI ставится глобально и не закрепляется в `package.json`: его транзитивная
зависимость `braces` имеет advisory без исправления в 3.x. Ожидаемая версия хранится
в `openspec/.cli-version`; `npm run check:openspec` проверяет Node из `.nvmrc`, версию
CLI и доступность store, и при расхождении выводит команду исправления. Structural
validation не заменяет semantic verification и independent read-only review перед archive.
Node задаётся собственной `.nvmrc`; после `nvm use --silent` выполните `npm ci`.
`npm run verify` проверяет diff. OpenSpec validation остаётся отдельной проверкой
при изменениях требований. История релизов — [CHANGELOG](CHANGELOG.md).
## Релизы ## Релизы
Node задаётся собственной `.nvmrc`; после `nvm use --silent` выполните `npm ci`. Репозиторий выпускается независимо. `develop` — интеграция, `main` — принятые
`npm run verify` проверяет diff. Release tooling проверяется из root командой релизы. Обычная работа: issue-linked ветка от актуального `develop`, PR в
`node test-release.cjs specs`. OpenSpec validation остаётся отдельной проверкой `develop`; общие правила — в Engineering Workflow по ссылке из AGENTS.
при изменениях требований. [Ветки и выпуск](../README.md#релизы-и-ветки), PR сливается squash или rebase-merge; merge-коммит не используется. `main`
[история релизов](CHANGELOG.md). обновляется только fast-forward из `develop`, поэтому история линейна:
```text
<type>/<issue>-<slug> --squash--> develop --ff-only--> main --> tag vX.Y.Z
```
Инвариант: `origin/main` всегда предок `origin/develop`. Проверка:
```bash
git fetch -q origin
git merge-base --is-ancestor origin/main origin/develop && echo ok || echo BROKEN
```
Commits и PR titles — Conventional Commits со ссылкой на Issue (кроме release),
включая `spec:`. Версии вручную не повышай.
1. Получи `main`, `develop` и теги: `git fetch origin --tags`. Проверь
инвариант. Создай `release/X.Y.Z` от проверенного `develop`;
`nvm use --silent`, `npm ci`.
2. На чистой ветке выполни `npm run release:preview`, просмотри версию/changelog,
затем `npm run release:prepare`. Команда создаёт `chore(release): X.Y.Z`,
обновляет manifest/lockfile/CHANGELOG; тег и push не выполняет. Не обходи
guards ветки, чистого дерева, отсутствующих тегов или уже подготовленной версии.
3. Проверь diff и штатные gates этого репозитория. Отправь только release-ветку,
открой PR в **`develop`** с заголовком `chore(release): X.Y.Z`. После зелёных
`verify`, `security`, `commits` выполни fast-forward-only merge. Если
`develop` изменился, обнови release-ветку и повтори проверки кандидата.
4. Открой PR `develop → main` с тем же заголовком и слей fast-forward-only.
5. Получи удалённый `main`, сравни его HEAD с проверенным release SHA и версией
manifest/lockfile. Создай аннотированный `vX.Y.Z` именно на этом SHA:
`git tag -a vX.Y.Z <release-sha> -m "Release X.Y.Z"`, затем отправь только
`git push origin refs/tags/vX.Y.Z`. Не запускай release-it повторно на `main`.
Обратная синхронизация не нужна: release-коммит попал в `develop` на шаге 3.
Опубликованные теги не перемещай. При сбое после merge создай отсутствующий тег
без нового bump; до merge исправь и проверь существующий кандидат без
повторного повышения версии.
Freeze релиза не поддерживается: пока открыт релизный PR, `develop` не двигают.
Срочный fix — обычная ветка `fix/*` от `develop` с последующим релизом;
пути от `main` нет, он сделал бы `main` не-предком `develop`. Исключение
требует явного решения: `release/hotfix-<name>` от `main`, PR в `main`, тег,
затем merge `main → develop` для восстановления инварианта.
`feat:` повышает minor, `fix:`/`perf:` и прочие разрешённые типы — patch;
`!`/`BREAKING CHANGE:` — major, включая версии до 1.0. `chore(release):`
исключён из рекомендации и changelog. Конфигурация — `.release-it.json`.
В Gitea для `main`/`develop` должны быть запрещены прямой/force push и обход
администратором, включена блокировка устаревшей ветки, обязательны `verify`,
`security`, `commits`; теги `v*` защищены. Линейность обеспечивают merge styles
репозитория: merge-коммиты и rebase `--no-ff` выключены, доступны squash,
rebase-merge и fast-forward-only; стиль по умолчанию — squash.
Workflow сам не включает эти настройки. Отдельное review approval для одного
владельца не требуется. Npm publishing, deploy и Gitea Releases не включены.
+9 -1
View File
@@ -1,4 +1,12 @@
module.exports = { module.exports = {
extends: ["@commitlint/config-conventional"], 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"],
},
}; };
+1 -1
View File
@@ -1 +1 @@
1.10.0 1.13.1
@@ -0,0 +1,40 @@
# Completion Evidence: add-manual-bank-cards
## Finalization Authority
Change Owner authorized Finalization, canonical sync and archive after a current
independent read-only Validation PASS. The PASS found no Critical or Important
findings after the monthly-summary presentation correction.
## Delivery
| Area | Result | Delivery commit |
| --- | --- | --- |
| frontend | Manual card screens and summaries; Finalization correction shows exact expenses, refunds and net totals with a regression test. | `ff67a76`, `d3ac8c2`, `734c5bd` |
| backend | Bank-card persistence, calculations, API and isolated verification. | `e6f58ef` |
| docs | Cards overview states the three displayed summary totals. | `3a27268` |
| specs | Approved legacy Proposal, Delta Specs, Design, completed Tasks and Finalization evidence; native sync creates canonical behavior spec. | `834f2d1` plus finalization/archive commit |
## Observed Verification
| Area | Command or review | Result |
| --- | --- | --- |
| frontend | `npm run test:run -- src/entities/bank-card/ui/BankSummary/BankSummary.view.test.tsx` | PASS: 1 file, 1 test |
| frontend | `npm run typecheck` | PASS |
| docs | `npm run verify` | PASS |
| specs | `openspec schema validate wealth-ledger-sdd --verbose` | PASS |
| specs | `openspec schema validate wealth-ledger-sdd-v2 --verbose` | PASS |
| specs | `openspec validate add-manual-bank-cards --type change --strict --store wealth-ledger-specs` | PASS |
| specs | `openspec validate --specs --strict --store wealth-ledger-specs` | PASS: 25 specs, 0 failed |
| all affected areas | `git diff --check` | PASS |
## Independent Validation
PASS. The review compared Proposal, all 17 Delta Requirements, Design and
completed Tasks with current backend calculations/DTOs, shared frontend summary
rendering and cards documentation. It verified that `/cards` and `/cards/:id`
share the corrected summary view and found no Critical or Important findings.
Known non-blocking limitation: a plain Prisma fresh migration deploy still needs
`RUST_LOG=info`; the verified migration path and its evidence remain recorded in
the change Tasks.
@@ -0,0 +1,2 @@
schema: wealth-ledger-sdd
created: 2026-09-16
@@ -0,0 +1,31 @@
# Completion Evidence: containerized-application-delivery
## Outcome
The local and disposable single-server container delivery path is implemented
and verified. External publication and target-server gates remain unavailable in
the current environment and are recorded as blockers rather than represented as
successful results.
## Observed Results
- Frontend and backend development/production images build and run locally.
- Development HMR/watch, same-origin API routing, production SPA fallback,
readiness, persistence, migration failure handling, backup and isolated
restore were verified on disposable data.
- Docs verification and Compose configuration checks passed.
- No production data, secrets, registry credentials or real server were used.
## External Blockers
The following approved implementation tasks remain externally blocked:
- Docker-capable Gitea runner acceptance;
- registry publisher and server pull-only credentials;
- authenticated candidate publication and digest pull;
- target-server Docker/Compose verification;
- first server rollout pair confirmation.
These tasks remain unchecked in the archived checklist. The change is archived
as externally blocked after owner authorization; this is not a claim that the
external gates passed and does not authorize production rollout.
@@ -0,0 +1,247 @@
## Context
Frontend и backend выпускаются из независимых Git-репозиториев. Frontend —
Vite SPA, backend — NestJS API с embedded SQLite, Prisma migrations, native
Node dependencies и runtime `.proto` contracts. Браузерная сессия использует
Bearer access token и Secure httpOnly refresh-cookie с path `/api/v1/auth`.
На production-сервере уже работает Caddy. Он должен знать только домен и один
loopback port WealthLedger; TLS и server-wide routing остаются вне приложения.
Краткий простой обновляемого компонента допустим. Текущие кэши, rate limiter и
connection generation допускают только один активный backend instance.
Root workspace связывает независимые checkout, но его CI по действующему
контракту выполняет только commitlint. Поэтому orchestration принадлежит root,
а сборка и публикация каждого image — репозиторию компонента.
## Goals / Non-Goals
**Goals:**
- запускать весь application runtime локально через Docker с Linux, общей
container network, единым browser origin и быстрым frontend/backend watch;
- поставлять отдельные immutable frontend и backend images;
- публиковать наружу только frontend port, через который доступны SPA и API;
- хранить SQLite и backend secrets вне images и сохранять их при recreation;
- применять Prisma migrations до готовности новой версии backend;
- обновлять frontend или backend отдельно с явной парой установленных версий;
- дать оператору воспроизводимые backup, rollback и restore boundaries.
**Non-Goals:**
- zero-downtime, blue-green, несколько backend replicas и высокая доступность;
- PostgreSQL, Kubernetes, container orchestration вне одного сервера;
- управление Caddy, DNS, TLS, host firewall или provisioning сервера;
- изменение HTTP API, session semantics или пользовательского интерфейса;
- общий release номер для независимых репозиториев.
## Decisions
Уточнение Change Owner от 2026-09-16: обязательная настройка digests отменена.
Production принимает `FRONTEND_IMAGE`/`BACKEND_IMAGE` с release version tags.
Публикация сохраняет запрет перезаписи release tags; `latest` не используется.
### 1. Docker Compose владеет application topology
Root workspace получает production Compose и development override. Production
Compose использует опубликованные image references; development override
заменяет их local development targets, подключает исходники и отдельное local
data storage. Новые custom orchestration scripts не создаются: lifecycle
выражается штатными Compose commands и документированной последовательностью.
Production содержит два long-running services:
- `frontend`: nginx со статической SPA и proxy `/api/` в `backend:3000`;
- `backend`: один NestJS process с доступом к persistent SQLite storage.
Одноразовый migration service использует тот же backend image и data storage,
завершается до старта новой версии backend. Наружу публикуется только nginx на
настраиваемый loopback host port. Caddy проксирует весь домен на этот port и не
маршрутизирует `/api/` отдельно.
Development сохраняет те же component и network boundaries, но frontend target
запускает Vite с proxy `/api/` в `backend:3000`, а backend target запускает Nest
watch. Browser видит только frontend port. Source mounts не включают host
`node_modules`; Linux dependencies принадлежат images/volumes. Изменение
manifest или lockfile требует rebuild соответствующего development image.
Отдельный nginx gateway в development не добавляется: он создал бы третий
постоянный service, но всё равно использовал бы отличающуюся proxy-конфигурацию
для Vite HMR. Production image и его nginx routing проверяются отдельным
production Compose smoke scenario.
### 2. Frontend и backend имеют собственные multi-stage images
Frontend image собирает SPA точной project Node version и переносит только
build output и nginx configuration в version-pinned nginx runtime. Production
SPA использует same-origin API base. nginx поддерживает SPA history fallback,
передаёт `/api/` без переписывания prefix и повторно разрешает Docker DNS имя
backend после recreation.
Backend image собирается под Linux точной project Node version. Runtime содержит
compiled application, production dependencies, generated Prisma client,
migrations и T-Bank `.proto` contracts по пути, который использует production
process. Prisma CLI становится явным runtime tool backend image; test, lint и
compiler tooling в runtime не переносится. Process работает не от root и пишет
только в выделенный data path.
Оба images получают OCI metadata с repository revision и version. Base images
и package installs закрепляются воспроизводимыми версиями; новая platform или
architecture не обещается без отдельной проверки.
### 3. Gitea Container Registry хранит независимые release images
Frontend и backend workflows публикуют собственный image после push защищённого
release tag `vX.Y.Z`. Publish job повторяет канонический repository gate,
проверяет совпадение tag с package version и только затем отправляет semver tags в Gitea Container Registry. `latest` не используется.
Server deployment фиксирует полные frontend/backend image references с
semver tags; оператор выбирает опубликованную версию каждого компонента. CI использует отдельный secret с
package write permission, сервер — отдельный credential только для pull. Ни один
credential не хранится в repository или image. Возможность встроенного Gitea
workflow token публиковать packages не предполагается без проверки фактической
server version.
Root не получает image workflow: каждый repository самостоятельно доказывает и
публикует свой artifact, а Compose только фиксирует установленную пару image references.
### 4. Same-origin routing сохраняет действующую auth boundary
Production frontend nginx и development Vite направляют исходный `/api/...` в
backend без изменения path. Frontend client использует relative API base.
Backend получает `FRONTEND_ORIGIN`, равный browser-visible origin; production
работает с `NODE_ENV=production`, поэтому refresh-cookie остаётся Secure.
nginx передаёт host, forwarding headers и request ID context, не журналирует
Authorization, Cookie или request body. Backend port доступен только Compose
network. Server host публикует frontend на loopback, чтобы Caddy оставался
единственным внешним listener.
### 5. Readiness опирается на существующий runtime contract
Migration service должен завершиться успешно до запуска backend. Backend
healthcheck подтверждает завершённый HTTP-ответ от существующего auth route;
статус `401` без cookie является допустимым доказательством готового HTTP/auth
boundary. Новый публичный health endpoint не добавляется. Frontend healthcheck
получает статический root и зависит от healthy backend при полном старте.
Compose `--wait` используется как bounded readiness gate. Он не является
автоматическим rollback и не обещает отсутствие краткого `502` во время
отдельного backend update или недоступность всей точки входа во время frontend
recreation.
### 6. SQLite остаётся single-server storage в явно ограниченной роли
Production использует один backend process и один persistent host data
directory. Это расширяет прежнее локальное решение ADR 001: docs получают новый
ADR, который разрешает SQLite только для текущего single-server deployment и
фиксирует условия пересмотра — несколько replicas, иной availability target,
рост write contention или потребность в отдельном database service.
Перед migration backend останавливается и создаётся согласованная копия всего
data directory. Остановка исключает конкурентные writes и позволяет сохранить
database вместе с возможными WAL companion files без нового backup service.
Encryption key хранится отдельно от data backup; восстановление требует оба
артефакта.
Local development использует другой named volume/data directory и другие
secrets. Production data никогда не монтируется в development environment.
### 7. Обновления меняют только выбранный service
Frontend update меняет только frontend image reference, загружает image и
recreates frontend без зависимостей. Backend и data остаются запущенными, но
единая внешняя точка входа кратко недоступна во время recreation.
Backend update сначала сохраняет backup при остановленном backend, затем меняет
backend image reference. Migration service и backend используют один reference; backend
стартует только после успешной migration и становится доступен после healthcheck.
Frontend не пересоздаётся. nginx повторно разрешает service DNS после смены
backend container address.
Release compatibility остаётся обязанностью API evolution: новая сторона
ставится первой только когда прежняя сторона совместима с ней. Compose-файл
фиксирует пару image references, но не вводит второй version negotiation mechanism.
## UI/UX Applicability and Package
**Applicability and scope:** `N/A` — change меняет development, packaging и
server operations, но не экраны, состояния, interactions или responsive
поведение WealthLedger. Новый визуальный пакет не требуется.
## Delivery Map
| Area | Status | Architectural outcome or N/A reason |
| --- | --- | --- |
| frontend | `affected` | Multi-stage development/production image, same-origin client configuration, Vite API proxy, nginx SPA/API routing и image publication workflow. |
| backend | `affected` | Multi-stage development/production image, runtime Prisma migration assets, container readiness и image publication workflow. |
| specs | `affected` | Новая capability `application-delivery` и утверждаемый SDD package. |
| docs | `affected` | Deployment/runbook, обновлённая container topology и ADR ограниченного single-server SQLite deployment. |
Root workspace дополнительно владеет Compose orchestration и кратким entrypoint
README как cross-area coordination; это не переносит component build ownership
из frontend/backend и не добавляет root CI помимо commitlint.
## Solution Assessment
| Вариант | Fit и зрелость | Security / operations | Ограничения и migration cost | Решение |
| --- | --- | --- | --- | --- |
| Docker Compose + два component images | Нативно покрывает один server, service DNS, healthchecks, volumes и targeted recreation; широко поддерживается | Малый operating surface; registry credentials и host Docker требуют обычной защиты | Краткий простой, один host, нет automatic rollback | Выбран как минимальное полное решение |
| Один объединённый application image | Простая установка одного artifact | Один lifecycle и один process boundary проще оператору | Связывает независимые релизы, пересобирает неизменённый component, усложняет watch и нарушает repository ownership | Отклонён |
| Kubernetes или Swarm | Зрелая orchestration для replicas и rolling updates | Добавляет control plane, политики и постоянную эксплуатацию | Не нужен одному server и не решает текущие SQLite/multi-instance ограничения | Отклонён до появления HA требований |
| Caddy маршрутизирует отдельно frontend и backend | Использует существующий server proxy | Убирает internal gateway layer | Server должен знать внутреннюю topology, появляются два application upstream и deployment перестаёт быть self-contained | Отклонён по выбранной внешней границе |
| Отдельный постоянный gateway service внутри Compose | Стабильный port при frontend recreation | Может уменьшить внешний простой frontend update | Третий runtime service и конфигурация без требования zero-downtime | Отклонён; пересмотреть при SLO без краткого простоя |
| SQLite на persistent directory | Уже реализован, зрел для single-process embedded workload, не требует database service | Минимальная сеть и credentials; backup требует остановки | Один writer/process topology, нет HA; новая ADR boundary обязательна | Выбран в ограниченной single-server роли |
| PostgreSQL service | Зрел для concurrency, replicas и независимого DB lifecycle | Требует отдельные credentials, backup, upgrades и monitoring | Миграция Prisma provider и существующих данных вне scope; текущая нагрузка не требует | Отклонён до появления multi-instance/availability требования |
| Собственная packaging/orchestration layer | Может точно повторить частные команды | Создаёт собственный security и maintenance surface | Дублирует Compose, registry и standard image lifecycle | Отклонён |
Gitea Container Registry и Actions используются как уже доступные mature
platform capabilities. Предварительное условие Apply — runner с Docker build/push
capability и package permissions; при отсутствии этого условия Design не
заменяется самописным registry uploader, а publish task остаётся blocked.
## Risks / Trade-offs
- Frontend recreation кратко отключает и SPA, и API ingress; это принято
текущим non-goal zero-downtime.
- Backend update может кратко возвращать `502`; healthcheck ограничивает окно,
но не устраняет его.
- Старые browser tabs могут запрашивать chunks прежнего frontend. nginx задаёт
revalidation для HTML и immutable cache для hashed assets, но прежние assets
после recreation не сохраняются. Пользователю может потребоваться reload;
автоматическое восстановление старых вкладок остаётся вне текущего scope.
- SQLite на server расширяет прежний local-only baseline. Новый ADR делает это
осознанным ограничением, а не молчаливым изменением production guarantee.
- Воспроизводимость version tags зависит от запрета их перезаписи: workflow
отклоняет существующий tag. Переназначать опубликованные tags нельзя.
- Development работает через Vite, production — через nginx. Production Compose
smoke проверяет различающийся web-server boundary до поставки.
- Полная runtime image воспроизводимость зависит от base-image availability;
digests и сохранённые release images уменьшают, но не устраняют registry risk.
## Migration Plan
1. Утвердить SDD package и новый ADR о границе SQLite на одном сервере.
2. Подготовить component images и проверить их отдельно с каноническими gates.
3. Добавить root Compose topology и development override; подтвердить watch,
same-origin API, отсутствие внешнего backend port и изоляцию local data.
4. Добавить release-tag publication в component repositories и получить version tags
проверенных images из Gitea Registry.
5. На server создать restricted environment/secrets и data directories,
сохранить исходную базу и encryption key, затем выполнить первый migration
service и production smoke через loopback port.
6. Настроить Caddy на единственный loopback port и проверить login, refresh,
чтение существующих данных и один внешний financial request.
7. Зафиксировать установленную пару image references и удалить прежний runtime
только после успешной проверки нового.
### Rollback Contract
| Field | Decision |
| --- | --- |
| Status | `required` |
| Reason | Change вводит server persistence, migrations, secrets и новую delivery boundary. |
| Trigger | Migration failure, unhealthy component, невозможность login/refresh, нечитаемое encrypted connection, повреждение данных или критическая регрессия smoke scenario. |
| Actions | Не направлять Caddy на первый deployment либо вернуть предыдущий component image reference. Старую версию backend запускать только после проверки совместимости с текущей schema. Если schema несовместима, остановить приложение и отдельно предложить восстановление согласованного backup; не выполнять restore автоматически. |
| Data safety | Backup создаётся при остановленном backend из всего data directory; encryption key сохраняется отдельно. Данные, созданные после backup, считаются более новыми и не заменяются без явного решения оператора о потере этого интервала. |
| Verification | Проверить component health, schema migration status, login/refresh, чтение существующих user/card data, decrypt/use сохранённого T-Bank connection и отсутствие повторной migration ошибки; затем записать фактически установленную пару image references. |
@@ -0,0 +1,46 @@
## Why
WealthLedger сейчас запускается как набор локальных процессов, а поставка на
сервер не определена. Разработка не воспроизводит границы runtime-среды, а
оператор не имеет согласованного способа установить, обновить, сохранить или
восстановить приложение.
Нужен единый поддерживаемый путь запуска для локальной разработки и одного
сервера, чтобы поставка каждой версии была воспроизводимой и не требовала
ручного пересоздания окружения.
## What Changes
Будет определён поддерживаемый способ запускать WealthLedger локально и на
одном сервере. Внешняя серверная маршрутизация передаёт весь трафик
приложения на одну точку входа WealthLedger; приложение само обслуживает
интерфейс и API.
Оператор сможет независимо обновлять интерфейс или API согласованными
версиями поставки. Пользовательские данные, сессии и секреты сохраняются
между штатными перезапусками и обновлениями. Для обновления и восстановления
будут определены необходимые безопасные действия и их границы.
Не входят непрерывное обновление без краткого простоя, несколько экземпляров
API, высокая доступность, смена хранилища данных, изменение внешнего reverse
proxy и управление серверной инфраструктурой вне WealthLedger.
## Capabilities
### New Capabilities
- `application-delivery`: поддерживаемый локальный и серверный запуск,
обновление компонентов и сохранность состояния WealthLedger.
### Modified Capabilities
Нет.
## Impact
Пользователь получает одинаково работающую среду разработки и поставленное
приложение. Оператор получает воспроизводимую установку, независимое
обновление частей приложения и понятные границы восстановления данных.
Изменение затронет frontend, backend, specs, docs и общую orchestration
workspace; детали решений и delivery будут определены в последующих
утверждаемых артефактах.
@@ -0,0 +1,105 @@
## Purpose
Определяет поддерживаемые эксплуатационные свойства локального и серверного
запуска WealthLedger, включая поставку компонентов и сохранность состояния.
## ADDED Requirements
### Requirement: Локальная среда воспроизводит границы поставленного приложения
Поддерживаемый локальный запуск SHALL предоставлять один browser origin для
интерфейса и API WealthLedger. Разработка интерфейса MUST сохранять быстрый
цикл обратной связи при изменении исходного кода, а разработка API MUST
перезапускать затронутый runtime без ручной пересборки всей среды. Локальные
данные MUST быть изолированы от server data.
#### Scenario: Разработчик открывает локальное приложение
- **WHEN** поддерживаемая локальная среда успешно запущена
- **THEN** браузер получает интерфейс и обращается к API через тот же origin,
не зная внутреннего адреса API
#### Scenario: Разработчик меняет интерфейс
- **WHEN** разработчик изменяет исходный код интерфейса в запущенной локальной
среде
- **THEN** браузер получает изменение без ручной полной пересборки среды
### Requirement: Серверное приложение имеет одну внешнюю точку входа
Поддерживаемый server deployment SHALL принимать HTTP-трафик WealthLedger на
одной внешней точке входа. Через неё MUST быть доступны интерфейс и API с
сохранением их действующих путей и session semantics. API и хранилище данных
MUST NOT требовать отдельного внешнего сетевого доступа.
#### Scenario: Внешний reverse proxy направляет запрос приложению
- **WHEN** server reverse proxy передаёт запрос на единственную точку входа
WealthLedger
- **THEN** пользователь получает интерфейс, а запросы к действующему API
обрабатываются приложением без отдельного маршрута reverse proxy к API
### Requirement: Поставка допускает независимое обновление компонентов
Система SHALL предоставлять versioned delivery artifacts для интерфейса и API
как независимых компонентов. Оператор MUST иметь возможность установить
выбранную версию только одного компонента, сохранив другой компонент и
постоянное пользовательское состояние работающими. Поддерживаемая поставка
MUST явно фиксировать выбранные версии обоих компонентов.
#### Scenario: Обновляется только интерфейс
- **WHEN** оператор устанавливает новую версию интерфейса при неизменной
совместимой версии API
- **THEN** API и его постоянное состояние не пересоздаются, а приложение
начинает отдавать новую версию интерфейса
#### Scenario: Обновляется только API
- **WHEN** оператор устанавливает новую версию API при неизменной совместимой
версии интерфейса
- **THEN** интерфейс не пересоздаётся, а приложение начинает обрабатывать
запросы новой версией API после её готовности
### Requirement: Постоянное состояние и секреты переживают штатный lifecycle
Пользовательские данные, session state и ключи, необходимые для чтения
зашифрованных подключений, MUST сохраняться при штатных перезапусках и
обновлениях компонентов. Секреты MUST задаваться отдельно от versioned delivery
artifacts и MUST NOT попадать в интерфейс, журналы или публичный HTTP-ответ.
#### Scenario: Приложение штатно перезапускается
- **WHEN** оператор штатно перезапускает приложение без восстановления данных
- **THEN** после запуска доступны прежние пользовательские данные и действующие
session semantics, а сохранённые зашифрованные подключения остаются читаемыми
тем же ключом
### Requirement: Обновление API сохраняет целостность данных
Перед тем как новая версия API начинает принимать запросы, необходимые
изменения схемы постоянных данных MUST быть применены успешно. При ошибке
изменения данных новая версия API MUST NOT объявляться готовой. Краткий простой
обновляемого компонента допустим; непрерывность обслуживания и несколько
одновременных экземпляров API не являются обещанием этой capability.
#### Scenario: Подготовка данных новой версии не прошла
- **WHEN** обязательное изменение постоянных данных завершается ошибкой
- **THEN** новая версия API не начинает обслуживать запросы, а оператор получает
диагностируемый неуспешный результат обновления
### Requirement: Восстановление не выдаёт откат приложения за откат данных
Поддерживаемая процедура обновления и восстановления MUST различать возврат
версии компонентов и восстановление пользовательских данных. Она MUST требовать
согласованную резервную копию перед изменением постоянных данных и MUST NOT
заменять более новые пользовательские данные прежней копией как побочный эффект
возврата версии приложения.
#### Scenario: Оператор возвращает предыдущую версию API
- **WHEN** оператор возвращает прежнюю версию API после неуспешного обновления
- **THEN** процедура явно проверяет совместимость этой версии с текущими
данными и не выполняет восстановление старой копии данных без отдельного
решения оператора
@@ -0,0 +1,219 @@
# Контейнерная разработка и поставка WealthLedger
Основание — утверждённые [Proposal](proposal.md),
[Delta Specs](specs/application-delivery/spec.md) и [Design revision 1](design.md).
UI/UX package неприменим: пользовательские экраны и interactions не меняются.
Checkbox закрывается только после выполнения действия и его Verification.
Перед Node-командами активируется `.nvmrc` соответствующего независимого
репозитория. Git-команды выполняются из репозитория-владельца; production data,
secrets и опубликованные release tags не используются как test fixtures.
Задачи выполняются по порядку разделов. Frontend 2.1–2.2 и backend 3.1–3.2
могут идти параллельно после 1.2. Workspace orchestration начинается после
готовности обоих component images. Реальный server rollout выполняется только
после локального production smoke и документированного backup.
## 1. SDD и внешние prerequisites
- [x] 1.1 [specs] Проверить утверждённый SDD package закреплённым OpenSpec CLI и сохранить canonical change структурно валидным до Apply.
- **Covers:** `N/A` — обязательный gate canonical SDD authority; он не добавляет runtime behavior, но разрешает реализацию утверждённых требований и Design.
- **Verification:** с версией из `openspec/.cli-version` выполнить `openspec schema validate wealth-ledger-sdd --verbose`, `openspec validate containerized-application-delivery --type change --strict --store wealth-ledger-specs`, `openspec validate --specs --strict --store wealth-ledger-specs`, затем `npm run verify`.
- **Result (2026-09-16):** по явному уточнению владельца использован установленный OpenSpec 1.13.0 вместо 1.10.0. Все четыре проверки прошли; 25 specs passed, 0 failed. Repository-wide pin не изменён.
- [ ] 1.2 [workspace] Подтвердить prerequisites выбранного Solution Assessment: Docker Compose на development/server hosts, Docker-capable Gitea runner, Container Registry и раздельные package write/read credentials.
- **Covers:** `N/A` — техническое предусловие утверждённой Compose/Registry delivery; оно не вводит самостоятельное поведение приложения и блокирует custom fallback при отсутствии platform capability.
- **Verification:** зафиксировать поддерживаемые Docker/Compose и Gitea versions; `docker version`, `docker compose version`, успешные authenticated registry login для CI publisher и server pull-only identity без вывода credentials.
- **Partial result (2026-09-16):** локально Docker Desktop 4.91.0, Engine 29.8.0 (Linux arm64), Compose 5.5.1; Gitea API сообщает 1.27.3. Server Docker/Compose и Docker-capable runner пока не подтверждены. MCP identity не имеет доступа к Actions secrets (`user should be the owner of the repo`); раздельные publisher/pull-only login не проверены. Publication и server rollout остаются внешними gates; независимая локальная реализация продолжается.
## 2. Frontend image и ingress
- [x] 2.1 [frontend] Добавить development и production image targets: Dockerized Vite watch с same-origin `/api/` proxy и nginx runtime со SPA fallback, API proxy, runtime DNS re-resolution, cache policy и non-root-compatible filesystem.
- **Covers:** `application-delivery` — `Локальная среда воспроизводит границы поставленного приложения`; `application-delivery` — `Серверное приложение имеет одну внешнюю точку входа`.
- **Verification:** собрать оба targets; development container через container network отдаёт HMR и сохраняет `/api/v1/...`; production container отдаёт SPA/deep link, проксирует API без rewrite, не раскрывает backend port и после backend recreation подключается к новому container address.
- [ ] 2.2 [frontend] Добавить release-tag workflow публикации frontend image с проверкой версии, canonical frontend gate, OCI revision metadata и immutable semver tags без `latest`.
- **Covers:** `application-delivery` — `Поставка допускает независимое обновление компонентов`.
- **Verification:** workflow syntax принимается Gitea; controlled candidate run после `npm run verify` публикует image, registry возвращает digest, clean pull по digest запускает тот же static artifact; credential отсутствует в logs и image history.
- [x] 2.3 [frontend] Выполнить frontend repository completion gate после packaging и routing changes.
- **Covers:** `N/A` — repository-owned проверка реализации Design и отсутствие regression в существующем frontend; отдельного observable requirement не создаёт.
- **Verification:** `npm ci`, `npm run verify`, `git diff --check`; вручную просмотреть новые Docker/nginx/workflow files и отсутствие secrets.
### Результаты frontend (2026-09-16)
- 2.1: оба targets собраны на Linux arm64; Vite HMR WebSocket connected,
`/api/v1/probe?check=path` сохранён. nginx read-only + tmpfs отдаёт deep link
с `no-cache`, proxy сохраняет path/query/host/proto/request ID. Backend без
host port пересоздан с другим IP (.2 → .5); nginx восстановил proxy без restart.
- 2.2: release-tag workflow реализован с `ubuntu-latest` (label подтверждён
владельцем). Реальный runner Docker access, registry write/read credentials,
Gitea workflow acceptance и controlled publication пока не проверены.
- 2.3: `npm ci`, `npm run verify`, `git diff --check` прошли. Docker context
ограничен allowlist; `.env`, credentials и host dependencies не копируются.
Новые Docker/nginx/workflow files просмотрены; publication secrets идут
только в отдельный Docker login, не в image build arguments.
## 3. Backend image, migration и readiness
- [x] 3.1 [backend] Добавить development и production image targets с Linux native dependencies, compiled application, generated Prisma client, runtime Prisma CLI/migrations, T-Bank proto assets, non-root process и выделенным data path.
- **Covers:** `application-delivery` — `Локальная среда воспроизводит границы поставленного приложения`; `application-delivery` — `Постоянное состояние и секреты переживают штатный lifecycle`; `application-delivery` — `Обновление API сохраняет целостность данных`.
- **Verification:** development target выполняет Nest watch; production target стартует из чистого image, находит proto contracts, принимает config только из environment, пишет только в data path; одноразовый `prisma migrate deploy` проходит на fresh и representative existing SQLite, а приложение не стартует после контролируемой migration failure.
- [ ] 3.2 [backend] Добавить release-tag workflow публикации backend image с проверкой версии, полными backend gates, OCI revision metadata и immutable semver tags без `latest`.
- **Covers:** `application-delivery` — `Поставка допускает независимое обновление компонентов`.
- **Verification:** workflow syntax принимается Gitea; controlled candidate run выполняет `npm run verify` и `npm run test:e2e`, публикует image и получает digest; clean pull по digest выполняет migration и production start, credential отсутствует в logs и image history.
- [x] 3.3 [backend] Выполнить backend repository completion gates и проверить container readiness без нового публичного endpoint.
- **Covers:** `N/A` — repository-owned проверка packaging, existing auth boundary и SQLite integration из Design; самостоятельного нового API behavior нет.
- **Verification:** `npm ci`, `npm run verify`, `npm run test:e2e`, `git diff --check`; healthcheck принимает завершённый HTTP-ответ existing auth route, а stopped/uninitialized process остаётся unhealthy.
## 4. Workspace Compose orchestration
- [x] 4.1 [workspace] Добавить production Compose topology с version-tagged frontend/backend references, migration service, healthchecks, persistent host data directory, internal-only backend и единственным configurable frontend loopback port.
- **Covers:** `application-delivery` — `Серверное приложение имеет одну внешнюю точку входа`; `application-delivery` — `Поставка допускает независимое обновление компонентов`; `application-delivery` — `Постоянное состояние и секреты переживают штатный lifecycle`; `application-delivery` — `Обновление API сохраняет целостность данных`.
- **Verification:** `docker compose config` с non-secret fixture environment; rendered config содержит один host port, один backend replica, одинаковый backend image reference у migration/runtime и отдельный data mount; missing required image/config/secret values дают blocking configuration/start failure.
- [x] 4.2 [workspace] Добавить development Compose override с local image targets, source watch/mounts, container-owned dependencies, отдельным local data volume и одним frontend browser origin.
- **Covers:** `application-delivery` — `Локальная среда воспроизводит границы поставленного приложения`; `application-delivery` — `Постоянное состояние и секреты переживают штатный lifecycle`.
- **Verification:** с чистыми local volumes запустить Compose; frontend edit появляется через HMR, backend edit перезапускает Nest, `/api/v1/...` идёт через frontend origin, host `node_modules` не используется, restart сохраняет local data, production data path отсутствует в rendered config.
- [x] 4.3 [workspace] Обновить root tracking allowlist и README entrypoints без custom scripts или нового root CI, затем проверить orchestration configuration.
- **Covers:** `N/A` — root только связывает независимые component repositories и объясняет стандартные Compose entrypoints согласно Design; runtime behavior реализуется component images и Compose tasks 4.1–4.2.
- **Verification:** `docker compose config` для production и development variants, `git diff --check`, просмотр `git status --short` и staged paths подтверждают только явно разрешённые root files; root workflow остаётся только commitlint.
## 5. Эксплуатационная документация
- [x] 5.1 [docs] Добавить ADR об ограниченном single-server SQLite deployment и обновить container architecture фактической Docker/Caddy topology, secret и persistence boundaries.
- **Covers:** `application-delivery` — `Серверное приложение имеет одну внешнюю точку входа`; `application-delivery` — `Постоянное состояние и секреты переживают штатный lifecycle`.
- **Verification:** ADR явно связывает решение с прежним local-only baseline, одним backend instance и triggers пересмотра; architecture diagram показывает только frontend port наружу, internal backend и persistent SQLite; ссылки и термины согласованы.
- [x] 5.2 [docs] Описать development, first deployment, independent component update, stopped-backend backup, compatibility check, rollback и explicit restore runbooks.
- **Covers:** `application-delivery` — `Локальная среда воспроизводит границы поставленного приложения`; `application-delivery` — `Поставка допускает независимое обновление компонентов`; `application-delivery` — `Обновление API сохраняет целостность данных`; `application-delivery` — `Восстановление не выдаёт откат приложения за откат данных`.
- **Verification:** dry-run runbook на disposable data проходит без неописанного шага; frontend-only и backend-only paths не пересоздают другой component; restore требует отдельного решения и сообщает о потере данных после backup.
- [x] 5.3 [docs] Выполнить docs repository completion gate.
- **Covers:** `N/A` — repository-owned проверка durable as-built documentation и ссылок без нового runtime behavior.
- **Verification:** `npm ci`, `npm run verify`, `git diff --check`; проверить Mermaid rendering, взаимные ADR/architecture/runbook links и отсутствие credentials или host-private values.
## 6. Сквозная проверка поставки
- [x] 6.1 [workspace] Проверить development Compose на чистом checkout и production Compose на локально собранных release-equivalent images.
- **Covers:** `application-delivery` — `Локальная среда воспроизводит границы поставленного приложения`; `application-delivery` — `Серверное приложение имеет одну внешнюю точку входа`.
- **Verification:** development подтверждает HMR/watch и same-origin login; production smoke подтверждает SPA deep link, registration/login/refresh, API proxy, отсутствие host backend port и clean startup через migration service.
- [x] 6.2 [workspace] На disposable representative SQLite проверить persistence, frontend-only update, backend-only update, migration failure и rollback boundaries.
- **Covers:** `application-delivery` — `Поставка допускает независимое обновление компонентов`; `application-delivery` — `Постоянное состояние и секреты переживают штатный lifecycle`; `application-delivery` — `Обновление API сохраняет целостность данных`; `application-delivery` — `Восстановление не выдаёт откат приложения за откат данных`.
- **Verification:** restart сохраняет user/session/card data и decryptable test connection; frontend update не recreates backend/data, backend update не recreates frontend; failed migration не делает backend healthy; возврат совместимого image сохраняет current data, а restore отдельной backup copy выполняется только в изолированном scenario с явной проверкой потерянного интервала.
- [ ] 6.3 [workspace] Проверить release images из Gitea по выбранным version tags перед первым server rollout и сохранить точную установленную пару версий.
- **Covers:** `application-delivery` — `Поставка допускает независимое обновление компонентов`; `application-delivery` — `Постоянное состояние и секреты переживают штатный lifecycle`.
- **Verification:** pull-only server identity получает оба versioned images; `docker image inspect` подтверждает expected revisions; production smoke проходит на этих images с disposable config/data, после чего оператор записывает выбранные image references без secrets.
## Apply verification — 2026-09-16
### Backend (3.1–3.3)
- Development и production targets собраны на Linux arm64 / Node 24.21.0.
Node/nginx base images закреплены digest; OS packages — точными версиями.
Чистый production runtime обнаружил неверный прежний fallback proto path:
Nest копирует assets в `dist/src`; loader теперь использует `__dirname`.
Nest watch использует `ps`; `procps` добавлен только в development target.
- `npm ci`, `npm run verify` — PASS (35 suites). `npm audit` при переносе
существующего Prisma 7.10.0 в runtime dependencies — 0 vulnerabilities.
- `npm run test:e2e` на macOS дважды завершился native Node SIGABRT
(`RemoveEnvironmentCleanupHook`, `env != nullptr`). Тот же штатный gate
выполнен в Linux development image: **8 suites / 98 tests PASS**. Это
Linux delivery proof; macOS native cleanup limitation не скрывается.
- `npm run test:container -- wealthledger-backend:production` — runnable
image regression: fresh/existing migration, runtime без source/.env/compiler,
non-root + read-only filesystem, существующий auth readiness, user/session
persistence. Он же вызывается publish workflow после clean pull.
- Controlled broken SQLite migration возвращает non-zero; backend остаётся
остановленным. После возвращения исправного data path migration идемпотентна.
- 3.2 остаётся открытой: YAML и shell syntax проверены локально, но Gitea
acceptance, authenticated publication и pull по registry digest не выполнены.
### Compose, docs и lifecycle (4–6)
- Production/development `docker compose config` — PASS. Проверены один
loopback host port, backend scale 1, общий backend image/data mount у
migration/runtime, readiness dependency и отсутствие production bind mount
в development services. Пустая production environment блокирует config.
- `docker compose up -d --wait` — PASS для development и production на
локально собранных release-equivalent images. Production smoke использовал
временный override local image references; canonical config остаётся
digest-pinned. Реальный Gitea digest pull этим не подменяется.
- Development: source edit вызвал HMR event и изменённый HTTP module, backend
source edit — новый Nest process и готовый API. Временные изменения
восстановлены. Same-origin registration/login/refresh и data restart PASS.
- Clean checkout proof: отдельный HEAD export + task diff, без host .env и
node_modules, новый Compose project/volume; startup и HMR/watch PASS.
- Browser proof (bundled Playwright/Chrome): clean development и production
registration/login/reload-refresh PASS; все API requests same-origin,
httpOnly/path сохранены, production cookie Secure. Caddy/TLS вне локальной проверки.
- Production: SPA deep link, registration/login/refresh, неизменённый API path,
отсутствие backend host port, non-root read-only runtime — PASS.
- Representative disposable SQLite содержит user/session/card и connection,
зашифрованный настоящим TbankCrypto на вымышленном token. Restart, migrations
и independent recreation сохраняют данные и расшифровку без внешнего T-Bank.
- Отдельные local fixture image IDs проверили frontend-only/backend-only
update без recreation другого компонента. Возврат прежнего совместимого
backend image сохраняет запись после backup. Это compatibility fixture,
не обещание совместимости произвольных будущих релизов.
- Stopped-backend backup и isolated restore с отдельным project/port/data path
— PASS: login, карта до backup и decryptable connection доступны; запись
после backup отсутствует в восстановленной копии. Текущая база не заменялась.
- Docs: ADR 015 расширяет ADR 001; topology и runbook взаимно связаны.
`npm ci`, `npm run verify` — PASS (44 Markdown files), `git diff --check`
— PASS. Mermaid source семантически просмотрен, но renderer в доступном
окружении не найден; **5.3 оставлена открытой до проверки rendering**.
- Root Boy Scout/allowlist review: только Compose, два безопасных env examples,
README и .gitignore; новых root scripts/dependencies/CI нет. Source markers
watch checks восстановлены. Чужие runtime data и checkout changes сохранены.
- Docker disk был заполнен. После адресной очистки intermediates этой задачи
и отдельно разрешённой очистки unused build cache старше суток проверки
продолжены; чужие images/containers/volumes не удалялись.
### Оставшиеся внешние gates
- 1.2: server Docker/Compose, Docker capability runner `ubuntu-latest`,
раздельные package writer и server pull-only authenticated login.
- 2.2/3.2: реальные Gitea candidate workflows и публикация frontend/backend,
OCI revisions, registry digests, clean pull и отсутствие credentials в logs.
Нужны Actions secrets `REGISTRY_USERNAME`/`REGISTRY_TOKEN` в каждом component
repo и отдельная server identity. Workflow не публикует `latest` и fail-closed
отказывается от overwrite или недоказанного отсутствия tag.
- 6.3: smoke опубликованной пары version tags на disposable server config/data,
запись installed image references/revisions до первого server rollout.
Server rollout, Caddy, TLS и реальный финансовый запрос не выполнялись.
### Упрощение deployment configuration по запросу владельца
Обязательные `FRONTEND_DIGEST`/`BACKEND_DIGEST` и отдельные repository fields
заменены на `FRONTEND_IMAGE`/`BACKEND_IMAGE` с release version tags.
Design и runbook синхронизированы. Результаты выше описывают первоначальную
проверку; обязательный digest pinning более не является deployment contract.
Migration и backend по-прежнему используют один image reference.
Verification упрощения: production/development `docker compose config` PASS;
разные version tags выбираются независимо, migration/runtime получают один
backend image, один loopback port и development volume сохранены. Пустые
image/secret values блокируют config. Docs `npm run verify`, OpenSpec strict
change validation и `git diff --check` в affected repos — PASS.
По следующему запросу владельца из `FROM` удалены base image digests:
используются `node:24.21.0-bookworm-slim` и `nginx:1.28.0-alpine`.
Version tags не гарантируют неизменность содержимого базового image.
Verification: повторная сборка production/development targets обоих компонентов
— PASS (с использованием Docker cache). Dockerfile остаются в component repos,
чтобы standalone build и release CI не зависели от root checkout.
По запросу владельца удалены дополнительные commit tags из обоих workflows;
release tag защищён от перезаписи, revision остаётся в OCI label. Production
Compose наследует healthchecks из images; development содержит собственные
checks с прежними таймингами, migration отключает check.
Verification: production/development Compose config и invariants, healthchecks
локальных production images, shell syntax обоих workflows, docs verify,
OpenSpec strict validation и diff checks — PASS. Registry publication остаётся
внешней непроверенной частью задач 2.2/3.2.
@@ -0,0 +1 @@
schema: wealth-ledger-sdd
@@ -0,0 +1,440 @@
## Context
Локальная архитектура реализована; независимая публикация отложена. Baseline исследования — frontend `3c99deb`, версия `0.3.0`,
2026-09-20. Требования — в
[proposal](proposal.md) и [delta](specs/frontend-design-system/spec.md).
В приложении уже работают React, TypeScript, Vite, Mantine, TanStack Query,
RHF/Zod, JSON-токены, общий адаптер темы, Storybook, Vitest/RTL/MSW,
dependency-cruiser и Knip. Они сохраняются. Другие production-потребители
общего UI или клиентского SDK в исследованном workspace не обнаружены.
На 2026-09-22 workspace-пакеты `@wealthledger/tokens`, `@wealthledger/ui` и
`@wealthledger/icons` используются локально как private packages. Это принятая
часть перехода. Описанные далее независимый release и Nexus остаются
предложением, отложенным до оценки пилота. Фактическое выполнение и ограничения
сохраняются в [tasks](tasks.md).
## Goals / Non-Goals
**Goals:** три небольшие границы поставки общего фундамента; единый каталог
источников; сохранение внешнего вида и предметных инвариантов; потребление
собранного пакета; ограниченные задачи, исполнимые по одной сессии.
**Non-Goals:** другой UI framework, второй transport/store/form stack,
универсальные CRUD/table/form engines, монорепозиторий всего WealthLedger,
mobile adapter, тёмная тема, массовый rewrite CSS и тестирование внутренностей
Mantine. Календарь платежей требует отдельного продуктового запроса.
## Decisions
### 1. Репозиторий и направления зависимостей
Рекомендация: **npm workspaces внутри существующего frontend**. Корневое
приложение сохраняет `src`, Vite entry, собственные providers и `private: true`.
Новые пакеты располагаются в `packages/tokens`, `packages/icons`, `packages/ui`.
Имена `@wealthledger/*` приняты для локальных packages; регистрация scope
и внешняя registry-поставка не подтверждены.
| Вариант | Польза | Цена в этом проекте | Решение |
| --- | --- | --- | --- |
| Три workspace-пакета в frontend | Один lockfile, toolchain, Storybook и атомарный review источников с потребителем | Нужно явно расширить scopes проверок; локальный link может скрыть дефект поставки | Выбран; обязательна проверка packed consumer |
| Один отдельный design-system repo с тремя пакетами | Отдельный owner, release cadence, независимые потребители | Новый CI/catalog/install, синхронизация PR и доступа к registry при одном приложении | Отложить до второго независимого production consumer либо отдельного владельца/release cadence |
| Три отдельных Git repo | Полностью независимые права/жизненные циклы | Три workflow и совместимость без подтверждённой выгоды | Не выбирать |
| Только существующий shared | Уже работает, нет стоимости публикации | Не даёт запрошенной границы версионируемой поставки | Сохраняется до готовности package foundations, не конечная цель |
Направление стрелки — «потребляет»; циклов нет:
```text
frontend app / единственный Storybook
├─ @wealthledger/ui ── @wealthledger/tokens
│ └─ Mantine + React (peers у ui)
├─ @wealthledger/icons ── React (peer)
└─ entities/features/pages → shared/api, domain models, i18n
tokens → нет runtime dependencies
ui → нет icons dependency до появления реального общего consumer
icons → нет ui/tokens/Mantine/router/API dependencies
```
Общие компоненты принимают уже подготовленные строки и slots. `EntryCard`
принимает `ReactNode` и не знает, какой пакет рисует icon/action. Domain выбирает
цвет по смыслу и форматирует деньги; UI только отображает переданное значение.
### 2. Публичная поверхность и сборка
Имена ниже в колонке «Цель» — принятые публичные exports локальных пакетов.
Внутренние `src`, `dist/*` и application alias `@/` не являются package API.
| Пакет | Цель / entrypoints | Состав поставки | Зависимости |
| --- | --- | --- | --- |
| tokens | `@wealthledger/tokens/tokens.json` | Доверенный JSON v1; JS, CSS, React-адаптер и `.d.ts` на старте не нужны | Нет runtime dependencies |
| icons | Именованные `AddIcon`, `EditIcon`, `DeleteIcon` из `@wealthledger/icons` | ESM JS + declarations; CSS только если переносится собственная анимация | React peer + external; React DOM не используется |
| ui | `@wealthledger/ui`: `StateView`, `LoadingSkeleton`, `EntryCard` и их необходимые props types; `@wealthledger/ui/theme`: `theme`, `colorRoles`, `cssVariablesResolver`; `@wealthledger/ui/styles.css` | ESM JS + declarations + собранный CSS, включая CSS Modules | React/React DOM и используемые Mantine core/hooks — peers/external; tokens — точная dependency |
`designTokens` нужен foundations story: адаптер может сохранить существующий
одноимённый export из `ui/theme`, который ссылается на JSON, а не копирует его.
JSON остаётся отдельно доступным без Mantine. Не добавлять JS-обёртку в tokens
только ради импорта JSON. Не выдавать non-web adapter за готовый.
Использовать установленный Vite для library build ui/icons и TypeScript для
declarations. Один `tsc` недостаточен для CSS Modules `EntryCard`. Формат — ESM;
CJS/SSR build не нужен текущему Vite SPA. Настройки library build ещё предстоит
проверить на зафиксированной Vite 8.3.0; готовых конфигураций здесь нет.
Публикуемые `files`/`exports` ограничивают поверхность. CSS явно включается в
side effects; JS импорт сам не устанавливает provider и не читает приложение.
Library build использует отдельную конфигурацию нужных plugins, не наследует
app Vite config целиком: PWA/manifest/service worker и dev API proxy не должны
попасть в поставку библиотеки.
Exports указывают на собранные artifacts, поэтому обычные local/CI entrypoints
сначала собирают packages, затем выполняют consumer typecheck/tests/build.
Сейчас `verify` начинает проверки до app build; этот порядок потребуется
дополнить при 2.2. Acceptance начинается на чистом checkout после `npm ci`,
без старого package dist/cache. Достаточно последовательности npm scripts,
без собственного build orchestrator.
Docker — отдельный потребитель workspace: нынешний `Dockerfile` копирует только
root manifests перед `npm ci`, а `.dockerignore` не допускает packages.
В 2.1–2.2 нужны точечные manifests/source/build-config allowlists, установка
с workspace manifests и package build перед consumer. Host node_modules,
готовый локальный dist и реальные `.env` в контекст не попадают. Development
и production targets проверяются отдельно. В текущем Compose монтируются
только app src/public: изменения packages сначала требуют rebuild dev image.
Отдельный package bind-mount/watch в Compose отложен до фактической потребности;
host development может использовать package watch без изменения root repo.
Сначала peer ranges узкие, по проверенной комбинации React 19.3.0 и Mantine
9.6.1; расширять их только после проверки потребителя. React/React DOM
devDependencies для разработки не должны попадать в bundle. Проверять
`npm ls react react-dom @mantine/core` и содержимое bundle/packed consumer;
`dedupe` не заменяет корректную упаковку. Две несовместимые версии Mantine
также нельзя скрыть локальным symlink.
### 3. Токены: использовать уже существующий JSON
В [tokens.json](../../../../frontend/packages/tokens/tokens.json) уже есть
переносимые значения и описания. **Не менять формат v1 ради упаковки.**
`primitive → semantic → component` — модель владения и ссылок, а не повод
немедленно переименовать все ключи в новый стандарт.
| Уровень | Текущая основа и целевое правило |
| --- | --- |
| Primitive | `color.base`, `color.palettes`, `typography`, `spacing.values`, `radius.values`, `shadows.values`, `breakpoints.values`; исходные эффективные значения сохраняются |
| Semantic | Существующие `color.roles.*` с `color` + `shade` ссылаются на primitive; `primary` с `shade: null` сохраняет разрешение через primaryShade |
| Component | Пока новые не нужны. Добавлять только именованный совместно управляемый контракт реального общего компонента, если semantic role недостаточно; ссылаться на semantic, затем primitive при отсутствии смысловой роли |
Имена описывают роль (`surface`, `border`, `positive`), а не экран или случайный
цвет. Ссылки однонаправленные и разрешимые; обратных ссылок из primitive на
semantic/component нет. Новый color role включает назначение и ограничение
применения. Семантический положительный цвет не вычисляет знак финансовой суммы.
Сохранить единицы: размеры/тени в px адаптируются существующими `rem`/`rgba`,
breakpoints в em не масштабируются, line-height без единицы. Имеющиеся
[theme tests](../../../../frontend/packages/ui/src/theme.test.ts) защищают
именно преобразования и ссылки, не каждое значение JSON.
Общие CSS-роли `--wl-color-*` производит прежний `CSSVariablesResolver`.
Spacing/radius/type scales продолжают поступать в Mantine theme и его CSS
variables. Статическая генерация второго `tokens.css` не нужна: появился бы
ещё один адаптер и риск расхождения scale. JSON экспортируется без сборочного
token engine; Style Dictionary, DTCG-converter и runtime schema отложены до
конкретного внешнего потребителя/формата. Источник остаётся доверенным файлом,
не внешним пользовательским документом.
Светлая схема сохраняется. `primaryShade.dark` не означает готовую dark theme.
Бизнес-коды, суммы/валюты, московские даты, pagination limits, brand gradients
`BankCardFace` и случайная геометрия SVG не превращаются в глобальные токены.
Первое применение semantic roles — точные эквиваленты gray-3/gray-0/green-6/
red-6 в `EntryCard`; перенос и эта правка идут отдельно.
### 4. Mantine, тема и providers
`Stack`, `Group`, `Button`, `Paper`, `ActionIcon`, поля, Modal, Dates/Charts
остаются прямыми импортами Mantine. Изменение defaults решается темой.
Wrapper нужен только собственному контракту: busy region у `LoadingSkeleton`,
empty/error composition у `StateView`, layout/image fallback/actions у `EntryCard`.
App владеет MantineProvider, DatesProvider, QueryClientProvider, AuthProvider,
Router и i18n. Storybook имеет свой Mantine/Dates/i18n runtime с той же темой.
UI не создаёт `WealthLedgerProvider`, не импортирует локализацию продукта и
не включает app providers. `ui/styles.css` содержит существующую визуальную
основу и стили общих компонентов; app/preview подключают его явно после
Mantine CSS. Продуктовые route transitions и bank-card styles остаются в app.
Theme Modal transition и глобальная CSS animation сейчас сосуществуют.
Их визуальный конфликт не доказан. После выделения выбрать один механизм
открытия по browser comparison, предпочтительно уже настроенный Mantine
transition; нельзя удалить второй механизм на основании одного сходства кода.
### 5. Иконки и движение
Начальный набор — реально повторяющиеся plus/pencil/trash из Card/Cards views.
Имена `AddIcon`, `EditIcon`, `DeleteIcon` — предлагаемая поверхность, не найденные
exports. Размер задаёт `size` (default 24), геометрия сохраняет `viewBox`, цвет
получается через `currentColor`. Существующие размеры и strokeWidth 1.5/2
сохраняются у callers; выравнивание рисунка не совмещается с переносом.
Канонический исходник SVG для небольшого набора — **SVG-разметка в именованных
React-компонентах** `packages/icons/src/svg/<Name>.tsx`. Это один редактируемый
источник, без параллельных ручных `.svg` и `.tsx`. Отдельные raw SVG-файлы в
первой поставке не публикуются: non-React consumer пока отсутствует. Если
потребуются исходные `.svg` как самостоятельный продуктовый артефакт, перенести
канон в `src/svg/*.svg` и сравнить штатный SVGR CLI с ручным поддержанием;
не писать собственный генератор. Сейчас ни dependency, ни pipeline для этого
не нужны. JS/types несут SVG-компоненты, это явно отражено в составе поставки.
По умолчанию рисунок декоративный (`aria-hidden`, без фокуса). У standalone
meaningful icon потребитель задаёт доступное имя и image semantics. У
`ActionIcon` имя остаётся на действии; SVG не дублирует его. Общий пакет не
выбирает подпись «редактировать операцию» и не получает DTO.
`BankOperationIcon` и `operation-appearance.ts` остаются в entity: mapping
category/kind → рисунок/цвет/анимация — бизнес-логика. Собственная геометрия и
движение рисунка могут перейти в icons позже при реальном reuse. Hover/focus
trigger по сценарию, route transitions, bank/card state остаются приложению.
Существующий `bind-icon-animation` переиспользуется, если переносится тот же
animation contract; он не копируется в пакет ради трёх статичных иконок.
Все переносимые движения обязаны сохранять reduced-motion поведение.
### 6. Каталог источников компонентов
Порядок поиска: **существующие компоненты приложения → собственная библиотека
→ Mantine → уже разрешённые dependencies → обоснованное новое решение**.
Новый компонент появляется у владельца реальной ответственности, а не потому,
что текущий компонент отсутствует в Storybook. Таблица — компактный маршрут,
не каталог каждого JSX-узла. Номера обозначают этапы в [tasks](tasks.md).
| Назначение | Текущий источник | Целевой источник | Правило применения | Этап |
| --- | --- | --- | --- | --- |
| Layout, buttons, inputs, dialogs | `@mantine/core`, `@mantine/dates` | Те же пакеты | Прямые imports; variant/default через theme, не WL-копия каждого primitive | 1, 2 |
| Цветовые роли, шкалы | `shared/theme/tokens.json` | `tokens/tokens.json` + адаптер `ui/theme` | JSON — значения, адаптер — Mantine, бизнес-смысл — consumer | 2 |
| Тема/общие стили | `shared/theme` | `ui/theme`, `ui/styles.css` | Одна тема App/Storybook; providers у runtimes | 2 |
| Busy skeleton region | `shared/ui/LoadingSkeleton` | `ui` | Label и region общие, skeleton shapes принадлежат странице | 3.1 |
| Empty/error | `shared/ui/StateView` | `ui` | Строки/action props; query/error classification вне UI | 3.1 |
| Строка с изображением и значением | `shared/ui/EntryCard` | `ui` | Готовые strings/slots, fallback и responsive actions; без money parsing | 3.2–3.3 |
| Plus/edit/delete | Inline SVG Card/Cards views | `icons` | Named imports; label и disabled у ActionIcon | 4 |
| Навигация и active route | `shared/ui/AppShell` | `app/ui` | Product shell; не публичный компонент ui-пакета | 1.1 |
| Карта и её операции | `entities/bank-card` | Там же | `BankCardFace`, `BankOperationIcon`, `BankSummaryView`, `BankCardFields`, `BankFeedback` — domain | Без переноса |
| Период банковских данных | `entities/bank-card/BankPeriodPicker`, model/period | Там же | Draft/URL/query semantics сохраняются; не универсальный date service | 5, без extraction |
| Период дохода | `pages/account-income/.../IncomePeriodForm.view` | Та же page; pure helpers в её model | Изолировать story от VM, сохранить validation/submit | 5.3 |
| Позиции/структура портфеля | `pages/account/ui/components/PositionCard.view`, `PortfolioStructure.view` | Там же | Page-local композиция допустима; не переделывать под EntryCard | Без переноса |
| Недавние события | `entities/brokerage-event` + Account page | Там же; общий EntryCard из ui | Первый продуктовый пилот, финансовое представление у entity | 3.2 |
| Общие instrument details | `features/instrument-details` | Там же | Реальный reuse stocks/bonds; typed presentation contract отдельно от общего UI | Без переноса |
Последние непакетные источники выбираются через существующие `index.ts`.
В таблице обозначения `shared/ui/...` описывают файлы до миграции; это не разрешение
на deep import. Текущие владельцы указаны в колонке цели.
### 7. Предметный UI, API и конечные словари
Рекомендация: оставить domain UI в `entities/features/pages`. Второй
производственный consumer, которому нужен тот же стабильный domain contract,
и отдельный release owner — проверяемое условие рассмотрения business package.
Storybook не считается таким потребителем. Повторная карточка может потребовать
переноса page → entity внутри приложения, но не нового npm-пакета.
`shared/api` сохраняет snapshot, generated types, typed transport и auth-aware
boundary. Генерируются **типы**, а не весь handwritten client. Query keys,
normalization, freshness и invalidation остаются в entity/feature owner;
не входят ни в generated artifact, ни в ui. Отдельный `api-client` сейчас
потребовал бы отделить Vite environment, auth/retry lifecycle и query policy без
второго потребителя. Вернуться к вопросу при втором независимом приложении.
Backend остаётся единственным editable HTTP source. Frontend получает
предоставленный snapshot, локально генерирует и сравнивает declarations;
соседний backend checkout не становится входом CI. DTO aliases из generated
`paths` сохраняются. UI form models допустимы, когда отличаются от transport.
Для frontend собственных конечных словарей — принятый `as const` + union:
`enum` несовместим с текущим `erasableSyntaxOnly`. Набор operation kinds можно
свести у bank entity и проверить через generated `BankOperation['kind']`;
не объявлять вторую DTO enum. Wire literals сохраняются. `ProblemDetailsDto.code`
остаётся `string`, unknown code получает fallback; нельзя объявлять закрытый
список ошибок. Общий `apiErrorCode` извлекает code, одинаковое recovery
классифицируется ближайшим допустимым owner без обратных FSD imports.
### 8. Политика библиотек
Версии закреплены в frontend package manifests и lockfile. Решения ниже
не означают новой установки или обновления.
| Область | Сейчас | Решение и причина | Этап |
| --- | --- | --- | --- |
| Build/React/Router | Vite, TS, React Compiler, React Router | Сохранить; расширить имеющийся build для library artifacts | 2 |
| Server/local state | TanStack Query / React state / URL | Сохранить owners; Redux из transitive deps не делает его app store | Все |
| Forms/validation | RHF, resolvers, Zod | Сохранить; vocabulary у entity, controller/view банковской формы отдельно | 5.2, 5.4 |
| UI/charts | Mantine; Recharts/react-is обеспечивают peer graph charts | Сохранить, не удалять peer deps из-за отсутствия direct imports | Все |
| Styles/условные классы | CSS Modules, template/ternary, data-* | Сохранить один этот подход; clsx/classnames не вводить без сложности, которой здесь нет | 3–5 |
| Icons | Inline SVG | Унифицировать повторяющиеся общие рисунки в малом собственном наборе | 4 |
| Motion | motion для routes, CSS/native для рисунков | Сохранить по разным владельцам; проверить дублирование Modal отдельно | 5.1 |
| Dates | Day.js + Mantine Dates + native Date/Intl | Сохранить роль каждого; Moscow/ISO policy остаётся domain/page | 5.3 |
| Money | String + BigInt cents, display formatters | Сохранить точность и unknown≠zero; decimal library не добавлять | Все |
| API/errors | openapi-fetch/react-query/typescript | Сохранить; не добавлять axios, второй generator или schema registry | Все |
| Tests | Vitest/RTL/MSW | Сохранить; `fireEvent` принят, user-event только transitively | 2–5 |
| Catalog | Storybook 10.6.0 | Один runtime, расширить glob и границы на packages | 2–4 |
| Quality | Biome, Knip, dependency-cruiser | Дополнить действующие scopes, не ставить Steiger/ещё один analyzer | 1–2 |
| Browser/visual CI | Принятого E2E runner/visual diff нет | Ручной browser proof пилота; Playwright — при повторяемой browser-only регрессии и владельце CI | Отложено |
| Release | release-it + native npm + Gitea | Использовать текущий подход, адаптировать workspace releases; Changesets/Lerna не нужны для трёх пакетов | 6 |
### 9. Предлагаемое уточнение conventions
Это предложения для будущих задач; действующие инструкции сейчас не меняются.
AGENTS остаётся коротким router, постоянный каталог переносится в подходящий
раздел existing UI convention/README после подтверждённого перехода.
| Владелец правила | Конкретное уточнение | Когда |
| --- | --- | --- |
| `.ai/architecture.md` | Package graph вне FSD; src public APIs; app shell; никакого package → app или same-layer обхода | 1–2 |
| `.ai/conventions/ui.md` | Каталог источников и search order; тема против wrapper; props-only общие компоненты | 2–3 |
| UI + Storybook | JSON/semantic roles, explicit CSS import, светлая схема, иконки/currentColor/reduced-motion | 2–4 |
| `.ai/conventions/data.md`, TypeScript/React | Generated vocabulary vs local const unions; errors открыты; package не владеет query state | 5.2 |
| Engineering/dependencies | Workspace direct deps, peers, exact lock, packed consumer, ограничения exports | 2, 6 |
| Storybook | Один runtime, colocated package/product stories, новые reachability scopes | 2–3 |
| Testing | Поведение → риск → минимальная boundary; package artifact contract; browser proof не равен build | 2–3, 6 |
### 10. Storybook и проверки
Один существующий frontend Storybook показывает `Design System` и `Product`.
Общие stories переезжают вместе с ui/icons; foundations читают опубликованный
JSON/адаптер. Product stories остаются рядом с entities/pages. Обновляются
только globs/aliases/check scopes, не создаётся отдельное приложение каталога.
Mantine/theme/Dates/i18n уже есть в preview; Query/Auth/API providers не нужны.
Synthetic props и callbacks сохраняются; MSW остаётся в Vitest, не в worker.
| Наблюдаемый контракт | Минимальный способ доказать |
| --- | --- |
| JSON → размеры/тени/semantic CSS, unscaled breakpoints | Перенести существующие theme unit tests; не проверять каждый literal |
| JS/types/CSS/JSON доступны вне workspace | Pack + чистый consumer с type/build/render и без source aliases |
| EntryCard image fallback, actions при focus/touch | Один небольшой RTL case на собственный fallback/действие, synthetic story и browser review |
| Деньги/version/idempotency/invalid input | Существующие targeted RTL/MSW tests формы; новые cases только если меняется соответствующая boundary |
| Query/auth/recovery | Имеющиеся MSW tests; no real backend; не тестировать это через stories |
| Keyboard/name/reduced motion/narrow layout | Выбранные собственные interactions в browser; примитивные гарантии Mantine не переписывать |
`build:catalog` не исполняет `play`; существующий `storybook.test.tsx` проверяет
лишь выбранные группы. Не обещать screenshot regression по факту сборки.
Snapshot всех компонентов и полный новый E2E stack не планируются.
Новые workspace-папки не охвачены автоматически: нынешние TS/Knip/deps/story
настройки главным образом ориентированы на `src`. Package type/build/lint,
dependencies и запрет app imports вводятся вместе с соответствующим пакетом.
Нельзя временно исключить packages целиком и назвать gate полным. Same-layer
rule сначала проверяется на текущем graph, затем включается для чистого scope;
legacy exception имеет имя, причину и задачу удаления. Existing coverage floors
не понижаются; перенос tests/coverage ownership не должен скрыть потерю сценария.
### 11. Локальная работа и потребление
Приняты приватные npm workspaces внутри frontend: один install/lockfile,
сборка пакетов и потребление через exports. Проверки охватывают приложение,
пакеты и Storybook. Собранные tarballs проверены вне workspace без source aliases;
эта проверка выявляет отсутствующие exports, типы, CSS и несовместимые peers.
Публикация, независимый SemVer, package release automation и настройка Nexus
не входят в текущий переход. Вернуться к ним можно при подтверждённой пользе
для независимого потребителя; тогда отдельно определить registry, права,
неизменность версий, совместимость и откат. Текущий app release не меняется.
## UI/UX Applicability and Package
**Applicability: N/A для нового визуального дизайна.** Цель — сохранение уже
существующего приложения, без новых экранов и рисунков. Новый макет/visuals и
hash registry не создаются. Для проверки переноса выбран следующий scope.
Пилот: `AccountPage → useAccountPageViewModel → useBrokerageEvents →
BrokerageEventsPreview → BrokerageEventCard → EntryCard`. Это read-only
preview выплат; `PositionCard` не использует EntryCard и не подходит для
переноса без изменения представления. Generic icons в пилоте отсутствуют и
мигрируют отдельно в Card/Cards.
Сохраняются loaded/loading/error/empty, missing amount как unknown, а не ноль,
registry-date/account metadata, валюта, logo fallback, длинные строки,
preview limit и ссылка полного списка. Сверяются desktop и viewport 375px,
доступный focus и отсутствие горизонтального overflow. У generic EntryCard
actions дополнительно проверяются fine/coarse pointer и reduced motion на
synthetic story. Текущий RecentEvents error-priority не меняется молча.
## Delivery Map
| Area | Status | Architectural outcome or N/A reason |
| --- | --- | --- |
| frontend | affected | Workspaces tokens/icons/ui, ограниченная миграция, checks и package release integration |
| backend | N/A | Пакеты не меняют HTTP/auth/storage; замечание об errors вынесено в отложенный scope |
| specs | affected | Этот change и новые требования к поставке/иконкам; main specs обновятся только после принятой реализации |
| docs | affected | После поставки уточняются design overview и описание frontend container/release boundary |
## Solution Assessment
| Решение | Fit / зрелость / ограничения / стоимость | Выбор |
| --- | --- | --- |
| Существующие npm workspaces + Vite/TS | Стандартная упаковка; не требует нового Git repo; CSS/peers и published surface нуждаются в проверке | Да |
| Отдельный design-system repo | Ясная независимость ownership, но удваивает окружение каталога/CI при одном app | Позже по явному consumer/owner trigger |
| Style Dictionary или собственный token compiler | Полезны нескольким форматам/платформам; сейчас JSON + Mantine adapter уже решают задачу | Нет; runtime trust boundary не меняется |
| Ручные React SVG / SVGR pipeline | Несколько рисунков дешевле поддерживать в одном TSX source; SVGR нужен каноническим raw SVG exports | Ручные SVG-компоненты сейчас, без генератора |
| release-it/native npm / Changesets | Первый стек уже принят; второй полезен большому потоку независимых release contributions | Сохранить существующий app release; независимый package release отложен |
Новые внешние runtime dependencies не требуются. Риск supply chain сохраняется
под существующим security gate; peer/version checks и registry ACL нельзя
заменить предположением о готовом Nexus.
## Risks / Trade-offs
- Workspace links скрывают отсутствующие exports/assets и конфликт React:
независимый packed consumer обязателен.
- Package graph расширяет проверяемую область: старый зелёный `deps src` не
подтверждает новые packages. Scopes расширяются при появлении владельца.
- CSS order/global reset влияют на всё приложение. Сначала перенос без
изменения значений, затем отдельно semantic roles и Modal consolidation.
- Общие исходники в frontend — осознанный выбор для одного потребителя;
публикация не означает уже существующий независимый каталог/команду.
- Изменение view/controller рядом с денежной формой рискованнее UI extraction:
это отдельная поздняя задача с сохранением tests version/idempotency.
## Migration Plan
Границы owners → package foundations → общие компоненты и read-only пилот →
иконки и точечное выравнивание → packed consumer → удаление совместимости.
Единственный подробный backlog и зависимости находятся в [tasks](tasks.md).
Во время перехода `shared/theme` и три старых UI entrypoints могут быть тонкими
re-exports package API. Они не содержат второй JSON, CSS или реализации.
Новые consumers используют package paths; добавлять exports/логику в facade
запрещено. Удаление — когда поиск оставшихся runtime/test/story imports даёт
ноль и app/catalog/packed consumer прошли gates. Старые inline icons живут до
своего consumer batch, но новые копии извлечённых рисунков не добавляются.
### Rollback Contract
| Field | Decision |
| --- | --- |
| Status | required: меняются локальные imports, сборка и владельцы компонентов |
| Trigger | Нет exports/CSS/types, peer conflict, несовместимое представление либо failed pilot/recovery |
| Actions | Вернуть согласованный diff приложения, packages, imports и lockfile; при выпуске приложения использовать прежний app image |
| Data safety | API/storage/money wire format не меняются; не откатывать чужие продуктовые изменения и не сбрасывать пользовательские данные |
| Verification | Повторить соответствующий consumer gate и пилот на прежней версии; проверить восстановление согласованного состояния приложения и packages |
### Осознанно отложенное
| Находка/возможность | Причина и условие возврата |
| --- | --- |
| F8: RecentEvents error приоритетнее cached data | Реальное отличие от общего recovery convention. Не менять при extraction; отдельная product recovery задача после согласования expected stale/error UX, с refetch-error regression test |
| F9: ProblemDetails errors schema/example и validation mapping | Отдельная backend contract задача: metadata + HTTP behavior test, затем предоставленный snapshot; не основание вручную исправлять frontend DTO |
| F12: Основная frontend-quality-guardrails spec требует initial 500 kB и baseline lazy chunks, а действующий verify не содержит такого gate | Отдельно согласовать актуальность сохранённого требования и способ enforcement. Не считать Vite warning блокирующим budget gate; package migration сравнивает фактический размер сборки, не заявляя выполнения отсутствующей проверки |
| Единый recovery classifier всех investment errors | Одинаковые codes ещё не доказывают одинаковый UX; объединять только подтверждённый общий recovery без нарушения FSD |
| Business/API packages, dark/mobile, raw SVG pipeline, Playwright/visual service | Нет принятого второго consumer или значимого browser-only regression scope |
## Решения для подтверждения перед зависимыми действиями
1. Публичные имена `@wealthledger/tokens`, `@wealthledger/icons`, `@wealthledger/ui`,
scope ownership и видимость. Рекомендуется приватная поставка WealthLedger;
другое решение меняет доступ и release setup, не текущие данные.
2. Nexus npm endpoint, владелец publish/read credentials, политика неизменности
и хранения прошлых версий. Эти решения нужны только при отдельном запросе внешней публикации.
3. Расхождение F12 между сохранённой spec и текущими quality-командами:
рекомендуемый default — сохранять требование до отдельного решения владельца,
не ослаблять его и не объявлять выполненным. Автоматизация либо корректировка
требования оформляются отдельным ограниченным scope после уточнения причины.
Остальное — аргументированные defaults данного предложения, а не список
неразрешённых вопросов. Перед реализацией нужен отдельный запрос пользователя;
workspaces внутри frontend уже реализованы, независимые версии остаются отложенными.
@@ -0,0 +1,69 @@
## Why
У WealthLedger уже есть общий визуальный фундамент, каталог компонентов и
типизированное взаимодействие с сервером. Следующий шаг — сделать границы
переиспользования и поставки этого фундамента явными: участник должен быстро
находить подходящее решение, а приложение — получать проверенную совместимую
версию без копирования реализаций.
Исследование текущего frontend выявило конкретные незавершённые границы:
повторяющиеся рисунки иконок, обход существующих цветовых ролей, продуктовую
навигацию в общем UI и неполную автоматизацию архитектурных ограничений.
## What Changes
**Статус: локальная реализация и итоговая проверка завершены.**
Локальные границы приложения и private workspace-пакеты реализованы;
проверенные пункты и оставшиеся ограничения перечислены в [tasks](tasks.md).
Независимое версионирование и публикация через Nexus отложены до оценки пользы
пилота и не входят в требования этого перехода.
Первоначальный комплект плановых Markdown-файлов был подготовлен по запросу
пользователя в одной сессии. Последующие разрешения касались ограниченных
этапов; они не разрешают публикацию, deployment или archive.
- Выделить используемый общий визуальный фундамент в приватные workspace-пакеты,
сохранив внешний вид и работоспособность приложения между небольшими этапами.
- Установить единый порядок выбора компонентов и понятную границу между
общим представлением, предметным UI и сценариями приложения.
- Сделать повторно используемые собственные иконки доступными с единым
визуальным и доступным контрактом.
- Подтвердить переход существующим небольшим продуктовым срезом и проверкой
потребления собранных артефактов, а не только локальных исходников.
- Уточнить применимые инженерные правила после соответствующих изменений,
сохранить существующие проверки и поэтапно закрыть найденные пробелы.
Сохраняются денежная точность, обработка неизвестных значений, поведение
авторизации, запросов, форм, восстановления после ошибок и существующих страниц.
Предметные расчёты и серверные контракты не становятся частью общего UI.
Не входят редизайн, тёмная тема, мобильный адаптер, календарь обязательных
платежей, смена работающего стека, переустройство всех репозиториев и массовое
перемещение компонентов. Публикация, deployment, commit, push, PR и archive не разрешены.
Локальная реализация ограничена отдельно согласованными этапами.
## Capabilities
### New Capabilities
Нет: новая capability на каждый пакет или каталог не требуется.
### Modified Capabilities
- `specs/frontend-design-system/spec.md`: добавить требования к потреблению
локальных пакетов общего фундамента и доступному контракту собственных
общих иконок. Существующие требования к единой теме и предметной независимости
сохраняются. [Delta](specs/frontend-design-system/spec.md).
## Impact
- Frontend: поставка общей основы, выбор источников компонентов, ограниченная
миграция потребителей и развитие существующих проверок.
- Backend: производственный контракт и поведение не меняются; отдельное
замечание к описанию ошибок остаётся отложенным исследованием владельца API.
- Specs: этот change и delta; основные as-built требования пока не изменяются.
- Docs: описание текущего локального устройства обновлено в 7.2;
независимая registry-поставка не заявляется.
Решения, альтернативы и каталог источников — в [design](design.md), единственная
очередь работ и текущее состояние — в [tasks](tasks.md).
@@ -0,0 +1,60 @@
## ADDED Requirements
### Requirement: Общий фундамент доступен через проверяемые локальные пакеты
Общий визуальный фундамент SHALL принадлежать приватным npm workspaces внутри
frontend и иметь явную публичную поверхность. Приложение SHALL использовать
пакеты через объявленные exports. Собранные артефакты MUST содержать модули,
типы, стили и данные согласно контракту каждого пакета и быть пригодными для
проверки вне рабочего дерева frontend. Общие пакеты MUST NOT требовать серверных
данных, авторизации, маршрутов или состояния WealthLedger. Совместимость пакетов
между собой и с приложением SHALL проверяться; workspace links MUST NOT быть
единственным подтверждением пригодности артефактов. Независимые релизы и
публикация в registry не входят в этот переход.
#### Scenario: Приложение потребляет собранную поставку
- **WHEN** потребитель устанавливает собранные артефакты в чистом окружении без
ссылок на рабочее дерево поставщика
- **THEN** публичные модули, типы, стили и данные доступны, общий компонент
отображается на заявленной теме без зависимостей от приложения поставщика
#### Scenario: Локальная разработка скрыла отсутствующий артефакт
- **WHEN** локальная ссылка разрешает импорт, отсутствующий в собранной поставке
- **THEN** поставка не признаётся готовой по результату одной локальной сборки;
потребление именно собранных артефактов должно быть подтверждено отдельно
#### Scenario: Приложение использует локальные пакеты
- **WHEN** frontend устанавливает зависимости из своего lockfile и собирается
- **THEN** приватные workspaces доступны через публичные exports без публикации
в registry; предметные компоненты и API-клиент остаются в приложении
### Requirement: Собственные общие иконки имеют доступный контракт
Общие иконки SHALL иметь предсказуемые размеры и возможность получать цвет
от потребителя, не определяя бизнес-смысл операции или состояние приложения.
Декоративная иконка MUST NOT создавать отдельную точку фокуса или повторять
доступное имя соседнего действия. Смысловая иконка без сопровождающего текста
MUST иметь доступное имя. Собственное движение иконки SHALL учитывать
предпочтение уменьшенного движения; выбор момента запуска по пользовательскому
сценарию SHALL оставаться у потребителя.
#### Scenario: Иконка находится в именованном действии
- **WHEN** собственная иконка дополняет кнопку с доступным именем
- **THEN** кнопка сохраняет имя и клавиатурное поведение, а рисунок декоративен
и не создаёт дополнительную точку фокуса
#### Scenario: Рисунок самостоятельно передаёт смысл
- **WHEN** иконка используется без текста и без именованного родительского
элемента как самостоятельное изображение
- **THEN** потребитель задаёт доступное имя, передающее её назначение
#### Scenario: Пользователь уменьшил движение
- **WHEN** включено предпочтение уменьшенного движения
- **THEN** необязательная анимация общей иконки не выполняется, а значение и
доступность действия сохраняются
@@ -0,0 +1,239 @@
# План перехода frontend
**На 2026-09-22 выполнены и проверены 1.1–1.2, 2.1–2.2, 3.1–3.3, 4.1–4.2, 5.1–5.4 и 7.1–7.3** по отдельному
разрешению пользователя. Остальные задачи — предложения, не разрешение
на реализацию или публикацию. Результаты и ограничения приведены ниже.
Прочитать [proposal](proposal.md), затем нужное решение [design](design.md).
Здесь единственный backlog инициативы, отдельного roadmap нет.
## Дорожная карта и зависимости
```text
1.1 AppShell owner ────────┐
1.2 import enforcement ────┴─ 2.1 tokens/workspaces ─ 2.2 ui theme
│
3.1 common states
│
3.2 EntryCard + pilot ─ 3.3 roles
2.1 ─ 4.1 icons ─ 4.2 Card/Cards consumers
2.2 ─ 5.1 Modal review
1.2 ─ 5.2 operation vocabulary ─ 5.4 form boundary
1.2 ─ 5.3 income period helper boundary
3.3 + 4.2 + packed consumer proof ─ 7.1 retire facades
Выполненные локальные 1–5 ─ 7.2 текущее as-built описание
3.2–3.3 + 5.1–5.4 + 7.1–7.2 ─ 7.3 acceptance всей инициативы
```
1.1 и 1.2 независимы по смыслу, но могут менять общие конфиги: параллельные PR
согласуют diff. 5.1–5.4 не блокируют выпуск основы и не входят скрыто в пилот.
Иконки не нужны read-only пилоту RecentEvents. Пункты внешней публикации удалены из текущего плана по решению пользователя:
пакеты остаются приватными workspaces frontend. Остальные
задачи используют предложенные defaults после отдельного запроса реализации.
### Общие условия каждой будущей сессии
- Работать в repo владельца, сначала его AGENTS и только triggered conventions;
проверить Git status, текущий HEAD и чужие изменения. План привязан к
исследованным ревизиям, не к обещанию актуального remote develop.
- Перед Node-командой активировать местную `.nvmrc`: `nvm use --silent`.
Для code/dependency PR frontend требует `npm ci` и итоговый `npm run verify`;
targeted команды ниже дополняют этот gate, не заменяют его.
- Scope PR — одна задача. Не смешивать перемещение с новым поведением,
обновлением версий библиотек и исправлением соседнего продукта.
- Значимый тест начинается с наблюдаемого результата/риска; сохранять имеющиеся
regression tests. Не добавлять тест wrapper existence, Mantine internals
или snapshots всех вариантов ради переноса.
- По завершении повторить routing, проверить diff/новые файлы, Boy Scout review,
`git diff --check`, записать реально выполненные команды и ограничения.
Непройденная проверка не отмечается как выполненная задача.
### Реальные команды и будущие проверки
Сейчас доступны: `npm run deps`, `npm run deps:storybook`, `npm run knip`,
`npm run lint`, `npm run typecheck`, `npm run openapi:check`,
`npm run test:run -- <существующий path>`, `npm run test:coverage`,
`npm run build`, `npm run build:catalog`, `npm run verify`,
`npm run release:preview`. Их поведение исследовано, но они **не запускались**
в текущей Markdown-сессии. `typecheck` может записать tsbuildinfo;
tests/builds пишут cache/coverage/dist/catalog. `openapi:generate` меняет
tracked declarations и не нужен этой миграции.
Package build/declarations/check scopes и packed consumer проверены.
Независимый package release не входит в текущий план. Не
считать вымышленный `test:packages` или `release:packages` действующей командой.
Native `npm pack --dry-run`, `npm pack`, `npm ls` использованы для проверки пакетов. `release:prepare` приложения
создаёт commit; не использовать для пробного package release.
## 1. Уточнить владельцев и существующие ограничения
- [x] 1.1 [frontend] Перенести route-aware AppShell к владельцу приложения.
- **Covers:** `N/A` — техническое исправление F3; product shell не должен стать публичной частью ui, согласно design §1/4.
- **Verification:** `npm run deps`, `npm run typecheck`, `npm run knip`, существующие route/navigation tests, итоговый `npm run verify`; browser navigation/focus и active route.
- **Читать / область:** `.ai/architecture.md`, UI, `src/app/ui/AppShell.tsx` и CSS (до переноса — shared/ui), его callers в `app/routes`, связанные auth/route tests. Найти все imports до переноса.
- **Изменить / готово:** AppShell и принадлежащие ему navigation styles находятся в `app/ui` с допустимым public import. Labels, links, Outlet, transitions, responsive shell прежние; shared/ui не знает product routes. Коротко уточнён legacy пункт architecture.
- **Не входит / сохранить:** Новая навигация, новый wrapper shell, миграция всех SVG, auth policy. Никаких изменений URL и bootstrap states.
- **Откат:** Вернуть перенос и imports одним PR; данных/внешних artifacts нет. Зависимость: нет.
- [x] 1.2 [frontend] Закрыть same-layer imports действующим dependency-cruiser.
- **Covers:** `N/A` — enforcement уже принятого FSD profile, F4; не новая пользовательская возможность.
- **Verification:** Зафиксировать вывод существующего `npm run deps` до правки; после неё тот же gate отклоняет временный контролируемый forbidden import между двумя slices, чистый graph проходит; удалить пробный import, затем `npm run verify`.
- **Читать / область:** Architecture/imports, `dependency-cruiser.cjs`, реальные alias/relative imports в pages/features/entities; public API примеры account-operations и bank-card.
- **Изменить / готово:** Точные запреты соседних slices pages/features/entities, без запрета imports внутри одного slice и без ошибочного применения к shared segments. Если baseline выявит legacy, правило включается для чистой области с адресным исключением и условием удаления, без broad ignore.
- **Не входит / сохранить:** Новый analyzer, автоматический fixer, массовая раскладка FSD, запрет page-local composition. Не писать постоянный анализатор проверки анализатора.
- **Откат:** Вернуть только rule change, обязательный semantic review остаётся. Зависимость: нет.
## 2. Подготовить пакеты без изменения представления
- [x] 2.1 [frontend] Ввести workspace tokens и охват его контрактов.
- **Covers:** `frontend-design-system` — `Общий фундамент доступен через проверяемые локальные пакеты`
- **Verification:** Будущая package-конфигурация: `npm pack --dry-run` показывает JSON; чистый временный consumer вне workspace читает JSON обычным parser; `npm ci`, расширенные существующие type/lint/Knip/deps scopes, `npm run verify`; чистые Docker builds development/production с новым workspace manifest и JSON.
- **Читать / область:** Engineering/dependencies, package/lock, TS/Knip/deps configs, `Dockerfile`, `.dockerignore`, `packages/tokens/tokens.json`, `shared/theme/theme.ts`, `theme.test.ts`; design §1–3/10. После 1.1–1.2; имена остаются предлагаемыми до принятия design.
- **Изменить / готово:** `packages/tokens` владеет тем же JSON v1; root app private; один npm workspace lock, прямые зависимости, ограниченные files/exports. Адаптер читает package JSON. Проверки явно видят packages и запрещают package → app. Docker получает workspace manifests перед npm ci и только нужные package inputs через узкую allowlist, без host node_modules/dist/.env.
- **Не входит / сохранить:** Новая schema, Style Dictionary, mobile/dark, преобразование всех токенов и CSS. Не поддерживать две редактируемые копии JSON; app appearance и все units прежние.
- **Откат:** Вернуть JSON/imports/manifest/lock как связанный diff; package ещё не опубликован.
- [x] 2.2 [frontend] Упаковать Mantine adapter и общие стили в ui.
- **Covers:** `frontend-design-system` — `Общий фундамент доступен через проверяемые локальные пакеты`
- **Verification:** Существующие theme tests на новом owner; будущие Vite library build + declaration emit и packed consumer type/build/render без app alias; на чистом checkout после `npm ci`, без прежнего package dist/cache, `npm run verify` сначала собирает packages, затем проверяет consumer. `npm ls react react-dom @mantine/core`; отдельные чистые Docker development/production builds и render темы.
- **Читать / область:** `packages/ui/src/{theme.ts,styles.css,theme.test.ts}`, facade `shared/theme/index.ts`, `app/ui/App.tsx`, `.storybook/{main.ts,preview.tsx,dependency-cruiser.cjs}`, `vite.config.ts`, `vitest.config.ts`, package scripts, `Dockerfile`, `.dockerignore`, design §2–4. Зависимость: 2.1.
- **Изменить / готово:** `ui/theme`/`ui/styles.css`, dependency tokens, peers/external React/Mantine, CSS side effects, JS/d.ts/CSS в package. App/preview явно подключают styles и прежние providers. Package build предшествует всем проверкам потребителя в local/CI/Docker; scope tests/coverage следует owner. `shared/theme` только re-export. Host dev допускает package watch; в Docker изменение packages требует документированного rebuild dev image, без нового root orchestration.
- **Не входит / сохранить:** Новый global provider, новый Storybook app, component wrappers, Modal animation cleanup. Library build не наследует app PWA/service-worker/proxy plugins. Не менять light scheme, source values, CSS order и effective theme; сравнить фактический bundle size с baseline, не заявляя отсутствующий budget gate F12.
- **Откат:** Вернуть adapter/styles/imports и manifest/lock единым PR. Временный facade удаляется в 7.1, новые consumers его не расширяют.
## 3. Общие компоненты и продуктовый пилот
- [x] 3.1 [frontend] Перенести StateView и LoadingSkeleton с их stories.
- **Covers:** `frontend-design-system` — `Общий фундамент доступен через проверяемые локальные пакеты`
- **Verification:** `npm run deps:storybook`, `npm run build:catalog`, существующие проверки StateView/LoadingSkeleton/composeStories в новом scope, будущий packed component render, `npm run verify`.
- **Читать / область:** Три UI-conventions темы, два `src/shared/ui` компонента/stories и callers, `src/test/storybook.test.tsx`, package ui. Зависимость: 2.2.
- **Изменить / готово:** Реализации и CSF stories у ui, один catalog glob включает package stories. Props без query/i18n/router; busy label/status и error alert/action сохраняются. Проверка Storybook reachability включает новый каталог. Если нужен facade, он только переэкспортирует.
- **Не входит / сохранить:** Перемещение page skeleton shapes, новый loading framework, полный набор stories для Mantine. No backend и реальные финансовые данные.
- **Откат:** Вернуть перенос; содержимое/поведение исходных components не менялось. Удаление facade — 7.1.
- [x] 3.2 [frontend] Перенести EntryCard и подтвердить пилот RecentEvents.
- **Статус 2026-09-22:** Реализация, 278 tests/full verify, Docker, packed consumer и browser desktop/375px прошли. Пользователь подтвердил успешную ручную проверку coarse pointer/reduced motion по предложенному чек-листу; browser-ограничение закрыто. CSS перенесён побайтово.
- **Covers:** `frontend-design-system` — `Общий фундамент доступен через проверяемые локальные пакеты`
- **Verification:** `npm run test:run -- src/pages/account/ui/AccountPage.test.tsx`, существующие ledger/CardPage cases использующих EntryCard; минимальный UI-test собственного image fallback при необходимости; `npm run deps:storybook`, `npm run build:catalog`, packed consumer render, `npm run verify`; browser сравнение 375px/desktop.
- **Читать / область:** `EntryCard.tsx`/CSS и все callers; `AccountPage.vm.ts::toRecentEvents`, `useBrokerageEvents`, `BrokerageEventsPreview`, `BrokerageEventCard` и их stories, design UI/UX scope. Зависимость: 3.1.
- **Изменить / готово:** Один EntryCard в ui с тем же CSS/props; entity пилота импортирует публичный ui package. В Storybook — существующие RecentEvents states и малая generic story длинного контента/image fallback/actions. Пилот реально проходит через API owner до прежнего presentation, без смены data flow. Остальные callers могут пока использовать re-export.
- **Не входит / сохранить:** Generic icons, PositionCard rewrite, исправление F8. Сохраняются unknown≠zero, currency/date/account, logo fallback, preview limit/full-list link; у других callers — sign и disabled placeholder/archive guards.
- **Откат:** Вернуть consumer import и extraction PR. Не вводить вторую реализацию EntryCard для пилота. Facade removal — 7.1.
- [x] 3.3 [frontend] Подключить существующие semantic roles внутри EntryCard.
- **Статус 2026-09-22:** Реализованы border/subtle/surface. Positive/negative уже были semantic (shade 9); neutral gray-7 оставлен без неточного сопоставления. npm ci/full verify (278 tests) и browser computed colors/narrow/focus прошли. Пользователь подтвердил ручную проверку coarse pointer/reduced motion по чек-листу; пункт закрыт.
- **Covers:** `N/A` — F1: применение уже существующей темы после структурного переноса, без нового UI behavior.
- **Verification:** Сравнить computed colors и выбранные EntryCard/RecentEvents states до/после; gray-3→border, gray-0→subtle эквивалентны; текущие positive/negative (shade 9) сохранены; background tint/white→surface проверен; `npm run verify` и browser narrow/focus/coarse-pointer/reduced-motion.
- **Читать / область:** ui EntryCard/CSS, tokens color.roles, ui/theme resolver, generic и product stories. Зависимость: 3.2.
- **Изменить / готово:** Прямые эквиваленты заменены semantic CSS variables; текущий вид и actions не меняются. Если эквивалентность конкретной пары не доказана, оставить её локальной и пояснить, не создавать токен автоматически.
- **Не входит / сохранить:** Новые palettes, card brand gradients, компонентный токен на каждое свойство, массовый CSS sweep.
- **Откат:** Вернуть только CSS/inline-role diff; package ownership остаётся.
## 4. Собственные общие иконки
- [x] 4.1 [frontend] Подготовить минимальный icons package.
- **Covers:** `frontend-design-system` — `Собственные общие иконки имеют доступный контракт`; `frontend-design-system` — `Общий фундамент доступен через проверяемые локальные пакеты`
- **Verification:** Будущие package build/declarations/pack consumer checks; synthetic decorative и meaningful icon examples с именем/размером/currentColor; `npm ls react`, `npm run deps:storybook`, `npm run verify`.
- **Читать / область:** Card/Cards inline plus/pencil/trash, `BankOperationIcon` как пример accessibility, `bind-icon-animation` только для границ ответственности, design §5. Зависимость: 2.1; после 2.2 можно переиспользовать package build pattern без общего самописного генератора.
- **Изменить / готово:** Именованные Add/Edit/Delete SVG-компоненты, один TSX source на рисунок, React peer/external, public types, ограниченные exports. Static package не тянет motion/UI/domain; декоративность по умолчанию и доступное meaningful использование документированы.
- **Не входит / сохранить:** SVG registry по строке, raw SVG+TSX дубли, SVGR/dependency, перенос категорий/навигационных анимаций. Сохранить geometry/stroke/size существующих рисунков, без редизайна.
- **Откат:** Удалить ещё не используемый package diff; до публикации внешнего контракта нет. Если 4.2 уже интегрирован, откатывать согласованно.
- [x] 4.2 [frontend] Перевести только повторяющиеся Card/Cards actions на icons.
- **Covers:** `frontend-design-system` — `Собственные общие иконки имеют доступный контракт`
- **Verification:** `npm run test:run -- src/pages/card/ui/CardPage.test.tsx`, `npm run test:run -- src/pages/cards/ui/CardsPage.test.tsx`, `npm run verify`; browser accessible names/keyboard и appearance выбранных Add/Edit/Delete actions.
- **Читать / область:** `pages/card/ui/CardPage.view.tsx`, `pages/cards/ui/CardsPage.view.tsx`, ActionIcon consumers и imports. Зависимость: 4.1.
- **Изменить / готово:** Повторяющиеся SVG заменены named imports; размеры/strokeWidth задаются явно, если раньше различались. Имя и disabled остаются на action; внутри SVG нет дополнительного focus. В изменённом scope больше нет старых копий этих рисунков.
- **Не входит / сохранить:** BankOperationIcon mappings, формы/mutations, поведение edit/delete и confirm dialogs, иной визуальный стиль.
- **Откат:** Вернуть caller diff; пакет может оставаться неиспользуемым до отдельного решения, опубликованную версию не удалять.
## 5. Адресные улучшения после основы
Эти задачи самостоятельны. Их незавершённость не оправдывает расширение
пакетного PR или утверждение, что изменено поведение формы в пилоте.
- [x] 5.1 [frontend] Проверить и при необходимости свести Modal animation к одному владельцу.
- **Статус 2026-09-22:** BankCardForm browser A/B показал различный lifecycle: CSS даёт mount entrance, Mantine — exit/callback. Production-код сохранён; причины документированы в UI convention. Пользователь подтвердил ручную проверку reduced motion, Escape/Отмена и возврата фокуса; пункт закрыт.
- **Covers:** `N/A` — F5, устранение подтверждённого дублирования механизмов без нового продукта.
- **Verification:** Browser comparison открытия/закрытия существующей банковской Modal: overlay, motion, focus return, reduced motion; relevant Modal/form tests и `npm run verify` только при code diff.
- **Читать / область:** ui theme Modal defaults, ui global styles, BankCardForm/OperationForm consumers. Зависимость: 2.2.
- **Изменить / готово:** Выбран один механизм либо документирован конкретный различный смысл обоих. При эквивалентности предпочтён Mantine transition и убран только лишний CSS. Наблюдение «видимых проблем нет» само по себе не требует нового кода.
- **Не входит / сохранить:** Новые timings, анимационная система, замена motion маршрутов. Keyboard/focus/closing rules неизменны.
- **Откат:** Вернуть animation-only diff; component API/data не меняются.
- [x] 5.2 [frontend] Свести runtime vocabulary банковских operation kinds у entity.
- **Covers:** `N/A` — часть F6, одна типизированная коллекция для существующих schema/choices без изменения wire contract.
- **Verification:** `npm run typecheck`, `npm run test:run -- src/pages/card/ui/OperationForm.test.tsx`, `npm run deps`, `npm run verify`; все четыре прежних вида и defaults сохранены.
- **Читать / область:** `entities/bank-card/api/queries.ts` и index, `OperationForm.tsx`/`.view.tsx`/`.types.ts`, TypeScript/data conventions. Зависимость: 1.2.
- **Изменить / готово:** Schema и choices используют один `as const` набор, проверенный относительно generated `BankOperation['kind']`. Не добавлен enum, DTO copy или cast неизвестного code; строки UI остаются i18n.
- **Не входит / сохранить:** Generic errors registry, backend enum generation, механическое именование каждого literal, controller refactor.
- **Откат:** Вернуть маленький vocabulary/import diff; значения wire прежние.
- [x] 5.3 [frontend] Отделить pure period helpers от AccountIncomePage VM.
- **Covers:** `N/A` — F7, устранение транзитивной зависимости presentation story от VM по design §6/10.
- **Verification:** Existing account-income targeted tests, `npm run deps:storybook`, `npm run build:catalog`, `npm run verify`; synthetic form submit/reset/error states без API/query providers.
- **Читать / область:** `AccountIncomePage.vm.ts`, `components/IncomePeriodForm.view.tsx`, их imports/tests, соседний page model и Storybook convention. Зависимость: 1.2.
- **Изменить / готово:** Только реально используемые pure date helpers у page model и оба callers используют их; полезная isolated presentation story проходит reachability. Нового global date helper/service нет.
- **Не входит / сохранить:** Общий DateRangePicker, изменение диапазонов/часового пояса и перенос логики в UI package. Moscow dates, validation и submit/reset прежние.
- **Откат:** Вернуть helper move/story diff; URL/API поведение не менялось.
- [x] 5.4 [frontend] Отделить controller банковской OperationForm от presentation.
- **Covers:** `N/A` — часть F6, применение существующей container/VM/view границы без изменения денежных сценариев.
- **Verification:** До и после `npm run test:run -- src/pages/card/ui/OperationForm.test.tsx`; особенно conflict/draft/explicit version adoption, retained UUID, full refund и max money; `npm run deps:storybook`, затем `npm run verify`.
- **Читать / область:** Полные OperationForm/OperationForm.view/types/tests, entity form-validation/mutations и UI/data/testing conventions. Зависимость: 5.2.
- **Изменить / готово:** Query/mutation/recovery orchestration принадлежит page VM/controller; существующий fields view расширен только нужными presentation props/callbacks/slots. Runtime helpers сейчас приходят из banking barrel, который переэкспортирует API/hooks: эту транзитивную связь убрать через props/callbacks, type-only DTO imports допустимы. Не вводить deep-import exception и не ослаблять story gate. RHF context в fields остаётся; `CardPage` целиком не переписывается.
- **Не входит / сохранить:** Generic form engine, extraction в feature/package без второго consumer, новые requests/optimistic updates. Не менять amount string/BigInt, intent lifecycle, version adoption, source constraints, pending close guards и вложенное создание категории.
- **Откат:** Вернуть этот behavior-preserving PR. Любое необходимое изменение поведения остановить и согласовать отдельно, не прятать в refactor.
## 7. Закончить переход и описать поставленное состояние
- [x] 7.1 [frontend] Удалить только исчерпанные compatibility facades.
- **Уточнение scope:** Пользователь разрешил удалить локальные facades с проверкой packed consumer вместо registry consumer. Nexus и независимый release остаются отложенными.
- **Covers:** `N/A` — прекращение временного двойного import path после подтверждённого потребления, design Migration Plan.
- **Verification:** `rg` по runtime/test/story imports старых theme/UI paths даёт ноль; `npm run knip`, `npm run deps`, `npm run deps:storybook`, app/catalog/packed consumer checks и `npm run verify`.
- **Читать / область:** Facades `shared/theme` и EntryCard/StateView/LoadingSkeleton, все оставшиеся callers, component source catalog, frontend README и conventions. Зависимости: 3.3, 4.2 и подтверждённый packed consumer без workspace/source aliases.
- **Изменить / готово:** Оставшиеся consumers используют package API, ненужные re-exports удалены, нет дубля JSON/CSS/implementation. В existing conventions закреплены каталог и import rules, AGENTS только маршрутизирует темы.
- **Не входит / сохранить:** Массовая чистка unrelated shared/lib, запрет прямого Mantine, перенос domain UI. Старые подходы вне migrated scope не блокируются новым широким правилом.
- **Откат:** Вернуть thin facades/imports; они не восстанавливают копии реализаций. Условия удаления снова проверяются перед повтором.
- [x] 7.2 [docs] Обновить только подтверждённое as-built описание.
- **Уточнение scope 2026-09-22:** По запросу следующего пункта без browser verification описано текущее локальное состояние после 5.4. Завершение 7.1 не требуется для этого описания: переходное состояние и отсутствие registry-поставки указаны явно; после 7.1 описание facades актуализировано. После будущей поставки документация обновляется вместе с ней.
- **Covers:** `N/A` — описание фактически принятой реализации и поставки; не новый behavior contract.
- **Verification:** Docs `npm run verify` по местному CONTRIBUTING, проверка локальных ссылок/диаграмм и semantic review по поставленному frontend; `git diff --check`.
- **Читать / область:** Docs AGENTS/README/CONTRIBUTING, `design/overview.md`, `architecture/containers.md`, при необходимости release boundary в `architecture/deployment.md`; frontend принятый package contract. Основание текущего описания: выполненные 1–5; удаление facades по 7.1 отражено в документации.
- **Изменить / готово:** Тематические страницы говорят о реально используемой теме/пакетах/границах frontend и каталога. Указывается, что raw SVG/mobile/dark/API package не поставлены. При неполной 5.x не обещать завершение этих пунктов.
- **Не входит / сохранить:** Анализ/план в permanent docs, новый roadmap, переописание backend, ADR на каждый маленький выбор. Один устойчивый ADR возможен лишь если принято значимое repository/release решение и нужен его rationale.
- **Откат:** Вернуть описание вместе с отменённой поставкой; не менять исторические archives.
- [x] 7.3 [specs] Сверить реализацию с delta и закрыть плановые расхождения.
- **Статус 2026-09-22:** Semantic сверка завершена для согласованного локального объёма: proposal/design/delta согласованы с private workspaces и packed consumer; внешняя публикация исключена. Код и ручные browser checks проверены ранее. Specs verify, локальные ссылки и whitespace проверены после правок. После установки OpenSpec выполнена команда `openspec validate evolve-frontend-stack --type change --strict --store wealth-ledger-specs`: change valid. Пункт завершён.
- **Covers:** `frontend-design-system` — `Общий фундамент доступен через проверяемые локальные пакеты`; `frontend-design-system` — `Собственные общие иконки имеют доступный контракт`
- **Verification:** Semantic проверка всех delta scenarios, `npm run verify` specs, при доступном CLI `openspec validate evolve-frontend-stack --type change --strict --store wealth-ledger-specs`; локальные ссылки и `git diff --check`.
- **Читать / область:** Specs AGENTS/README, proposal/delta/design/tasks и фактические результаты consumer/rollback/accessibility checks. Зависимости полного acceptance: 3.2–3.3, 5.1–5.4 и 7.1–7.2; требования delta должны соответствовать согласованному локальному объёму.
- **Изменить / готово:** Tasks отражают только фактически проверенные результаты; согласованные изменения scope возвращены в planning artifacts, не скрыты. Main specs обновляются только в отдельно разрешённом штатном завершении после реального внедрения; untouched deferred findings остаются явно отложенными.
- **Не входит / сохранить:** Автоматический archive/approval, Evidence/state/hash registry, декларация успеха на основании одного build. Промежуточная сверка не означает завершения change.
- **Откат:** Исправить несоответствующее предложение/отметку; main specs не описывают ещё не реализованный переход.
## Переходный период и отложенные вопросы
Точный перечень отложенного — в design: F8 recovery RecentEvents, F9 backend
errors contract, F12 расхождение bundle-budget spec/gate, общий investment recovery classifier и новые потребители.
Они не входят скрыто в tasks выше. Если требуется менять HTTP/observable
recovery, нужен отдельный согласованный scope и соответствующий spec contract.
Facades допустимы только от 2.2/3.x до 7.1; новых возможностей они не получают.
Новые components/imports следуют целевому каталогу. Старые inline рисунки
допустимы лишь до миграции своего caller batch, а новые дубли уже вынесенных
рисунков не создаются. Совместимость снимается по нулю imports и успешному
consumer proof, а не по истечении произвольного срока.
Проверены задачи **1.1–1.2, 2.1–2.2, 3.1–3.3, 4.1–4.2, 5.1–5.4 и 7.1–7.3**.
Недостающие browser checks 3.2/3.3/5.1 выполнены вручную пользователем:
он подтвердил корректность всех пунктов переданного чек-листа.
Независимые версии и Nexus отложены до оценки пользы после пилота;
checkbox не разрешает дальнейшую реализацию или публикацию.
Последний полный frontend `npm run verify`: 49 test files / 282 tests,
проверки зависимостей, типов, lint, Knip, OpenAPI и сборки app/catalog прошли.
Свежий UI tarball проверен вне workspace: install, typecheck, build и browser render.
Docs `npm run verify`: 45 Markdown-файлов, 0 issues.
@@ -0,0 +1,2 @@
schema: wealth-ledger-sdd
created: 2026-10-07
@@ -0,0 +1,96 @@
## Context
Backend — один NestJS process и SQLite. Сейчас screen-driven синхронизация раскладывает запросы экранов на source work; владельцы membership, portfolio, operations, forecast, catalog и candles используют reservations, attempt/revision fences и короткие транзакции без RPC внутри них. Текущий portfolio snapshot перезаписывается и каскадно удаляется при замене подключения или исчезновении счёта. `TbankSyncService` принимает экранный POST; frontend `screen-sync` запускает его и наблюдает status. Справочные данные, результаты поиска и отрицательные lookup живут в сохранённых таблицах, а detail является зависимостью событий и рыночных данных. Новый режим требует смены инициатора, не второй конкурирующей системы записи.
## Goals / Non-Goals
**Goals:** сохранить единственного владельца публикации каждого источника; сделать расписание устойчивым к рестарту в рамках одного процесса; связать точки истории с успешной публикацией портфеля и подтверждённым доступом; отделить сетевое чтение интерфейса от брокерского обновления; перевести подробные сведения инструментов на онлайн-источник без потери сохранённых рыночных и личных данных.
**Non-Goals:** график и публичная история точек, новый механизм синхронизации между процессами, повторное сканирование полной рыночной истории и уже полных операций, изменение банковских карт и auth-refresh.
## Decisions
### Один durable clock поверх существующих source owners
В `tbank-sync` вводится одиночный backend scheduler с persisted последней попыткой для текущего подключения: начало, завершение/состояние и диагностические разделы без токенов. При успешном PUT подключения фоновой работе передаётся новая граница; при старте process scheduler сверяет её и последнее начало, не опираясь на frontend GET. Проверка часовым таймером только предлагает работу; атомарное резервирование сверяет user/revision, время начала предыдущей попытки и отсутствие ещё активной попытки. Во время работы новое резервирование не допускается; пересечённый срок пропускается, а не исполняется «вдогонку». После рестарта незавершённая попытка старого process становится прерванной, но её время начала остаётся основанием следующего запуска; новый process не публикует её поздние результаты. Успешное подключение начинает новую первую попытку, даже если прежний токен был заменён незадолго до срока. Штатный процесс инициализирует таймер и освобождает его при shutdown.
Scheduler делает один обход подтверждённого membership, затем даёт portfolio owner обновить каждый доступный счёт, operations owner — только изменяемый хвост, forecast/source owners — события бумаг состава. Передаёт доступ и ревизию владельцам, не пишет их личные наборы сам и не держит транзакцию при сетевых вызовах. Результаты отдельных источников дают общий итог `succeeded/partial/failed` и безопасные идентификаторы счёта/раздела. При недействительном токене применяется существующая блокировка личных ответов. Альтернатива — reuse screen intent с поддельным экраном `accounts`: он не покрывает операции и события для каждого счёта и случайно включает ненужные зависимости; выделение фонового orchestration над теми же owners точнее.
### История отдельно от revision, но внутри атомарной публикации
Состояние истории принадлежит `(userId, accountId)` и переживает смену token/revision; текущий snapshot и source work по-прежнему принадлежат `(userId, revision, accountId)`. Точки используют точную десятичную строку, ISO-валюту и фактическое время успешной публикации, без состава позиций. Проверка полноты оценки всех позиций предшествует вставке любых валютных точек; пустой безвалютный портфель не даёт строк. Для однозначности повторной публикации точка привязана к идентификатору успешной попытки portfolio, а её запись и замена snapshot происходят в одной короткой транзакции после conditional attempt claim. При потере revision/доступа вся транзакция отменяется. Неоценённый состав сохраняется текущим без исторических точек.
Видимость истории определяется текущим подтверждённым membership и признаком удаления счёта, а не одной лишь идентичностью исторических строк. Успешное подтверждение списка, успешный PUT нового token и DELETE обновляют пометки доступа вместе со сменой текущей границы; неуспешный PUT/GET membership не меняет их. Чтение возможной истории в будущем обязано проверять session, текущее неотклонённое подключение и активное подтверждение конкретного accountId: `deletedAt = null` само по себе не является полномочием. Это позволяет сохранить прошлые точки, не воскресить приватный снимок прежней revision и не раскрыть историю между отключением и повторным подтверждением. Поскольку endpoint истории сейчас не предусмотрен, API для её чтения не проектируется.
### Сначала разделить источники по смыслу, затем удалить screen import
Обычные GET инвестиционных областей становятся локальными для membership/portfolio и изменяемых операций/прогнозов; их freshness отражает час, а ошибки попыток живут отдельно. Исключения — первичная загрузка отсутствующего исторического интервала операций и впервые запрошенного диапазона свечей; покрытие остаётся владельцем операций/candles. Текущие quotes/незавершённые свечи обновляются по запросу не чаще часа. Удаление ручного screen POST и его наблюдения следует после перехода всех consumers на чистые reads и статус последнего обхода. Иначе нынешние `sync=observe` GET и retry-кнопка могут обходить новый график; транспортный retry сохраняется, но не вызывает брокерский импорт. Публичный DTO статуса описывает только последнюю попытку и читается под той же auth/access boundary. Backend публикует OpenAPI как контракт, frontend обновляет свой snapshot из него отдельно.
### Онлайн-справочник без разрушения зависимостей
Публичный instrument detail, company и catalog search идут через существующие T-Bank/MOEX/NCR clients по запросу с проверкой UID/ticker и сохранением различия «не найдено / неоднозначно / источник недоступен». Общая активная работа может коалесцировать одинаковые сетевые вызовы в процессе, но отдельная постоянная копия справочника и отрицательного lookup не создаётся. Для прогноза необходимые атрибуты запрашиваются внутри его работы; ошибка не заменяет последний успешный forecast. Для сохранённых quotes, candle coverage, событий и portfolio labels остаётся только минимальная стабильная identity, нужная для ключей и чтения; ссылки и пользовательские данные не зависят от удаляемого detail payload. Старые справочные записи выводятся из использования до рассмотрения их удаления; каскады не должны уничтожать свечи. При сбое онлайн-источника блок detail показывает отдельную ошибку, остальные сохранённые данные доступны в своих границах.
## UI/UX Applicability and Package
**Applicability and scope:** `new/changed` — на инвестиционных экранах убираются ручные команды синхронизации, добавляются 30-минутное перечитывание и статус последнего обхода на кабинете. Макет графика не нужен. Пакет полностью inline; локальные PNG/SVG не требуются.
**Package revision:** `1` (этот Design).
**Screens, states and interactions:**
| Экран / блок | Без результата | Данные сохранены | Сбой / частичный успех | Действие |
| --- | --- | --- | --- | --- |
| Кабинет/счета | штатная первая загрузка и отсутствие доступных счетов | прежние карточки и время обновления | сообщение о последнем обходе: состояние, время, названия проблемных счетов/разделов без token/error payload | переход к счёту или настройкам; кнопки синхронизации нет |
| Портфель/операции/доход/события | локальная загрузка либо недостающий исторический период | сохранённые значения и исходное время остаются при перечитывании | server-side stale отдельно от transport error; прежние значения не становятся новым snapshot | повтор только HTTP-чтения при сетевой ошибке |
| Карточка инструмента и поиск | справочный блок ожидает онлайн-ответ; свечи могут впервые загружаться отдельно | сохранённые позиции, операции, подписи, котировки/свечи и события сохраняют самостоятельные состояния | ошибка справочного блока не скрывает независимые данные; подпись snapshot не обещает свежие подробности | открытие и повтор HTTP-чтения сведений без команды синхронизации личных счетов |
30-минутное перечитывание действует только пока инвестиционный экран открыт; для онлайн-справочного блока оно также означает новый запрос сведений. Время последней попытки не подменяет `lastSuccessfulAt` отдельного счёта. Текст частичного успеха включает доступный список проблемных разделов; не показываются сырые upstream сообщения.
**Responsive behavior:** использовать существующие адаптивные контейнеры и ширины списков/карточек; строка статуса переносит названия счетов и разделов на узком экране, не закрывает содержимое и имеет доступное текстовое обозначение помимо цвета. Новых плотных таблиц, графиков или мобильной навигации нет.
**Reused source and differences:** существующие as-built страницы кабинета, портфеля, операций и карточек инструмента сохраняют компоновку; новый пакет не зависит от изменяемой внешней ссылки либо архивного макета. Отличия перечислены выше: статус обхода вместо ручного refresh и локальная ошибка online detail. Идентификатор immutable archived reuse не применяется: новый статус и новые переходы состояний описаны здесь inline, не объявляются точной копией прошлой версии.
## Delivery Map
| Area | Status | Architectural outcome or N/A reason |
| --- | --- | --- |
| frontend | `affected` | Убрать screen-triggered mutation и инвестиционные sync-кнопки, сохранить transport retry, добавить периодическое чтение и статус на кабинете, независимо показывать ошибку online detail. |
| backend | `affected` | Durable scheduler, учёт частичного результата и доступа, точки истории, локальные read boundaries, онлайн-справочник и опубликованный API статуса. |
| specs | `affected` | Нормативные delta requirements для нового расписания, истории и обновлённых инвестиционных/клиентских сценариев. |
| docs | `affected` | Обновить объяснение свежести, областей и глоссарий, зафиксировать новое решение относительно ADR 017 без переписывания его исторического контекста. |
## Solution Assessment
| Вариант | Fit и maturity | Security / data integrity | Operating cost и ограничения | Migration cost |
| --- | --- | --- | --- | --- |
| Внешний cron/worker с отдельным экземпляром | Зрелый для fleet, но дублирует приложение при single-process SQLite | Нужны распределённые lock/lease и граница токенов между процессами | Отдельный deployment и координация | Высокая; против текущей single-process модели |
| Nest scheduling dependency | Зрелые cron decorators/timers, но сама библиотека не хранит результат попыток | В любом случае нужны persisted CAS/fences для restart и подключения | Ещё один runtime package и жизненный цикл без выигрыша от cron expression | Небольшая, но ненужная зависимость |
| Native Node timer + существующие SQLite/Prisma reservations | Подходит для одного процесса; точность минутных тиков не подменяет persisted время начала | Conditional reservation + revision/attempt fence, история в DB transaction | Нет нового сервиса или пакета; при нескольких процессах требуется отдельная координация | Умеренная, повторное использование владельцев источников |
Выбран native timer с durable CAS: уже существующий `TbankSyncWork` и SQLite дают необходимые primitives, но сам таймер не является гарантом уникальности. Для истории использовать уже установленный Prisma/SQLite вместо time-series базы; данные малы, rollback и backup живут в существующем deployment. Для справочных сведений использовать действующих integration clients и online reads; новая cache/service boundary не оправдана, а долговременное кэширование прямо противоречит принятым требованиям. Отказ от него повышает зависимость detail от T-Bank, поэтому ошибки изолируются от сохранённых финансовых наборов. Перед настройкой фактического числа upstream вызовов требуется сверить действующие первичные ограничения провайдера и измерить размер/прирост локальной базы на реальном объёме; произвольные внешние числовые лимиты не фиксируются как допущение.
## Risks / Trade-offs
- При простое backend точки пропускаются; нет искусственной интерполяции и накопленной очереди RPC.
- Один timer без отдельной межпроцессной аренды небезопасен для нескольких экземпляров: deployment сохраняет единственный process.
- Переход на онлайн-справочник увеличивает latency и чувствительность detail/search к upstream; нельзя распространять ошибку на сохранённый портфель или его события.
- Исчезновение/возврат счёта и одновременная смена token требуют fence внутри коротких транзакций; `deletedAt` без актуального подтверждения membership не даёт права чтения.
- Удаление таблиц справочника до развязки FK/dependencies может стереть историю свечей; destructive cleanup откладывается до проверки сохранности рыночных данных.
- Старый frontend и новый backend должны сосуществовать в переходе: публикация DTO статуса предшествует удалению screen POST; потребителю передаётся явный OpenAPI artifact.
## Migration Plan
1. Добавить историю и metadata последней попытки совместимым расширением SQLite; не переписывать уже применённые migrations и не выполнять reset на рабочей базе. Привязать запись истории к существующему portfolio publisher и видимость к подтверждённому membership/connection lifecycle.
2. Ввести backend-only расписание поверх существующих source owners и read-only статус; включить первичную загрузку и восстановление после рестарта при сохранённом времени попытки. Только после проверки idempotency и отказов перевести GET на read-only для текущих личных данных.
3. Развязать online reference от долговременных detail/search/lookup записей, сохранив минимальные identity и таблицы quotes/candles/forecast; сначала перевести readers, затем совместимой миграцией удалять лишь действительно неиспользуемые справочные payload, не каскадируя рыночную историю.
4. Получить опубликованный backend OpenAPI и отдельно обновить frontend query/статус/кнопки в рамках согласованного contract. Затем убрать неиспользуемый screen-triggered endpoint; зафиксировать изменение ADR и as-built страниц. Измерить рост SQLite и provider call volume до изменения частоты/лимитера.
### Rollback Contract
| Field | Decision |
| --- | --- |
| Status | `required` |
| Trigger | Нарушение доступа к истории после замены token, повторная публикация точек, порча сохранённых свечей при переходе справочника либо несовместимость нового frontend с API статуса. |
| Actions | Остановить scheduler, вернуть предыдущие совместимые сборки из проверенного комплекта и восстановить DB из согласованного backup перед destructive переходом; при additive стадии допустимо вернуть старый runtime с сохранением новых неиспользуемых таблиц. Не возобновлять прежний screen-triggered режим одновременно с включённым hourly scheduler. |
| Data safety | Сохранить исходные snapshots, coverage, forecast и точки истории; никогда не удалять или заново создавать production SQLite ради rollback. Перед очисткой справочника обеспечить проверенный backup/restore и доказать отсутствие каскадов в candles. |
| Verification | Проверить на копии с представительными данными доступность прежних личных данных только у текущего подключения, идемпотентность точек, сохранность candles/coverage и совместимость опубликованного API до возврата трафика. |
@@ -0,0 +1,32 @@
## Why
Сейчас актуализация инвестиционных данных зависит от открытия экранов и ручного действия. Неактивный кабинет не получает новые данные, а долгосрочную динамику стоимости счёта восстановить по последнему снимку невозможно. Нужны предсказуемое часовое обновление независимо от frontend и накопление пригодных для будущего графика оценок. Согласованный объём — [issue #22](https://git.personal.ksv741.ru/WealthLedger/main/issues/22).
## What Changes
- Обновлять доступные счета, состав и стоимость портфелей, актуальные операции и будущие события по бумагам портфеля раз в час силами backend; первое обновление выполнять после подключения. Сохранённые данные отдавать сразу, сохраняя последнее успешное состояние при ошибке и показывая результат последней попытки, включая частичный успех.
- Накапливать оценку стоимости каждого счёта по валютам на фактическое время успешного получения полной оценки, без конвертации и подмены отсутствующей цены нулём. Сохранять историю при смене токена и отключении, скрывать её без подтверждённого доступа и продолжать при восстановлении доступа к тому же счёту.
- Убрать ручную синхронизацию с T-Bank и обновление личных данных при открытии инвестиционных экранов. Открытые экраны перечитывают сохранённые данные каждые 30 минут. Сохранить первичную загрузку отсутствующих исторических интервалов и диапазонов свечей, но не перечитывать полностью загруженную старую историю автоматически.
- Получать справочные данные инструмента и результаты поиска онлайн без отдельного долговременного хранения; сохранить подписи последнего снимка портфеля, события, котировки и свечи согласно их разным правилам актуализации.
- Вне объёма: сам график и публичное чтение его точек, валютная конвертация, многопроцессное расписание, автоматическая повторная сверка уже полных старых интервалов операций и свечей, а также изменение действий для ручных банковских карт и повторного чтения после сетевой ошибки.
## Capabilities
### New Capabilities
- `specs/hourly-investment-sync/spec.md`: независимый от открытых экранов часовой обход и наблюдаемый результат последней попытки.
- `specs/portfolio-value-history/spec.md`: точная история оценки по счёту и валюте с учётом полноты и границы доступа.
- `specs/online-instrument-information/spec.md`: получение справочных сведений и поиска онлайн без сохранённой самостоятельной копии, отдельно от котировок, свечей и событий.
### Modified Capabilities
- `specs/portfolio-view/spec.md`: чтение последнего снимка без запуска синхронизации, часовая свежесть, отсутствие ручной синхронизации и сохранение доступных подписей позиций.
- `specs/brokerage-accounts/spec.md`: быстрое чтение сохранённых счетов и сводок, видимость частичных ошибок и периодическое перечитывание экрана.
- `specs/tbank-connection/spec.md`: первая загрузка после подключения и сохранение скрытой истории стоимости при отключении/замене токена.
- `specs/brokerage-account-upcoming-events/spec.md`: обновление прогноза по расписанию с сохранением последнего полного результата при сбое.
- `specs/frontend-data-resilience/spec.md`: разделение повтора HTTP-чтения и запрещённой ручной синхронизации при часовом обновлении.
- `specs/brokerage-account-ledger/spec.md`: первичная загрузка отсутствующего периода без повторной сверки уже полного исторического периода при открытии страницы.
## Impact
Пользователь быстрее открывает инвестиционные экраны и видит время и возможную неполноту последней синхронизации; открытый экран получает новые сохранённые данные без команды обновления. Затронуты инвестиционные экраны, фоновое обновление и хранение личных данных, правила доступа после смены подключения, отображение ошибок и онлайн-получение информации об инструментах. Переход требует согласовать действующие требования, которые пока предписывают короткие сроки свежести и ручные повторы.
@@ -0,0 +1,20 @@
## ADDED Requirements
### Requirement: Отсутствующий исторический интервал загружается только впервые
Система MUST позволять авторизованному User при первом открытии ещё не загруженного периода операций доступного Brokerage Account начать получение недостающего интервала без ожидания T-Bank в локальном ответе. Полностью загруженный прошлый интервал MUST NOT повторно сверяться из-за чтения страницы, часового обхода или удалённой команды ручной синхронизации. Изменяемый хвост операций MUST обновляться часовым обходом. Незавершённый интервал и ошибка загрузки MUST NOT обозначаться полными, а последний подтверждённый результат MUST оставаться доступным.
#### Scenario: Первый просмотр отсутствующего прошлого периода
- **WHEN** User открывает доступный исторический период без подтверждённой полноты
- **THEN** система возвращает локальный результат с явным состоянием неполноты и начинает загрузку недостающих данных
#### Scenario: Повторное открытие полного периода
- **WHEN** User повторно открывает уже полностью загруженный прошлый период
- **THEN** система читает сохранённые операции без повторной сверки с T-Bank
#### Scenario: Ошибка страницы загрузки
- **WHEN** загрузка части отсутствующего периода завершилась ошибкой
- **THEN** ранее подтверждённые операции сохраняются, а неполная часть не помечается как полная
@@ -0,0 +1,20 @@
## MODIFIED Requirements
### Requirement: Upcoming events use complete source results
Система MUST возвращать только последний полностью успешно сформированный прогноз по доступному счёту. Если необходимые данные брокера или MOEX недоступны во время часового обновления, система MUST NOT заменять последний успешный прогноз частичным списком или изменять время его успеха. Инструменты, для которых источник не предоставляет поддерживаемые события, MUST пропускаться без синтетических строк «нет данных». Получение сохранённого прогноза MUST NOT запускать синхронизацию при открытии экрана; ошибку последней попытки MUST показывать отдельно.
#### Scenario: Недоступность источника
- **WHEN** необходимый источник данных брокера или MOEX временно недоступен при часовом обновлении
- **THEN** последний полный прогноз остаётся доступен с исходным временем, частичный список не публикуется, а ошибка отражается в статусе обхода
#### Scenario: Инструмент без поддерживаемых данных
- **WHEN** Portfolio содержит инструмент, для которого источник не предоставляет поддерживаемые будущие события
- **THEN** система не добавляет для него синтетическую строку в результат
#### Scenario: MOEX отвечает без запрошенного блока
- **WHEN** MOEX отвечает успешно, но не содержит запрошенного блока данных (например, дивидендов)
- **THEN** система трактует ответ как отсутствие данных по инструменту и не завершает обновление ошибкой
#### Scenario: Чтение при временном сбое обновления
- **WHEN** User открывает события доступного счёта после неудачного часового обновления и для счёта существует прежний полный прогноз
- **THEN** система возвращает прежний прогноз без ожидания источника и обозначает ошибку обновления отдельно
@@ -0,0 +1,84 @@
## MODIFIED Requirements
### Requirement: Состояния недоступности списка счетов
Система MUST читать подтверждённый доступный список счетов текущего подключения из последнего сохранённого результата и MUST NOT ожидать T-Bank или запускать проверку только из-за GET. При отсутствии T-Bank connection система MUST отвечать `409` Problem Details с code `TBANK_NOT_CONNECTED`. Если T-Bank уже отклонил сохранённый token как неаутентифицированный или без прав, система MUST отвечать `400` Problem Details с code `INVALID_TBANK_TOKEN` и сохранять connection, не выдавая личных данных. Временная ошибка часового обновления MUST оставлять последний подтверждённый список доступным с исходным временем и отдельным состоянием ошибки; при ещё неизвестном списке система MUST обозначать ожидание первой загрузки, не выдавая частичный список за полный.
#### Scenario: Подключение отсутствует
- **WHEN** User без сохранённого T-Bank connection запрашивает список
- **THEN** система не обращается к T-Bank и отвечает `409` Problem Details с code `TBANK_NOT_CONNECTED`
#### Scenario: Сохранённый token больше не действителен
- **WHEN** T-Bank отклоняет сохранённый token при часовом обновлении списка как неаутентифицированный или без прав
- **THEN** последующее чтение получает `400` Problem Details с code `INVALID_TBANK_TOKEN` и не раскрывает личные данные, но connection сохраняется
#### Scenario: T-Bank временно недоступен
- **WHEN** часовая проверка списка завершается временной либо иной upstream-ошибкой после ранее успешной проверки
- **THEN** обычное чтение возвращает последний подтверждённый список без ожидания T-Bank, сохраняя его исходное время и отдельную ошибку обновления
#### Scenario: Первый обход ещё не завершён
- **WHEN** User открывает список после подключения до получения первого подтверждённого списка
- **THEN** система не выдаёт частичный или неподтверждённый список за полный и сообщает о продолжающейся загрузке
### Requirement: Экран списка брокерских счетов
Клиент SHALL загружать список при открытии защищённого маршрута `/accounts` без ручной синхронизации и без автоматического refetch при возврате фокуса. Клиент MUST использовать успешный frontend-кэш списка в течение 60 секунд. После истечения этого срока клиент MAY перечитать локальный результат; пока экран открыт, он MUST перечитывать результат backend каждые 30 минут без запуска импорта. При уже имеющемся успешном списке он MUST сохранять последний успешный Client Cached Snapshot видимым во время фонового перечитывания и после его ошибки, ясно отделяя это состояние от успешного нового ответа. При успешном непустом ответе клиент MUST отобразить адаптивный одноколоночный список широких интерактивных карточек счетов с локализованными типом и, если доступна, датой открытия без времени; каждая карточка MUST ясно показывать тип, имя, финансовую сводку, вторичные метаданные и заметное действие открытия счёта, вести на `/accounts/:accountId` по всей области и сохранять доступное клавиатурное состояние. После успешной замены или удаления T-Bank connection клиент MUST инвалидировать список счетов.
Для доступной `portfolioSummary` карточка MUST показывать не более двух Money-итогов в исходных валютах и обозначать наличие остальных валют, если они есть; она MUST NOT показывать или подразумевать FX-конвертацию, межвалютный агрегированный итог либо Cash Balance как отдельный второй итог. При `portfolioSummary: null` карточка MUST показывать локальное безопасное состояние временной недоступности сводки, сохраняя метаданные счёта и возможность открыть его. При доступной пустой сводке карточка MUST не выдавать отсутствие итогов за временную ошибку.
#### Scenario: Пользователь открывает счёт из списка
- **WHEN** клиент успешно загрузил непустой список и User выбирает карточку счёта
- **THEN** клиент переходит на `/accounts/:accountId` с `id` выбранного счёта
#### Scenario: Список пуст
- **WHEN** клиент получает успешный пустой массив
- **THEN** клиент показывает штатное сообщение об отсутствии инвестиционных счетов T-Bank
#### Scenario: Список берётся из свежего frontend-кэша
- **WHEN** User повторно открывает список в течение 60 секунд после успешной загрузки
- **THEN** клиент показывает сохранённые счета без нового API-запроса
#### Scenario: Устаревший список загружается повторно
- **WHEN** User открывает список после истечения 60 секунд frontend-кэша и уже существует последний успешный список
- **THEN** клиент оставляет этот список видимым во время нового чтения сохранённого результата и ясно обозначает фоновое перечитывание без команды импорта
#### Scenario: Ошибка фонового обновления не скрывает snapshot
- **WHEN** перечитывание устаревшего списка завершается ошибкой при наличии последнего успешного Client Cached Snapshot
- **THEN** клиент сохраняет snapshot видимым, безопасно сообщает о неуспешном чтении и предоставляет повтор именно чтения
#### Scenario: Карточка ограничивает валютные итоги
- **WHEN** доступная сводка счёта содержит итоги в трёх или более валютах
- **THEN** карточка показывает не более двух исходных валютных итогов и обозначает наличие остальных без общего сконвертированного значения
#### Scenario: Недоступность сводки локальна карточке
- **WHEN** клиент получает список, в котором `portfolioSummary` одного счёта равна `null`
- **THEN** он показывает временную недоступность только на этой карточке, не скрывает остальные карточки и сохраняет переход на детали недоступной сводки счёта
#### Scenario: Пустая сводка не является ошибкой
- **WHEN** клиент получает доступную `portfolioSummary` с пустым `totalsByCurrency`
- **THEN** он показывает нейтральное состояние отсутствия финансовых итогов, а не состояние временной недоступности
### Requirement: Клиентские состояния ошибки списка счетов
Клиент MUST NOT автоматически повторять неуспешный запрос списка вне согласованного периодического перечитывания открытого экрана. При `TBANK_NOT_CONNECTED` клиент SHALL показать объяснение отсутствия подключения и действие «Открыть настройки T-Bank». При `INVALID_TBANK_TOKEN` клиент SHALL показать объяснение недействительного token и то же действие. При `TBANK_UNAVAILABLE` клиент SHALL показать безопасное объяснение временной недоступности, действие повторного HTTP-чтения и действие открытия настроек T-Bank. При иной ошибке клиент MUST показать общий безопасный error-state только с действием повторного чтения. Если последний успешный Client Cached Snapshot отсутствует, эти состояния MUST NOT показывать список; если snapshot существует во время фонового перечитывания, клиент MUST сохранить его видимым и показать соответствующее безопасное предупреждение и только применимые действия. Эти действия MUST NOT инициировать синхронизацию с T-Bank.
#### Scenario: Подключение требуется
- **WHEN** клиент получает `TBANK_NOT_CONNECTED`
- **THEN** клиент не показывает список и предлагает открыть настройки T-Bank
#### Scenario: Известная ошибка не повторяется автоматически
- **WHEN** запрос списка завершается известной ошибкой
- **THEN** клиент не выполняет немедленный повтор, пока User не выберет действие повторного чтения либо не наступит очередной 30-минутный срок открытого экрана
#### Scenario: Token требует внимания
- **WHEN** клиент получает `INVALID_TBANK_TOKEN`
- **THEN** клиент не показывает список и предлагает открыть настройки T-Bank
#### Scenario: Временная ошибка T-Bank
- **WHEN** клиент получает `TBANK_UNAVAILABLE`
- **THEN** клиент не показывает список и предлагает повторить локальное чтение либо открыть настройки T-Bank без команды импорта
#### Scenario: Неизвестная ошибка загрузки
- **WHEN** клиент получает ошибку без известного кода списка счетов
- **THEN** клиент не показывает список и предлагает только повторить чтение
#### Scenario: Известная ошибка при сохранённом snapshot
- **WHEN** фоновое перечитывание списка с последним успешным Client Cached Snapshot получает `TBANK_UNAVAILABLE`
- **THEN** клиент сохраняет snapshot видимым, сообщает о временной недоступности и предлагает повторить чтение либо открыть настройки T-Bank
@@ -0,0 +1,16 @@
## MODIFIED Requirements
### Requirement: Client Cached Snapshot остаётся доступным при обновлении Accounts и Portfolio
Frontend MUST сохранять Client Cached Snapshot списка Brokerage Accounts или Portfolio на экране, если для этого же набора данных уже был успешно получен ответ. Начальная загрузка без такого ответа может показывать блокирующее состояние загрузки. Client Cached Snapshot MUST оставаться видимым во время перечитывания сохранённых данных и после его ошибки; он не является server-side fallback и MUST NOT изменять или интерпретировать поле `Portfolio.isStale`. Повтор после транспортной ошибки MUST повторять чтение, а не инициировать синхронизацию с T-Bank.
#### Scenario: Список Brokerage Accounts обновляется в фоне
- **WHEN** экран Accounts уже показывает Client Cached Snapshot и начинается фоновое перечитывание того же списка
- **THEN** frontend оставляет список видимым и показывает только неблокирующий индикатор обновления
#### Scenario: Обновление Portfolio завершается ошибкой после успешного ответа
- **WHEN** экран Brokerage Account уже показывает Client Cached Snapshot Portfolio, а последующее чтение этого же Portfolio завершается ошибкой
- **THEN** frontend сохраняет Portfolio видимым, показывает локализованное неблокирующее предупреждение о показе последних полученных данных и предоставляет повторную попытку чтения без команды синхронизации
#### Scenario: Server-side fallback и client cached snapshot различаются
- **WHEN** backend возвращает Portfolio с `isStale` либо перечитывание Client Cached Snapshot завершается ошибкой
- **THEN** frontend отображает соответствующее server-side либо client-side предупреждение независимо, не представляя один вид состояния другим
@@ -0,0 +1,50 @@
## Purpose
Задаёт независимое от открытых экранов часовое обновление инвестиционных данных пользователя и наблюдаемый результат последней попытки при частичных сбоях.
## ADDED Requirements
### Requirement: Часовой обход личных инвестиционных данных
Backend SHALL начинать первый обход после успешного подключения T-Bank и последующие обходы не ранее чем через 60 минут от начала предыдущей попытки для подключения. При старте после простоя он MUST учитывать время последней попытки, выполнять не более одного актуального обхода и MUST NOT воспроизводить пропущенные часы. Если попытки ещё не было и данных нет либо они старше часа, обход MUST начинаться при старте. Неуспешная попытка MUST NOT повторяться ранее следующего часового срока даже после перезапуска. Перекрывающиеся обходы MUST NOT выполняться; если срок настал во время ещё идущего обхода, соответствующий запуск пропускается.
Обход MUST проверять доступные счета и обновлять портфель каждого доступного счёта, изменяемый хвост операций и будущие события по бумагам текущего портфеля. Он MUST NOT повторно загружать уже полные прошлые интервалы операций, завершённые свечи или полную рыночную историю. Он MUST NOT зависеть от открытого frontend. Смена подключения MUST NOT позволять завершившейся поздно попытке старого подключения изменять личные данные или время их успеха.
#### Scenario: Первое подключение между часовыми запусками
- **WHEN** пользователь успешно подключает T-Bank после запуска backend
- **THEN** первый обход начинается без ожидания следующего календарного часа, а следующий допускается через час от начала первого
#### Scenario: Рестарт после неуспешной попытки
- **WHEN** backend перезапускается до истечения часа после неуспешного обхода с отсутствующими данными
- **THEN** обход не повторяется при рестарте, а последнее успешное состояние остаётся доступным согласно действующей границе доступа
#### Scenario: Перекрытый часовой срок
- **WHEN** начатый час назад обход всё ещё выполняется
- **THEN** второй обход не запускается, а пропущенная попытка не ставится в очередь сразу после завершения первой
#### Scenario: Возобновление после простоя
- **WHEN** backend не работал несколько часов и после старта обнаружил, что последняя попытка старше часа
- **THEN** он запускает только один текущий обход без синтетических прошлых обновлений
### Requirement: Сохранность и статус последней попытки
Успех одного счёта или источника MUST сохраняться независимо от ошибки другого; неполный либо неуспешный результат источника MUST NOT заменять его последнее успешное состояние или время. Backend SHALL предоставлять аутентифицированному пользователю статус только его последнего обхода: время начала, состояние (выполняется, полностью успешен, частично успешен или неуспешен) и проблемные счета и разделы без чувствительных сведений. Кабинет MUST показывать результат, время и проблемные счета/разделы при частичном успехе; отсутствие состоявшейся попытки MUST быть отличимо от успеха. Статус не предоставляет возможности начать синхронизацию с frontend.
#### Scenario: Ошибка одного счёта
- **WHEN** портфели двух счетов обновились, а третий не обновился
- **THEN** новые данные первых двух сохраняются, у третьего остаётся последнее успешное состояние, а статус обхода обозначает частичный успех и проблемный счёт
#### Scenario: Ошибка событий при успешных портфелях
- **WHEN** все портфели обновлены, но событие одной бумаги не обновилось
- **THEN** последний успешный прогноз сохраняется и статус обхода обозначает частичный успех с проблемным разделом
#### Scenario: Пользователь другого подключения запрашивает статус
- **WHEN** авторизованный пользователь запрашивает статус чужого обхода либо попытки старого подключения
- **THEN** система не раскрывает детали чужой или прежней границы доступа
@@ -0,0 +1,33 @@
## Purpose
Определяет получение справочных сведений и результатов поиска инструментов непосредственно от источника без самостоятельной долговременной копии, отдельно от личных данных и рыночной истории.
## ADDED Requirements
### Requirement: Справочные сведения и поиск запрашиваются онлайн
При чтении карточки акции или облигации и выполнении поиска система MUST получать справочные сведения инструмента, включая доступные название, тикер, ISIN, описание и показатели компании, онлайн, а не из отдельной долговременной сохранённой копии. Повторное чтение открытой страницы MUST запрашивать эти сведения снова. Недоступность источника MUST отображаться как ошибка соответствующего справочного блока, не скрывая доступные сохранённые позиции, операции, котировки, свечи и события. Подписи в последнем успешном снимке портфеля MAY использоваться как fallback для идентификации инструмента; они MUST NOT представляться как успешно полученные сейчас подробные сведения. Для расчёта будущих событий MAY запрашиваться необходимые справочные данные без их отдельного хранения.
#### Scenario: Повторное открытие карточки акции
- **WHEN** пользователь повторно открывает страницу акции после прежнего успешного чтения
- **THEN** справочные сведения запрашиваются онлайн заново, а сохранённая копия карточки не выдаётся за актуальный ответ
#### Scenario: Источник справочника недоступен
- **WHEN** подробные сведения акции получить не удалось, но доступен последний сохранённый снимок портфеля
- **THEN** справочный блок показывает локальную ошибку, а сохранённые позиции и их доступные подписи остаются видимы без обещания актуальности подробных сведений
### Requirement: Рыночные и событийные данные имеют отдельную политику
Система MUST продолжать сохранять котировки и запрошенные диапазоны свечей; при открытии страницы обновление текущей котировки и незавершённой свечи MUST выполняться не чаще одного раза в час. Будущие дивиденды, купоны и другие поддерживаемые события MUST сохраняться и обновляться в составе часового обхода бумаг портфеля. Полностью загруженные прошлые диапазоны свечей MUST NOT повторно загружаться автоматически. Получение этих данных MUST NOT запускать синхронизацию личных счетов при открытии страницы.
#### Scenario: Свежая котировка
- **WHEN** пользователь повторно открывает страницу инструмента менее чем через час после получения котировки
- **THEN** система использует сохранённую котировку без преждевременного повторного запроса цены
#### Scenario: Исторический диапазон свечей открыт впервые
- **WHEN** пользователь открывает диапазон свечей, которого ещё нет локально
- **THEN** система загружает его для просмотра, не начиная синхронизацию личного портфеля
@@ -0,0 +1,48 @@
## Purpose
Определяет накопление достоверных точек стоимости брокерского счёта по исходным валютам с сохранением истории между подключениями без раскрытия её вне подтверждённого доступа.
## ADDED Requirements
### Requirement: Полная оценка становится точкой истории
Система MUST добавлять точки на фактическое время каждого успешно полученного полного финансового снимка отдельного Brokerage Account, включая первую загрузку; стоимость MUST храниться раздельно по валютам без конвертации, полным JSON состава или подстановки искусственных точек за часы простоя. Если хотя бы у одной ненулевой позиции текущая стоимость не определена, точки MUST NOT записываться ни для одной валюты этого обновления, хотя новый состав счёта MAY оставаться текущим снимком. Пустой счёт без известной валюты MUST NOT создавать нулевую денежную точку. Ошибка получения обязательных данных MUST NOT менять предыдущие точки или последний успешный снимок. История MUST NOT иметь заранее установленного срока удаления.
#### Scenario: Полная оценка двух валют
- **WHEN** успешно получен полный снимок счёта со стоимостью в RUB и USD
- **THEN** появляются отдельные точки RUB и USD с фактическим временем получения, без общего конвертированного значения
#### Scenario: Неизвестная цена одной позиции
- **WHEN** получен новый состав счёта с оценёнными RUB-активами и позицией без известной цены
- **THEN** состав доступен как текущий снимок с неполной оценкой, но новые точки истории не появляются ни по одной валюте
#### Scenario: Пустой счёт без валюты
- **WHEN** успешно полученный счёт не имеет позиций, денег и известной валюты
- **THEN** история не получает точку с придуманной валютой или суммой
### Requirement: История переживает смену подключения без потери приватности
История MUST продолжаться для того же User и идентификатора Brokerage Account после замены токена или повторного подключения, если доступ к счёту подтверждён текущим подключением. При отключении либо после успешно полученного списка без ранее доступного счёта история MUST оставаться сохранённой, но помечаться удалённой и скрываться от чтения; при повторном подтверждении того же счёта пометка MUST сниматься. Ошибка получения списка MUST NOT менять видимость истории. Подтверждённый отказ токена MUST скрывать личные данные до восстановления доступа; временный сбой MUST сохранять видимым последний успешный результат при действующей границе доступа. История другого пользователя MUST NOT раскрываться.
#### Scenario: Замена токена с прежним счётом
- **WHEN** новый проверенный токен пользователя даёт доступ к прежнему идентификатору счёта
- **THEN** линия его истории продолжается, а поздняя работа старого токена не добавляет точек
#### Scenario: Счёт исчезает из подтверждённого списка
- **WHEN** успешно полученный полный список доступных счетов не содержит ранее доступный счёт
- **THEN** его история сохраняется, помечается удалённой и не раскрывается до повторного подтверждения доступа
#### Scenario: Временный сбой списка
- **WHEN** обновление списка счетов завершилось временной ошибкой
- **THEN** пометки удалённых счетов не меняются
#### Scenario: Отключение и повторное подключение
- **WHEN** пользователь отключил T-Bank, а позже подключил проверенный токен с тем же счётом
- **THEN** до подтверждения доступа история скрыта, после подтверждения она снова видима и пополняется без разрыва идентичности счёта
@@ -0,0 +1,70 @@
## MODIFIED Requirements
### Requirement: Свежесть и fallback полного Portfolio Snapshot
Система MUST хранить последний полный успешно полученный Portfolio Snapshot с принадлежностью User, экземпляру подключения и Brokerage Account. Обычный GET MUST читать сохранённые данные без ожидания T-Bank и MUST NOT запускать повторное обновление личного портфеля, даже если снимок отсутствует или старше часа; актуализацией владеет часовой обход. Portfolio source MUST считаться свежим в течение 60 минут после успешного получения. Ошибка обновления MUST сохранять прежний полный Snapshot и его время без лимита fallback. Если состав получен полностью, но отдельная позиция не имеет цены, система MUST сохранить новый состав с неполной оценкой; metadata MUST NOT определять успешность либо свежесть финансового снимка. Сохранённые подписи позиций MAY оставаться доступны при недоступности онлайн-справочника.
Система MUST делать прежние личные datasets недоступными после успешной замены или удаления подключения. Поздняя работа прежнего подключения MUST NOT публиковаться в новом. Самостоятельные справочные сведения и результаты lookup MUST запрашиваться онлайн, а не сохраняться в виде долговременного справочника; ошибка lookup не подтверждает отсутствие инструмента.
#### Scenario: Свежий Snapshot
- **WHEN** User повторно запрашивает тот же Portfolio в течение 60 минут после успешного получения
- **THEN** система возвращает свежий Snapshot без нового обращения к T-Bank
#### Scenario: Медленный lookup инструмента
- **WHEN** сведения об инструменте ещё запрашиваются онлайн
- **THEN** система не задерживает локальный ответ Portfolio ради lookup и показывает сохранённую в Snapshot доступную identity; подробные сведения остаются отдельным состоянием
#### Scenario: Fallback после временной upstream-ошибки
- **WHEN** обновление устаревшего Snapshot неуспешно из-за временной недоступности T-Bank, а в текущей границе подключения есть полный успешный Snapshot
- **THEN** система отвечает `200` этим Snapshot с `isStale: true` и исходным `lastSuccessfulAt`
#### Scenario: Недействительный token не маскируется fallback-данными
- **WHEN** T-Bank отклоняет token как неаутентифицированный или без прав при обновлении Portfolio
- **THEN** система не возвращает fallback Snapshot и отвечает `400` Problem Details с code `INVALID_TBANK_TOKEN`
#### Scenario: Неполная оценка при новом составе
- **WHEN** успешно получен полный состав позиций, но у одной позиции нет вычислимой цены
- **THEN** новый состав становится текущим Portfolio Snapshot с неизвестной стоимостью позиции, без подстановки нуля
### Requirement: Состояния и ошибки Portfolio на клиенте
Клиент SHALL загружать Portfolio при открытии защищённого маршрута `/accounts/:accountId`. Он MUST использовать успешный ответ в течение 30 секунд, без automatic retry или refetch при возврате фокуса. Открытый инвестиционный экран MUST перечитывать сохранённый результат каждые 30 минут без запуска синхронизации с T-Bank. Наблюдение известной активной работы MAY обновлять отображаемые ресурсы; чтение статуса и данных MUST NOT запускать импорт. Status сообщает состояние и ошибку без hash notifications и численных progress counters. После ухода со страницы наблюдение MUST прекращаться.
Предзагрузка всей истории свечей бумаг портфеля MUST NOT выполняться. Новый диапазон свечей загружается при его открытии; initial loading допустим. При загрузке без ранее полученного успешного ответа для этого же Brokerage Account клиент MUST показывать структурное skeleton-состояние ожидаемого содержимого, включая сводку, без вымышленных сумм. Область загрузки MUST иметь доступное обозначение занятости; skeleton MUST NOT быть интерактивным или получать клавиатурный фокус.
Истечение срока свежести MUST NOT само по себе скрывать уже доступный Client Cached Snapshot. Во время фонового обновления и после его ошибки клиент MUST сохранять ранее полученные данные того же Portfolio, включая валютные итоги и исходное `lastSuccessfulAt`. При обновлении клиент MUST показывать компактную неблокирующую индикацию без замены данных skeleton. При ошибке перечитывания он MUST показывать локализованное неблокирующее предупреждение о последних полученных данных и предоставлять действие повтора только HTTP-чтения. Сохранённые данные MUST NOT считаться новым успешным снимком или подтверждением текущей доступности Brokerage Account. Клиент MUST NOT предоставлять действие ручной синхронизации портфеля с T-Bank.
Клиент MUST показать штатное empty-состояние только при пустых `positions` и `cashBalances`, без нулевых валютных карточек. Для показываемого состава Portfolio клиент MUST сохранять время успешного получения снимка `lastSuccessfulAt`, форматируемое в UTC. При `isStale: true` клиент MUST показывать Snapshot, его исходное время и нейтральное предупреждение об устаревших данных. Это предупреждение само по себе MUST NOT добавлять действие ручного обновления. Server-side stale и ошибка перечитывания Client Cached Snapshot MUST оставаться раздельными состояниями: ошибка клиента MUST NOT менять или подменять `isStale`, а оба предупреждения и действие повтора чтения MUST оставаться доступными при одновременном наличии этих состояний.
При отсутствии ранее успешно полученного Client Cached Snapshot этого же Portfolio и ответе `404` клиент MUST показать состояние недоступного счёта с действием возврата к списку счетов без действия повтора. При отсутствии Snapshot и `TBANK_NOT_CONNECTED`, `INVALID_TBANK_TOKEN`, `TBANK_UNAVAILABLE` или иной ошибке клиент MUST показать безопасное error-состояние с доступными для ошибки действиями; действие повтора чтения разрешено только для временной и неизвестной ошибки.
#### Scenario: Показ устаревшего Portfolio
- **WHEN** клиент получает успешный ответ с `isStale: true`
- **THEN** клиент показывает позиции, остатки, валютные итоги, время последнего успешного снимка и нейтральное предупреждение; само это предупреждение не добавляет действие ручного обновления
#### Scenario: Недоступный счёт на клиенте
- **WHEN** для текущего Portfolio отсутствует ранее успешно полученный Client Cached Snapshot и клиент получает `404`
- **THEN** клиент не показывает Portfolio и предлагает вернуться к списку Brokerage Account без действия повтора
#### Scenario: Первичная загрузка Portfolio
- **WHEN** клиент ожидает Portfolio этого Brokerage Account без ранее полученного успешного ответа
- **THEN** он показывает структурный skeleton, включая место сводки, с доступным обозначением загрузки, без вымышленных или нулевых сумм и без фокусируемых placeholders
#### Scenario: Обновление после истечения срока свежести
- **WHEN** срок свежести показанного Portfolio истёк и начался часовой обход данных того же Brokerage Account
- **THEN** клиент сохраняет прежние валютные итоги и время их успешного получения, показывает неблокирующую индикацию обновления и не заменяет доступный состав и сводку skeleton-состоянием
#### Scenario: Ошибка обновления после успешного ответа
- **WHEN** клиент уже показывает Client Cached Snapshot Portfolio, а повторное HTTP-чтение этих же данных завершается ошибкой
- **THEN** клиент сохраняет данные, суммы и время прежнего снимка, показывает неблокирующее предупреждение и действие повтора чтения; ошибка не делает сохранённый снимок свежим и не изменяет `isStale`
#### Scenario: Ошибка клиентского обновления при server-side stale
- **WHEN** клиент показывает Portfolio с `isStale: true`, а фоновое перечитывание этого же Portfolio завершается ошибкой
- **THEN** прежние суммы и время остаются видимыми одновременно с нейтральным server-side предупреждением, отдельным предупреждением об ошибке клиентского перечитывания и его действием повтора чтения
#### Scenario: Повторное обновление успешно
- **WHEN** после ошибки HTTP-чтения User запускает повторное чтение и получает успешный Portfolio Snapshot
- **THEN** до ответа прежние суммы и время остаются видимыми, затем клиент показывает значения и `lastSuccessfulAt` успешного ответа, убирает предупреждение об ошибке клиентского чтения и показывает server-side предупреждение только в соответствии с новым `isStale`
#### Scenario: Штатный пустой Portfolio на клиенте
- **WHEN** успешно полученный Portfolio содержит пустые `positions` и `cashBalances`
- **THEN** клиент показывает существующее empty-состояние без валютных карточек с вымышленными нулевыми итогами
@@ -0,0 +1,31 @@
## MODIFIED Requirements
### Requirement: Создание и замена подключения T-Bank
Система SHALL позволять авторизованному User создать или заменить единственное подключение через `PUT /api/v1/tbank-connection` с token. Система MUST удалить внешние пробелы token, отклонить пустое значение как ошибку валидации и проверить token запросом T-Bank открытых счетов до сохранения. Пустой список счетов MUST считаться успешной проверкой. Успех MUST вернуть `200` с текущим статусом подключения; успешная замена MUST установить новый `connectedAt`. После успешного подключения MUST начинаться первая фоновая загрузка доступных личных данных без команды frontend. История стоимости прежних счетов MUST сохраняться, но показываться только для счетов, доступ к которым подтверждён новым подключением.
#### Scenario: Успешное создание подключения без счетов
- **WHEN** User отправляет непустой token и T-Bank успешно возвращает пустой список открытых счетов
- **THEN** система сохраняет подключение и отвечает `200` с `{ connected: true, connectedAt: <RFC 3339 UTC timestamp> }`; первая загрузка не создаёт фиктивные счета
#### Scenario: Успешная замена подключения
- **WHEN** User с подключением отправляет другой token и T-Bank успешно его проверяет
- **THEN** система заменяет token подключения, обновляет `connectedAt`, отвечает текущим статусом без token и начинает обновление подтверждённых счетов, не теряя их прежние исторические точки
#### Scenario: Невалидный token не заменяет существующее подключение
- **WHEN** T-Bank отклоняет token как неаутентифицированный или не имеющий прав
- **THEN** система отвечает `400` Problem Details с code `INVALID_TBANK_TOKEN` и сохраняет прежнее подключение без изменений
#### Scenario: T-Bank временно недоступен при проверке token
- **WHEN** проверка token не завершается из-за временной или иной upstream-ошибки
- **THEN** система отвечает `503` Problem Details с code `TBANK_UNAVAILABLE` и не создаёт либо не изменяет подключение
### Requirement: Удаление подключения T-Bank
Система SHALL позволять авторизованному User удалить своё подключение через `DELETE /api/v1/tbank-connection`. Удаление MUST быть идемпотентным и отвечать `204`; после успешного удаления backend MUST прекратить использовать удалённый token и удалить относящиеся к User кэшированные текущие данные T-Bank. История стоимости счёта MUST сохраняться с пометкой удаления и MUST NOT раскрываться до подтверждения нового доступа к тому же счёту.
#### Scenario: Удаление существующего подключения
- **WHEN** User удаляет существующее подключение
- **THEN** система удаляет token и связанные кэшированные текущие данные User, скрывает сохранённую историю стоимости и отвечает `204`
#### Scenario: Повторное удаление подключения
- **WHEN** User удаляет уже отсутствующее подключение
- **THEN** система отвечает `204`, не раскрывая сохранённую историю
@@ -0,0 +1,68 @@
## 1. Данные и граница доступа
- [x] 1.1 [backend] Добавить совместимое хранение точек оценки и видимости истории отдельно от revision; сохранять по валютам только полную оценку в той же транзакции, что и успешно заявленный снимок, с дедупликацией попытки.
- **Covers:** `portfolio-value-history` — `Полная оценка становится точкой истории`; `portfolio-view` — `Свежесть и fallback полного Portfolio Snapshot`
- **Verification:** Изолированная SQLite с применёнными прежними migrations: новая миграция сохраняет старые данные; повторная/поздняя попытка не создаёт лишних точек, неизвестная цена не записывает ни одной валюты, неуспешная публикация сохраняет снимок и историю.
- [x] 1.2 [backend] Связать видимость истории с успешными PUT/DELETE подключения и публикацией полного membership; закрыть доступ старой revision и оставить историю для повторно подтверждённого accountId.
- **Covers:** `portfolio-value-history` — `История переживает смену подключения без потери приватности`; `tbank-connection` — `Создание и замена подключения T-Bank`; `tbank-connection` — `Удаление подключения T-Bank`
- **Verification:** SQLite/HTTP проверки смены токена, отключения, отсутствующего и вернувшегося счёта, ошибки membership, чужого User и поздней попытки; прежние текущие datasets не раскрываются и история не исчезает.
## 2. Часовой обход и локальные чтения
- [x] 2.1 [backend] Применить выбранный в Design Solution Assessment: один native timer и durable conditional reservation для запуска и восстановления часовой попытки без параллельного обхода.
- **Covers:** `hourly-investment-sync` — `Часовой обход личных инвестиционных данных`
- **Verification:** SQLite/integration сценарии первого подключения, рестарта до и после 60 минут, пропущенных часов, перекрытого тика, замены revision и перезапуска незавершённой работы; ни один сценарий не создаёт конкурирующих попыток.
- [x] 2.2 [backend] Оркестрировать подтверждённый membership, портфели счетов, изменяемый хвост операций и события через существующих владельцев источников; агрегировать результат последней попытки и открыть только read-only статус авторизованному User.
- **Covers:** `hourly-investment-sync` — `Часовой обход личных инвестиционных данных`; `hourly-investment-sync` — `Сохранность и статус последней попытки`; `brokerage-account-upcoming-events` — `Upcoming events use complete source results`
- **Verification:** Integration/HTTP проверки: ошибка одного счёта или события даёт частичный статус с безопасным разделом, успешные источники публикуются, старый прогноз и время переживают сбой; чужой User/старая revision не видны, OpenAPI contract parity проходит.
- [x] 2.3 [backend] Перевести обычные GET счетов, портфеля и прогноза на локальное чтение без запуска обновления личных источников; согласовать часовую свежесть, первый незавершённый обход и отдельно отображаемый сбой.
- **Covers:** `portfolio-view` — `Свежесть и fallback полного Portfolio Snapshot`; `brokerage-accounts` — `Состояния недоступности списка счетов`; `brokerage-account-upcoming-events` — `Upcoming events use complete source results`
- **Verification:** HTTP/integration проверки локального ответа без RPC при missing/stale, неизвестного membership, временного сбоя, известного invalid token, неполной оценки и доступности последнего успешного снимка.
- [x] 2.4 [backend] Оставить для журнала запуск только первичной загрузки отсутствующих интервалов; обновление изменяемого хвоста перенести в часовой обход, не перечитывая полные прошлые интервалы.
- **Covers:** `brokerage-account-ledger` — `Отсутствующий исторический интервал загружается только впервые`; `hourly-investment-sync` — `Часовой обход личных инвестиционных данных`
- **Verification:** SQLite/integration проверка пустого полного периода, неполной страницы, повторного открытия прошлого месяца, движения конца текущего периода и неповторного чтения сохранённой истории.
## 3. Инструменты и граница публичного API
- [x] 3.1 [backend] Развязать сохранённые котировки, свечи и события от справочных payload; обновлять текущую котировку и изменяемую свечу не чаще часа, сохраняя покрытия завершённых диапазонов и полные прогнозы.
- **Covers:** `online-instrument-information` — `Рыночные и событийные данные имеют отдельную политику`; `brokerage-account-upcoming-events` — `Upcoming events use complete source results`
- **Verification:** Integration с изолированной SQLite: первая загрузка диапазона, отсутствие повторной загрузки полного прошлого диапазона, запрос свежей котировки, неудача онлайн-справочника и сохранность forecast/coverage/candles; миграция не удаляет связанные рыночные строки.
- [x] 3.2 [backend] Перевести detail, company, поиск и lookup инструментов на онлайн-ответ через существующие integration clients; отделить их ошибку от сохранённых подписей и финансовых ресурсов без самостоятельной долговременной копии.
- **Covers:** `online-instrument-information` — `Справочные сведения и поиск запрашиваются онлайн`; `portfolio-view` — `Свежесть и fallback полного Portfolio Snapshot`
- **Verification:** HTTP/service проверки повторного онлайн-запроса, совпадения UID и ticker, пустого/неоднозначного результата и upstream-сбоя при видимом сохранённом портфеле; не создаются новые долговременные detail/search/negative lookup.
- [x] 3.3 [backend] Сформировать публичный OpenAPI со статусом обхода, сохранив переходный контракт screen POST до готовности frontend.
- **Covers:** `hourly-investment-sync` — `Сохранность и статус последней попытки`; `portfolio-view` — `Состояния и ошибки Portfolio на клиенте`
- **Verification:** `npm run openapi:generate`, `npm run test:openapi-contract` и configured HTTP suite для действующего и переходного wire contract; обычный GET не инициирует screen POST.
- [x] 3.4 [backend] Проверить рост SQLite на представительном объёме и фактическую частоту upstream-вызовов; сопоставить её с действующей первичной документацией T-Bank перед настройкой частоты, выполнить штатные backend gates и durability для новой схемы.
- **Covers:** `N/A` — операционная проверка выбранного в Design хранения и scheduler, без нового пользовательского поведения.
- **Verification:** Выполнено: SQLite 617,263,104 bytes на synthetic profile (3 accounts, 300,000 operations, 200 instruments, 1,000,000 candles), persisted-read phase сделал 0 upstream calls; `npx prisma validate`, durability с pre-history baseline, `npm run test:e2e` и performance budget проходят. Ограничение `TBANK_RATE_LIMIT_PER_SECOND` (default 5 per backend instance) зафиксировано в `docs/adr/020-hourly-sync-verification-evidence.md`; production quota не расширяется.
## 4. Клиентские экраны
- [x] 4.1 [frontend] Получить опубликованный backend OpenAPI artifact и обновить локальный snapshot/generated types для статуса без импорта backend internals.
- **Covers:** `hourly-investment-sync` — `Сохранность и статус последней попытки`
- **Verification:** `npm run openapi:generate` и `npm run openapi:check` из frontend на предоставленном artifact; статус типизирован, отсутствие попытки и изоляция ошибки не заменены вымышленным успехом.
- [x] 4.2 [frontend] Удалить инвестиционные кнопки/POST ручной синхронизации и source-recovery triggers; сохранить транспортный retry только как повтор чтения, а открытые инвестиционные экраны перечитывать раз в 30 минут.
- **Covers:** `portfolio-view` — `Состояния и ошибки Portfolio на клиенте`; `brokerage-accounts` — `Экран списка брокерских счетов`; `brokerage-accounts` — `Клиентские состояния ошибки списка счетов`; `frontend-data-resilience` — `Client Cached Snapshot остаётся доступным при обновлении Accounts и Portfolio`
- **Verification:** UI/query проверки 30-минутного активного экрана, прекращения опроса после ухода, сохранения снимка при сбое и повторного HTTP GET без POST; auth-refresh и действия ручных карт не меняются.
- [x] 4.3 [frontend] Показать в кабинете время/исход последнего обхода и названия проблемных счетов и разделов; справочный блок карточки отделить от позиций, событий, свечей и локальной ошибки чтения.
- **Covers:** `hourly-investment-sync` — `Сохранность и статус последней попытки`; `online-instrument-information` — `Справочные сведения и поиск запрашиваются онлайн`; `brokerage-accounts` — `Экран списка брокерских счетов`
- **Verification:** Проверки состояний `none/running/succeeded/partial/failed`, локального сбоя detail при видимых финансовых данных, доступных текстовых подписей и переноса статуса на узком экране согласно inline UI package Design revision 1.
- [x] 4.4 [frontend] Завершить клиентский переход штатными gates и сверкой критического маршрута нового подключения → первый обход → статус → сохранённый портфель на открытом экране.
- **Covers:** `N/A` — интеграционная проверка принятого UI package и публичного контракта после изменения нескольких экранов, без отдельного нового требования.
- **Verification:** `npm run verify` в frontend и наблюдаемые состояния критического маршрута с предоставленным backend OpenAPI.
## 5. Завершение перехода
- [x] 5.1 [backend] После перехода frontend убрать неиспользуемый screen POST и наблюдение прежней работы без затрагивания нового статуса обхода.
- **Covers:** `hourly-investment-sync` — `Сохранность и статус последней попытки`; `portfolio-view` — `Состояния и ошибки Portfolio на клиенте`
- **Verification:** `npm run openapi:generate`, `npm run test:openapi-contract` и configured HTTP suite: старый trigger недоступен, обычный GET и повтор чтения не запускают импорт.
## 6. Нормативная и as-built документация
- [x] 6.1 [specs] Проверить полное соответствие девяти Delta Specs одобренному Proposal и Design, сохранить точные имена Requirements и валидировать change и основные specs.
- **Covers:** `N/A` — самостоятельная проверка нормативного артефакта и согласованной границы изменения, без добавления нового поведения.
- **Verification:** `openspec schema validate wealth-ledger-sdd --verbose`, `openspec validate hourly-portfolio-sync-history --type change --strict --store wealth-ledger-specs`, `openspec validate --specs --strict --store wealth-ledger-specs`, `npm run verify` в specs.
- [x] 6.2 [docs] После проверки реализованного поведения дополнить глоссарий и объяснение свежести/истории, обновить соответствующие страницы инвестиций и зафиксировать новый ADR, который связывает изменённое решение с ADR 017, не переписывая исторический контекст.
- **Covers:** `N/A` — as-built объяснение подтверждённых решений Design в отдельном docs owner, не параллельная норма требований.
- **Verification:** Проверить маршруты чтения, локальные ссылки и смысл финансовых значений; `npm run verify` из docs.
@@ -0,0 +1,2 @@
schema: wealth-ledger-sdd
created: 2026-09-24
@@ -0,0 +1,566 @@
## Context
Основание — принятый [ADR 017](../../../../docs/adr/017-local-tbank-reads-and-background-sync.md)
и [дельты поведения](specs/tbank-data-sync/spec.md). Это детализация согласованной
модели для реализации. Компоненты приложения сейчас не изменены.
Исследованы локальные владельцы после чтения AGENTS каждого репозитория.
Свидетельства относятся к checkout на 2026-09-24, а не к обещаниям API:
| Граница | Текущий owner и переиспользуемое поведение |
| --- | --- |
| Хранение | `backend/prisma/schema.prisma` и `src/prisma/prisma.service.ts`: SQLite/Prisma, пользователи, сессии, подключение и ручные карты; T-Bank datasets отсутствуют. Условия владения и транзакции уже применяются в `modules/bank-cards/infrastructure/bank-cards.repository.ts`. |
| Доступ | `backend/src/integrations/tbank/tbank-access.service.ts`: `withSupportedAccounts` и `withAccountForConnection` вызывают `getOpenAccounts`; generation вычисляется из encryptedToken. `tbank-account.policy.ts` владеет допустимыми типами счетов. |
| Интеграция | `backend/src/integrations/tbank/tbank.client.ts`: общий FIFO, лимит 5 RPC/с по умолчанию, максимум 50 ожидающих, deadline и ограниченные повторы; повторный limiter не нужен. |
| Личный источник | `portfolio/application/portfolio.service.ts` хранит RAM-снимок 30 с / fallback 5 мин; `operations/application/brokerage-operations.service.ts` — страницы 30 с и обход курсоров. `income/income-analytics.service.ts` строит selected/previous/yearAgo через общий сборщик операций. Пути внутри `backend/src/modules/investments/`. |
| Общий источник | `backend/src/integrations/tbank/instrument-metadata.service.ts` уже разделяет общие metadata и pending lookup. Каталоги stocks/bonds смешивают общую информацию и личную позицию в user+generation RAM-кэше. Market services режут свечи по 365 суток. |
| Выплаты и смежные данные | `upcoming-events/upcoming-events.service.ts` отдельно получает portfolio; stock dividends приходят из T-Bank, bond events — из MOEX. Stock company use case получает asset description/fundamentals; bond metrics включают MOEX. Источники не подменяются. |
| Frontend | `frontend/src/entities/*/api` владеют generated queries; page VM собирает зависимости. `features/instrument-details/model/section.ts` знает только HTTP/query lifecycle. `features/tbank-connection/model/connection-boundary.ts` уже сбрасывает финансовую границу после успешной mutation. |
| Контракт | Backend DTO/controllers/Swagger — editable source. `frontend/src/shared/api/spec/openapi.json` — явно передаваемый snapshot. Accounts и search сейчас массивы; общего sync/coverage API нет. |
Исходный proto допускает изменение operation ID
(`backend/src/integrations/tbank/proto/contracts/operations.proto:496`),
но проект явно принимает стабильность. `is_complete` свечи задаёт источник
(`marketdata.proto:438`). В `operations.proto:472–489` не определены
inclusive/exclusive границы, срок жизни cursor и значение пропущенного `from`.
Эти пробелы не заменяются предположениями о гарантиях брокера.
## Goals / Non-Goals
**Goals:** один постоянный источник чтения в существующей БД; независимые
полнота, свежесть и состояние работы; атомарная публикация согласованного
набора; безопасный перезапуск; отсутствие RPC на пути ответа заполненной БД;
контракт всех экранных зависимостей и минимальный наблюдаемый worker.
**Non-Goals:** Redis, новый сервер БД, отдельный сервис очередей, cron,
распределённое выполнение, версии каждого портфельного снимка, live
подтверждение доступа при каждом чтении, архив отключённых счетов,
новый frontend store, изменение денежных правил или источников MOEX.
## Decisions
### 1. Существующие domain owners читают БД; RPC принадлежит синхронизации
Сохраняются controllers, application services, нормализация Money и domain
политики. Их read path получает сохранённые источники и согласованное
metadata, а запрос работы передаёт небольшому T-Bank sync coordinator.
PortfolioService становится общим владельцем последнего полного portfolio
для accounts summary, личных позиций инструментов и будущих событий.
Сборщик операций становится одним локальным источником ledger/income;
filtered HTTP page больше не является единицей источниковой полноты.
Рассчитанные доходы не требуют отдельной постоянно дублируемой таблицы.
Синхронизация использует существующие TbankClient, metadata mapping,
market-window splitting, точную денежную арифметику и MOEX adapters.
Нормализованные числа сохраняются десятичными строками, не float.
Presentation metadata не блокирует выдачу финансового снимка; fallback
UID/FIGI/name остаётся доступен до её фоновой загрузки.
Синхронная проверка token при PUT подключения сохраняется. Успешно полученный
в этой проверке список можно сразу опубликовать как membership нового
подключения в той же транзакции, без второго GetAccounts.
### 2. Личные и общие модели в SQLite
Это логические группы таблиц, не универсальный JSON-кэш всех HTTP-ответов:
| Группа | Ключ, содержимое и инвариант |
| --- | --- |
| Подключение и membership | Существующий `TbankConnection` получает opaque `revision` и состояние известного отказа token. Список открытых поддерживаемых счетов принадлежит `(userId, revision)`; хранит accountId, type, name, openedOn, upstream order, fetchedAt, membershipRevision. Список заменяется целиком атомарно, включая успешный пустой. |
| Portfolio | `(userId, revision, accountId)` → последний полный нормализованный snapshot, fetchedAt и dataRevision. Допустимо хранить состав snapshot как versioned JSON: он читается целиком, не нужен отдельный исторический агрегат. |
| Operations | Уникальность `(userId, revision, accountId, brokerOperationId)`; индекс `(userId, revision, accountId, occurredAt, brokerOperationId)`, дополнительные instrument/category поля для фильтров. Полный исходный нормализуемый payload сохраняет точные поля, executed state и version маппинга. Cursor/пустая строка не заменяют отсутствующий ID. |
| Инструменты | Общий ключ provider + instrument UID; FIGI/ticker — aliases с существующими правилами неоднозначности. Metadata, company/fundamentals и catalog discovery имеют собственные успешные timestamps и coverage; отрицательный успешный lookup отличается от ошибки. |
| Рыночные данные | Общие quote/trading-status и необходимые price dependencies; дневная свеча уникальна по `(provider, instrumentUid, interval, time)`, содержит `isComplete`. Календари имеют source/type/instrument/range и исходные события. MOEX enrichment сохраняет собственный source и существующую семантику недоступности. |
| Последний полный прогноз | Private snapshot account events и instrument personal payout estimates по `(userId, revision, account scope, instrument при наличии, applied range)`, с dependency revisions, source timestamps и calculatedAt. Заменяется только после готовности всех обязательных источников; это fallback результата, не история portfolio. |
| Покрытие и работа | Завершённые диапазоны, fetchedThrough/fetchedAt, dataRevision, известная нижняя граница и её происхождение; pending intent, attemptId, processRunId, диапазон, стадия и прогресс страниц. Staging страниц текущей попытки изолирован от опубликованного набора. |
Общие таблицы не содержат user/account/quantity/estimatedAmount или token.
Личные payout estimates, holdings и фильтрованная личная история вычисляются
из разрешённых accounts и portfolio/operations. Статус пользовательской
работы хранится отдельно от общих instrument records; ошибка credentials
не становится общим отрицательным lookup.
FK и unique constraints обеспечивают идентичность; транзакции проверяют
владение и revision. Не создаются generic repository framework, event bus
или универсальный job engine. Точные SQL-имена выбираются по локальным
Prisma conventions без изменения этой модели.
### 3. Доступ без синхронного GetAccounts
Read path: локальная сессия → существующее подключение и его revision →
известная ошибка token → membership текущей revision → данные разрешённых
счетов. Token для этого не расшифровывается.
- Membership отсутствует: `200` с `data: null`, `availability: missing` и
состоянием discovery, без данных неизвестного accountId. После discovery
следующий read возвращает данные либо прежний `404`.
- Membership свежий: разрешённые данные читаются локально.
- Membership старше пяти минут: используется последнее успешное наблюдение,
обычный запрос ставит его фоновое обновление. Это ограниченная задержка
обнаружения отзыва доступа, принятая локальной моделью, не live authority.
- Успешный новый список немедленно ограничивает все последующие одиночные
и aggregate reads; добавленный ещё не загруженный счёт делает aggregate
неполным. Ответ «Все счета» использует одну согласованную DB read transaction
с фиксированной membershipRevision.
- Известный `INVALID_TBANK_TOKEN` блокирует личные reads и возвращает прежний
`400`; `409 TBANK_NOT_CONNECTED`, `404`, session/validation errors сохраняются.
Старые записи не удаляются вследствие transient failure, но не выдаются
вопреки известному отказу доступа. Исправление token выполняется существующим PUT.
При замене успешная транзакция назначает новую случайную revision, сохраняет
новый token и очищает/делает недоступными прежние private datasets/jobs
по существующей семантике invalidation. Delete остаётся идемпотентным и
каскадно убирает личные данные подключения; специальный архив не вводится.
Общие неперсональные данные могут оставаться.
Каждая публикация данных, coverage, ошибки, timestamp и результата работы
проверяет в той же короткой транзакции текущие connection revision и
attemptId. Проверка после RPC только в памяти недостаточна. Проверка нужна
и при общей публикации, начатой credentials старого подключения.
Поздняя попытка не вправе изменить даже freshness; результаты старой revision
отбрасываются. Транзакция никогда не удерживается во время сетевого вызова.
### 4. Интервалы и атомарная сверка операций
Внутри БД диапазоны — `[from, to)` в UTC. Public semantics не меняются:
журнал принимает действующие UTC timestamps, его default начинается 1 января
UTC; даты доходов включительны в `Europe/Moscow`, и существующий
`planIncomePeriod` вычисляет selected/previous/yearAgo. Преобразование границ
выполняется один раз до планирования. Полнота доходного месяца проверяется
по объединению UTC coverage, а не по совпадению названия месяца.
Источник по операциям загружается ограниченными календарными UTC-месяцами
и фрагментами нужных диапазонов. Верхняя граница текущей попытки фиксируется
в момент её создания. Для неизвестной inclusivity upstream применяется
перекрытие соседних окон с защитной секундой; записи локально отбираются
по точному `[from, to)` и дедуплицируются по исходному ID. Это защищает
стык, но не доказывает отсутствие будущих исправлений брокера.
Обычная полнота счёта строится только по всем EXECUTED операциям интервала
без category/instrument фильтров. UI filters и pagination применяются после
чтения. Ручной refresh личной истории инструмента может перечитать только
`instrumentUid + shown range`; его reconciliation удаляет отсутствующие
записи только этого predicate и не присваивает полноту всему счёту.
Уже полное общее покрытие может обслуживать такой subset; обратное неверно.
Каждая успешная страница записывается в staging attempt с page count и
прогрессом. При наличии полного прежнего набора staging никогда не виден
как его замена. Новые большие периоды становятся доступными по завершении
малых интервалов при первичном заполнении, поэтому экран может иметь `partial` coverage без показа
непроверенных страниц. Сбой, repeated/missing next cursor, malformed ID или
противоречащие версии одного ID внутри попытки оставляют интервал незавершённым.
Идентичные дубликаты страницы безопасно схлопываются.
Только успешное исчерпание всех страниц фиксированного интервала разрешает
транзакцию публикации: upsert новых/изменённых полей, удаление отсутствующих записей
в перечитанном predicate, публикация coverage и времени успешной сверки.
Account dataRevision увеличивается только при изменении rowset;
coverage/timestamp/status без изменения строк меняют только stateVersion.
Читатель видит старый либо новый набор,
не их смесь. Между интервалами частичная готовность допустима и явно видима;
сумма всего периода окончательна только при полном покрытии его зависимостей.
Для повторной сверки уже полного показанного периода единица публикации —
весь запрошенный scope, а не отдельный календарный chunk. Staging объединяет
его интервалы, сравнения и счета (`all`) в одну publication group; успешные
страницы/окна сохраняют прогресс, но commit/upsert/delete/coverage всех
участников выполняются только после успеха группы. Любая ошибка оставляет
весь прежний результат. Такое же правило действует для ручного refresh
показанного диапазона свечей. Первичная загрузка ранее отсутствующих данных
по малым интервалам остаётся инкрементальной. Пересекающиеся работы
присоединяются к группе и не публикуют её часть независимо. DB transaction
короткая и локальная; RPC выполняются до неё, а staging не держится в RAM.
Для изменяемого конца `to=now` полнота означает «все операции по состоянию
на `asOf` последней успешной сверки». Новый ещё не наблюдённый хвост времени
в пределах того же текущего месяца и TTL не считается пропуском, запускающим
новую загрузку; ответ сохраняет исходный `asOf`, не утверждая покрытие будущих
операций. Настоящие пропуски до `asOf`, новый явно запрошенный исторический
диапазон и переход месяца этому исключению не подчиняются.
Текущий месяц обновляется целиком до зафиксированного now при истечении
пяти минут, чтобы учесть изменения уже виденных операций. После смены месяца
его покрытие до прежнего now не распространяется до конца месяца:
следующий подходящий запрос завершает хвост. Полные прошлые интервалы
не сверяются автоматически. Успешная наблюдённая смена полей того же ID
обновляет запись и производные результаты; поиск сменившихся ID отсутствует.
### 5. Планирование, приоритет и восстановление
Поддерживаемая поставка — один backend process с существующей SQLite.
Запрос сохраняет intent, возвращает локальные данные и передаёт выполнение
in-process coordinator. На startup нет обхода очереди или запуска импорта.
Process получает случайный runId; следующий подходящий запрос перехватывает
незавершённую работу другого runId новой attemptId. Persisted provider cursor
не переиспользуется после перезапуска: незавершённый интервал начинается
с первой страницы, staging прежней попытки очищается. Завершённые интервалы
и опубликованные данные сохраняются.
Приоритеты: discovery доступа → нужные экрану зависимости и интервалы,
включая comparisons → остальная история счёта и рыночная история его бумаг.
Между окнами/страницами coordinator отдаёт приоритет новой экранной работе;
уже отправленный RPC не прерывается ради перестановки. Один producer
догрузки не ставит тысячи RPC заранее: страницы идут последовательно,
новые окна планируются постепенно, через прежний bounded limiter.
Очередь RPC и bounded retry не дублируются на уровне worker.
Ключ personal work: user + connectionRevision + account + source kind +
normalized interval/predicate. Координатор сериализует публикации одного
account source и для обычной загрузки вычитает покрытые/выполняемые
пересечения; одновременно запрошенные остатки объединяет. Force refresh
не вычитает полное покрытие: он обязан перечитать запрошенный target,
но присоединяется к уже выполняемой подходящей загрузке. Повторный force refresh присоединяется к
подходящей активной попытке, уже получающей полный запрошенный диапазон;
TTL при этом не отменяет запрос. Новая область расширяет pending intent.
Shared work использует provider/instrument/source/range identity.
Общий успешный результат переиспользуется независимо от пользователя.
У каждого наблюдателя свой авторизованный handle, без выдачи чужих job IDs
и credentials errors. Ошибка token инициатора завершает его попытку, не
портит общий источник; новый допустимый запрос другого пользователя может
запустить попытку со своими credentials. Старый job не перехватывает token
другого пользователя сам.
Остальная история начинается с известной даты открытия; если её нет,
используется существующая техническая граница `1970-01-01T00:00:00Z`,
явно помеченная как requested floor, а не доказанная дата открытия/минимум
источника. Сохраняются confirmed intervals и `historyBoundary: unknown`;
«вся история загружена» не показывается до подтверждения нижней границы.
Отказ источника за пределами доступного диапазона сохраняется как ограничение,
не как пустой полный интервал. Отсутствие `from` не используется как
недокументированное обещание all-time. Это не мешает полной готовности
подтверждённых периодов экрана.
После transient failure попытка заканчивается с error; новый обычный запрос
может возобновить недостающее/устаревшее, ручной — повторить независимо от TTL.
Наблюдение результата этого не делает. Logout/уход останавливает наблюдение
frontend, но не отменяет уже запрошенную допустимую серверную работу.
Замена/delete подключения останавливает публикацию по revision fence.
### 6. Общие market data и производные выплаты
Quotes и portfolio обновляются через минуту; metadata/catalog/company и
календари — через сутки. Freshness принадлежит конкретному source/range,
поэтому успешный lookup одного инструмента не делает свежим весь каталог.
Catalog discovery сохраняет успешный набор UID/порядок отдельно от
enrichment по нормализованному поисковому query и типу инструмента, сохраняя
существующий предел результатов. `findShares/findBonds` не загружают весь
каталог: успешный поиск SBER не подтверждает результат GAZP. Новый query
без подтверждённого покрытия запускает discovery, а не возвращает ложный
пустой результат. Metadata уже известных UID переиспользуется; повтор того
же свежего полного query не вызывает discovery RPC. Отсутствие enrichment
не превращает найденный набор в пустой поиск.
Свечи сохраняют исходную `isComplete`, диапазоны и `historyFrom`.
Переиспользуются окна до 365 суток. Завершённый диапазон без свечей хранится
как проверенное пустое покрытие. Текущий UTC-день всегда изменяемый,
включая пустое окно до торгов; его TTL — минута. Смена UTC-дня сама не
закрывает ни пустое открытое окно, ни незавершённую свечу: следующий запрос
повторяет прошедшее окно; успешный закрытый пустой диапазон подтверждает
отсутствие, для имеющейся свечи нужно `isComplete=true`.
Известный `first_1dayCandleDate` определяет нижнюю границу all-range.
Если его нет, сохраняется существующее `*_HISTORY_START_UNAVAILABLE`,
не создаётся ложная полная история. Предзагрузка касается бумаг сохранённого
текущего портфеля и использует ту же общую coverage, что открытый график.
События инструментов сохраняются без пользовательской ожидаемой суммы.
Календари купонов/дивидендов и company data охватываются общим read path;
future events используют сохранённый portfolio и исходный календарь,
сохраняя неопределённость прогноза, cancelled-dividend и offer semantics.
MOEX bond events/metrics остаются MOEX: необходимые для экрана результаты
также сохраняются и обновляются в этой работе, без нового MOEX scheduler.
Недоступность optional enrichment не делает финансовые данные ошибочными;
неполнота обязательного календаря не выдаётся за полный пустой прогноз.
Последний полный прогноз/личная оценка сохраняется отдельным private
snapshot по показанному scope/range. Если новый portfolio добавил бумагу,
а её календарь не загрузился, следующий read, в том числе после restart,
возвращает прежний полный forecast с его прежними dependency timestamps и
ошибкой обновления. При отсутствии такого snapshot возвращается missing/partial
без окончательного списка. Готовый новый результат атомарно заменяет snapshot;
снимок с иным периодом или membership не выдаётся под новым scope, а доступ
всегда повторно ограничивается текущим membership.
### 7. Публичный контракт и наблюдение работы
**Contract Gate Decision: required.** Меняется wire shape всех T-Bank data
GET, включая bare-array accounts и stock/bond search. Backend сохраняет
маршруты и тип внутреннего payload, добавляя единый envelope:
```text
{ data: T | null, meta: {
resourceKey, dataRevision, stateVersion,
availability: missing | partial | complete,
freshness: unknown | fresh | stale, lastSuccessfulAt: timestamp | null,
sync: { id: opaque | null, state: idle | queued | running | failed,
errorCode: safeCode | null },
coverage?: { requested, completeRanges, missingRanges,
asOf, historyBoundary, historyState },
dependencies?: [{ resourceKey, availability, freshness,
lastSuccessfulAt, sync, coverage?, unavailableReason? }]
} }
```
Connection generation/revision остаётся внутренней: существующая security
policy запрещает её в API/logs/errors, даже если она случайная. Frontend
использует свою существующую session/connection boundary.
`resourceKey` — нормализованная идентичность запроса без user secrets и
connection generation. `dataRevision` меняется при публикации данных;
изменение статуса работы не меняет страницу операций. Availability означает
готовность обязательных источников данного результата, а не обещание полноты
рынка или будущих выплат. `data:null` используется для missing и для
неполных финансовых агрегатов; `partial` ledger/candles может содержать
подтверждённые части. Полнота selected/comparisons задаётся отдельно:
analytics `data` доступен после полного selected, а `previous` и
`yearComparison` nullable до готовности соответствующего сравнения.
`meta.dependencies` различает missing/loading/failed comparison и
заведомо недоступное по календарю/границе истории сравнение; последнее
имеет отдельный `unavailableReason` и не планируется как пропуск.
Это явное изменение вложенного analytics DTO, а не только envelope.
Для зависимостей с разными TTL metadata содержит их состояния по
resourceKey; freshness производного результата — худшее обязательное
состояние, lastSuccessfulAt — самый ранний из использованных успешных
источников. Чтение/`calculatedAt`/`observedAt` не продлевает source freshness.
Portfolio `data.lastSuccessfulAt` и `data.isStale` сохраняются для текущих
consumers и отображают тот же portfolio source, без прежнего пятиминутного
предела хранения. Error фонового обновления не меняет успешный timestamp
и сама по себе не отменяет ещё действующий TTL.
| Операция | Контракт |
| --- | --- |
| Обычный GET существующего resource | `200` envelope, в том числе cold/partial; планирует missing/stale зависимости. До постановки проверяет auth, connection и известную принадлежность. |
| Тот же GET с `sync=observe` | Только локальное чтение; никогда не создаёт и не возобновляет work, даже при missing/stale/error. Нужен для получения изменившихся данных после status observation. |
| `POST /api/v1/tbank-sync` | Валидированный screen descriptor, force по TTL; `202` с авторизованным work handle и normalized scope. При совпадении присоединяет к work. Приём не означает успех. |
| `GET /api/v1/tbank-sync/:id` | Только статус разрешённого handle, progress, changed resource revisions и terminal outcome; никаких новых запусков/повторов. Чужой/прежней connection handle — `404`. |
Descriptor — discriminated union `screen: accounts | account | operations |
income | events | stocks | bonds | stock | bond`. Поля разрешены только
соответствующему screen: account scope (конкретный ID/`all`), applied period,
filters, instrumentUid, candle period/range. Сервер использует прежние
валидаторы дат/лимитов/UID и сам раскрывает dependency graph; произвольного
списка RPC/таблиц/чужого userId во входе нет. Зафиксированный now и нормализованный
scope возвращаются клиенту, чтобы ручная операция не переезжала на другой период.
| Экран | Зависимости force refresh |
| --- | --- |
| Accounts | Membership + portfolio summary каждого текущего доступного счёта; metadata показанных позиций по необходимости. Не вся история операций. |
| Account / all overview | Membership, portfolio, показанный recent-ledger range, current-month income, upcoming events и необходимые instrument metadata/calendars. |
| Operations | Применённые account scope и период; категории — локальная проекция. Пагинация не расширяет источниковой период. |
| Income | Применённый период + previous/yearAgo из существующего planner, membership, требуемая metadata выплат. |
| Events | Membership, portfolio и прогноз применённого включительного from/to из сохранённых календарей; default from=today UTC, to=from+30 дней. |
| Stocks / bonds search | Общий каталог, результаты/enrichment текущего поиска и показываемые prices; без истории всего каталога. |
| Stock / bond | Detail/company либо metrics, quote и price dependencies, видимый candle range, payout calendar, личная position и применённый диапазон/инструмент personal operations. |
HTTP ошибки auth/input/access сохраняют `application/problem+json`.
`TBANK_UNAVAILABLE` из принятой фоновой работы передаётся как безопасный
`meta.sync.errorCode`, включая cold failure; `503` остаётся для запроса,
который не удалось принять/обслужить. Известный отказ credentials остаётся
`400 INVALID_TBANK_TOKEN`, не успешным envelope с недоступными private data.
Frontend различает server work error и transport failure.
Работа имеет terminal `succeeded | failed | cancelled` в status endpoint;
envelope отображает successful отсутствие активной работы как `idle`.
Статус имеет `stateVersion` нужных экрану ресурсов и отдельный прогресс background
history, поэтому готовые данные можно прочитать до окончания всей догрузки.
Frontend наблюдает только известный active handle на открытом экране
с интервалом 1–5 секунд и без автоматического retry failed work. После
изменения stateVersion или terminal status выполняется `sync=observe` GET.
После ухода/terminal status наблюдение прекращается. Это наблюдение уже
запрошенной работы, не периодический запуск импорта.
`stateVersion` — opaque идентичность состояния ответа, меняется при изменении
данных, coverage (включая пустое завершённое окно), successfulAt, ошибок,
работы и доступа; это не pagination revision. Он вычисляется из версий
зависимостей и состояния без выдачи внутренних connection identifiers.
Прогресс далёкой истории, не меняющий этот resource, не меняет его
stateVersion и не заставляет повторно читать уже готовый экран.
Истечение времени не создаёт работу: freshness вычисляется из timestamp.
Entity query owner записывает observe-ответ в ту же canonical cache identity
исходного GET; не создаётся второй cache по параметру observe. Query keys
берутся из generated owner, не собираются вручную. Все новые status/mutation
ответы участвуют в существующих session/connection boundary fences.
Кроме них owner использует latest-request sequence для каждого resource:
ответ более раннего запроса не заменяет уже принятый более поздний ответ.
При status notification запоминается ожидаемый stateVersion; observe-ответ
с прежней версией не публикуется, выполняется только новое observational read.
Подтверждённый access loss отменяет/инвалидирует соответствующие personal
queries и повышает локальную access epoch; даже поздний успешный ответ той же
connection, начатый до отзыва, не восстанавливает скрытые данные.
### 8. Локальная пагинация и финансовые результаты
Порядок операций сохраняет действующую сортировку с детерминированным
tie-break по accountId/operationId. Оpaque cursor связан с user, connection,
membershipRevision для all, нормализованными filters/period/limit и
account operations dataRevision. Любое изменение relevant rowset делает
cursor недействительным. Binding хранится серверно либо проверяется keyed
digest: connection generation не находится даже в декодируемом cursor payload.
Изменение rowset возвращает существующий `INVALID_OPERATIONS_CURSOR`;
исторические копии всех rowsets не хранятся. Первая реализация использует
revision на счёт: изменение другого периода тоже может сбросить его cursor;
более узкая revision нужна только при доказанно мешающих сбросах.
Frontend сбрасывает cursor stack на первую страницу, сохраняя applied filters
и объясняя обновление списка; ни страницы, ни суммы разных revisions не
склеиваются. `hasNext=false` означает конец текущей локальной выдачи,
а не полноту истории. Coverage/status без изменения строк cursor не ломают.
Income сохраняет точные signed coupon/dividend payments и существующие
comparison rules. Stable operation ID устраняет повторный финансовый учёт;
прежняя норма о разных entries с одинаковым ID явно изменена delta spec.
Analytics читает согласованный набор операций и coverage в read transaction.
Неполнота selected скрывает его окончательные totals; неполнота comparison
скрывает только его суммы/разницы и не создаёт нулевую базу. Уже опубликованный полный результат того же
period/context сохраняется при неудачной повторной сверке; данные другого
периода никогда не подписываются новым диапазоном.
### 9. Измеримый бюджет и стоимость интеграции
500 мс — цель, не текущий результат. Начальный профиль приёмки: один backend
process, локальная SQLite на SSD, 4 CPU / 8 GiB, 3 счёта, по 100 000 операций
за 10 лет, 200 инструментов и по 5 000 дневных свечей, включая пустые
интервалы; до 1 000 операций/200 доходных выплат на счёт в активном месяце.
Измеряются account/all reads, ledger page 50, monthly income с comparisons,
instrument detail/quote/company/metrics, year/all candles и payouts/search.
Бюджет — p95 ≤ 500 мс от входа HTTP на backend до окончания ответа при
10 одновременных клиентах и не менее 100 чтений каждой группы после прогрева.
Отдельно сохраняются p50/p99/max, размер ответа, состояние БД, hardware и
время первого DB-only ответа после перезапуска. Cold import исключён из
бюджета заполненной БД, но его HTTP ответ также не ожидает RPC. Больший
реальный профиль не объявляется измеренным по этим данным.
Изоляция от брокера проверяется задержанным/отказывающим T-Bank; источник
замеряемых данных — HTTP timing и счётчики RPC существующего logger. Для
fresh полной выборки ожидается 0 RPC; для 20 simultaneous stale requests —
один логический проход каждой нужной зависимости, с явным учётом pages и
разрешённых retries. После завершения, без screen/manual requests — 0 RPC.
Повторные старые полные intervals/candles, в том числе после restart, —
0 повторных RPC этих диапазонов. Сравнение с исходной реализацией выполняется
на одинаковом navigation trace; сохраняются абсолютные числа и доля снижения
`1 - new/old`, если baseline ненулевой. Измерения не требуют real tokens
в артефактах или логах.
## UI/UX Applicability and Package
**Applicability and scope:** `new/changed` — общие состояния и действие
обновления всех T-Bank экранов, без перестройки их содержимого.
**Package revision:** `1`. Пакет полностью inline; локальные PNG/SVG не
нужны, поскольку меняются информационные состояния и существующие controls,
а не композиция экранов. Self-hash и внешние визуальные зависимости отсутствуют.
**Screens, states and interactions:**
| Состояние | Представление и действие |
| --- | --- |
| Missing без результата | Существующий структурный skeleton только отсутствующих секций, «Загружаем данные», доступное busy-состояние; без нулей/«операций нет». |
| Partial period | Подтверждённые строки видны с «Период загружен не полностью». Финансовые totals/comparisons ждут полноты. «История догружается» отдельно от состояния показанного периода. |
| Complete fresh | Контент, время источника и «Обновить» в заголовке экрана. Истечение TTL само не запускает UI timer refresh. |
| Complete stale / active | Прежний контент и timestamp, «Обновляем» либо «Данные на …»; без skeleton поверх сумм. Повторная кнопка disabled для уже запрошенного текущего scope с доступным пояснением. |
| Failed with data | Прежний контент, исходное время, безопасное предупреждение и повторное «Обновить». Transport failure и server work failure различимы; статус не сообщает об успешном обновлении. |
| Failed without data | Локальное error-состояние секции, безопасный retry либо действие настроек согласно коду. |
| Complete empty | Только подтверждённое пустое состояние; empty portfolio не создаёт нулевые карточки, пустой доход объясняет отсутствие купонов/дивидендов. |
| Access lost / connection changed | Очистка недоступного personal view, настройки для token/connection, возврат к счетам для 404. Нельзя удерживать чужую/прежнюю revision под предупреждением. |
Accounts показывают freshness списка отдельно от локального состояния
portfolio summary каждой карточки. Account/all overview сохраняет независимые
секции. Operations сохраняет применённые фильтры; income не переименовывает
старый snapshot новым period. Stock/bond показывают состояния по sections,
не смешивая общий график с личными holdings/payout estimates. Детали candle
по-прежнему обозначают незавершённость.
**Responsive behavior:** на 320–375 px timestamp, предупреждение и кнопка
переносятся без горизонтального overflow и обрезания сумм; на широком экране
используют существующий header/section layout. Focus сохраняется после
обновления; button доступен с клавиатуры, текст статуса не зависит от цвета,
`aria-live=polite` объявляет смену стадии/ошибку, но не каждую страницу.
Skeleton не фокусируется; reduced motion сохраняется.
**Reused source and differences:** runtime Mantine controls, существующие
Section/VM patterns и generated query owners переиспользуются как код.
Это новый inline пакет, не reuse изменяемого или архивного design package;
его состояние полностью определено выше.
## Delivery Map
| Area | Status | Architectural outcome or N/A reason |
| --- | --- | --- |
| frontend | `affected` | Новый snapshot/types, metadata-aware entity queries/VMs всех экранов, scoped refresh и observe-only status, connection/cursor boundaries, inline UI package. |
| backend | `affected` | Prisma storage/migration, local access/read owners, durable interval sync/fencing, shared market sources, DTO и OpenAPI; evidence корректности, latency и RPC. |
| specs | `affected` | Этот change и семь delta capabilities; traceability реализации без преждевременного изменения main specs или чужих changes. |
| docs | `affected` | As-built freshness/storage/access/flows/API/glossary и актуальный статус ADR после поставки; не второй источник требований. |
## Solution Assessment
Trigger: storage, external integration, caching, concurrency и public API.
| Вариант | Fit, maturity, security, operating cost и migration cost |
| --- | --- |
| Существующие SQLite/Prisma + транзакции, unique keys и небольшой request-driven coordinator | Выбран: зрелая используемая БД, один процесс, отсутствие нового сервиса; ownership/fencing доступны в SQL. Миграция добавляет таблицы и revision. Специфичные broker intervals/reconciliation остаются доменной логикой. |
| Продлить RAM/TanStack кэш | Зрелый дешёвый механизм показа, но несовместим с durability и полнотой после restart. Остаётся presentation cache, не решение хранения. |
| Redis + BullMQ | Зрелые retries/locks/queues, но требует нового сервиса, persistence/security/backup и двойной согласованности с SQLite; его jobs всё равно не решают domain coverage/атомарную сверку. Нет нужды в распределённых workers, поэтому цена миграции/эксплуатации не оправдана. |
| Temporal либо аналогичный workflow server | Зрелое recovery, но отдельная инфраструктура и execution model для одной последовательности RPC; domain data transactions всё равно необходимы. Не соответствует минимальному одному backend. |
| Cron/планировщик | Зрелый механизм, но несовместим с явным запретом периодических запусков. |
Собственный код ограничен доменными интервалами, dependency planning,
staging/publish и coalescing; transport queue/retries и DB lifecycle уже
существуют. Существующий runtime не предоставляет durable job framework;
добавлять его ради этих конечных стадий не требуется. При переходе к нескольким
backend processes понадобится отдельное решение о durable claims/leases;
эта поставка не выдаёт processRunId за распределённую блокировку.
## Risks / Trade-offs
- Локальный membership не замечает отзыв доступа мгновенно. Он обновляется
по пятиминутной политике; известный отказ применяется немедленно.
- Stable operation ID противоречит возможностям upstream proto, но принят
как допущение; сменившийся ID может потребовать отдельной будущей сверки.
- Broker cursor не обещает snapshot isolation: полнота означает успешный
обход доступного интервала на момент сверки, не отсутствие последующих
исправлений. Staging защищает от наших частичных публикаций.
- Долгая первичная история расходует rate-limit и диск. Ограниченные окна,
приоритет экрана и отсутствие предварительной массовой очереди сдерживают
ресурсы; объём и время первой загрузки пока не измерены.
- Запись SQLite сериализуется. Короткие транзакции и индексы должны уложиться
в бюджет; это проверяется, а не считается доказанным архитектурой.
- Envelope — несовместимое wire изменение, поэтому backend/frontend
поставляются согласованной парой. API version path сам по себе не делает
старый frontend совместимым.
## Migration Plan
1. Зафиксировать текущие DB/API invariants и measurement baseline на
изолированной базе. Добавить additive schema с revision и пустыми новыми
datasets; текущие users/sessions/connection/manual-card записи сохранить.
У существующих подключений появляется новая revision без изменения token.
2. Реализовать storage/coordinator и local owners; старые RAM snapshots
не мигрировать как доказанно полную историю. Проверить crash/rollback и
fences до включения нового read path.
3. Backend публикует OpenAPI artifact; frontend получает его явно и
регенерирует типы локально. Совместно поставляется новый UI и server
read path, включая observer semantics и terminal errors.
4. Первые пользовательские запросы заполняют данные. Startup/deploy не
запускает импорт. После подтверждённых checks обновляются as-built docs;
specs остаётся единственным SDD authority.
### Rollback Contract
| Field | Decision |
| --- | --- |
| Status | `required` |
| Trigger | Cross-user/connection leak, частичная публикация финансовых данных, повреждение миграции, несовместимый API pair либо существенная регрессия latency. |
| Actions | Остановить новый worker/backend, сохранить согласованную копию SQLite, вернуть совместимую пару backend/frontend. Не запускать старую и новую write-модель одновременно. Отдельно проверить Prisma migration state; не удалять additive tables как способ отката. |
| Data safety | Сохранить новые datasets, progress и все прежние User/Session/TbankConnection/manual-card записи. Если старая версия не поддерживает новую Prisma schema, использовать совместимый rollback build либо восстановить backup в отдельной БД; нельзя перезаписать рабочую БД старой копией, потеряв новые ручные операции. |
| Verification | Проверить integrity/FK и равенство прежних пользовательских данных, невозможность публикации остановленного worker, auth/connection boundaries, согласованность API pair и повторное безопасное чтение/возобновление после возврата исправленной версии. |
## Open Questions
Блокирующих продуктовых вопросов нет. Неизвестны измеренные latency/объём
истории и provider semantics при отсутствующем `from`/openedDate. Они не
меняют выбранный путь: явные интервалы, replay с начала, unknown lower bound
и отсутствие ложного признака полной истории. Подтверждение минимальной
даты источника может уточнить только boundary metadata; до него используется
описанный безопасный fallback. Числовые результаты будут получены задачами
проверки, а не вписаны заранее.
@@ -0,0 +1,73 @@
## Why
Повторные открытия инвестиционных экранов зависят от скорости и доступности
T-Bank, а перезапуск приложения теряет уже загруженную историю. Пользователь
согласовал [ADR 017](../../../../docs/adr/017-local-tbank-reads-and-background-sync.md):
показывать сохранённые данные сразу и обновлять их в фоне только по запросу.
## What Changes
- Все экраны T-Bank — счета, портфель, операции, доходы, инструменты,
текущие цены, свечи и выплаты — повторно используют постоянно сохранённые
данные, в том числе после перезапуска. Обычный ответ не ждёт брокера;
целевой ориентир до 500 мс ещё должен быть подтверждён измерениями.
- Запрос экрана запускает обновление отсутствующих или устаревших данных:
портфель, текущие цены и незавершённая дневная свеча — 1 минута;
текущий месяц операций и список счетов — 5 минут;
инструменты и календари выплат — сутки. Само течение времени работу не запускает.
- Полные прошлые месяцы операций и завершённые свечи сохраняются.
Пропуски догружаются при запросах; ручное обновление перечитывает показанный
период и зависимости текущего экрана, а не весь накопленный архив.
- При первой загрузке сначала доступны нужный экрану период и сравнения
доходов, затем догружается вся доступная история счёта. История бумаг
текущего портфеля загружается заранее в рамках этой работы; остальных —
при открытии инструмента. Начатая работа может завершиться после ухода.
- Пользователь различает свежесть, полноту, загрузку и ошибку обновления.
Неполный период не считается окончательным финансовым результатом или
подтверждением отсутствия операций. Ошибка сохраняет прежние данные.
- Личные данные доступны только своему пользователю в границах текущего
подключения. Старое подключение не может опубликовать поздний результат.
Существующие проверки доступа и ошибки интеграции сохраняются.
«Полнота истории счёта» используется в смысле
[глоссария](../../../../docs/glossary.md#полнота-истории-счёта): все доступные
исполненные операции периода на момент успешной сверки, включая полный пустой
период. Полнота не означает свежесть и не исключает поздние исправления.
Стабильность ID операции — принятое допущение проекта, не гарантия T-Bank;
повторно полученные поля операции могут изменяться.
**Non-goals:** периодическая синхронизация, автоматический поиск исправлений
полной старой истории, обнаружение сменившихся ID, история стоимости портфеля,
предзагрузка всего каталога, архив без подключения, новые сценарии отключения
и закрытых счетов. Эта сессия создаёт planning artifacts; реализация не выполняется.
## Capabilities
### New Capabilities
- [specs/tbank-data-sync/spec.md](specs/tbank-data-sync/spec.md): постоянное локальное чтение, запросы
синхронизации, интервальная полнота, восстановление, общие данные инструментов,
границы подключения и проверяемые критерии быстродействия.
### Modified Capabilities
- [specs/brokerage-accounts/spec.md](specs/brokerage-accounts/spec.md): локальный список, сохранённые сводки,
состояния загрузки и ошибок без синхронного ожидания брокера.
- [specs/portfolio-view/spec.md](specs/portfolio-view/spec.md): постоянный последний полный снимок,
минутная свежесть и ручное обновление.
- [specs/brokerage-account-ledger/spec.md](specs/brokerage-account-ledger/spec.md): локальная пагинация и фильтрация,
полнота периода вместо обязательного перечитывания RAM-кэша.
- [specs/brokerage-account-income-analytics/spec.md](specs/brokerage-account-income-analytics/spec.md): полнота выбранного периода
и сравнений, сохранение результата, идентичность повторно загруженных операций.
- [specs/brokerage-account-upcoming-events/spec.md](specs/brokerage-account-upcoming-events/spec.md): прогноз из сохранённых
источников с видимой полнотой и ошибкой обновления.
- [specs/frontend-data-resilience/spec.md](specs/frontend-data-resilience/spec.md): различимые состояния серверного
сохранённого результата и клиентского кэша на всех экранах T-Bank.
## Impact
Изменение затрагивает пользователей всех инвестиционных экранов, серверное
хранение и получение данных, публичный контракт состояний, интерфейс обновления
и объясняющую документацию. Финансовые правила, валютная точность, значение
прогнозов и изоляция пользователей сохраняются. Основные specs и чужие active
changes не изменяются до отдельного завершения lifecycle этого change.
@@ -0,0 +1,145 @@
## MODIFIED Requirements
### Requirement: Realized income analytics is account-scoped and period-bound
Система MUST предоставлять аутентифицированному User аналитику фактически полученного дохода выбранного открытого поддерживаемого Brokerage Account либо существующего режима «Все счета» (`all`) из счетов, доступных через текущий T-Bank connection User. В режиме `all` система MUST проверять доступ каждого входящего счёта и подтверждать полноту каждого исходного периода по всем входящим счетам; отсутствующий вклад счёта MUST NOT заменяться нулём. Применённый период MUST задаваться включительными календарными датами `from` и `to` в Europe/Moscow, MUST NOT начинаться после своего окончания, MUST NOT содержать будущую дату. Система MUST NOT ограничивать максимальную длительность периода. При отсутствии применённого периода система MUST использовать интервал от первого дня текущего месяца до сегодняшнего дня включительно.
Чтение MUST использовать сохранённые операции текущего подключения и локальное подтверждение доступа без синхронного запроса T-Bank. Ответ MUST иметь envelope `{ data, meta }` по `tbank-data-sync`. Полнота MUST определяться для выбранного периода и каждого необходимого периода сравнения отдельно. Запрос MUST приоритетно загружать их пропуски до фоновой догрузки остальной истории; известные правила календарных дат MUST сохраняться.
#### Scenario: Аналитика за период по умолчанию
- **WHEN** аутентифицированный User запрашивает аналитику доступного Brokerage Account без периода
- **THEN** система рассчитывает результат от первого дня текущего месяца по Europe/Moscow до сегодняшнего дня включительно
#### Scenario: Аналитика за допустимый пользовательский период
- **WHEN** User применяет включительный период любой длительности, в том числе более 366 дней, без будущих дат
- **THEN** система рассчитывает аналитику доступного Brokerage Account за все календарные даты этого периода по Europe/Moscow
#### Scenario: Отклонение недопустимого периода
- **WHEN** User запрашивает период с некорректной или будущей датой либо с `from` после `to`
- **THEN** система отклоняет запрос и не возвращает результат для другого или усечённого периода
#### Scenario: Отклонение недоступного счёта
- **WHEN** User запрашивает аналитику Brokerage Account, который не является открытым поддерживаемым счётом, доступным через его текущий T-Bank connection
- **THEN** система возвращает существующий ответ о недоступности счёта и не раскрывает его доход или операции
#### Scenario: Общий доход требует полноты всех счетов
- **WHEN** User запрашивает режим `all` и выбранный период одного доступного счёта ещё не загружен полностью
- **THEN** система не возвращает окончательные общие суммы, сохраняет раздельное состояние покрытия счетов и запрашивает недостающую разрешённую историю
#### Scenario: Открыт ещё не загруженный период
- **WHEN** запрошенный допустимый период ещё не имеет сохранённых операций или подтверждённого полного пустого покрытия
- **THEN** система отвечает `200` со статусом первичной загрузки или частичного покрытия, запрашивает нужные интервалы и не выдаёт нулевой окончательный доход
### Requirement: Period comparisons preserve calendar and currency meaning
Система MUST сопоставлять выбранный период с непосредственно предшествующим периодом. Для полного календарного месяца предыдущим периодом MUST быть предыдущий полный календарный месяц; для любого другого периода предыдущий период MUST иметь ту же включительную продолжительность и непосредственно предшествовать выбранному. Когда соответствующие календарные даты годом ранее доступны, система MUST также показывать year-over-year comparison; когда они недоступны, система MUST обозначать сравнение как недоступное, а не подменять его нулём. Только сравнение с подтверждённым полным покрытием обоих периодов MUST показывать абсолютные coupon, dividend и total значения и их денежные разницы отдельно по исходным валютам и MUST NOT характеризовать их как инвестиционную эффективность.
Отсутствие ещё не загруженной истории сравнения MUST отличаться от невозможности сравнения по календарю или доступной истории счёта. Система MUST сообщать загрузку, неполноту либо ошибку каждого периода сравнения и MUST NOT подменять их нулевыми суммами или денежными разницами. Полный основной период MAY отображаться при отдельно обозначенной незавершённой загрузке сравнения; его значения MUST NOT смешиваться с частичным сравнением.
#### Scenario: Сравнение полного календарного месяца
- **WHEN** User применяет период от первого до последнего дня календарного месяца
- **THEN** система сравнивает его с полным непосредственно предшествующим календарным месяцем
#### Scenario: Сравнение произвольного периода
- **WHEN** User применяет период, не являющийся полным календарным месяцем
- **THEN** система сравнивает его с непосредственно предшествующим включительным периодом той же продолжительности
#### Scenario: Доступно сравнение год к году
- **WHEN** для соответствующих календарных дат годом ранее доступна полная история подходящих операций
- **THEN** система показывает отдельные source-currency значения и денежные разницы относительно этого периода
#### Scenario: Годовая история недоступна
- **WHEN** полная история для соответствующих календарных дат годом ранее недоступна
- **THEN** система обозначает year-over-year comparison как недоступное и не показывает отсутствующие данные как нулевой доход
#### Scenario: Основной период полон, сравнение ещё загружается
- **WHEN** выбранный период имеет подтверждённое полное покрытие, а необходимый предыдущий период ещё загружается
- **THEN** система возвращает полный итог выбранного периода, отмечает загрузку сравнения и не возвращает для него окончательные суммы или разницы
#### Scenario: Сравнение догружено после основного периода
- **WHEN** все необходимые интервалы периода сравнения успешно завершены
- **THEN** следующее локальное чтение возвращает его точные валютные суммы и разницы без ожидания T-Bank
### Requirement: Realized analytics never presents an incomplete result as complete
Система MUST формировать окончательные totals, trend buckets, instrument breakdown и drill-down выбранного периода только по подтверждённому полному покрытию его исполненных операций. Каждое comparison MUST отдельно требовать полного покрытия своих исходных периодов. Повторное локальное чтение полного периода MUST NOT ожидать повторного получения cursor pages T-Bank. Ошибка любой необходимой страницы фоновой загрузки MUST сохранять подтверждённый ранее результат и покрытие, фиксировать восстанавливаемую ошибку синхронизации и MUST NOT возвращать частичные totals, comparisons, buckets или breakdown как полный результат. Если полный выбранный результат ещё не получен, envelope MUST возвращать `data: null` с отсутствующим либо частичным покрытием в `meta`, а интерфейс MUST показывать незавершённую загрузку либо её ошибку без окончательных финансовых итогов. Успешный период без подходящих выплат MUST возвращать нулевой результат и отображаться с явным объяснением пустого состояния.
#### Scenario: Все страницы операций успешно получены
- **WHEN** для покрытия расчёта требуется несколько cursor pages и каждая страница успешно сохранена с подтверждением окончания диапазона
- **THEN** система формирует результат по всем подходящим Ledger Entries со всех страниц
#### Scenario: Одна из страниц операций недоступна
- **WHEN** получение любой необходимой cursor page завершается ошибкой
- **THEN** ответ сообщает ошибку синхронизации, а страница сохраняет ранее полный результат с прежним временем либо показывает ошибку незавершённой загрузки без окончательных частичных итогов
#### Scenario: В периоде нет купонов и дивидендов
- **WHEN** все необходимые операции успешно получены, но среди них нет подходящих coupon или dividend Ledger Entries
- **THEN** страница показывает нулевой результат и объясняет, что за выбранный период купоны и дивиденды не найдены
#### Scenario: Частичная загрузка пока не нашла дохода
- **WHEN** только часть операций выбранного периода получена и в этой части нет купонов или дивидендов
- **THEN** система не возвращает окончательный нулевой результат и сообщает неполноту периода
#### Scenario: Ошибка ручной сверки полного периода
- **WHEN** ранее полный период перечитывается вручную и промежуточная страница не получена
- **THEN** прежний полный результат и время его успешной сверки остаются доступными с ошибкой обновления, а частичная новая выборка не заменяет их
### Requirement: Account overview has an independent current-month income summary
Обзор выбранного Brokerage Account MUST показывать компактный source-currency summary «Получено купонами и дивидендами» за период от первого дня текущего месяца по Europe/Moscow до сегодняшнего дня включительно и действие перехода на страницу «Пассивный доход». Summary MUST всегда использовать этот current-month период и MUST NOT наследовать последний пользовательский период страницы аналитики. Пустой или неуспешный расчёт summary MUST оставаться локальным состоянием блока и MUST NOT заменять успешно отображённый Portfolio или другие блоки обзора.
Summary MUST использовать тот же локальный источник операций и правило полноты, что и полная аналитика. До завершения требуемого текущего периода он MUST показывать состояние загрузки или неполноты без окончательного нулевого дохода. Ошибка фонового обновления MUST сохранять ранее полный summary с прежним временем и безопасным предупреждением.
#### Scenario: Обзор показывает доход текущего месяца
- **WHEN** обзор Brokerage Account загружен и current-month analytics успешно рассчитана
- **THEN** он показывает отдельные source-currency итоги с начала текущего месяца по сегодняшний день и переход к полной аналитике
#### Scenario: Пользовательский период не влияет на обзор
- **WHEN** User ранее применил на странице аналитики период, отличный от текущего месяца
- **THEN** summary обзора по-прежнему рассчитывается от первого дня текущего месяца по сегодняшний день
#### Scenario: Current-month summary недоступен
- **WHEN** Portfolio обзора успешно загружен, но расчёт current-month summary завершается ошибкой
- **THEN** Portfolio и остальные успешно загруженные блоки остаются доступными, а summary показывает собственное состояние восстановления
#### Scenario: Текущий месяц ещё не загружен полностью
- **WHEN** обзор доступен, но текущий месяц операций имеет пропуски
- **THEN** блок дохода обозначает неполноту или загрузку, не выдаёт частичный доход за окончательный и не скрывает Portfolio
### Requirement: Income payments remain traceable to exact Ledger Entries
В разделе «Выплаты» страница MUST показывать единый список карточек точных исполненных Ledger Entries применённого периода в исходном серверном порядке entries. Каждая выплата MUST иметь собственную сразу видимую карточку с описанием, датой, доступной identity Instrument и signed Money в исходной валюте. Страница MUST NOT добавлять внешние группировочные карточки или заголовки по Instrument либо валюте, subtotals инструмента или количество выплат инструмента. Список MUST сохранять отрицательные корректировки как самостоятельные Ledger Entries и MUST отражать одну запись на идентичность User, Brokerage Account, текущее подключение и operation ID; повторная загрузка того же ID MUST обновлять поля и MUST NOT удваивать выплату. Одинаковые operation IDs разных счетов MUST оставаться отдельными записями. Список MUST NOT включать записи вне аналитического итога. Отказ от группировочного UI MUST NOT отменять проверки принадлежности entries к instrumentKey, валюты и receipt count ответа или приводить к пересчёту денежных итогов на клиенте.
#### Scenario: Каждая выплата показана отдельной карточкой
- **WHEN** выбранный период содержит несколько включённых Ledger Entries одного или разных Instrument
- **THEN** раздел «Выплаты» сразу показывает отдельную карточку каждой записи в исходном порядке без внешних группировочных блоков, раскрытия, итогов инструмента и receipt count на UI
#### Scenario: Список воспроизводит точный состав дохода
- **WHEN** User просматривает загруженный раздел «Выплаты»
- **THEN** он видит все и только включённые coupon и dividend Ledger Entries применённого периода с датой, описанием, доступной identity и исходной суммой
#### Scenario: Отрицательная корректировка и повторяющиеся IDs сохраняются
- **WHEN** доход содержит отрицательную корректировку, повторно загруженный ID того же счёта либо одинаковые operation IDs разных счетов
- **THEN** корректировка отображается с исходным знаком; одна операция того же счёта имеет одну карточку с актуальными полями, а операции разных счетов остаются отдельными карточками
#### Scenario: Валюта видна в сумме каждой выплаты
- **WHEN** подходящие выплаты одного Instrument представлены в нескольких исходных валютах
- **THEN** каждая карточка показывает свою Money с исходной валютой, без отдельного заголовка валютной группы или объединённого мультивалютного итога
@@ -0,0 +1,95 @@
## MODIFIED Requirements
### Requirement: User can retrieve an account ledger
Система MUST предоставлять аутентифицированный журнал только для чтения выбранного Brokerage Account либо существующего режима «Все счета» (`all`) в границах текущего User и его T-Bank connection. В режиме `all` доступ MUST проверяться для каждого входящего счёта, операции MUST сохранять принадлежность исходному счёту, а полнота периода MUST учитывать покрытие каждого входящего счёта. Журнал MUST содержать только исполненные операции T-Bank и при отсутствии периода по умолчанию охватывать интервал от начала текущего календарного года в UTC до момента запроса.
Система MUST читать сохранённые операции и локальное подтверждение доступа текущего подключения без синхронного обращения к T-Bank, включая запрос списка счетов. Ответ MUST содержать envelope `{ data, meta }` по `tbank-data-sync`, отражающий полноту запрошенного периода независимо от текущей страницы. Отсутствующие данные MUST возвращаться с `data: null` и состоянием загрузки, а частично сохранённый период MUST обозначаться как `partial`. Только подтверждённо полный пустой период MUST обозначать отсутствие операций.
#### Scenario: Получение журнала с периодом по умолчанию
- **WHEN** аутентифицированный User запрашивает журнал принадлежащего ему Brokerage Account без периода
- **THEN** система возвращает сохранённые исполненные операции в `data` и полноту периода в `meta` за период от начала текущего календарного года в UTC до момента запроса, запрашивая отсутствующие интервалы в фоне
#### Scenario: Отклонение недоступного счёта
- **WHEN** аутентифицированный User запрашивает журнал счёта, для которого локальные данные текущего подключения подтверждают отсутствие доступа либо неподдерживаемое или закрытое состояние
- **THEN** система возвращает существующий ответ о недоступности счёта и не раскрывает операции
#### Scenario: Общий журнал содержит неполный счёт
- **WHEN** User запрашивает режим `all` и у одного из разрешённых счетов есть незагруженный интервал периода
- **THEN** система возвращает только разрешённые сохранённые записи с принадлежностью счёту и обозначает общий период неполным, не подменяя отсутствующий вклад пустым счётом
#### Scenario: Пустая загруженная часть не доказывает отсутствие операций
- **WHEN** в сохранённой части периода нет записей, но покрытие периода не завершено
- **THEN** ответ обозначает неполноту и запрошенную загрузку, а интерфейс не сообщает, что операций за период нет
### Requirement: User can browse a filtered ledger
Система MUST позволять User ограничивать журнал включительным периодом дат и любым подмножеством категорий trade, income, tax, fee, transfer, other. Система MUST фильтровать сохранённые операции локально, не перечитывая T-Bank из-за категории, instrument UID, размера страницы или перехода между страницами полного периода. Система MUST разбивать упорядоченный результат на страницы с непрозрачным cursor и возвращать признак доступности следующей страницы.
Cursor MUST быть привязан к User, текущему подключению, составу счетов, применённым фильтрам, зафиксированной верхней границе периода и версии читаемых данных. Курсор другой границы или изменившейся версии MUST отклоняться существующим `INVALID_OPERATIONS_CURSOR`, позволяя повторить первую страницу с прежними фильтрами. Доступность следующей локальной страницы MUST NOT заменять признак полноты периода. Равнозначные порядок и дубли categories MUST NOT создавать другую идентичность запроса.
#### Scenario: Фильтрация доходных операций за период
- **WHEN** User запрашивает период дат и категорию income
- **THEN** результат содержит только исполненные доходные операции с датой в пределах этого периода
#### Scenario: Продолжение истории
- **WHEN** User запрашивает cursor следующей страницы, возвращённый предыдущим ответом журнала, и версия данных не изменилась
- **THEN** система возвращает следующую страницу без повторения записей предыдущей страницы
#### Scenario: Обновление изменило данные между страницами
- **WHEN** после выдачи первой страницы успешная синхронизация изменила её набор операций и User передаёт прежний cursor
- **THEN** система возвращает `INVALID_OPERATIONS_CURSOR`, а повторная загрузка начинает первую страницу с сохранёнными применёнными фильтрами без пропуска или дублирования записей
#### Scenario: Полный период фильтруется локально
- **WHEN** User меняет категории или страницы в полностью сохранённом прошлом периоде
- **THEN** система формирует результат из сохранённых операций без нового запроса операций T-Bank
### Requirement: Ledger entries preserve broker operation meaning
Система MUST представлять одну upstream-операцию одной записью журнала и MUST передавать её timestamp, тип операции, категорию, описание, доступные идентификаторы инструмента, доступную presentation metadata инструмента, денежную выплату, количество, цену, НКД и комиссию. Денежные поля MUST сохранять исходную валюту и точное десятичное значение.
В пределах текущего подключения идентичность Ledger Entry MUST включать User, Brokerage Account и upstream operation ID. Повторное получение того же ID MUST обновлять доступные поля этой операции без создания второй записи; одинаковые ID разных счетов MUST оставаться разными записями. Стабильность ID MUST считаться допущением проекта, а не гарантией T-Bank.
#### Scenario: Отображение купонной выплаты
- **WHEN** T-Bank возвращает исполненную купонную операцию с выплатой и данными инструмента
- **THEN** запись журнала определяет её как income и передаёт исходную сумму выплаты, валюту и данные инструмента
#### Scenario: Отображение комиссии сделки без синтетической записи
- **WHEN** T-Bank возвращает исполненную сделку, содержащую выплату и комиссию
- **THEN** журнал содержит одну запись сделки с комиссией в качестве её детали
#### Scenario: Отображение движения бумаг без денег
- **WHEN** T-Bank возвращает исполненную операцию, изменяющую состав инструментов, но без денежной выплаты
- **THEN** запись журнала передаёт доступное количество и не содержит искусственной денежной выплаты с нулевым значением
#### Scenario: Доступна визуальная идентичность инструмента
- **WHEN** для инструмента исполненной операции доступна presentation metadata бренда
- **THEN** запись журнала передаёт её вместе с доступными идентификаторами инструмента, не меняя остальные сведения операции
#### Scenario: Повторная операция содержит исправленные поля
- **WHEN** успешная синхронизация получает операцию с уже сохранённым ID того же счёта и изменёнными полями
- **THEN** последующее чтение возвращает одну запись с обновлёнными полями без удвоения денежного движения
### Requirement: Account overview includes recent ledger events
Обзор счёта MUST показывать не более пяти последних сохранённых исполненных записей журнала этого счёта с состоянием полноты и загрузки требуемого периода и действие перехода к его полному журналу. Каждая показанная запись MUST иметь ту же визуальную идентичность инструмента и семантику денежного движения, что и запись полного журнала. Неуспешный или пустой запрос последних записей MUST оставаться локальным состоянием блока последних событий и MUST NOT заменять успешно отображённый обзор Portfolio.
#### Scenario: Отображение последних событий
- **WHEN** загружается обзор счёта и его журнал содержит исполненные операции
- **THEN** обзор показывает не более пяти последних записей и ссылку на полный журнал
#### Scenario: Последние записи недоступны
- **WHEN** обзор счёта успешно загружается, но запрос последних записей завершается ошибкой
- **THEN** обзор Portfolio остаётся доступным, а блок последних событий отображает собственное состояние восстановления
#### Scenario: Последняя запись не содержит visual metadata инструмента
- **WHEN** одна из последних записей не имеет доступной presentation metadata инструмента
- **THEN** обзор показывает для неё понятный текстовый fallback и сохраняет дату, тип, инструмент и денежное изменение записи доступными для чтения
#### Scenario: Первичная история обзора неполна
- **WHEN** обзор получил только часть требуемых операций или `data: null`
- **THEN** блок обозначает загрузку или неполноту и не утверждает, что показанные записи окончательны либо операции отсутствуют
## REMOVED Requirements
### Requirement: Кэш страниц журнала ограничивает локальные ресурсы
**Reason:** Постоянное хранение операций и запросная синхронизация заменяют обязательный RAM-кэш на 100 страниц с TTL 30 секунд; его запрет на выдачу старой страницы противоречит локальному чтению сохранённой истории.
**Migration:** Чтение, свежесть, объединение загрузок и сохранение прогресса переходят к `tbank-data-sync`; локальная фильтрация и привязанный к версии данных cursor регулируются `User can browse a filtered ledger`. Существующие transient cursors не являются доказательством полноты и при несовместимости возвращают `INVALID_OPERATIONS_CURSOR`.
@@ -0,0 +1,104 @@
## MODIFIED Requirements
### Requirement: User can retrieve upcoming events for an account
Система MUST предоставлять аутентифицированному User read-only список известных будущих событий выбранного открытого поддерживаемого Brokerage Account либо существующего режима «Все счета» (`all`) в границах текущего T-Bank connection User. Параметры `from` и `to` MUST сохранять существующий контракт: необязательные строгие календарные даты `YYYY-MM-DD` с включительными границами в UTC. Без `from` начало MUST соответствовать текущей календарной дате UTC; без `to` конец MUST соответствовать дате `from` плюс 30 календарных дней. При `from` позже `to` система MUST сохранять существующий отказ `INVALID_EVENTS_PERIOD`, не подменяя период. В режиме `all` доступ и полнота обязательных источников MUST проверяться для каждого входящего счёта, события MUST сохранять принадлежность счёту. Ответ MUST иметь envelope `{ data, meta }` по `tbank-data-sync`; доступный прогноз в `data` MUST содержать только события coupon, dividend, maturity и offer, отсортированные по дате по возрастанию, и момент расчёта прогноза.
Список будущих событий является прогнозом по последнему успешно сохранённому наблюдаемому состоянию счёта и MUST NOT означать, что WealthLedger хранит долговременное право владения Brokerage Account независимо от T-Bank connection.
Система MUST формировать прогноз из сохранённых позиций и календарей, не ожидая T-Bank при локальном чтении и подтверждении доступа. Свежесть и загрузка зависимостей MUST быть видимы. Отсутствующий подтверждённый результат MUST возвращаться как `data: null` с состоянием загрузки, а не пустой список. Полнота локальной загрузки поддерживаемых данных MUST NOT означать полноту информации внешнего источника обо всех возможных событиях.
#### Scenario: Получение событий доступного счёта
- **WHEN** аутентифицированный User запрашивает будущие события открытого поддерживаемого Brokerage Account, доступного через текущий T-Bank connection, без явного периода
- **THEN** система возвращает известные события от сегодняшней даты UTC до даты через 30 календарных дней включительно и момент, на который был выполнен расчёт
#### Scenario: Отсутствие событий в периоде
- **WHEN** все необходимые данные применённого периода успешно сохранены и для счёта в нём нет известных событий
- **THEN** система возвращает успешный пустой список и момент расчёта
#### Scenario: Отклонение недоступного счёта
- **WHEN** аутентифицированный User запрашивает события счёта, который не является открытым поддерживаемым счётом, доступным через его T-Bank connection
- **THEN** система возвращает существующий ответ о недоступности счёта и не раскрывает события
#### Scenario: Недопустимый период сохраняет ошибку
- **WHEN** User передаёт `from` позже `to` для выбранного счёта либо режима `all`
- **THEN** система возвращает существующую ошибку `INVALID_EVENTS_PERIOD`, не запускает загрузку другого периода и не возвращает его прогноз
#### Scenario: Прогноз ещё не может быть рассчитан
- **WHEN** позиции либо необходимый поддерживаемый календарь ещё не сохранены полностью
- **THEN** система возвращает `200` с `data: null` и состоянием запрошенной загрузки или её ошибки вместо утверждения об отсутствии событий
### Requirement: Forecasts preserve event meaning and uncertainty
Система MUST передавать для каждого события его календарную дату, тип, доступные идентификаторы и название Instrument, а также прогнозную сумму до налогов как Money, если она известна. Прогнозная сумма MUST рассчитываться по количеству бумаг полного успешно сохранённого Portfolio Snapshot, использованного при последнем успешном расчёте показываемого прогноза, с указанием времени этого наблюдения. При неудачном обновлении сохранённый прогноз MUST сохранять эту исходную зависимость и MUST NOT приписываться более новому Portfolio Snapshot. Если денежные условия события неизвестны или неприменимы, прогнозная сумма MUST быть `null`, а не нулём и не парой разрозненных полей. Дивиденд MUST обозначать переданную дату как дату реестра, а offer MUST передаваться без вымышленной суммы.
Instrument в Future Cash Event MUST иметь тот же доменный смысл, что и в Portfolio: это финансовый инструмент, к которому относится событие, даже если событие не является Portfolio Position.
#### Scenario: Прогноз купона по текущей позиции
- **WHEN** поддерживаемая облигация в текущей Portfolio Position имеет известный купон в пределах периода
- **THEN** система возвращает событие coupon с прогнозной суммой до налогов как Money в исходной валюте, рассчитанной по текущему количеству облигаций
#### Scenario: Прогноз дивиденда
- **WHEN** поддерживаемая акция в текущей Portfolio Position имеет объявленный дивиденд с датой фиксации реестра в пределах периода
- **THEN** система возвращает событие dividend с датой, обозначенной как дата реестра (`record_date`), и прогнозной суммой до налогов как Money, равной `dividend_net` на одну бумагу, умноженной на текущее количество акций
#### Scenario: Отменённый дивиденд не попадает в прогноз
- **WHEN** запись дивиденда T-Bank имеет тип выплаты `Cancelled`
- **THEN** система не добавляет такое событие в результат
#### Scenario: Отображение оферты без суммы
- **WHEN** для поддерживаемой облигации известна оферта в пределах периода, но её денежные условия недоступны
- **THEN** система возвращает событие offer с прогнозной суммой `null`
#### Scenario: Неподдерживаемое частичное погашение
- **WHEN** источник не предоставляет расписание будущих частичных погашений или амортизации облигации
- **THEN** система НЕ создаёт синтетическое событие частичного погашения
### Requirement: Upcoming events use complete source results
Система MUST использовать последний прогноз по полным успешно полученным необходимым источникам и MUST NOT возвращать частичный список как полный результат. Если обновление необходимых данных брокера или MOEX неуспешно, система MUST сохранять прежний подтверждённый прогноз и время его расчёта, возвращая `200` с ошибкой синхронизации в `meta`. Если полного прогноза ещё нет, ответ MUST содержать `data: null` и отсутствующее либо частичное покрытие с ошибкой или состоянием загрузки. Подтверждённые ошибки сессии, текущего подключения и доступа MUST сохранять существующие Problem Details и MUST NOT маскироваться прогнозом. Система MUST пропускать инструменты, для которых источник не предоставляет поддерживаемые события, не создавая строки «нет данных».
#### Scenario: Недоступность источника
- **WHEN** необходимый источник данных брокера или MOEX временно недоступен
- **THEN** система сообщает ошибку обновления, сохраняя ранее полный прогноз и время его расчёта либо `data: null`, без выдачи частичного списка за полный
#### Scenario: Инструмент без поддерживаемых данных
- **WHEN** Portfolio содержит инструмент, для которого источник не предоставляет поддерживаемые будущие события
- **THEN** система не добавляет для него синтетическую строку в результат
#### Scenario: MOEX отвечает без запрошенного блока
- **WHEN** MOEX отвечает успешно, но не содержит запрошенного блока данных (например, дивидендов)
- **THEN** система трактует ответ как отсутствие данных по инструменту и не завершает запрос ошибкой
### Requirement: Account page presents an upcoming events list
Страница Brokerage Account MUST предоставлять действие перехода на отдельную страницу «Будущие события». Эта страница MUST сохранять существующий выбор и явное применение периода через `from`/`to` и показывать все полученные события применённого периода компактным списком без пагинации и суммарного итога, с датой, типом, инструментом и прогнозной суммой в исходной валюте, если она известна. Страница MUST объяснять, что прогноз основан на текущей позиции, указан до налогов и может быть неполным из-за доступности данных источника.
Страница MUST показывать время сохранённого прогноза, состояние свежести и загрузки, а также действие «Обновить» для текущего применённого периода и его зависимостей. Неполученный результат MUST отличаться от подтверждённого пустого списка. Временная ошибка обновления MUST NOT скрывать сохранённый полный прогноз.
#### Scenario: Просмотр ближайших событий
- **WHEN** User открывает «Будущие события» из обзора Brokerage Account без явного периода
- **THEN** он видит отсортированный список событий периода по умолчанию от сегодняшней даты UTC до даты через 30 календарных дней включительно, где offer обозначен без суммы
#### Scenario: Пустое состояние страницы
- **WHEN** запрос будущих событий возвращает подтверждённый полный пустой список в `data`
- **THEN** страница сообщает, что за применённый период событий не найдено, и объясняет ограничения покрытия источника
#### Scenario: Обновление применённого периода
- **WHEN** User применил допустимый период будущих событий и нажал «Обновить»
- **THEN** обновляются необходимые позиции и календари для применённого периода, его фильтры сохраняются и обновление не подменяется периодом по умолчанию
#### Scenario: Ошибка страницы
- **WHEN** фоновое обновление будущих событий завершается временной ошибкой
- **THEN** страница сохраняет прежний полный прогноз с его временем и предупреждением либо показывает ошибку первичной загрузки при его отсутствии, не выдаёт частичный список за полный и предоставляет действие повторить обновление
### Requirement: Account overview includes upcoming events
Обзор Brokerage Account MUST показывать не более пяти ближайших Future Cash Event и действие перехода на полную страницу «Будущие события». Неуспешный или пустой запрос этого блока MUST оставаться локальным состоянием блока и не заменять успешно отображённый Portfolio.
#### Scenario: Отображение ближайших событий
- **WHEN** обзор счёта успешно загружен и календарь содержит будущие события
- **THEN** обзор показывает первые пять событий в порядке даты и действие перехода к полной странице
#### Scenario: Ближайшие события недоступны
- **WHEN** обзор счёта успешно загружен, но запрос будущих событий завершается ошибкой
- **THEN** Portfolio остаётся доступным, а блок будущих событий отображает собственную ошибку
#### Scenario: Ошибка обновления сохранённого прогноза обзора
- **WHEN** блок обзора уже показывает полный прогноз, а его обновление временно неуспешно
- **THEN** блок сохраняет показанные события и время расчёта с локальным предупреждением, не скрывая Portfolio
@@ -0,0 +1,133 @@
## MODIFIED Requirements
### Requirement: Список активных брокерских счетов
Система SHALL предоставлять авторизованному User `GET /api/v1/brokerage-accounts`, возвращающий envelope `{ data, meta }`, где `data` содержит сохранённый массив открытых поддерживаемых инвестиционных счетов T-Bank, доступных через текущий T-Bank connection User, в порядке, полученном от T-Bank. Каждый элемент MUST содержать стабильный `id`, непустое `name`, нормализованный `type` (`BROKERAGE`, `INDIVIDUAL_INVESTMENT`, `INVEST_BOX` или `MONEY_MARKET_FUND`) и nullable `openedOn` в формате `YYYY-MM-DD`. Система MUST включать только счета T-Bank типов «брокерский счёт», «ИИС», «Инвесткопилка» и «фонд денежного рынка»; банковские и неизвестные типы MUST NOT возвращаться. Система MUST удалить внешние пробелы из имени и при пустом результате использовать непустое название соответствующего типа.
Каждый элемент MUST также содержать nullable `portfolioSummary`. Доступная сводка MUST содержать `totalsByCurrency`: точные Money-итоги Portfolio по исходным ISO-валютам, включающие стоимость позиций и Cash Balance в той же валюте. Она MUST NOT содержать FX-конвертированный или межвалютный итог и MUST NOT выдавать Cash Balance за самостоятельный второй итог. Если успешная сводка Portfolio конкретного доступного счёта ещё не сохранена, `portfolioSummary` MUST быть `null`, не исключая этот счёт из списка и не делая список неуспешным. Пустой доступный Portfolio MUST передаваться доступной сводкой с пустым `totalsByCurrency`, а не `null`.
Список Brokerage Account и его сводки постоянно сохраняются и являются результатом последнего успешного наблюдения текущего брокерского подключения и MUST NOT создавать ожидание, что WealthLedger хранит долговременное право владения счётом или Portfolio независимо от T-Bank connection.
Обычный запрос MUST читать сохранённый список и доступные сводки без ожидания T-Bank. Срок свежести списка MUST составлять пять минут; сводки MUST следовать минутной свежести Portfolio. Отсутствующий список MUST возвращаться как `200` с `data: null`, `meta.availability: missing` и состоянием запрошенной загрузки. Только успешно полученный полный пустой список MUST возвращаться как `data: []` с `meta.availability: complete`. Время, свежесть и состояние загрузки MUST передаваться по контракту `tbank-data-sync`. Отсутствие либо ошибка загрузки сводки MUST отличаться от отсутствия самого счёта; сохранённая успешная сводка MUST оставаться доступной после временной ошибки её обновления.
#### Scenario: Поддерживаемые счета возвращаются в upstream-порядке
- **WHEN** User с действующим T-Bank connection запрашивает список, а последняя успешная синхронизация сохранила открытые брокерский счёт, ИИС и неподдержанный дебетовый счёт
- **THEN** система отвечает `200` с массивом брокерского счёта и ИИС в `data` в исходном порядке T-Bank без дебетового счёта
#### Scenario: Пустой список поддерживаемых счетов
- **WHEN** User с действующим T-Bank connection имеет сохранённый успешно подтверждённый пустой список открытых поддерживаемых инвестиционных счетов
- **THEN** система отвечает `200` с `data: []` и `meta.availability: complete`
#### Scenario: Пустое название получает fallback
- **WHEN** T-Bank возвращает поддерживаемый открытый счёт с пустым либо состоящим из пробелов именем
- **THEN** система отвечает этим счётом с непустым названием его нормализованного типа
#### Scenario: Дата открытия отсутствует
- **WHEN** T-Bank не возвращает дату открытия поддерживаемого открытого счёта
- **THEN** система отвечает этим счётом с `openedOn: null`
#### Scenario: Мультивалютная сводка счёта
- **WHEN** доступный Portfolio счёта содержит итоги в RUB и USD, включая Cash Balance в USD
- **THEN** ответ содержит одну доступную `portfolioSummary` с отдельными Money-итогами RUB и USD без FX-конвертации, общего итога или отдельного итога Cash Balance
#### Scenario: Сводка одного счёта временно недоступна
- **WHEN** список доступных счетов получен, но Portfolio одного из них ещё не сохранён и его загрузка временно недоступна
- **THEN** система отвечает `200` всеми счетами в исходном порядке, передаёт для этого счёта `portfolioSummary: null` и не заменяет успешные сводки остальных счетов ошибкой списка
#### Scenario: Доступный Portfolio пуст
- **WHEN** доступный счёт не имеет ненулевых позиций и доступных денежных остатков
- **THEN** система передаёт доступную `portfolioSummary` с пустым `totalsByCurrency`
#### Scenario: Список ещё не загружен
- **WHEN** User впервые запрашивает список текущего подключения и успешного сохранённого результата ещё нет
- **THEN** система возвращает `200` с `data: null`, состоянием загрузки в `meta`, запрашивает фоновую синхронизацию и не выдаёт отсутствие данных за отсутствие счетов
#### Scenario: Ошибка обновления сохранённой сводки
- **WHEN** обновление Portfolio одного счёта временно неуспешно и его успешная сводка уже сохранена
- **THEN** список сохраняет эту сводку с прежним временем и видимым состоянием ошибки обновления вместо `null`
### Requirement: Состояния недоступности списка счетов
Система MUST возвращать последний полностью сохранённый список текущего подключения без синхронной проверки T-Bank. Неполный ответ обновления MUST NOT заменять полный список. При отсутствии T-Bank connection система MUST отвечать `409` Problem Details с code `TBANK_NOT_CONNECTED`. Если для текущего подключения уже подтверждено отклонение token как неаутентифицированного или без прав, система MUST отвечать `400` Problem Details с code `INVALID_TBANK_TOKEN` и сохранять connection. При временной либо иной upstream-ошибке фонового обновления система MUST сохранять прежний список и время его успешной загрузки, при наблюдении завершённой попытки возвращая `200` с `meta.sync.state: failed` и безопасным `meta.sync.errorCode`. Обычный запрос экрана либо ручное обновление MAY запросить новую попытку согласно `tbank-data-sync`; наблюдение статуса MUST NOT запускать её. При отсутствии сохранённого списка `data` MUST оставаться `null`. Ошибка обновления MUST NOT менять время успешной загрузки и сама по себе MUST NOT делать ещё свежие данные устаревшими; свежесть MUST продолжать определяться сроком соответствующего источника. Подтверждённая недействительность текущего token MUST запрещать выдачу личных данных, включая сохранённый список; локальная сессия и текущая граница подключения MUST проверяться до чтения.
#### Scenario: Подключение отсутствует
- **WHEN** User без сохранённого T-Bank connection запрашивает список
- **THEN** система не обращается к T-Bank и отвечает `409` Problem Details с code `TBANK_NOT_CONNECTED`
#### Scenario: Сохранённый token больше не действителен
- **WHEN** фоновая проверка подтвердила, что T-Bank отклоняет token текущего подключения как неаутентифицированный или без прав, и User снова запрашивает список
- **THEN** система отвечает `400` Problem Details с code `INVALID_TBANK_TOKEN` и не удаляет connection
#### Scenario: T-Bank временно недоступен
- **WHEN** фоновое обновление списка завершается временной либо иной upstream-ошибкой
- **THEN** последующее наблюдение результата без нового запуска отвечает `200` с последним сохранённым списком либо `data: null`, `meta.sync.state: failed` и `meta.sync.errorCode: TBANK_UNAVAILABLE`, не подтверждая новую успешную загрузку
### Requirement: Экран списка брокерских счетов
Клиент SHALL загружать список при открытии защищённого маршрута `/accounts` и предоставлять действие «Обновить», без автоматического refetch при возврате фокуса. Клиент MUST использовать успешный frontend-кэш списка в течение 60 секунд. После истечения этого срока клиент MUST запрашивать локальный результат и состояние синхронизации; при уже имеющемся успешном списке он MUST сохранять последний успешный Client Cached Snapshot видимым во время фонового обновления и после его ошибки, ясно отделяя это состояние от свежего результата. Клиент MUST читать список из `data`, показывать свежесть и состояние синхронизации из `meta` и MUST NOT считать успешный HTTP-ответ с `data: null` пустым списком. Ручное обновление MUST обходить проверку давности списка и показанных сводок, не обновляя всю историю счетов. При успешном непустом ответе клиент MUST отобразить адаптивный одноколоночный список широких интерактивных карточек счетов с локализованными типом и, если доступна, датой открытия без времени; каждая карточка MUST ясно показывать тип, имя, финансовую сводку, вторичные метаданные и заметное действие открытия счёта, вести на `/accounts/:accountId` по всей области и сохранять доступное клавиатурное состояние. После успешной замены или удаления T-Bank connection клиент MUST инвалидировать список счетов.
Для доступной `portfolioSummary` карточка MUST показывать не более двух Money-итогов в исходных валютах и обозначать наличие остальных валют, если они есть; она MUST NOT показывать или подразумевать FX-конвертацию, межвалютный агрегированный итог либо Cash Balance как отдельный второй итог. При `portfolioSummary: null` карточка MUST показывать локальное безопасное состояние временной недоступности сводки, сохраняя метаданные счёта и возможность открыть его. При доступной пустой сводке карточка MUST не выдавать отсутствие итогов за временную ошибку.
#### Scenario: Пользователь открывает счёт из списка
- **WHEN** клиент успешно загрузил непустой список и User выбирает карточку счёта
- **THEN** клиент переходит на `/accounts/:accountId` с `id` выбранного счёта
#### Scenario: Список пуст
- **WHEN** клиент получает `data: []` с `meta.availability: complete`
- **THEN** клиент показывает штатное сообщение об отсутствии инвестиционных счетов T-Bank
#### Scenario: Список берётся из свежего frontend-кэша
- **WHEN** User повторно открывает список в течение 60 секунд после успешной загрузки
- **THEN** клиент показывает сохранённые счета без нового API-запроса
#### Scenario: Устаревший список загружается повторно
- **WHEN** User открывает список после истечения 60 секунд frontend-кэша и уже существует последний успешный список
- **THEN** клиент оставляет этот список видимым во время нового запроса и ясно обозначает фоновое обновление
#### Scenario: Ошибка фонового обновления не скрывает snapshot
- **WHEN** обновление устаревшего списка завершается временной ошибкой при наличии последнего успешного Client Cached Snapshot
- **THEN** клиент сохраняет snapshot видимым, безопасно сообщает о неуспешном обновлении и предоставляет применимое действие повторной загрузки
#### Scenario: Карточка ограничивает валютные итоги
- **WHEN** доступная сводка счёта содержит итоги в трёх или более валютах
- **THEN** карточка показывает не более двух исходных валютных итогов и обозначает наличие остальных без общего сконвертированного значения
#### Scenario: Недоступность сводки локальна карточке
- **WHEN** клиент получает список, в котором `portfolioSummary` одного счёта равна `null`
- **THEN** он показывает временную недоступность только на этой карточке, не скрывает остальные карточки и сохраняет переход на детали недоступной сводки счёта
#### Scenario: Пустая сводка не является ошибкой
- **WHEN** клиент получает доступную `portfolioSummary` с пустым `totalsByCurrency`
- **THEN** он показывает нейтральное состояние отсутствия финансовых итогов, а не состояние временной недоступности
#### Scenario: Начальная загрузка не становится пустым списком
- **WHEN** клиент получает `data: null` со статусом запрошенной синхронизации
- **THEN** он показывает состояние первичной загрузки или её ошибки, а не сообщение об отсутствии инвестиционных счетов
#### Scenario: Ручное обновление списка
- **WHEN** User нажимает «Обновить» на экране списка счетов
- **THEN** клиент запрашивает обновление списка и показанных сводок независимо от давности, сохраняя доступные данные и отображая прогресс
### Requirement: Клиентские состояния ошибки списка счетов
Клиент MUST NOT автоматически повторять неуспешный запрос списка. При `TBANK_NOT_CONNECTED` клиент SHALL показать объяснение отсутствия подключения и действие «Открыть настройки T-Bank». При `INVALID_TBANK_TOKEN` клиент SHALL показать объяснение недействительного token и то же действие. При `TBANK_UNAVAILABLE` клиент SHALL показать безопасное объяснение временной недоступности, действие повторной загрузки и действие открытия настроек T-Bank. При иной ошибке клиент MUST показать общий безопасный error-state только с действием повторной загрузки. При `TBANK_NOT_CONNECTED` или `INVALID_TBANK_TOKEN` клиент MUST скрыть личные данные даже при наличии Client Cached Snapshot. Для временной и неизвестной ошибки без snapshot клиент MUST показать ошибку без списка; при наличии snapshot он MUST сохранить его видимым с безопасным предупреждением. `meta.sync.state: failed` MUST отображаться как ошибка фоновой синхронизации, отдельно от ошибки HTTP-запроса; успешный HTTP-ответ сам по себе MUST NOT скрывать такую ошибку.
#### Scenario: Подключение требуется
- **WHEN** клиент получает `TBANK_NOT_CONNECTED`
- **THEN** клиент не показывает список и предлагает открыть настройки T-Bank
#### Scenario: Известная ошибка не повторяется автоматически
- **WHEN** запрос списка завершается известной ошибкой либо сервер сообщает завершённую неуспешную синхронизацию
- **THEN** клиент не выполняет дополнительные запросы, пока User не выберет действие повторной загрузки
#### Scenario: Token требует внимания
- **WHEN** клиент получает `INVALID_TBANK_TOKEN`
- **THEN** клиент не показывает список и предлагает открыть настройки T-Bank
#### Scenario: Временная ошибка T-Bank
- **WHEN** клиент получает `TBANK_UNAVAILABLE` и сохранённого списка нет
- **THEN** клиент не показывает список и предлагает повторить загрузку либо открыть настройки T-Bank
#### Scenario: Неизвестная ошибка загрузки
- **WHEN** клиент получает ошибку без известного кода списка счетов и сохранённого списка нет
- **THEN** клиент не показывает список и предлагает только повторить загрузку
#### Scenario: Известная ошибка при сохранённом snapshot
- **WHEN** фоновое обновление списка с последним успешным Client Cached Snapshot получает `TBANK_UNAVAILABLE` через HTTP-ошибку либо `meta.sync.errorCode`
- **THEN** клиент сохраняет snapshot видимым, сообщает о временной недоступности и предлагает повторить загрузку либо открыть настройки T-Bank
@@ -0,0 +1,59 @@
## MODIFIED Requirements
### Requirement: Client Cached Snapshot остаётся доступным при обновлении Accounts и Portfolio
Frontend MUST сохранять Client Cached Snapshot на всех экранах с данными T-Bank: список Brokerage Accounts, Portfolio, операции, доходы, инструменты, котировки, свечи и выплаты. Снимок MUST сохраняться для того же пользователя, текущего подключения и набора данных, если ранее получен успешный результат с непустым `data`. Подтверждённые `data: []` или пустой полный Portfolio также MUST считаться успешным результатом. Начальная загрузка без такого ответа может показывать блокирующее
состояние загрузки. Client Cached Snapshot MUST оставаться видимым во время
фонового обновления и после его временной ошибки; он отличается от постоянно сохранённых серверных данных и MUST NOT изменять поле `Portfolio.isStale`. Клиент MUST независимо показывать свежесть серверного результата, полноту требуемых периодов, ход синхронизации и ошибку HTTP-refetch. Успешный HTTP-ответ с `meta.sync.state: failed` MUST NOT представляться успешным обновлением источника. Поля `meta.lastSuccessfulAt`, `meta.availability`, `meta.freshness`, `meta.sync` и при наличии `meta.coverage` MUST определять эти состояния по `tbank-data-sync`; клиент MUST NOT выводить их из возраста своего query-кэша. Ошибка ручного обновления ещё свежего источника MUST показываться отдельно от его свежести: клиент MUST NOT объявлять данные устаревшими до истечения серверного TTL либо изменять исходное успешное время.
Состояние `data: null` MUST показывать отсутствие загруженного результата и ход первичной загрузки либо её ошибку. Неполный период MUST явно отличаться от полного и MUST NOT представляться окончательным финансовым итогом или подтверждённым отсутствием операций. Неполное сравнение доходов MUST иметь собственное состояние и MUST NOT подменяться нулём. Статусы MUST быть доступны текстом, не только цветом; действия обновления и повтора MUST быть доступны с клавиатуры и сохранять читаемость на узком экране.
Действие «Обновить» MUST обходить серверную проверку давности для текущего экрана, его применённого периода и зависимостей, сохраняя доступные данные и фильтры. Обычное повторное получение HTTP-ответа MUST NOT считаться ручным принудительным обновлением. Наблюдение за уже запрошенной работой MUST прекращаться после её завершения, ошибки или ухода с экрана, MUST NOT запускать новую синхронизацию и MUST NOT создавать периодическое обновление источника. Уже начатая серверная работа MAY завершиться после ухода.
При подтверждённом `TBANK_NOT_CONNECTED`, `INVALID_TBANK_TOKEN`, недоступности счёта либо изменении пользователя/подключения frontend MUST скрыть личный Snapshot прежней границы. Поздние ответы данных и статусов прежнего подключения MUST NOT попадать в новое состояние; обычная временная ошибка MUST NOT менять эту границу.
#### Scenario: Список Brokerage Accounts обновляется в фоне
- **WHEN** экран Accounts уже показывает Client Cached Snapshot и начинается
фоновое обновление того же списка
- **THEN** frontend оставляет список видимым и показывает только
неблокирующий индикатор обновления
#### Scenario: Обновление Portfolio завершается ошибкой после успешного ответа
- **WHEN** экран Brokerage Account уже показывает Client Cached Snapshot
Portfolio, а последующее обновление этого же Portfolio завершается временной ошибкой
- **THEN** frontend сохраняет Portfolio видимым, показывает локализованное
неблокирующее предупреждение о показе последних полученных данных и
предоставляет повторную попытку обновления
#### Scenario: Server-side fallback и client cached snapshot различаются
- **WHEN** backend возвращает Portfolio с `isStale` либо фоновое обновление
Client Cached Snapshot завершается временной ошибкой
- **THEN** frontend отображает соответствующее server-side либо client-side
предупреждение независимо, не представляя один вид состояния другим
#### Scenario: Серверная синхронизация не завершена после успешного HTTP-ответа
- **WHEN** экран получает `200` с сохранёнными данными и `meta.sync.state: running`
- **THEN** он показывает данные и прежнее успешное время вместе с состоянием обновления, не считая HTTP-успех завершением синхронизации
#### Scenario: Первичная загрузка без данных
- **WHEN** любой экран T-Bank получает `data: null` с отсутствующим либо частичным покрытием
- **THEN** он показывает загрузку либо её ошибку без вымышленных нулей, окончательных итогов и утверждения об отсутствии операций
#### Scenario: Ошибка синхронизации при успешном HTTP-ответе
- **WHEN** экран получает `200` с сохранёнными данными, `meta.sync.state: failed` и безопасным кодом ошибки
- **THEN** данные и время их успешного получения остаются видимыми с отдельным предупреждением об ошибке обновления и действием повтора
#### Scenario: Ошибка обновления не подменяет свежесть
- **WHEN** экран получает ещё свежие данные с `meta.freshness: fresh` и `meta.sync.state: failed` после ручного обновления
- **THEN** он показывает ошибку обновления и сохранённое успешное время без предупреждения об устаревших данных только из-за этой ошибки
#### Scenario: Ручное обновление сохраняет текущий контекст
- **WHEN** User нажимает «Обновить» на экране с выбранным счётом, периодом и фильтрами
- **THEN** клиент запрашивает принудительное обновление этого экрана и зависимостей, сохраняет контекст и доступные данные и не запрашивает обновление всей накопленной истории
#### Scenario: Недействительный token скрывает личный снимок
- **WHEN** источник подтвердил недействительность текущего token и клиент получает `INVALID_TBANK_TOKEN` при наличии сохранённого личного Snapshot
- **THEN** клиент скрывает личные данные и показывает существующее действие настройки подключения вместо маскировки отказа устаревшими данными
#### Scenario: Наблюдение не превращается в расписание
- **WHEN** запрошенная синхронизация завершилась, завершилась ошибкой либо User ушёл со страницы
- **THEN** клиент прекращает наблюдение и не начинает новые периодические синхронизации; уже начатая серверная работа может завершиться
@@ -0,0 +1,144 @@
## MODIFIED Requirements
### Requirement: Получение Portfolio выбранного Brokerage Account
Система SHALL предоставлять авторизованному User `GET /api/v1/brokerage-accounts/:accountId/portfolio`. Она MUST возвращать нормализованный полный Portfolio Snapshot только для открытого поддерживаемого Brokerage Account, доступного через текущий T-Bank connection User. Ответ MUST иметь envelope `{ data, meta }` по контракту `tbank-data-sync`; сохранённый полный Snapshot в `data` MUST содержать `lastSuccessfulAt` в RFC 3339 UTC без миллисекунд, boolean `isStale`, `totalsByCurrency`, `positions` и `cashBalances`.
Portfolio Snapshot является результатом наблюдения Portfolio на конкретный момент. Система MUST сохранять последний полный Snapshot постоянно, без накопления исторических снимков стоимости. Это MUST NOT подтверждать доступность Brokerage Account вне текущего T-Bank connection. Проверка локальной сессии, текущего подключения и сохранённого состава доступных счетов MUST выполняться без синхронного запроса списка счетов T-Bank. При отсутствии сохранённого Snapshot ответ MUST иметь `200`, `data: null`, `meta.availability: missing` и состояние запрошенной загрузки; это MUST NOT обозначать пустой Portfolio.
#### Scenario: Получение текущего Portfolio
- **WHEN** User запрашивает Portfolio доступного Brokerage Account, а полный Snapshot текущего подключения успешно сохранён и ещё свеж
- **THEN** система отвечает `200` с полным нормализованным Portfolio Snapshot в `data` без ожидания T-Bank с `isStale: false` и временем успешного получения финансового снимка
#### Scenario: Недоступный Brokerage Account
- **WHEN** для текущего подключения подтверждено, что `accountId` не принадлежит User, закрыт, неподдерживаем или больше не доступен
- **THEN** система отвечает `404` без Portfolio Snapshot
#### Scenario: Сохранённый Portfolio читается после перезапуска
- **WHEN** backend перезапущен, текущий connection не изменился и полный Snapshot уже сохранён
- **THEN** запрос возвращает его из постоянного хранилища без ожидания T-Bank и отдельно сообщает свежесть и состояние фонового обновления
### Requirement: Свежесть и fallback полного Portfolio Snapshot
Система MUST постоянно сохранять последний полный успешный Portfolio Snapshot отдельно для User, Brokerage Account и текущего T-Bank connection. Обычный запрос MUST возвращать сохранённый Snapshot без ожидания T-Bank; при возрасте не менее одной минуты он MUST запросить фоновое обновление. Временная ошибка MUST NOT удалять Snapshot или ограничивать его выдачу прежним пределом пяти минут. `lastSuccessfulAt` MUST сохранять время успешного получения финансового снимка и совпадать с соответствующим временем в `meta`; `isStale` MUST отражать `meta.freshness: stale`, а не состояние клиентского кэша. Ручное действие «Обновить» MUST обходить проверку давности текущего Portfolio и его показанных зависимостей. Неуспешное ручное обновление MUST сохранять исходное `lastSuccessfulAt` и MUST NOT само по себе устанавливать `isStale: true`, пока минутный срок свежести сохранённого финансового снимка не истёк; ошибка MUST отображаться отдельно через `meta.sync`.
Metadata инструмента MUST NOT влиять на успешность или свежесть Portfolio Snapshot. Сохранённые metadata MUST использоваться без ожидания их обновления, а отсутствующие сведения MUST допускать существующий fallback идентификатора. Новые доступные metadata MUST дополнять представление, не меняя времени финансового снимка. Общие данные инструментов MUST следовать суточной свежести `tbank-data-sync`, без сохранения персональных позиций в общих данных.
После успешной замены или удаления T-Bank connection прежние Snapshot и поздние результаты его запросов MUST NOT обслуживать новое подключение. Известный отказ token текущего подключения MUST блокировать личное чтение с существующим `INVALID_TBANK_TOKEN`; он MUST NOT маскироваться сохранённым Snapshot.
#### Scenario: Свежий Snapshot
- **WHEN** User повторно запрашивает тот же Portfolio ранее чем через одну минуту после успешного получения
- **THEN** система возвращает свежий Snapshot без нового обращения к T-Bank
#### Scenario: Медленный lookup инструмента
- **WHEN** metadata инструмента ещё отсутствует и его lookup не завершился
- **THEN** система возвращает сохранённый финансовый Snapshot без ожидания lookup с доступным fallback, а после успешного lookup последующее чтение использует полученное name
#### Scenario: Fallback после временной upstream-ошибки
- **WHEN** обновление устаревшего Snapshot неуспешно из-за временной недоступности T-Bank, а последний полный успешный Snapshot получен более пяти минут назад
- **THEN** система отвечает `200` этим Snapshot с `isStale: true`, исходным `lastSuccessfulAt` и ошибкой обновления в `meta.sync`, без повторной синхронной попытки
#### Scenario: Ручное обновление ещё свежего Snapshot неуспешно
- **WHEN** User принудительно обновляет Snapshot, полученный менее минуты назад, и попытка завершается временной ошибкой до истечения его TTL
- **THEN** наблюдение возвращает прежний Snapshot с `isStale: false`, исходным `lastSuccessfulAt` и отдельной ошибкой синхронизации; ошибка не продлевает TTL и не делает данные устаревшими раньше срока
#### Scenario: Недействительный token не маскируется fallback-данными
- **WHEN** фоновое обновление подтвердило отклонение token текущего подключения как неаутентифицированного или без прав, после чего запрошен Portfolio
- **THEN** система не возвращает fallback Snapshot и отвечает `400` Problem Details с code `INVALID_TBANK_TOKEN`
### Requirement: Состояния и ошибки Portfolio на клиенте
Клиент SHALL загружать Portfolio при открытии защищённого маршрута
`/accounts/:accountId`. Он MUST использовать успешный ответ в течение
30 секунд, без automatic retry неуспешного запроса, периодической синхронизации или refetch при возврате фокуса. Наблюдение за состоянием уже запрошенной работы MUST следовать `tbank-data-sync` и MUST NOT запускать новую работу.
При загрузке без ранее полученного успешного ответа либо при `data: null`
с состоянием загрузки для этого же Brokerage Account клиент MUST показывать структурное skeleton-состояние ожидаемого
содержимого, включая сводку, без вымышленных сумм. Область загрузки MUST
иметь доступное обозначение занятости; skeleton MUST NOT быть интерактивным
или получать клавиатурный фокус.
Истечение срока свежести MUST NOT само по себе скрывать уже доступный
Client Cached Snapshot. Во время фонового обновления и после его временной ошибки
клиент MUST сохранять ранее полученные данные того же Portfolio, включая
валютные итоги и исходное `lastSuccessfulAt`. При обновлении клиент MUST
показывать компактную неблокирующую индикацию без замены данных skeleton.
При ошибке обновления он MUST показывать локализованное неблокирующее
предупреждение о последних полученных данных и предоставлять действие
повторной попытки. Сохранённые данные MUST NOT считаться новым успешным
снимком или подтверждением текущей доступности Brokerage Account.
Клиент MUST показать штатное empty-состояние только при `meta.availability: complete`, непустом `data` и пустых `positions`
и `cashBalances`, без нулевых валютных карточек. Для показываемого состава
Portfolio клиент MUST сохранять время успешного получения снимка
`lastSuccessfulAt`, форматируемое в UTC. При `isStale: true` клиент MUST
показывать Snapshot, его исходное время и нейтральное предупреждение
об устаревших данных. Ошибка синхронизации MUST иметь отдельное безопасное объяснение. На экране MUST быть доступно действие «Обновить», обновляющее текущий экран и его зависимости независимо от давности. Server-side stale и ошибка фонового
обновления Client Cached Snapshot MUST оставаться раздельными состояниями:
ошибка клиента MUST NOT менять или подменять `isStale`, а оба предупреждения
и действие повтора клиентского обновления MUST оставаться доступными при
одновременном наличии этих состояний.
При ответе `404` для этого же Portfolio клиент MUST показать состояние недоступного счёта
с действием возврата к списку счетов без действия повтора. При `TBANK_NOT_CONNECTED` или `INVALID_TBANK_TOKEN` клиент MUST скрыть личные данные даже при наличии Snapshot. При отсутствии Snapshot и `TBANK_UNAVAILABLE`
или иной ошибке клиент MUST показать безопасное error-состояние с доступными
для ошибки действиями; действие повтора разрешено только для временной
и неизвестной ошибки.
#### Scenario: Показ устаревшего Portfolio
- **WHEN** клиент получает успешный ответ с `isStale: true`
- **THEN** клиент показывает позиции, остатки, валютные итоги, время
последнего успешного снимка, нейтральное предупреждение об устаревании
и доступное действие «Обновить»
#### Scenario: Недоступный счёт на клиенте
- **WHEN** клиент получает `404` для текущего Portfolio
- **THEN** клиент не показывает Portfolio и предлагает вернуться к списку
Brokerage Account без действия повтора
#### Scenario: Первичная загрузка Portfolio
- **WHEN** клиент ожидает Portfolio этого Brokerage Account без ранее
полученного успешного ответа либо получает `data: null` с активной загрузкой
- **THEN** он показывает структурный skeleton, включая место сводки,
с доступным обозначением загрузки, без вымышленных или нулевых сумм
и без фокусируемых placeholders
#### Scenario: Обновление после истечения срока свежести
- **WHEN** срок свежести показанного Portfolio истёк и началось фоновое
обновление данных того же Brokerage Account
- **THEN** клиент сохраняет прежние валютные итоги и время их успешного
получения, показывает неблокирующую индикацию обновления и не заменяет
доступный состав и сводку skeleton-состоянием
#### Scenario: Ошибка обновления после успешного ответа
- **WHEN** клиент уже показывает Client Cached Snapshot Portfolio,
а обновление этих же данных завершается временной ошибкой
- **THEN** клиент сохраняет данные, суммы и время прежнего снимка,
показывает неблокирующее предупреждение и действие повтора;
ошибка не делает сохранённый снимок свежим и не изменяет `isStale`
#### Scenario: Ошибка клиентского обновления при server-side stale
- **WHEN** клиент показывает Portfolio с `isStale: true`, а фоновое
обновление этого же Portfolio завершается временной ошибкой
- **THEN** прежние суммы и время остаются видимыми одновременно
с нейтральным server-side предупреждением, отдельным предупреждением
об ошибке клиентского обновления и его действием повтора
#### Scenario: Повторное обновление успешно
- **WHEN** после ошибки фонового обновления User запускает повторную
попытку и получает успешный Portfolio Snapshot
- **THEN** до ответа прежние суммы и время остаются видимыми, затем
клиент показывает значения и `lastSuccessfulAt` успешного ответа,
убирает предупреждение об ошибке клиентского обновления и показывает
server-side предупреждение только в соответствии с новым `isStale`
#### Scenario: Штатный пустой Portfolio на клиенте
- **WHEN** успешно полученный ответ имеет `meta.availability: complete` и Portfolio в `data` содержит пустые `positions`
и `cashBalances`
- **THEN** клиент показывает существующее empty-состояние без валютных
карточек с вымышленными нулевыми итогами
@@ -0,0 +1,313 @@
## Purpose
Определяет постоянное хранение данных T-Bank, локальное чтение и синхронизацию
по запросу с проверяемыми полнотой, свежестью и изоляцией подключения.
## ADDED Requirements
### Requirement: Все экраны читают сохранённые данные без ожидания T-Bank
Система SHALL постоянно сохранять уже загруженные счета, последний полный
портфель, исполненные операции, данные инструментов, текущие котировки,
дневные свечи и календари выплат. Доходы, личные позиции и прогнозные выплаты
SHALL рассчитываться по сохранённым источникам. Все соответствующие экраны,
включая режим «Все счета», поиск, сведения о компании и зависимости котировок,
MUST читать доступные данные без ожидания ответа T-Bank. Перезапуск backend
MUST NOT требовать повторной загрузки сохранённых полных интервалов.
#### Scenario: Брокер медленно отвечает при заполненном хранилище
- **WHEN** User открывает любой экран с сохранёнными данными текущего подключения, а T-Bank задерживает ответ
- **THEN** обычный серверный ответ содержит сохранённые данные и их состояние, не ожидая T-Bank, включая проверку доступных счетов
#### Scenario: Перезапуск сохраняет историю
- **WHEN** backend перезапущен и User запрашивает ранее полностью загруженный прошлый период
- **THEN** система возвращает его из постоянного хранилища без повторного получения операций или завершённых свечей этого периода
#### Scenario: Данные ещё не загружены
- **WHEN** User впервые запрашивает отсутствующие данные
- **THEN** ответ различает отсутствие загруженного результата и подтверждённый пустой результат, сообщает о запрошенной загрузке и не ожидает её завершения
### Requirement: Обновление запускается только запросом с учётом свежести
Система MUST запускать синхронизацию только запросом экрана либо ручным
обновлением. Обычный запрос SHALL планировать только отсутствующие зависимости,
пропуски и данные с истекшим сроком свежести: портфель и текущие цены —
60 секунд; операции текущего месяца и доступные счета — 300 секунд;
справочник инструментов, включая сведения о компании, и календари выплат —
24 часа; незавершённая дневная свеча — 60 секунд. Свежесть MUST отсчитываться
от последнего успешного получения соответствующего источника, а не чтения
ответа или расчёта производного представления. Полнота и свежесть MUST
оставаться независимыми признаками. Периодические запуски MUST NOT выполняться.
Для текущего изменяемого периода полнота SHALL относиться к состоянию на
последний успешный `asOf`: рост `to=now` внутри TTL не является новым
пропуском, требующим RPC. Настоящие пропуски до `asOf` и незавершённый хвост
при переходе месяца MUST NOT маскироваться этим правилом.
#### Scenario: Данные ещё свежие
- **WHEN** экран повторно запрошен до истечения сроков всех его полных зависимостей
- **THEN** запрос не создаёт новых обращений к T-Bank
#### Scenario: Текущий период запрошен через секунду
- **WHEN** текущий месяц полностью загружен на `asOf` и повторный запрос через секунду имеет более поздний `to=now`
- **THEN** система возвращает сохранённый результат с исходным `asOf` без нового RPC до истечения пяти минут, не утверждая наблюдение новых операций после `asOf`
#### Scenario: Истекла только свежесть портфеля
- **WHEN** портфель получен 60 секунд назад, а полные счета и metadata ещё свежие
- **THEN** обычный запрос инициирует обновление портфеля и только иных необходимых устаревших зависимостей, сохраняя прежний ответ доступным
#### Scenario: Пользователь не обращается к системе
- **WHEN** запрошенная работа завершилась и сроки свежести истекают без новых запросов
- **THEN** никакая синхронизация не запускается, в том числе при запуске backend
### Requirement: Ручное обновление ограничено текущим экраном
Действие «Обновить» SHALL обходить давность данных и запрашивать обновление
экрана с его зависимостями: текущего счёта либо доступных счетов в режиме
«Все счета», применённого периода и сравнений доходов, либо инструмента
с показанными свечами, выплатами и личной историей. Сервер MUST определять
разрешённые зависимости сам, сохраняя проверку входа и доступа. Обновление
MUST NOT перечитывать всю накопленную историю других периодов и инструментов.
Подтверждение приёма работы MUST NOT означать успешное обновление данных.
#### Scenario: Обновление старого месяца
- **WHEN** User нажимает «Обновить» на журнале за полностью загруженный прошлый месяц
- **THEN** перечитывается показанный период счёта и необходимые зависимости без принудительного перечитывания остальных полных месяцев
#### Scenario: Обновление доходов
- **WHEN** User обновляет аналитику за выбранный период
- **THEN** обновление охватывает выбранный и требуемые периоды сравнения, а результаты разных периодов не подменяют друг друга
#### Scenario: Обновление инструмента
- **WHEN** User обновляет инструмент с выбранным диапазоном графика и показанной личной историей
- **THEN** обновляются зависимости этого инструмента, диапазон свечей и показанный период личной истории разрешённых счетов без обновления всего каталога
### Requirement: Первая загрузка приоритизирует экран и продолжает историю
Система SHALL сначала загружать данные нужного экрану периода, включая
периоды сравнения доходов, затем всю доступную историю запрошенного счёта.
Для режима «Все счета» это применяется к каждому доступному счёту.
В рамках уже запрошенной работы SHALL заранее загружаться доступная рыночная
история бумаг текущего портфеля; для остальных инструментов — при первом
открытии. Новый запрос нужного экрану пропуска MUST получать приоритет перед
ещё не начатой догрузкой старой истории. Начатая работа MAY завершиться
после ухода со страницы. Недоказанная нижняя граница доступной истории
MUST NOT выдаваться за подтверждение её полной загрузки.
#### Scenario: Сначала сравнения доходов
- **WHEN** история счёта отсутствует и User открывает доходы за месяц
- **THEN** выбранный месяц и доступные обязательные периоды сравнения загружаются перед остальной историей; состояние старой истории не задерживает уже полный результат экрана
#### Scenario: Уход со страницы
- **WHEN** User уходит после начала догрузки истории
- **THEN** запрошенная работа может закончиться и сохранить результат, но не создаёт расписание последующих обновлений
#### Scenario: Инструмент отсутствует в портфеле
- **WHEN** User впервые открывает такой инструмент
- **THEN** система запрашивает и сохраняет его историю, не предзагружая весь каталог
### Requirement: Полнота подтверждается успешным покрытием интервалов
Система MUST считать историю счёта полной за период только после успешного
получения всех доступных исполненных операций всего этого интервала на момент
сверки. Полный период MAY быть пустым. Фильтр категорий, инструмента, размер
страницы, отсутствие записей, максимальная дата операции и наличие курсора
MUST NOT служить доказательством полноты. Производное представление «Все
счета» MUST учитывать покрытие каждого входящего счёта. Полные прошлые месяцы
SHALL сохраняться без автоматической повторной сверки; пропуски SHALL
догружаться при запросе. Смена месяца MUST NOT автоматически закрывать
незагруженный хвост прошлого месяца. Существующая семантика входных дат
журнала и доходов MUST сохраняться без пропусков и двойного учёта на границах.
#### Scenario: Пустой полный интервал
- **WHEN** все страницы интервала успешно получены и операций нет
- **THEN** период обозначается полным и пустым; последующее обычное чтение прошлого месяца не обращается к брокеру за этим интервалом
#### Scenario: Сбой промежуточной страницы
- **WHEN** одна из нескольких страниц интервала недоступна
- **THEN** интервал остаётся неполным, сохранённый результат не удаляется, частичные записи не выдаются за полный период
#### Scenario: Граница месяца
- **WHEN** последний успешный запрос прошлого месяца закончился до его конца
- **THEN** первый подходящий запрос нового месяца догружает недостающий хвост; смена даты сама не подтверждает полноту прошлого месяца
#### Scenario: Неполон один из счетов
- **WHEN** все счета, кроме одного, полностью покрывают период общего дохода
- **THEN** общий период не считается полным, а отсутствующий вклад счёта не заменяется нулём
### Requirement: Сверка операций сохраняет идентичность и прежний результат
Система SHALL использовать стабильный ID брокерской операции в границах
пользователя, подключения и счёта как принятое допущение проекта, а не гарантию
API. Повторное получение ID MUST обновлять поля той же операции без дубля
и двойного финансового учёта. Ручная сверка MUST согласовывать состав и поля
исполненных операций показанного интервала: отсутствующие в полном новом
ответе записи могут быть удалены только вместе с успешным завершением всех
страниц этого интервала. Частичный либо ошибочный новый ответ MUST NOT
изменять ранее опубликованный полный финансовый результат и время его
успешной сверки. Автоматическое обнаружение изменений полной старой истории
и смены ID не требуется.
#### Scenario: Поля операции исправлены
- **WHEN** успешная повторная сверка получает прежний ID с другой суммой или статусом, меняющим вхождение в исполненную историю
- **THEN** итоговый набор отражает новый полный ответ без дублирования ID, а доходы пересчитываются из согласованного набора
#### Scenario: Операция исчезла при ручной сверке
- **WHEN** все страницы перечитанного интервала успешно завершились и прежняя операция отсутствует
- **THEN** операция исключается из опубликованного набора этого интервала и производных финансовых результатов
#### Scenario: Перезагрузка полного периода оборвалась
- **WHEN** после нескольких страниц ручной сверки возникает ошибка
- **THEN** прежний полный набор, его суммы, полнота и время успешной сверки остаются доступными вместе с ошибкой обновления
#### Scenario: Сбой во втором месяце перечитываемого квартала
- **WHEN** ручная сверка полного квартала получила первый месяц, но оборвалась на странице второго
- **THEN** весь прежний результат показанного квартала, включая режим «Все счета», остаётся опубликованным; успешный новый первый месяц не смешивается с прежними остальными месяцами
### Requirement: Прогресс переживает перезапуск и одновременные запросы
Система MUST сохранять завершённые интервалы, незавершённую работу и её
прогресс. После перезапуска следующий подходящий запрос экрана или ручное
обновление SHALL возобновлять запрошенную работу, повторяя незавершённый
интервал безопасно и без повторной загрузки полных старых интервалов.
Одновременные запросы одной границы данных SHALL объединять одинаковую
работу, а пересекающиеся интервалы SHALL использовать общую загрузку
пересечения. Ошибка MUST оставлять возможность следующего явного запроса,
не создавая бесконечный цикл повторов. Поздняя публикация устаревшей попытки
MUST NOT перезаписывать результат новой попытки.
#### Scenario: Двадцать одновременных запросов
- **WHEN** двадцать запросов требуют один отсутствующий интервал одного счёта
- **THEN** выполняется один проход страниц этого интервала с разрешёнными транспортными повторами, а все запросы видят общее состояние работы
#### Scenario: Перезапуск между страницами
- **WHEN** процесс остановлен после сохранения прогресса, но до полного завершения интервала
- **THEN** до следующего запроса работа не возобновляется; после запроса интервал перечитывается без дублей и без ложного признака полноты
#### Scenario: Ошибка завершила попытку
- **WHEN** запрошенная синхронизация исчерпала допустимые транспортные повторы
- **THEN** она показывает безопасную ошибку и прекращает попытки до следующего запроса; наблюдение её состояния не запускает её заново
### Requirement: Общие данные инструментов отделены от личных
Система SHALL совместно использовать неперсональные metadata, котировки,
свечи и календари одного инструмента между пользователями. Общие записи
MUST NOT содержать аккаунты, личные позиции, остатки, операции, оценки выплат
пользователя, token либо персональные ошибки. Личные данные и вычисления
MUST оставаться ограничены пользователем, доступным счётом и текущим
подключением. Успешный общий результат MAY переиспользоваться между счетами
без повторного получения того же источника.
#### Scenario: Два пользователя открывают инструмент
- **WHEN** оба имеют право пользоваться интеграцией и общие данные инструмента уже загружены
- **THEN** они используют одни общие данные, но видят только собственные позиции, операции и оценки выплат
#### Scenario: Ошибка token одного пользователя
- **WHEN** источник отклоняет token одного пользователя при загрузке общего инструмента
- **THEN** личная ошибка не становится состоянием инструмента для другого пользователя и не удаляет ранее сохранённые общие данные
### Requirement: Локальная проверка доступа сохраняет границу подключения
Каждое чтение и действие синхронизации MUST проверять сессию, текущее
подключение и локально подтверждённую принадлежность счёта этому подключению.
Отсутствие ранее подтверждённого списка SHALL означать загрузку доступа,
а не доступ к произвольному accountId или доказанный пустой список.
Устаревший подтверждённый список MAY использоваться до фоновой сверки;
успешная сверка MUST отзывать доступ к исчезнувшим, закрытым или неподдержанным
счетам. Известный отказ token MUST сохранять существующую ошибку
`INVALID_TBANK_TOKEN`, отсутствие подключения — `TBANK_NOT_CONNECTED`,
недоступный счёт — `404`. Хранение не вводит архив без подключения.
Успешная замена или удаление подключения MUST блокировать выдачу прежних
личных данных и публикацию любых поздних результатов прежней работы.
Неуспешная замена MUST сохранять действующую границу.
#### Scenario: Подтверждённый счёт проверяется локально
- **WHEN** User читает заполненные данные доступного счёта текущего подключения
- **THEN** проверка принадлежности не обращается синхронно к T-Bank и не использует список другого пользователя или подключения
#### Scenario: Прямой переход до загрузки счетов
- **WHEN** список текущего подключения ещё неизвестен и User запрашивает конкретный accountId
- **THEN** система сообщает о загрузке проверки доступа, не выдаёт личных данных этого ID и загружает доступные счета
#### Scenario: Доступ к счёту отозван
- **WHEN** успешная фоновая сверка больше не подтверждает доступ к ранее видимому счёту
- **THEN** последующие запросы этого счёта возвращают `404`, а клиент прекращает показывать его как доступный
#### Scenario: Поздний результат прежнего подключения
- **WHEN** старый запрос заканчивается после успешной замены или удаления подключения
- **THEN** его данные, прогресс, ошибки и признаки свежести не публикуются и не становятся результатом нового подключения
### Requirement: Завершённость свечей подтверждается источником
Система SHALL сохранять дневные свечи по инструменту и времени источника.
Завершённые свечи MUST NOT перечитываться обычным запросом при полном
покрытии диапазона. Незавершённая свеча SHALL обновляться при запросе после
60 секунд до явного подтверждения завершённости источником, даже если её
календарный день прошёл. Успешное пустое окно MAY подтверждать отсутствие
свечей только в завершённом диапазоне и MUST NOT создавать синтетические
свечи. Открытое окно текущего дня, в том числе пока пустое, SHALL оставаться
изменяемым с минутным сроком свежести. Ручное обновление SHALL
перечитывать показанный диапазон, сохраняя прежние данные при ошибке.
#### Scenario: Наступил новый день
- **WHEN** сохранённая свеча предыдущего дня не подтверждена как завершённая источником
- **THEN** новый день не делает её окончательной, и подходящий запрос продолжает обновлять её с минутным сроком свежести
#### Scenario: Выходной без свечей
- **WHEN** источник успешно подтвердил пустое окно без торговых свечей
- **THEN** окно сохраняется как проверенное без выдуманных цен и повторной обычной загрузки завершённого диапазона
#### Scenario: До начала торгов свеча отсутствует
- **WHEN** запрос текущего дня вернул пустое окно, а после начала торгов User снова открывает график по истечении минуты
- **THEN** система запрашивает изменяемое окно и сохраняет появившуюся свечу; утренний ответ не закрепляет окончательное отсутствие свечи
### Requirement: Интерфейс различает свежесть полноту и состояние работы
На всех экранах T-Bank клиент MUST показывать время последнего успешного
получения, свежесть, загрузку и безопасную ошибку обновления применимых
данных. Полнота MUST относиться к показанному периоду и его зависимостям;
общий прогресс истории счёта MUST NOT подменять полноту экрана. Отсутствующие
данные SHALL иметь состояние первичной загрузки; доступные данные SHALL
оставаться видимыми во время обновления. Неполные totals/comparisons MUST
NOT показываться как окончательные или как ноль; неполный пустой журнал
MUST NOT сообщать, что операций нет. Наблюдение запрошенной работы MUST
прекращаться после завершения либо ухода и MUST NOT запускать синхронизацию.
Известные ошибки доступа MUST NOT маскироваться сохранённым результатом.
#### Scenario: Недавний период готов раньше всей истории
- **WHEN** показанный период и его зависимости полны, но старая история ещё загружается
- **THEN** пользователь видит готовый результат периода и отдельное состояние догрузки истории
#### Scenario: Полнота изменилась без новых записей
- **WHEN** источник подтвердил пустой период экрана, а остальная история ещё загружается
- **THEN** наблюдающий клиент получает изменение полноты и показывает подтверждённый пустой результат, не ожидая конца всей истории
#### Scenario: Ответы наблюдения завершились в обратном порядке
- **WHEN** поздний ответ чтения содержит прежние данные или доступ, а клиент уже принял более новое состояние этого ресурса либо отзыв доступа
- **THEN** прежний ответ не откатывает показанные данные и не восстанавливает недоступный личный результат
#### Scenario: Источник временно недоступен
- **WHEN** обновление сохранённого полного результата завершается временной ошибкой
- **THEN** прежние данные и время сохраняются, ошибка видима и доступно ручное повторное обновление; ошибка не обновляет время успешного получения и не считается успешной сверкой
#### Scenario: Неполное сравнение дохода
- **WHEN** выбранный период готов, а необходимый период сравнения ещё не загружен
- **THEN** сравнение обозначается как незавершённое без нулевой базы или окончательной разницы
### Requirement: Быстродействие и сокращение запросов проверяются измерениями
Для обычных чтений заполненной БД система SHALL иметь целевой бюджет
серверного ответа до 500 мс в зафиксированном профиле нагрузки. Выполнение
цели MUST подтверждаться измерениями, а не предполагаться из наличия БД.
Свежие полные зависимости MUST давать ноль новых RPC T-Bank; двадцать
одновременных запросов одного устаревшего набора MUST создавать не более
одной логической загрузки каждой зависимости. Полные старые операции и
завершённые свечи MUST давать ноль повторных RPC для своих интервалов при
обычном чтении, включая чтение после перезапуска.
#### Scenario: Измеряется заполненное хранилище
- **WHEN** проверяется согласованный набор обычных запросов при задержанном или недоступном T-Bank
- **THEN** фиксируются распределение серверных задержек, число RPC и параметры нагрузки; цель 500 мс не объявляется достигнутой без этих результатов
#### Scenario: Повторные открытия не перечитывают историю
- **WHEN** один и тот же полный прошлый период читается повторно без ручного обновления
- **THEN** сохранённые данные выдаются без RPC этого интервала, независимо от числа страниц клиентской выдачи
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,2 @@
schema: wealth-ledger-sdd-v2
created: 2026-10-09
@@ -0,0 +1,57 @@
# Completion Evidence: reconcile-hourly-investment-sync
## Outcome
The approved reconciliation was implemented across backend, frontend and docs.
The hourly walk is the normal personal-data update owner; ordinary resource GETs
and retries remain local/read-only, with the approved first-load exceptions.
Resource metadata now distinguishes confirmed data state from hourly attempt state
and browser transport state.
## Delivery Map
| Area | Result | Commit |
| --- | --- | --- |
| backend | Resource envelope, hourly attempt projections, saved forecast window semantics, read-only events GET, candle read boundary and generated OpenAPI | `aee330c` |
| frontend | Generated contract consumer, Accounts/Portfolio/operations/events status presentation, HTTP-only retry, accessible status text and snapshot retention | `6b1b26a` |
| docs | Six approved AS-IS documentation and ADR updates | `a45d3e7` |
| specs | Approved v2 planning package; native sync/archive remains finalization work | pending archive commit |
| main | N/A: no workspace/config/Compose orchestration change | N/A |
| qa | N/A: separate QA repository does not exist | N/A |
## Verification
| Repository | Verification | Result |
| --- | --- | --- |
| backend | `npm run typecheck:app` | PASS |
| backend | `npm run typecheck:test` | PASS |
| backend | `npm test -- --runInBand` | PASS: 37 suites, 229 tests |
| backend | `npm run test:sqlite -- upcoming-events.e2e-spec.ts` | PASS: 22/22 |
| backend | `npm run openapi:generate` | PASS |
| backend | `npm run test:openapi-contract` | PASS: 3 tests |
| frontend | `npm run verify` | PASS: 65 files, 436 tests, typecheck, lint, OpenAPI, dependency gates, app and Storybook builds |
| docs | `npm run verify` | PASS |
| specs | `openspec validate reconcile-hourly-investment-sync --type change --strict --store wealth-ledger-specs` | PASS |
| specs | `openspec validate --specs --strict --store wealth-ledger-specs` | PASS: 26 specs, 0 failed |
| all | `git diff --check` | PASS |
The backend repository-wide lint command reports existing unused ESLint
suppressions. Changed-file lint with `--pass-on-unpruned-suppressions` passed;
all application/test/type/OpenAPI gates above passed. This repository hygiene
issue is not introduced by the reconciliation implementation.
## Independent Validation
Independent review found no remaining Critical or Important behavioral findings
after fixing ordinary events GET locality, saved forecast window/calculatedAt,
candle reread behavior and Accounts metadata presentation. The reviewed scope
covered approved Proposal, ten Delta capabilities, System Analysis, Design, QA
Analysis, Docs Impact, Tasks, current backend/frontend/docs diffs and generated
OpenAPI.
## Approval Binding
Completion uses the current approved planning artifacts in this change directory.
Finalization must re-run native sync/archive checks against the current files and
must not treat artifact existence as approval. Legacy active changes remain on
their legacy schema and are not implicitly migrated by this archive.
@@ -0,0 +1,358 @@
# Engineering Design
## Context and Constraints
The deployed backend is a single NestJS process with SQLite/Prisma. Its durable
hourly walk, connection revision publication fence, saved investment resources,
and frontend 30-minute rereads already provide most of the approved model.
The remaining mismatch is at the resource boundary: backend
`InvestmentResourceMetaDto` and `investmentResourceMeta` expose legacy
`missing|partial|complete` availability and `idle|running|failed` work state,
while the frontend projects that legacy metadata through `useResourceQuery` and
uses `sync=observe` as a transport mode. The generated backend `openapi.json`
and frontend snapshot agree with each other, but they describe this AS-IS
contract rather than the approved target.
The backend-generated OpenAPI remains the executable contract. The change-local
`system-analysis/contracts/investment-resource-envelope.schema.json` is a
semantic target and must not become a second OpenAPI source, generated client,
or runtime validator. Route identities, authentication/access boundaries,
Problem Details codes, and resource payload DTOs remain established unless an
approved target requirement explicitly changes them.
No normal personal resource GET may wait for or call T-Bank. The only
acquisition exceptions remain the first unconfirmed ledger interval and first
unconfirmed candle range. The design preserves the single-process boundary; it
does not add distributed scheduling, a job service, polling endpoint, manual
sync command, public value-history read, or a new UI package dependency.
## Technical Goals / Non-Goals
**Goals**
- Publish one generated HTTP envelope for Accounts, Portfolio, operations, and
events: `{ data, meta }`, with Accounts-only top-level `sync`.
- Make resource-result state and hourly-attempt state independently truthful:
confirmed data stays available after a failed/partial attempt, while an
initial result remains `null` until complete.
- Preserve local-read, connection-revision, and first-load invariants while
migrating Swagger DTOs, mapper inputs, runtime responses, and frontend
generated types together.
- Keep the existing successful TanStack Query snapshot during read retries and
retain the existing 30-minute open-screen reread cadence as a local HTTP
reread, not an import trigger.
**Non-Goals**
- Rebuild source owners, hourly scheduling, portfolio-value storage, or the
legacy status endpoint beyond removing their use as a source-work command
from these screens.
- Introduce a compatibility envelope, client-side DTO normalizer, duplicate
cache, new route/version, manual refresh/import action, or a copy of OpenAPI
in specs.
- Plan documentation edits, implementation tasks, or product behavior beyond
the approved proposal, delta specs, and System Analysis.
## Implementation Impact Map
### frontend
`affected` - generated transport types, `shared/api/resource-query.ts`, the
existing brokerage-account/portfolio/operations/events query owners, and their
page VMs/views consume the target metadata. `useScreenSync` remains the common
30-minute open-screen reread owner, but its source-error derivation must read
`meta.lastAttempt` rather than legacy `meta.sync`; retry stays HTTP-only.
### backend
`affected` - investment resource DTOs, mapper, endpoint response DTOs,
resource readers, connection/hourly-attempt projections, Swagger annotations,
and generated `openapi.json` implement the semantic envelope. Existing durable
attempt, revision, membership, snapshot, forecast, coverage, and history
owners remain the data owners.
### specs
`affected` - this Design makes the approved deltas and System Analysis
implementable without duplicating the executable contract.
### docs
`affected` - durable AS-IS references are limited to ADR 019, ADR 017,
`architecture/cross-cutting/data-freshness.md`,
`domains/portfolio/flows/portfolio-loading.md`, and
`domains/income/upcoming-events.md`. Their impact is recorded here only;
documentation sequencing is not designed in this artifact.
### qa
`N/A` - no separate QA/E2E repository exists.
## Decisions
### Backend: one resource-meta projection and one connection-walk projection
Replace the legacy shared metadata vocabulary at the existing
`InvestmentResourceMetaDto`/`investmentResourceMeta` seam. Every affected
resource envelope has required `meta.freshness`, `meta.completeness`,
`meta.lastSuccessfulAt`, `meta.lastAttempt`, and `meta.availability`.
`lastAttempt` is a resource-scoped projection with state
`none|running|succeeded|partial|failed`, nullable start/end times, and only
safe failed section/account/error-code entries. `none` is a concrete status,
not an omitted field or success.
The mapper derives freshness and successful time only from the returned
confirmed resource result. A later running, partial, or failed walk neither
changes that time nor makes data fresh. `data: null` pairs with `unknown`/
incomplete and either `initial-loading`, `temporarily-unavailable`, or
`connection-unavailable`; a confirmed empty collection pairs with available
and complete. The ledger's permitted first-load rows retain incomplete
coverage and are never represented as a confirmed complete result.
Accounts additionally projects the connection-wide walk as `sync: null` when
no attempt exists, otherwise the same complete safe attempt shape. Other
resource envelopes do not gain top-level `sync`. This preserves the single
shared metadata owner while avoiding the current ambiguity between per-resource
`meta.sync` and Accounts-level walk status.
The existing `resourceKey`, `dataRevision`, `coverage`, and dependency details
remain internal projection inputs or endpoint-specific metadata only where
their approved behavior still requires publication; they must not substitute
for any of the five required target meanings. No client must infer target state
from their presence.
### Backend: retain storage owners and publication fences
The durable hourly attempt remains the sole normal publisher. Resource readers
read the saved current connection revision and project its persisted result;
they do not reserve work or contact upstream. Each successful section can
persist independently, preserving the previous confirmed resource result and
its success time when a later section fails. Forecast publication remains
complete-source-only, and exact per-currency value-history writes remain in the
same successful Portfolio publication transaction.
Connection PUT establishes a new revision before its first walk; DELETE and
replacement prevent old personal snapshots and old attempt status from being
read or published. Invalid-token knowledge blocks disclosure rather than being
masked by saved data. Data and storage changes must be additive when a new
persisted distinction is necessary; existing snapshots, coverage, forecasts,
attempts, revisions, and retained value history are not reset or reclassified
by a destructive migration.
### Contract: Swagger is produced at the DTO boundary
Swagger DTO annotations and endpoint response classes are the only place the
wire schema is authored. The backend regenerates `openapi.json` from the
implemented DTOs, then the frontend receives that published snapshot and runs
its existing `openapi-typescript` generation. Hand-editing frontend
`schema.d.ts`, adding handwritten parallel interfaces, or retaining a
compatibility mapper would hide drift and is excluded.
The resulting generated contract must express nullable `data`, all required
`meta` fields, nullable attempt timestamps, nullable problem fields, Accounts
top-level nullable `sync`, and absence of top-level `sync` on the other
resource responses. It must retain `sync=observe` only as a read-observation
compatibility parameter while it exists in the generated route contract; it
cannot cause source work and is not used to represent the new hourly state.
The generated contract reconciles field spelling with the target schema before
release, not through a second contract artifact.
### Frontend: one query cache and metadata-aware screen state
`useResourceQuery` remains the sole adapter that projects resource envelopes
into `data`, `meta`, and Accounts `sync`, preserving its existing query key,
authenticated transport, access-group invalidation, and Client Cached Snapshot
behavior. It must project the generated target types directly. Resource query
owners remain at their existing entity slices; page VMs, not presentation
views, choose initial loading, unavailable/error, empty, and loaded states.
`useScreenSync` remains the single 30-minute timer for open investment
screens. It calls ordinary local `refetch()` and its retry path continues to
use the existing observation read without changing cache identity. It derives
server attempt warning state from `meta.lastAttempt`, keeps transport failures
separate, and never starts personal synchronization. Existing Accounts
30-minute `staleTime` remains aligned with this cadence; Portfolio and other
screen resources keep their current short cache freshness where already
established while the open-screen timer supplies the approved reread rhythm.
All affected account screens render the five metadata meanings in text or an
element with an accessible name, not color alone. A complete empty resource is
normal empty content. `data: null` renders initial-loading or unavailable
states without invented values. A failed/partial hourly attempt is an
independent nonblocking warning beside confirmed data and its original time.
`Portfolio.isStale` remains snapshot-specific and is shown separately from
hourly-attempt and client HTTP-error warnings. A client snapshot stays visible
during and after a failed background HTTP reread; connection replacement or
deletion still invalidates personal queries.
## Cross-cutting Implementation
The canonical state split is:
| Concern | Owner | Observable representation |
| --- | --- | --- |
| Returned confirmed resource result | Saved resource owner | `data`, freshness, completeness, `lastSuccessfulAt`, availability |
| Resource-relevant hourly outcome | Durable walk and resource projection | `meta.lastAttempt` |
| Connection-wide hourly outcome | Durable walk | Accounts `sync` only |
| In-flight/failed browser reread | TanStack Query | Existing query lifecycle and local retry warning |
| Snapshot-specific Portfolio source age | Portfolio owner | `Portfolio.isStale` |
This split prevents a source failure from becoming a synchronous GET failure,
prevents the UI from treating its cache as server fallback, and preserves safe
access invalidation. Safe attempt problems contain only the approved section,
optional account ID, and safe error code: no token, upstream payload, cursor,
connection revision, or raw exception text is serialized or logged as status
metadata.
Events retains its existing account and `all` scopes and its saved forecast
window/filter semantics. Its metadata independently reports the latest events
attempt, so a saved forecast can remain available when a later refresh fails.
Operations retains its coverage semantics; only its explicitly approved missing
historical first-load acquisition may work outside the hourly walk.
## Data / Storage Changes
No new storage architecture is needed. Existing durable tables/records for the
current connection revision, hourly attempts, snapshots, operation coverage,
forecast source windows, and retained portfolio-value points are authoritative.
If implementation needs persisted terminal-state or problem-detail values not
already durable, it extends the existing attempt record/schema additively and
backfills conservatively: absent historical attempt information is projected as
`none`, never as `succeeded`.
Migrations must preserve all current user, connection, personal-resource,
coverage, forecast, and history rows. They must not reset SQLite, rewrite an
old partial result as complete, expose retained history across a new connection
boundary, or delete old data merely to fit the new vocabulary. Compatibility is
achieved by deploying the backend DTO/runtime and its generated OpenAPI as one
server contract, followed by the frontend snapshot/types and rendering change;
there is no claim that an old frontend understands the target envelope.
## Contract Implementation
The implementation contract is the generated result of the following mapping:
| Target concept | Backend source | HTTP location |
| --- | --- | --- |
| Last confirmed result or no complete result | Existing local resource reader | `data` |
| Result freshness and completeness | Resource mapper from confirmed result/coverage | `meta.freshness`, `meta.completeness` |
| Original confirmed-result time | Saved source timestamp | `meta.lastSuccessfulAt` |
| Relevant hourly result | Durable attempt plus resource section projection | `meta.lastAttempt` |
| Whether complete data can be used | Connection/access/read result | `meta.availability` |
| Complete current-connection walk | Durable connection-wide attempt | Accounts `sync` |
The target schema's `problemSection` shape is used for both scopes. For a
connection-wide Accounts walk, its list may include all affected sections; for
a resource-level attempt, it is restricted to failures relevant to that
resource. A running attempt has `finishedAt: null`; terminal attempts have a
completion time; `none` has null timestamps and no problems. These invariants
are validated at runtime tests and in the generated OpenAPI, rather than by
shipping the JSON Schema to application consumers.
## Developer Verification Strategy
Backend verification proves the contract at DTO, mapper, controller, and
SQLite boundaries: successful empty data; first load with `data: null`; stale
fallback after a failed walk; partial walk with preserved data; resource versus
Accounts attempt scoping; events forecast-window filtering; invalid-token and
connection-revision disclosure fences; no upstream personal call on ordinary
GET; and both first-load exceptions staying narrow. It regenerates OpenAPI and
runs the existing OpenAPI contract, type, lint, and relevant E2E suites.
Frontend verification updates the supplied snapshot, runs `openapi:check`, and
uses MSW/page-VM tests to prove generated metadata projection, no automatic
retry, HTTP-only retry, 30-minute open-screen reread, cache retention after a
background error, connection invalidation, and distinct server stale/hourly
attempt/client transport states. UI checks cover keyboard access, accessible
status text, loading/error/empty/loaded states, and narrow viewport wrapping.
Before release, verify the backend-generated OpenAPI and frontend snapshot are
the same published artifact, then run each repository's configured verification
commands. In this specs store, run both v2 schema validation and strict change
validation; structural success is supplemented by a semantic comparison against
the approved deltas and System Analysis.
## UI/UX Package
**Applicability and scope:** `new/changed`. This is an inline package for the
existing Accounts, account Portfolio, operations, and events screens. It adds
the approved status vocabulary and preserves 30-minute local rereads; it does
not introduce new composition, graphics, navigation, or controls.
**Package revision:** `1` (this Design). No visual files, external references,
**Screens, states and interactions:**
| Screen/state | Presentation | Interaction |
| --- | --- | --- |
| No complete result, initial attempt running | Existing structural loading state, labelled as loading | No manual sync action |
| No complete result, attempt failed/unavailable | Safe unavailable/error state with target status text | Retry rereads HTTP only when recovery allows |
| Complete empty result | Normal domain empty state and target status text | Existing navigation/actions only |
| Complete data with running/succeeded/partial/failed attempt | Content and original success time remain visible; attempt appears as separate accessible status | No source-work action |
| Client Cached Snapshot during/after HTTP failure | Existing content remains visible; transport warning remains distinct from server state | Existing retry rereads HTTP only |
**Responsive behavior:** reuse existing Mantine/theme components and page
layouts. Status text wraps without hiding account content or money values on
narrow screens; color supplements an accessible label. Existing skeletons
remain noninteractive and unfocusable. Existing cards, buttons, and icons are
used through their public package exports; no wrapper, SVG asset, token, or
component package revision is introduced.
**Reused source and differences:** `N/A` for approved visual reuse. This is a
new inline status package revision `1`, not an immutable reuse of a prior
approved visual package. Existing cards, layouts, hooks, and UI packages are
implementation inputs only; they are not the approved visual source. The
package adds semantic presentation of the five target metadata fields and
separate hourly-attempt/client-transport warnings. No local visual export is
required.
## Risks / Trade-offs
- Envelope evolution is a coordinated backend/frontend release risk. Generated
artifacts prevent hidden type drift but cannot make an older client interpret
new enum meanings.
- Mapping resource relevance from connection-wide attempts can over-report or
under-report a failure if it is not constrained by resource/section scope;
mapper tests must cover each affected resource.
- A durable `none` projection for old rows is intentionally less informative
than inventing a historic success; it preserves truthfulness.
- The 30-minute client timer can issue local reads while an hourly walk runs;
this is acceptable because reads do not contend for upstream work and retain
the last confirmed result.
- Single-process scheduling remains unsuitable for multi-process deployment;
adding leases, a worker, or distributed coordination requires a separate
decision.
- Documentation currently contains legacy request-driven/manual-refresh
descriptions. Until the identified durable references are updated, they must
not be used as implementation authority over the approved behavior.
## Migration / Rollout / Rollback
Rollout is backend-first at the contract source: apply only additive persistence
changes if required, implement and test runtime projection/fences, regenerate
and publish backend OpenAPI, then update the frontend snapshot/generated types
and renderers. Do not enable the new projection alongside a second personal
write owner. A frontend built from the old snapshot is not the compatibility
target; rollout uses the matched generated artifact pair.
| Field | Decision |
| --- | --- |
| Status | `required` |
| Trigger | Personal-data disclosure across connection revisions, false complete/fresh state, malformed generated contract, failure to preserve confirmed fallback, or a frontend unable to read the published contract. |
| Actions | Stop the affected rollout, preserve a SQLite copy, return to the last compatible backend/frontend pair, and fix forward. Do not restore screen-triggered/manual synchronization while the hourly owner remains enabled. |
| Data safety | Never reset production SQLite, delete additive attempt metadata, delete snapshots/coverage/forecasts/value history, or use a destructive downgrade. Old rows without target details safely project `none`. |
| Verification | On a representative copy, verify migration integrity, revision/access fences, ordinary local GET behavior, preserved fallback timestamps, generated OpenAPI/snapshot identity, and frontend rendering before traffic resumes. |
## Solution Assessment
| Option | Assessment |
| --- | --- |
| Evolve the existing shared DTO/mapper and generated OpenAPI | Chosen. It is the narrowest path: one resource-meta projection already owns all affected envelope metadata, Swagger already produces the contract, and frontend types already derive from the supplied snapshot. |
| Add a target adapter/parallel endpoint or versioned envelope | Rejected. It duplicates mapping and cache paths, delays removal of ambiguous states, and creates an unnecessary second compatibility surface. |
| Handwrite frontend target types or runtime validation | Rejected. Generated `paths`/schemas already own transport typing; manual copies would drift and a validator adds no trust boundary. |
| Add a scheduler/queue/cache service to express status | Rejected. Durable single-process attempts and SQLite records already express the required state. A new service cannot improve this contract without changing the explicitly retained deployment model. |
| Add a new design-system package/component family | Rejected. Existing Mantine and `@wealthledger/ui` states cover the textual, accessible status presentation; a new package would be abstraction without a new visual primitive. |
The chosen solution is intentionally a contract-and-projection reconciliation,
not a new synchronization system. It minimizes code and migration surface while
making the approved observable model expressible through the existing owners.
@@ -0,0 +1,50 @@
# Documentation Impact
## Summary
Durable AS-IS documentation requires updates. The current pages still describe
request-driven freshness, screen-started work, and legacy `sync` metadata;
after delivery they must describe the hourly owner, local resource reads, and
the independent resource-result and hourly-attempt state.
## Documentation Changes
| Action | Target | Category | Resulting AS-IS | Source | Verification |
|---|---|---|---|---|---|
| update | `architecture/cross-cutting/data-freshness.md` | cross-cutting | Ordinary personal-resource GETs and HTTP retries read saved local results and never start personal T-Bank work. The hourly walk is the normal update owner; only an unconfirmed historical ledger interval and an unopened candle range may acquire their own missing result. Document the independent `data` freshness, completeness, `lastSuccessfulAt`, availability, and resource-relevant last hourly attempt; retained complete data remains available after a later failed or partial attempt, while no complete result is `data: null`. Keep Client Cached Snapshot, `Portfolio.isStale`, and failed browser rereads distinct from server result and hourly-attempt state. | `tbank-data-sync` — `Часовой обход владеет обновлением личных данных T-Bank`; `tbank-data-sync` — `Результат личного обновления остаётся наблюдаемым`; System Analysis §§ Interaction Flows, Data and State Semantics, Error and Recovery Semantics, Observability Semantics; Design § Cross-cutting Implementation. | Compare the page with delivered resource responses and UI: normal GET/retry has no upstream personal call; documented state meanings match generated OpenAPI/runtime responses; stale fallback, no-result state, client read failure, and hourly failure remain separately explained. |
| update | `domains/portfolio/flows/portfolio-loading.md` | domain | Portfolio loading reads the saved current-connection snapshot through a read-only envelope. The hourly walk refreshes Portfolio independently of the screen; the open screen's 30-minute reread and retry are local HTTP reads only. The page explains `data: null` before a complete snapshot, preserved snapshot/time after a failed or partial walk, separate resource metadata and `Portfolio.isStale`, and current-connection/revision disclosure fences without a manual refresh or status-observer flow. | `portfolio-view` — `Получение Portfolio выбранного Brokerage Account`; `portfolio-view` — `Свежесть и fallback полного Portfolio Snapshot`; `frontend-data-resilience` — `Client Cached Snapshot остаётся доступным при обновлении Accounts и Portfolio`; System Analysis §§ Contracts, Data and State Semantics, Error and Recovery Semantics; Design § Frontend: one query cache and metadata-aware screen state. | Read the delivered Portfolio route and screen against the page: GET and retry do not call T-Bank or expose a manual sync action; snapshot fallback retains its original time; no-data, empty, stale, failed hourly attempt, and failed HTTP reread are not conflated. |
| update | `domains/income/upcoming-events.md` | domain | Upcoming events reads the saved forecast for account or `all` scope without starting personal sync. It documents strict inclusive UTC request dates, defaults, filtering to the intersection with the forecast's original inclusive window, and retention of that forecast's `calculatedAt` and source window. A later failed hourly events update leaves the last complete forecast readable and reports its failure independently; absent complete forecast is `data: null`, while an empty intersection is a successful empty result. | `brokerage-account-upcoming-events` — `User can retrieve upcoming events for an account`; `brokerage-account-upcoming-events` — `Upcoming events use complete source results`; System Analysis § Contracts; System Analysis §§ Data and State Semantics, Error and Recovery Semantics; QA Analysis § Coverage Matrix: Events query and saved forecast. | Compare account and `all` responses/screens with the page: invalid range returns `INVALID_EVENTS_PERIOD` without source work; window intersection preserves original forecast metadata; empty intersection and failed-refresh fallback are correctly distinguished. |
| update | `architecture/http-api.md` | architecture | The investment API section describes the generated backend OpenAPI as the executable contract for `{ data, meta }` resource responses: nullable `data`; required independent resource-result meanings; resource-scoped hourly `lastAttempt`; and Accounts-only connection-wide `sync`. It states that legacy `sync=observe` may remain a read-observation compatibility parameter but cannot start source work, removes the request-started screen-sync command/status model as current AS-IS, and retains established route identities and Problem Details behavior. | System Analysis §§ Contracts, Compatibility / Migration; `tbank-data-sync` — `Результат личного обновления остаётся наблюдаемым`; `hourly-investment-sync` — `Последний результат обхода доступен только для наблюдения`; Design § Contract: Swagger is produced at the DTO boundary; QA Analysis §§ Cross-system Verification, Regression Impact. | Compare prose and examples with the backend-generated OpenAPI and runtime responses: affected envelopes expose target nullability/enums and required metadata, only Accounts has top-level `sync`, and ordinary reads cannot trigger personal work. |
| update | `adr/017-local-tbank-reads-and-background-sync.md` | ADR | Preserve ADR 017's historical decision and rationale, but mark its request-driven, manual-refresh, and no-periodic-walk decisions superseded by ADR 019. Retain its still-valid local-storage, completeness, and access-boundary context, with a link to current freshness documentation. | System Analysis § Durable Documentation Candidates; Proposal § Why; Design § Risks / Trade-offs. | ADR 017 explicitly identifies superseded decisions and links to ADR 019; it no longer presents a manual/request-driven sync model as current architecture. |
| update | `adr/019-hourly-reads-and-online-instrument-reference.md` | ADR | Clarify the accepted hourly architecture: the last walk is scoped to the current connection; ordinary GET/retry does not acquire personal data; only the first unconfirmed ledger interval and first unopened candle range are narrow acquisition exceptions. Clarify that resource-result state and hourly-attempt state remain independent and that observation is not a command. | System Analysis §§ Interaction Flows, Contracts, Observability Semantics; `tbank-data-sync` — `Часовой обход владеет обновлением личных данных T-Bank`; `hourly-investment-sync` — `Последний результат обхода доступен только для наблюдения`; Design § Cross-cutting Implementation. | ADR 019 describes only the delivered hourly model and its bounded exceptions; it agrees with the updated freshness page and does not introduce a second HTTP contract source. |
## Diagram Promotion
None. `system-analysis/sequences/hourly-investment-walk.mmd` and
`system-analysis/sequences/read-only-resource-get.mmd` are approval-time
interaction evidence. Their durable operational meaning is covered by the
updated freshness, portfolio-loading, events, and HTTP API pages; promoting
them would duplicate those explanations without adding a durable navigation or
architecture view.
## ADR Decision
N/A. No new architectural decision is required: ADR 019 already accepts the
durable hourly-read architecture and is clarified above. ADR 017 is an
historical decision whose conflicting request-driven/manual-refresh portions
must be explicitly superseded, not rewritten into a new decision.
## Glossary / Navigation Impact
N/A. The change refines existing concepts (freshness, completeness, Client
Cached Snapshot, Portfolio snapshot, and T-Bank connection) rather than adding
a durable term needing a glossary definition or a new navigation entry.
## No-Change Rationale
No new documentation page, diagram, or machine-readable HTTP artifact is
promoted. Backend-generated OpenAPI remains the executable HTTP contract; the
change-local schema and sequences remain archived planning evidence. No docs
entry is planned for private retained portfolio-value history because this
change does not introduce a public history read, and its access boundary is
already covered by the connection/freshness documentation.
@@ -0,0 +1,94 @@
# Proposal
## Why
Два завершённых legacy changes описывают несовместимые модели инвестиционной
синхронизации. `persist-tbank-data` закрепляет запуск обновления запросом и
ручное обновление, а `hourly-portfolio-sync-history` и фактическая система
используют часовой обход и локальные read-only GET. Пока canonical specs и
AS-IS документация содержат обе трактовки, ни один legacy change нельзя
безопасно синхронизировать или архивировать.
## What Changes
- Зафиксировать единственную observable model: часовой обход обновляет личные
инвестиционные данные, а обычные GET читают сохранённый результат и не
запускают личную синхронизацию.
- Сохранить узкие исключения первой загрузки отсутствующего исторического
интервала операций и впервые открытого диапазона свечей.
- Уточнить видимые состояния свежести, полноты, последней попытки обхода и
доступности личных данных после смены подключения.
- Согласовать canonical behavior specs и durable AS-IS documentation с этой
моделью, не создавая ещё один current OpenAPI source of truth.
## Scope
### In Scope
- Investment accounts, Portfolio, операции изменяемого периода, forecasts/events,
personal connection boundary и связанные frontend recovery states.
- Capabilities `brokerage-accounts`, `portfolio-view`,
`brokerage-account-ledger`, `brokerage-account-upcoming-events`,
`frontend-data-resilience`, `tbank-connection` и `tbank-data-sync`.
- Описание и compatibility guidance для `{ data, meta }` resource responses и
backend-generated OpenAPI как executable HTTP contract.
- Обновление durable documentation и ADR supersession, когда это необходимо.
### Out of Scope / Non-Goals
- Новый способ запуска синхронизации, публичное чтение истории стоимости,
валютная конвертация и многопроцессный scheduler.
- Переписывание historical legacy changes или автоматическое объявление их
approval/Validation успешными.
- Новая product feature, не требуемая для согласования уже поставленного
поведения.
## Capabilities
### New Capabilities
- `hourly-investment-sync`: нормативно зафиксировать текущий наблюдаемый
часовой обход и его статус, если capability ещё отсутствует в canonical specs.
- `portfolio-value-history`: зафиксировать существующую историю оценок и
границу её видимости, если capability ещё отсутствует в canonical specs.
- `online-instrument-information`: зафиксировать online-only справочные
сведения, если capability ещё отсутствует в canonical specs.
### Modified Capabilities
- `brokerage-accounts`: локальное чтение и результат последнего обхода.
- `portfolio-view`: read-only GET, часовая свежесть и отсутствие ручной
личной синхронизации.
- `brokerage-account-ledger`: первая загрузка отсутствующего интервала и
локальное чтение полного прошлого периода.
- `brokerage-account-upcoming-events`: последний complete forecast и
независимое состояние ошибки обновления.
- `frontend-data-resilience`: distinction between retrying an HTTP read and
starting source work.
- `tbank-connection`: границы текущих данных и скрытая история оценок после
смены либо удаления подключения.
## Impact
Пользователь получает предсказуемое обновление инвестиций без скрытого запуска
брокерских запросов при чтении экранов. Backend и frontend уже содержат большую
часть целевого поведения; change формализует его, устраняет расхождения
requirements/docs и создаёт основу для корректного завершения legacy history.
## Success Criteria
- Canonical specs дают один непротиворечивый ответ о владельце обновления,
исключениях первой загрузки и read-only GET.
- Backend-generated OpenAPI/runtime interface семантически соответствует
approved target contract при затронутых HTTP resource responses.
- AS-IS docs и ADR guidance не обещают request-driven/manual sync там, где
фактическая система использует hourly model.
- Legacy `persist-tbank-data` и `hourly-portfolio-sync-history` получают
безопасный дальнейший путь к archival finalization без конкурирующей sync.
## Assumptions / Dependencies
- Часовой scheduler, local resource reads и frontend 30-minute rereads остаются
целевой моделью, подтверждённой Change Owner.
- System Analysis определит точные contracts, semantics forecasts/events и
допустимые first-load exceptions до implementation tasks.
@@ -0,0 +1,92 @@
# QA Analysis
## Verification Scope
### In Scope
- Authorised, black-box verification of the affected personal-resource HTTP reads: Accounts, account and `all` Portfolio, operations, and account and `all` events.
- The distinction between a local ordinary GET/retry and the permitted acquisition paths: the hourly walk, a first unconfirmed historical operations interval, and a first unopened candle range.
- Observable hourly-walk scheduling and outcome semantics, including connection-scoped Accounts `sync` and resource `meta.lastAttempt`.
- Resource-envelope semantics for confirmed data, empty confirmed data, first load, stale fallback, partial/failed attempts, and connection unavailability.
- Current-connection access boundaries after T-Bank connection creation, replacement, and deletion, including retained value-history non-disclosure.
- Events scope, date-window, saved-forecast, and failed-refresh behavior.
- Compatibility of the backend-generated OpenAPI/runtime interface with the approved target envelope and preservation of established route identities and Problem Details behavior.
- User-visible resilience: retry performs an HTTP reread only; cached successful screen data, server fallback, and failed background HTTP reads remain distinguishable.
### Out of Scope
- Scheduler implementation, persistence layout, internal job reservations, upstream-client mechanics, and frontend test structure.
- New public reading of portfolio value-history, manual personal synchronization, currency conversion, multi-process scheduling, and changes to unrelated investment or banking resources.
- Verifying T-Bank or public-reference-provider correctness beyond controlled responses needed to observe WealthLedger's published behavior.
## Coverage Matrix
| Approved behavior | Black-box verification | Expected observable result |
| --- | --- | --- |
| Ordinary personal GETs are read-only | Read Accounts, Portfolio, operations, and events repeatedly, including user retry and screen reopen, while upstream personal calls are observable. | Each read returns only locally held state and causes no personal upstream work or new hourly attempt. |
| First-load exceptions stay narrow | Open an unconfirmed historical operations interval and, separately, an unopened candle range; then reread each after confirmation. | Only the requested missing interval/range may acquire data. It exposes incomplete local state until confirmed; no Accounts, Portfolio, events, other interval, or range refresh occurs. Confirmed reads stay local. |
| Hourly ownership and cadence | Connect successfully; observe initial work, an elapsed eligible term, a restart after downtime, and a term arriving during an active walk. | Connection starts one initial attempt. Subsequent attempts begin no earlier than 60 minutes from the preceding start; restart performs at most one current attempt; overlapping terms neither run nor queue. |
| Hourly result is observational | Observe no attempt, running, succeeded, partial, and failed results through affected resources and Accounts. | `meta.lastAttempt` distinguishes all five states with safe scoped problems; Accounts `sync` is `null` before an attempt and otherwise the connection-wide result. Reading it starts nothing. |
| Independent section persistence | Make one account/section succeed and another fail during a walk. | Successful current data persists; the earlier complete result for the failed section remains visible; the attempt is `partial` with only safe section/account/error-code problem data. |
| Envelope state meanings | Exercise a fresh complete result, stale complete fallback, no complete result while work runs, no complete result after failure, and confirmed empty Accounts/Portfolio/events. | Every success has `{ data, meta }`; `meta` separately reports freshness, completeness, `lastSuccessfulAt`, `lastAttempt`, and availability. `[]` is available and complete, while no complete result is `data: null` and never presented as complete or fresh. |
| Failed refresh preserves known data | Cause a temporary hourly failure after a confirmed Accounts, Portfolio, operations, or events result. | `200` retains the original complete data and success time; freshness reflects that result; the failed attempt is separately visible. A completed background failure is not a synchronous `503`. |
| Invalid connection boundary | Request personal resources with no connection, then after a known-invalid token result. | No connection returns established `409 TBANK_NOT_CONNECTED`; invalid token returns established `400 INVALID_TBANK_TOKEN`, retains the connection record, and discloses no personal fallback data. |
| Connection replacement and deletion | Replace a valid connection while an old walk is pending or running, then delete the replacement twice. | PUT validates before changing the prior connection, establishes a new boundary and initial walk on success; old data/status cannot reappear. DELETE returns `204` idempotently, stops use of its token, and hides current personal data and retained history until the same account is confirmed again. |
| Events query and saved forecast | Read account and `all` events for default, explicit, intersecting, non-intersecting, and empty windows; submit `from > to`; fail a later refresh. | Dates are strict inclusive UTC dates; defaults are current UTC date through +30 calendar days; invalid range returns `400 INVALID_EVENTS_PERIOD` without source work. Results are the intersection with the forecast's original window and retain its `calculatedAt`/window. Empty intersections are successful empty data. `all` is complete only with every included account contribution. Failed refresh retains the previous forecast and reports the attempt failure separately. |
| Frontend recovery and accessibility | Observe a successful resource screen during HTTP reread failure, a server stale/fallback result, a failed hourly attempt, and each no-data first-load state. | Successful cached screen data remains visible during/after background HTTP failure. Retry only rereads HTTP. Client-read failure, server stale data, and failed hourly work are distinct, and the five `meta` meanings are exposed in text or accessible names rather than color alone. |
## Cross-system Verification
1. Use a controlled upstream boundary to prove that normal GETs and retry actions do not cause personal upstream requests, while an eligible hourly walk and each permitted first-load exception have only their approved source effect.
2. Verify the API response and the rendered Accounts, Portfolio, operations, and events states together for the same server conditions. In particular, a saved complete result plus a failed attempt must remain readable and visibly distinguishable from a client HTTP-read failure.
3. Verify that successful PUT replacement and DELETE invalidate visible personal-resource state. Late responses or late old-connection work must not restore accounts, Portfolio, events, `sync`, or retained history across the new boundary.
4. Compare the generated backend OpenAPI published for delivery with runtime responses and the consumer-generated types. Confirm the target `{ data, meta }` resources, Accounts-only `sync`, all required meta meanings, nullability, and enum values are represented without relying on legacy `sync=observe` to start work.
## Risk-based Priorities
| Priority | Risk | Verification focus |
| --- | --- | --- |
| P0 | A screen read initiates brokerage work or leaks data across a connection boundary. | Upstream-call absence on GET/retry; replacement/deletion fence; invalid-token and no-connection responses. |
| P0 | A failed refresh hides or mislabels financial data. | Complete fallback retention; `data: null` when no complete result exists; empty complete results; independent meta and attempt states. |
| P0 | Events return an incorrect period, silently omit an account in `all`, or relabel an old forecast. | Strict UTC window validation/defaults/intersection; `all` completeness; preserved `calculatedAt` and original forecast window after failure. |
| P1 | Scheduler work overlaps, catches up, or reports unsafe/incorrect status. | First walk, 60-minute eligibility from start, downtime, overlap drop, partial result, safe problem scope. |
| P1 | Contract evolution breaks the generated frontend consumer or existing API callers. | Generated OpenAPI/runtime semantic comparison, route and established Problem Details compatibility, legacy observation endpoint non-command behavior. |
| P2 | Presentation obscures recovery and freshness meaning. | Accessible status text, separate client/server/hourly indicators, retained screen snapshot, empty states. |
## Test Data and Environment Needs
- An authenticated user with controlled valid, invalid, and unavailable T-Bank outcomes; at least two supported accounts plus an unsupported account to establish membership and partial-result cases.
- Persisted fixtures for fresh and stale complete Accounts, Portfolio, operations, and forecasts; no-result first-load state; confirmed empty Accounts; empty Portfolio; empty event intersection; and a forecast whose source window differs from the request window.
- Controllable clock and observable upstream boundary to establish hourly eligibility, running overlap, restart behavior, and proof that normal reads have no personal upstream activity.
- Two valid connection identities with disjoint accounts and one shared account, plus retained value-history data, to verify replacement/deletion concealment and re-confirmed access.
- Published backend OpenAPI for the delivered build and the corresponding generated frontend contract, both from the same build candidate.
## Manual / Exploratory Scope
- Navigate Accounts, account Portfolio, operations, and upcoming events through first load, available empty, stale fallback, failed hourly update, invalid token, and no-connection states. For operations, include an incomplete first-loaded interval and its later confirmed result. Confirm status meaning is understandable without color and no manual personal-sync control is exposed.
- Keep an investment screen open across its periodic reread and induce a failed HTTP read. Confirm the prior screen snapshot remains visible and that retry changes only the HTTP-read state.
- Replace or delete a connection while navigating between account and event views. Confirm neither navigation, browser history, nor a late response exposes data from the prior connection.
- Explore boundary dates around UTC midnight, the inclusive forecast window endpoints, leap-day dates, and empty intersections for both account and `all` events scopes.
## Automation Candidates
- Repeatable system checks for the resource-envelope state matrix, hourly cadence/overlap, local-read upstream-call absence, connection revision fence, and event window/scope matrix.
- Consumer-contract compatibility checks that compare the generated backend OpenAPI, runtime resource responses, and regenerated frontend transport types for affected public operations.
- Browser-level checks for retry-only reads, retained successful UI data, invalidation after connection change, and accessible status names.
## Observability Verification
- Confirm resource responses expose no attempt as `none`, running work without `finishedAt`, and terminal succeeded/partial/failed work with only safe section, account, and error-code problem details.
- Confirm `lastSuccessfulAt`, freshness, completeness, and availability continue to describe the returned data rather than the later attempt; `sync` is `null` before any Accounts walk and remains connection-scoped thereafter.
- Confirm no response, OpenAPI example, UI status, or observable problem detail discloses a token, upstream payload, cursor, internal connection revision, or old-connection status.
## Regression Impact
- Existing clients of the affected GET routes must retain route identities, account and `all` scopes, date validation/defaults, response ordering where specified, and established Problem Details codes unless the approved generated contract explicitly changes them.
- Existing `sync=observe` and `/api/v1/tbank-sync/{id}` observation behavior may remain compatible but must not regain request-started personal synchronization semantics.
- Accounts, Portfolio, operations, and events callers must handle the approved envelope evolution, including nullable `data`, independent attempt state, empty complete data, and Accounts-only `sync`; generated frontend types must match the published backend OpenAPI.
- Unaffected public history-reading capability is not introduced. Retained value history remains unavailable outside a confirmed current connection boundary.
## Gaps / Blockers
None identified in the approved System Analysis and deltas. Field names used by the generated OpenAPI remain a delivery compatibility check: they must faithfully represent the approved target semantics before release.
@@ -0,0 +1,16 @@
## ADDED Requirements
### Requirement: Отсутствующий исторический интервал загружается только впервые
Система MUST позволять авторизованному User при первом открытии ещё не загруженного периода операций доступного Brokerage Account начать получение только недостающего интервала без ожидания T-Bank в локальном ответе. Это является исключением первой загрузки и MUST NOT запускать обновление других личных данных. Полностью загруженный прошлый интервал MUST NOT повторно сверяться из-за чтения страницы или часового обхода. Изменяемый хвост операций MUST обновляться часовым обходом. Незавершённый интервал и ошибка загрузки MUST NOT обозначаться полными, а последний подтверждённый результат MUST оставаться доступным.
#### Scenario: Первый просмотр отсутствующего прошлого периода
- **WHEN** User открывает доступный исторический период без подтверждённой полноты
- **THEN** система возвращает локальный результат с явным состоянием неполноты и начинает загрузку недостающих данных
#### Scenario: Повторное открытие полного периода
- **WHEN** User повторно открывает уже полностью загруженный прошлый период
- **THEN** система читает сохранённые операции без повторной сверки с T-Bank
#### Scenario: Ошибка загрузки
- **WHEN** загрузка части отсутствующего периода завершается ошибкой
- **THEN** ранее подтверждённые операции сохраняются, а неполная часть не помечается полной
@@ -0,0 +1,120 @@
## MODIFIED Requirements
### Requirement: User can retrieve upcoming events for an account
Система MUST предоставлять аутентифицированному User read-only `GET /api/v1/brokerage-account-upcoming-events` для выбранного открытого поддерживаемого Brokerage Account либо существующего режима «Все счета» (`all`) в границах текущего T-Bank connection User. Параметры `from` и `to` MUST сохранять существующий контракт: необязательные строгие календарные даты `YYYY-MM-DD` с включительными границами в UTC. Без `from` начало MUST соответствовать текущей календарной дате UTC; без `to` конец MUST соответствовать дате `from` плюс 30 календарных дней. При `from` позже `to` система MUST возвращать существующую ошибку `INVALID_EVENTS_PERIOD`, не подменять период, не запускать загрузку другого периода и не возвращать его прогноз. Обычный GET MUST NOT запускать личную синхронизацию.
Ответ MUST иметь `{ data, meta }`, где `meta` соответствует наблюдаемому состоянию resource по `tbank-data-sync`. Доступный `data` MUST содержать только события coupon, dividend, maturity и offer, отсортированные по дате по возрастанию, `calculatedAt`, а в режиме `all` -- принадлежность каждого события счёту. Прогноз MUST быть рассчитан по количеству бумаг полного Portfolio Snapshot на `calculatedAt`; сохранённый прогноз MUST сохранять свой исходный `calculatedAt` и рассчитанное от него включительное окно, даже когда запрошенное `from`/`to` отличается. При чтении сохранённого прогноза система MUST возвращать только события, чьи даты попадают во включительное пересечение исходного окна прогноза и запрошенного UTC-окна; пустое пересечение либо отсутствие событий в пересечении MUST возвращать успешный пустой список с теми же `calculatedAt`, `meta` и исходным окном. В режиме `all` доступ и полнота обязательных источников MUST проверяться для каждого входящего счёта; отсутствие вклада счёта MUST NOT подменяться полным результатом. Список будущих событий является прогнозом по наблюдаемому состоянию счёта и количеству бумаг на `calculatedAt` и MUST NOT означать долговременное право владения Brokerage Account независимо от T-Bank connection.
#### Scenario: Получение событий доступного счёта
- **WHEN** аутентифицированный User запрашивает события доступного счёта с последним полным прогнозом
- **THEN** система возвращает из сохранённого прогноза события в запрошенном включительном UTC-окне, его исходные `calculatedAt` и рассчитанное окно без обращения к T-Bank
#### Scenario: Запрошенное окно не пересекается с сохранённым прогнозом
- **WHEN** запрошенный включительный UTC-окно не пересекается с исходным окном последнего полного прогноза
- **THEN** система возвращает успешный пустой `data` с исходными `calculatedAt`, окном и `meta`
#### Scenario: Отсутствие событий в периоде
- **WHEN** пересечение запрошенного и исходного окна прогноза не содержит событий
- **THEN** система возвращает успешный пустой список, момент расчёта, исходное окно и `meta`
#### Scenario: Некорректный период
- **WHEN** User передаёт `from` позже `to`
- **THEN** система возвращает существующую ошибку `INVALID_EVENTS_PERIOD` без прогноза и без запуска личной синхронизации
#### Scenario: Прогноз всех счетов
- **WHEN** User запрашивает режим `all` и для каждого входящего счёта доступен полный прогноз
- **THEN** система возвращает события всех счетов в запрошенном окне с принадлежностью каждого события счёту
#### Scenario: Отклонение недоступного счёта
- **WHEN** User запрашивает события счёта, недоступного через его текущий T-Bank connection
- **THEN** система возвращает существующий ответ о недоступности счёта и не раскрывает события
### Requirement: Forecasts preserve event meaning and uncertainty
Система MUST передавать для каждого события его календарную дату, тип, доступные идентификаторы и название Instrument, а также прогнозную сумму до налогов как Money, если она известна. Прогнозная сумма MUST рассчитываться по количеству бумаг полного Portfolio Snapshot на `calculatedAt`, использованного для сохранённого прогноза, а не по количеству бумаг на момент HTTP-запроса. Сохранённый прогноз MUST сохранять свой `calculatedAt` и эту исходную зависимость, в том числе после неуспешного обновления, и MUST NOT приписываться более новому Portfolio Snapshot. Если денежные условия события неизвестны или неприменимы, прогнозная сумма MUST быть `null`, а не нулём и не парой разрозненных полей. Дивиденд MUST обозначать переданную дату как дату реестра, а offer MUST передаваться без вымышленной суммы.
Instrument в Future Cash Event MUST иметь тот же доменный смысл, что и в Portfolio: это финансовый инструмент, к которому относится событие, даже если событие не является Portfolio Position.
#### Scenario: Прогноз купона по текущей позиции
- **WHEN** поддерживаемая облигация в Portfolio Snapshot на `calculatedAt` имеет известный купон в пределах исходного окна прогноза
- **THEN** система возвращает сохранённое событие coupon с прогнозной суммой до налогов как Money в исходной валюте, рассчитанной по количеству облигаций этого Snapshot
#### Scenario: Прогноз дивиденда
- **WHEN** поддерживаемая акция в Portfolio Snapshot на `calculatedAt` имеет объявленный дивиденд с датой фиксации реестра в пределах исходного окна прогноза
- **THEN** система возвращает сохранённое событие dividend с датой, обозначенной как дата реестра (`record_date`), и прогнозной суммой до налогов как Money, равной `dividend_net` на одну бумагу, умноженной на количество акций этого Snapshot
#### Scenario: Отменённый дивиденд не попадает в прогноз
- **WHEN** запись дивиденда T-Bank имеет тип выплаты `Cancelled`
- **THEN** система не добавляет такое событие в сохранённый прогноз
#### Scenario: Отображение оферты без суммы
- **WHEN** для поддерживаемой облигации известна оферта в пределах исходного окна прогноза, но её денежные условия недоступны
- **THEN** система возвращает событие offer с прогнозной суммой `null`
#### Scenario: Неподдерживаемое частичное погашение
- **WHEN** источник не предоставляет расписание будущих частичных погашений или амортизации облигации
- **THEN** система НЕ создаёт синтетическое событие частичного погашения
### Requirement: Upcoming events use complete source results
Система MUST публиковать прогноз только после полного получения необходимых данных брокера и MOEX для полного Portfolio Snapshot, по количеству бумаг которого он рассчитан. Неуспешное обновление MUST NOT заменять последний полный прогноз, его `calculatedAt` или окно событий частичным списком. При неуспешном обновлении и существующем последнем полном прогнозе система MUST возвращать отфильтрованную для запрошенного окна часть этого прогноза с его исходными `calculatedAt`, окном и `meta`, отдельно обозначающим неуспех последней попытки, даже если результат фильтрации пуст. При отсутствии последнего полного прогноза система MUST возвращать `data: null` и в `meta` обозначать выполнение первой загрузки либо её неуспех без частичного списка. Система MUST пропускать инструменты, для которых источник не предоставляет поддерживаемые события, не создавая строки «нет данных».
#### Scenario: Недоступность источника
- **WHEN** необходимый источник данных брокера или MOEX временно недоступен во время часового обновления
- **THEN** система не публикует частичный список как полный прогноз
#### Scenario: Ошибка обновления при сохранённом прогнозе
- **WHEN** часовой обход не завершает обновление необходимых источников, а последний полный прогноз существует
- **THEN** система возвращает его события только в запрошенном окне, исходные `calculatedAt` и окно, а `meta` отдельно обозначает ошибку обновления
#### Scenario: Ошибка обновления при сохранённом прогнозе вне запрошенного окна
- **WHEN** часовой обход не завершает обновление, последний полный прогноз существует, но его окно не пересекается с запрошенным
- **THEN** система возвращает успешный пустой `data` с исходными `calculatedAt`, окном и `meta`, обозначающим ошибку обновления
#### Scenario: Инструмент без поддерживаемых данных
- **WHEN** Portfolio содержит инструмент, для которого источник не предоставляет поддерживаемые будущие события
- **THEN** система не добавляет для него синтетическую строку в результат
#### Scenario: MOEX отвечает без запрошенного блока
- **WHEN** MOEX отвечает успешно, но не содержит запрошенного блока данных
- **THEN** система трактует ответ как отсутствие данных по инструменту и не завершает обновление ошибкой
### Requirement: Account page presents an upcoming events list
Страница Brokerage Account MUST предоставлять read-only действие перехода на отдельную страницу «Будущие события». Эта страница MUST показывать события сохранённого полного прогноза компактным списком без фильтров, пагинации, суммарного итога или действия ручной синхронизации, с датой, типом, инструментом и прогнозной суммой в исходной валюте, если она известна. Страница MUST показывать `calculatedAt`, исходное окно прогноза и наблюдаемые freshness, полноту, время последнего успешного прогноза, последнюю часовую попытку и доступность из `meta` понятным текстом или иконкой с доступным именем, а не только цветом. Она MUST объяснять, что прогноз рассчитан по количеству бумаг на `calculatedAt`, а показанные события дополнительно ограничены запрошенным включительным UTC-окном; прогноз указан до налогов и может быть неполным из-за доступности данных источника. Открытие страницы и повтор HTTP-чтения MUST NOT запускать личную синхронизацию.
#### Scenario: Просмотр ближайших событий
- **WHEN** User открывает «Будущие события» из обзора Brokerage Account
- **THEN** он видит отсортированный read-only список событий сохранённого 30-календарного окна и `calculatedAt`, где offer обозначен без суммы
#### Scenario: Пустое состояние страницы
- **WHEN** запрос будущих событий успешно возвращает пустой список
- **THEN** страница сообщает, что в сохранённом окне прогноза событий не найдено, показывает `calculatedAt` и объясняет ограничения покрытия источника
#### Scenario: Первая загрузка либо ошибка без сохранённого прогноза
- **WHEN** запрос будущих событий возвращает `data: null`, а `meta` обозначает выполнение первой загрузки либо её неуспех
- **THEN** страница показывает доступное состояние загрузки либо ошибки без частичного списка и предоставляет при ошибке действие повторить только HTTP-чтение
#### Scenario: Ошибка страницы
- **WHEN** запрос будущих событий завершается ошибкой
- **THEN** страница не показывает частичный список и предоставляет действие повторить только HTTP-чтение
#### Scenario: Ошибка обновления при сохранённом прогнозе на странице
- **WHEN** запрос будущих событий возвращает последний полный прогноз вместе с ошибкой его обновления
- **THEN** страница показывает этот прогноз с его исходными `calculatedAt` и окном, отдельно сообщает об ошибке обновления и предоставляет действие повторить только HTTP-чтение
### Requirement: Account overview includes upcoming events
Обзор Brokerage Account MUST показывать не более пяти ближайших Future Cash Event сохранённого полного прогноза в применённом запрошенном окне, его `calculatedAt`, исходное окно, наблюдаемую freshness, полноту, последнюю часовую попытку и доступность из `meta`, а также действие перехода на read-only полную страницу «Будущие события». Статус MUST быть понятен текстом или иконкой с доступным именем, а не только цветом. Неуспешное обновление при сохранённом прогнозе MUST оставаться локальным состоянием блока, сохранять показанные события и их исходное окно и MUST NOT заменять успешно отображённый Portfolio. Если полного прогноза нет, неуспешное обновление MUST оставаться локальным состоянием блока без частичного списка. Обзор MUST NOT предоставлять действие ручной синхронизации; его повтор MUST выполнять только HTTP-чтение.
#### Scenario: Отображение ближайших событий
- **WHEN** обзор счёта успешно загружен и сохранённый прогноз содержит будущие события
- **THEN** обзор показывает первые пять событий в порядке даты, `calculatedAt` и действие перехода к полной странице
#### Scenario: Ближайшие события недоступны без сохранённого прогноза
- **WHEN** обзор счёта успешно загружен, а события возвращают `data: null` и `meta` обозначает выполнение первой загрузки либо её неуспех
- **THEN** Portfolio остаётся доступным, а блок будущих событий отображает собственное доступное состояние загрузки либо ошибки без частичного списка
#### Scenario: Ближайшие события недоступны
- **WHEN** обзор счёта успешно загружен, но запрос будущих событий завершается ошибкой
- **THEN** Portfolio остаётся доступным, а блок будущих событий отображает собственную ошибку
#### Scenario: Неуспешное обновление не скрывает сохранённый прогноз
- **WHEN** обзор счёта получает сохранённый полный прогноз вместе с ошибкой его обновления
- **THEN** Portfolio и события сохранённого окна остаются доступными, а блок отдельно обозначает ошибку и предлагает повторить только HTTP-чтение
@@ -0,0 +1,135 @@
## MODIFIED Requirements
### Requirement: Список активных брокерских счетов
Система SHALL предоставлять авторизованному User `GET /api/v1/brokerage-accounts`, возвращающий `{ data, meta }`, где `data` является сохранённым подтверждённым массивом открытых поддерживаемых инвестиционных счетов T-Bank текущего подключения User в ранее подтверждённом upstream-порядке, а `meta` соответствует наблюдаемому состоянию resource по `tbank-data-sync`. Обычный GET MUST NOT обращаться к T-Bank или запускать личную синхронизацию. Каждый элемент MUST содержать стабильный `id`, непустое `name`, нормализованный `type` (`BROKERAGE`, `INDIVIDUAL_INVESTMENT`, `INVEST_BOX` или `MONEY_MARKET_FUND`) и nullable `openedOn` в формате `YYYY-MM-DD`. Система MUST включать только счета T-Bank типов «брокерский счёт», «ИИС», «Инвесткопилка» и «фонд денежного рынка»; банковские и неизвестные типы MUST NOT возвращаться. Система MUST удалить внешние пробелы из имени и при пустом результате использовать непустое название соответствующего типа.
Каждый элемент MUST также содержать nullable `portfolioSummary`. Доступная сводка MUST содержать `totalsByCurrency`: точные Money-итоги последнего Portfolio по исходным ISO-валютам, включающие стоимость позиций и Cash Balance в той же валюте. Она MUST NOT содержать FX-конвертированный или межвалютный итог и MUST NOT выдавать Cash Balance за самостоятельный второй итог. Если последний Portfolio конкретного доступного счёта отсутствует либо временно недоступен, `portfolioSummary` MUST быть `null`, не исключая этот счёт из списка и не делая список неуспешным. Пустой доступный Portfolio MUST передаваться доступной сводкой с пустым `totalsByCurrency`, а не `null`.
Список Brokerage Account и его сводки являются результатом наблюдения текущего брокерского подключения и MUST NOT создавать ожидание, что WealthLedger хранит долговременное право владения счётом или Portfolio независимо от T-Bank connection.
#### Scenario: Поддерживаемые счета возвращаются в upstream-порядке
- **WHEN** часовой обход подтвердил открытые брокерский счёт, ИИС и неподдержанный дебетовый счёт
- **THEN** обычный GET отвечает `200` этими счетами в `data` в исходном порядке и наблюдаемым состоянием в `meta` без обращения к T-Bank
#### Scenario: Пустой список поддерживаемых счетов
- **WHEN** последний успешный обход подтвердил отсутствие открытых поддерживаемых инвестиционных счетов
- **THEN** система отвечает `200` с `data: []` и `meta`, обозначающим доступный полный результат
#### Scenario: Пустое название получает fallback
- **WHEN** последний подтверждённый счёт имеет пустое либо состоящее из пробелов имя
- **THEN** система отвечает этим счётом с непустым названием его нормализованного типа
#### Scenario: Дата открытия отсутствует
- **WHEN** последний подтверждённый счёт не имеет даты открытия
- **THEN** система отвечает этим счётом с `openedOn: null`
#### Scenario: Мультивалютная сводка счёта
- **WHEN** последний Portfolio счёта содержит итоги в RUB и USD, включая Cash Balance в USD
- **THEN** ответ содержит одну доступную `portfolioSummary` с отдельными Money-итогами RUB и USD без FX-конвертации, общего итога или отдельного итога Cash Balance
#### Scenario: Сводка одного счёта временно недоступна
- **WHEN** подтверждённый список содержит счёт без последнего доступного Portfolio
- **THEN** система отвечает `200` всеми счетами в исходном порядке и передаёт для этого счёта `portfolioSummary: null`
#### Scenario: Доступный Portfolio пуст
- **WHEN** последний доступный Portfolio счёта не имеет ненулевых позиций и доступных денежных остатков
- **THEN** система передаёт доступную `portfolioSummary` с пустым `totalsByCurrency`
### Requirement: Состояния недоступности списка счетов
Система MUST читать подтверждённый список текущего подключения локально. При отсутствии T-Bank connection система MUST отвечать `409` Problem Details с code `TBANK_NOT_CONNECTED`. Если T-Bank отклонил сохранённый token как неаутентифицированный или без прав во время часового обхода, система MUST отвечать `400` Problem Details с code `INVALID_TBANK_TOKEN` и сохранять connection. Временная ошибка обхода MUST сохранять последний подтверждённый список доступным; до первого подтверждённого списка система MUST обозначать продолжающуюся первую загрузку и MUST NOT возвращать частичный список как полный.
#### Scenario: Подключение отсутствует
- **WHEN** User без сохранённого T-Bank connection запрашивает список
- **THEN** система не обращается к T-Bank и отвечает `409` Problem Details с code `TBANK_NOT_CONNECTED`
#### Scenario: Сохранённый token больше не действителен
- **WHEN** T-Bank отклоняет сохранённый token при часовом обходе списка как неаутентифицированный или без прав
- **THEN** последующее чтение отвечает `400` Problem Details с code `INVALID_TBANK_TOKEN` и не раскрывает личные данные
#### Scenario: Ошибка обхода после успешного списка
- **WHEN** часовой обход списка завершается временной ошибкой после ранее успешной проверки
- **THEN** обычный GET возвращает последний подтверждённый список без ожидания T-Bank
#### Scenario: T-Bank временно недоступен
- **WHEN** User запрашивает список до первого подтверждённого списка, а часовой обход завершился временной либо иной upstream-ошибкой
- **THEN** система сообщает о неуспешной первой загрузке без счетов
#### Scenario: Первый обход ещё не завершён
- **WHEN** User открывает список после подключения до получения первого подтверждённого списка
- **THEN** система сообщает о продолжающейся загрузке без частичного либо неподтверждённого списка
### Requirement: Экран списка брокерских счетов
Клиент SHALL загружать список при открытии защищённого маршрута `/accounts` без ручного обновления и без автоматического refetch при возврате фокуса. Он MUST использовать успешный Client Cached Snapshot списка в течение 30 минут. Открытый экран Accounts MUST перечитывать локальный backend-результат каждые 30 минут и MUST NOT запускать импорт T-Bank. Клиент MUST показывать наблюдаемые freshness, полноту, время последнего успешного результата, последнюю часовую попытку и доступность из `meta` понятным текстом или иконкой с доступным именем, а не только цветом. При уже имеющемся успешном списке клиент MUST сохранять последний успешный Client Cached Snapshot видимым во время фонового HTTP-перечитывания и после его ошибки, ясно отделяя это состояние от свежего backend-результата. При успешном непустом ответе клиент MUST отобразить адаптивный одноколоночный список широких интерактивных карточек счетов с локализованными типом и, если доступна, датой открытия без времени; каждая карточка MUST ясно показывать тип, имя, финансовую сводку, вторичные метаданные и заметное действие открытия счёта, вести на `/accounts/:accountId` по всей области и сохранять доступное клавиатурное состояние. После успешной замены или удаления T-Bank connection клиент MUST инвалидировать список счетов.
Для доступной `portfolioSummary` карточка MUST показывать не более двух Money-итогов в исходных валютах и обозначать наличие остальных валют, если они есть; она MUST NOT показывать или подразумевать FX-конвертацию, межвалютный агрегированный итог либо Cash Balance как отдельный второй итог. При `portfolioSummary: null` карточка MUST показывать локальное безопасное состояние временной недоступности сводки, сохраняя метаданные счёта и возможность открыть его. При доступной пустой сводке карточка MUST не выдавать отсутствие итогов за временную ошибку.
#### Scenario: Пользователь открывает счёт из списка
- **WHEN** клиент успешно загрузил непустой список и User выбирает карточку счёта
- **THEN** клиент переходит на `/accounts/:accountId` с `id` выбранного счёта
#### Scenario: Список пуст
- **WHEN** клиент получает успешный пустой массив
- **THEN** клиент показывает штатное сообщение об отсутствии инвестиционных счетов T-Bank
#### Scenario: Последняя попытка неуспешна после полного списка
- **WHEN** клиент получает полный `data` списка и `meta` с неуспехом последней часовой попытки
- **THEN** клиент сохраняет список видимым, показывает его исходное время и текстовое либо доступно именованное предупреждение о неуспешной попытке
#### Scenario: Первая загрузка списка выполняется или неуспешна
- **WHEN** клиент получает `data: null`, а `meta` обозначает выполнение первой загрузки либо её неуспех
- **THEN** клиент показывает соответственно доступное состояние загрузки либо ошибки без карточек счетов и без представления частичного списка как полного
#### Scenario: Список берётся из свежего frontend-кэша
- **WHEN** User повторно открывает список в течение 30 минут после успешной загрузки
- **THEN** клиент показывает сохранённые счета без нового API-запроса
#### Scenario: Устаревший список загружается повторно
- **WHEN** User открывает список после истечения 30 минут frontend-кэша и уже существует последний успешный список
- **THEN** клиент оставляет этот список видимым во время нового HTTP-чтения и ясно обозначает фоновое обновление
#### Scenario: Открытый экран перечитывает список
- **WHEN** экран Accounts остаётся открытым 30 минут после последнего успешного HTTP-чтения
- **THEN** клиент выполняет фоновое HTTP-перечитывание backend-результата, сохраняет видимый snapshot и не запускает импорт T-Bank
#### Scenario: Ошибка фонового обновления не скрывает snapshot
- **WHEN** фоновое HTTP-перечитывание списка завершается ошибкой при наличии последнего успешного Client Cached Snapshot
- **THEN** клиент сохраняет snapshot видимым, безопасно сообщает о неуспешном HTTP-чтении и предоставляет применимое действие повторной загрузки
#### Scenario: Карточка ограничивает валютные итоги
- **WHEN** доступная сводка счёта содержит итоги в трёх или более валютах
- **THEN** карточка показывает не более двух исходных валютных итогов и обозначает наличие остальных без общего сконвертированного значения
#### Scenario: Недоступность сводки локальна карточке
- **WHEN** клиент получает список, в котором `portfolioSummary` одного счёта равна `null`
- **THEN** он показывает временную недоступность только на этой карточке, не скрывает остальные карточки и сохраняет переход на детали недоступной сводки счёта
#### Scenario: Пустая сводка не является ошибкой
- **WHEN** клиент получает доступную `portfolioSummary` с пустым `totalsByCurrency`
- **THEN** он показывает нейтральное состояние отсутствия финансовых итогов, а не состояние временной недоступности
### Requirement: Клиентские состояния ошибки списка счетов
Клиент MUST NOT автоматически повторять неуспешное HTTP-чтение списка. При `TBANK_NOT_CONNECTED` клиент SHALL показать объяснение отсутствия подключения и действие «Открыть настройки T-Bank». При `INVALID_TBANK_TOKEN` клиент SHALL показать объяснение недействительного token и то же действие. При `TBANK_UNAVAILABLE` клиент SHALL показать безопасное объяснение временной недоступности, действие повторного HTTP-чтения и действие открытия настроек T-Bank. При иной ошибке клиент MUST показать общий безопасный error-state только с действием повторного HTTP-чтения. Повтор MUST перечитывать backend-результат и MUST NOT запускать импорт T-Bank. Если последний успешный Client Cached Snapshot отсутствует, эти состояния MUST NOT показывать список; если snapshot существует во время фонового обновления, клиент MUST сохранить его видимым и показать соответствующее безопасное предупреждение и только применимые действия.
#### Scenario: Подключение требуется
- **WHEN** клиент получает `TBANK_NOT_CONNECTED`
- **THEN** клиент не показывает список и предлагает открыть настройки T-Bank
#### Scenario: Известная ошибка не повторяется автоматически
- **WHEN** HTTP-чтение списка завершается известной ошибкой
- **THEN** клиент не выполняет дополнительные запросы, пока User не выберет действие повторного HTTP-чтения
#### Scenario: Token требует внимания
- **WHEN** клиент получает `INVALID_TBANK_TOKEN`
- **THEN** клиент не показывает список и предлагает открыть настройки T-Bank
#### Scenario: Временная ошибка T-Bank
- **WHEN** клиент получает `TBANK_UNAVAILABLE`
- **THEN** клиент не показывает список и предлагает повторить HTTP-чтение либо открыть настройки T-Bank
#### Scenario: Неизвестная ошибка загрузки
- **WHEN** клиент получает ошибку без известного кода списка счетов
- **THEN** клиент не показывает список и предлагает только повторить HTTP-чтение
#### Scenario: Известная ошибка при сохранённом snapshot
- **WHEN** фоновое HTTP-перечитывание списка с последним успешным Client Cached Snapshot получает `TBANK_UNAVAILABLE`
- **THEN** клиент сохраняет snapshot видимым, сообщает о временной недоступности и предлагает повторить HTTP-чтение либо открыть настройки T-Bank
@@ -0,0 +1,20 @@
## MODIFIED Requirements
### Requirement: Client Cached Snapshot остаётся доступным при обновлении Accounts и Portfolio
Frontend MUST сохранять Client Cached Snapshot списка Brokerage Accounts или Portfolio на экране, если для того же набора данных уже был успешно получен ответ. Начальная загрузка без такого ответа может показывать блокирующее состояние загрузки. Для каждого личного resource response клиент MUST представлять freshness, полноту, время последнего успешного результата, последнюю часовую попытку и доступность из `meta` текстом либо доступно именованным элементом, а не только цветом; он MUST различать отсутствие попытки, выполнение, полный успех, частичный успех, неуспех, доступный пустой полный результат и отсутствие полного результата. Client Cached Snapshot MUST оставаться видимым во время фонового HTTP-перечитывания и после его ошибки; он не является server-side fallback и MUST NOT изменять или интерпретировать поле `Portfolio.isStale`. Повторное действие User MUST повторять только HTTP-чтение и MUST NOT запускать личную синхронизацию. Открытый инвестиционный экран MAY периодически перечитывать backend-результат, но MUST NOT создавать импорт T-Bank.
#### Scenario: Список Brokerage Accounts обновляется в фоне
- **WHEN** экран Accounts уже показывает Client Cached Snapshot и начинается фоновое HTTP-перечитывание того же списка
- **THEN** frontend оставляет список видимым и показывает только неблокирующий индикатор перечитывания
#### Scenario: Обновление Portfolio завершается ошибкой после успешного ответа
- **WHEN** экран Brokerage Account уже показывает Client Cached Snapshot Portfolio, а последующее HTTP-чтение завершается ошибкой
- **THEN** frontend сохраняет Portfolio видимым, показывает локализованное неблокирующее предупреждение и предоставляет повтор только HTTP-чтения
#### Scenario: Server-side fallback и client cached snapshot различаются
- **WHEN** backend возвращает Portfolio с `isStale` либо HTTP-перечитывание Client Cached Snapshot завершается ошибкой
- **THEN** frontend отображает соответствующее server-side либо client-side предупреждение независимо, не представляя одно состояние другим
#### Scenario: Resource сохраняется после неуспешной часовой попытки
- **WHEN** response содержит полный `data` и `meta`, обозначающий неуспех последней часовой попытки
- **THEN** frontend сохраняет `data` и его исходное время видимыми и отдельно обозначает неуспех попытки
@@ -0,0 +1,39 @@
## Purpose
Определяет независимое от открытых экранов часовое обновление личных инвестиционных данных и безопасно наблюдаемый результат последней попытки.
## ADDED Requirements
### Requirement: Часовой обход личных инвестиционных данных
Система SHALL начинать первый часовой обход после успешного подключения T-Bank и последующие обходы не ранее чем через 60 минут от начала предыдущей попытки этого подключения. После простоя система MUST выполнить не более одного актуального обхода и MUST NOT воспроизводить пропущенные часы. Неуспешная попытка MUST NOT повторяться ранее следующего часового срока. Перекрывающиеся обходы MUST NOT выполняться: срок, наступивший во время выполняющегося обхода, MUST быть пропущен и MUST NOT ставиться в очередь; следующий обход становится допустимым только в следующий 60-минутный срок, отсчитанный от начала выполнявшейся попытки. Обход MUST обновлять доступные счета, Portfolio каждого доступного счёта, изменяемый хвост операций и будущие события по бумагам текущего Portfolio. Он MUST NOT повторно загружать полные прошлые интервалы операций, завершённые свечи или полную рыночную историю и MUST NOT зависеть от открытия frontend-экрана.
#### Scenario: Первое подключение
- **WHEN** User успешно подключает T-Bank между часовыми сроками
- **THEN** первый обход начинается без ожидания следующего календарного часа
#### Scenario: Возобновление после простоя
- **WHEN** backend запускается после пропуска нескольких часовых сроков
- **THEN** система выполняет один актуальный обход без синтетических прошлых обновлений
#### Scenario: Перекрытый срок
- **WHEN** предыдущий обход всё ещё выполняется в момент очередного срока
- **THEN** система не начинает второй обход и не ставит пропущенный обход в очередь
#### Scenario: Длительный обход пропускает один срок
- **WHEN** обход начался в 10:00, всё ещё выполняется в 11:00 и завершился до 12:00
- **THEN** система не запускает обход в 11:00 и может начать следующий не ранее 12:00
### Requirement: Последний результат обхода доступен только для наблюдения
Система SHALL предоставлять авторизованному User результат последнего обхода только в границе его текущего подключения: время начала, состояние выполнения, полного успеха, частичного успеха либо неуспеха и безопасный перечень проблемных счетов или разделов. Это состояние MUST быть доступно в `meta` каждого затронутого resource read вместе с freshness, полнотой и доступностью его `data`. Успех одного счёта или раздела MUST сохраняться независимо от ошибки другого; ошибка MUST NOT заменять его последнее успешное состояние или время. Отсутствие состоявшейся попытки MUST отличаться от успешной попытки. Наблюдение результата MUST NOT предоставлять способ начать личную синхронизацию.
#### Scenario: Частичный успех
- **WHEN** Portfolio одного доступного счёта обновился, а другого не обновился
- **THEN** новый Portfolio первого сохраняется, прежний успешный Portfolio второго остаётся доступным, а результат обхода обозначает частичный успех и проблемный счёт
#### Scenario: Наблюдение не запускает обход
- **WHEN** User запрашивает результат последнего обхода
- **THEN** система возвращает доступный сохранённый результат без нового обращения к T-Bank
#### Scenario: Старая граница подключения
- **WHEN** User заменил либо удалил подключение и запрашивает результат прежнего обхода
- **THEN** система не раскрывает результат прежней границы подключения
@@ -0,0 +1,16 @@
## Purpose
Определяет online-only получение справочных сведений и результатов поиска инструментов отдельно от сохранённых личных финансовых данных.
## ADDED Requirements
### Requirement: Справочные сведения и поиск инструментов получают онлайн
Система SHALL получать подробные справочные сведения об Instrument и результаты поиска онлайн при их запросе. Такие сведения MUST NOT становиться самостоятельной долговременной копией справочника. Недоступность либо отсутствие онлайн-сведений MUST NOT задерживать сохранённый финансовый ответ, изменять его свежесть или подтверждать отсутствие Instrument.
#### Scenario: Медленный online lookup
- **WHEN** подробные сведения об Instrument ещё не получены онлайн при чтении сохранённого Portfolio
- **THEN** система возвращает доступный сохранённый Portfolio без ожидания lookup
#### Scenario: Online lookup не найден
- **WHEN** online lookup не возвращает Instrument
- **THEN** система не интерпретирует этот результат как изменение сохранённых личных финансовых данных
@@ -0,0 +1,27 @@
## Purpose
Определяет сохранение точных оценок Portfolio по счёту и валюте для будущей истории без предоставления публичного чтения этих точек.
## ADDED Requirements
### Requirement: Полные оценки Portfolio сохраняются как история
Система SHALL сохранять для каждого доступного Brokerage Account точку оценки по каждой исходной валюте только после успешного получения полного Portfolio с вычислимой стоимостью всех его позиций. Точка MUST содержать точное значение без валютной конвертации и фактическое время успешного получения Portfolio. Система MUST NOT подставлять ноль за отсутствующую стоимость и MUST NOT создавать точку для неполной оценки. Повторная публикация одной попытки MUST NOT создавать дубликат точки.
#### Scenario: Полная мультивалютная оценка
- **WHEN** успешный полный Portfolio содержит вычислимые итоги RUB и USD
- **THEN** система сохраняет две отдельные точки с исходными значениями и валютами
#### Scenario: Неполная оценка
- **WHEN** в новом полном составе Portfolio у позиции отсутствует вычислимая стоимость
- **THEN** система сохраняет новый Portfolio без точки истории оценки
### Requirement: История оценки скрыта вне подтверждённого доступа
Система MUST сохранять историю оценки при замене token или удалении подключения, но MUST NOT раскрывать её без текущего подтверждённого доступа User к тому же Brokerage Account. Успешное новое подключение, подтверждающее доступ к счёту, MUST снова делать его сохранённую историю доступной для будущих поддерживаемых представлений. Система MUST NOT предоставлять отдельное публичное чтение точек истории этим требованием.
#### Scenario: Удалённое подключение
- **WHEN** User удаляет T-Bank connection
- **THEN** сохранённые точки оценки этого User не доступны для чтения
#### Scenario: Повторно подтверждённый счёт
- **WHEN** новое успешное подключение подтверждает доступ User к тому же Brokerage Account
- **THEN** ранее сохранённые точки этого счёта снова доступны для будущих поддерживаемых представлений
@@ -0,0 +1,82 @@
## MODIFIED Requirements
### Requirement: Получение Portfolio выбранного Brokerage Account
Система SHALL предоставлять авторизованному User `GET /api/v1/brokerage-accounts/:accountId/portfolio`. Она MUST возвращать нормализованный полный Portfolio Snapshot только для открытого поддерживаемого Brokerage Account, доступного через текущий T-Bank connection User. Ответ MUST иметь envelope `{ data, meta }`: `data` MUST содержать сохранённый полный Portfolio Snapshot с `lastSuccessfulAt` в RFC 3339 UTC без миллисекунд, boolean `isStale`, `totalsByCurrency`, `positions` и `cashBalances`; `meta` MUST содержать наблюдаемое состояние resource по `tbank-data-sync`, включая freshness, полноту, время последнего успешного Snapshot, последнюю часовую попытку и доступность. Обычный GET MUST читать сохранённый Snapshot и его наблюдаемое состояние без ожидания T-Bank и MUST NOT запускать обновление личного Portfolio.
Portfolio Snapshot является результатом наблюдения Portfolio на конкретный момент. Он MUST NOT означать, что WealthLedger хранит Portfolio как долговременный агрегат или подтверждает доступность Brokerage Account вне текущего T-Bank connection.
#### Scenario: Получение текущего Portfolio
- **WHEN** User запрашивает Portfolio доступного Brokerage Account, а полный Snapshot текущего подключения успешно сохранён
- **THEN** система отвечает `200` с полным нормализованным Portfolio Snapshot в `data`, его `lastSuccessfulAt` и `isStale`, а также наблюдаемым состоянием в `meta`
#### Scenario: Недоступный Brokerage Account
- **WHEN** `accountId` не принадлежит User, закрыт, неподдерживаем или больше не доступен через текущий T-Bank connection
- **THEN** система отвечает `404` без Portfolio Snapshot
### Requirement: Свежесть и fallback полного Portfolio Snapshot
Система MUST хранить последний полный успешно полученный Portfolio Snapshot с принадлежностью User, экземпляру подключения и Brokerage Account. Обычный GET MUST возвращать `{ data, meta }`, где `data` является сохранённым Snapshot, а `meta` соответствует наблюдаемому состоянию resource по `tbank-data-sync`, читать его без ожидания T-Bank и MUST NOT запускать обновление личного Portfolio, даже если Snapshot отсутствует или старше 60 минут; обновлением владеет часовой обход. Portfolio source MUST считаться свежим в течение 60 минут после успешного получения. Ошибка обхода MUST сохранять прежний полный Snapshot и его время без лимита fallback. Если состав получен полностью, но отдельная позиция не имеет цены, система MUST сохранить новый состав с неполной оценкой; metadata MUST NOT определять успешность или свежесть финансового Snapshot. Сохранённая доступная identity позиции MAY оставаться доступной при недоступности online-справочника.
Система MUST делать прежние личные datasets недоступными после успешной замены или удаления подключения. Поздняя работа прежнего подключения MUST NOT публиковаться в новом. Самостоятельные справочные сведения и результаты lookup MUST запрашиваться онлайн, а не сохраняться как долговременный справочник; ошибка lookup не подтверждает отсутствие инструмента.
#### Scenario: Свежий Snapshot
- **WHEN** User повторно запрашивает тот же Portfolio в течение 60 минут после успешного получения
- **THEN** система возвращает свежий Snapshot без нового обращения к T-Bank
#### Scenario: Медленный lookup инструмента
- **WHEN** сведения об инструменте ещё запрашиваются онлайн
- **THEN** система не задерживает локальный ответ Portfolio ради lookup и показывает сохранённую доступную identity
#### Scenario: Fallback после временной upstream-ошибки
- **WHEN** часовой обход Portfolio неуспешен из-за временной недоступности T-Bank, а в текущей границе подключения есть полный успешный Snapshot
- **THEN** система отвечает `200` этим Snapshot с `isStale: true` и исходным `lastSuccessfulAt`
#### Scenario: Недействительный token не маскируется fallback-данными
- **WHEN** T-Bank отклоняет token как неаутентифицированный или без прав при часовом обходе Portfolio
- **THEN** система не возвращает fallback Snapshot и отвечает `400` Problem Details с code `INVALID_TBANK_TOKEN`
#### Scenario: Неполная оценка при новом составе
- **WHEN** успешно получен полный состав позиций, но у одной позиции нет вычислимой цены
- **THEN** новый состав становится текущим Portfolio Snapshot с неизвестной стоимостью позиции без подстановки нуля
### Requirement: Состояния и ошибки Portfolio на клиенте
Клиент SHALL загружать Portfolio при открытии защищённого маршрута `/accounts/:accountId`. Он MUST использовать успешный ответ в течение 30 секунд без automatic retry или refetch при возврате фокуса. Открытый экран MUST перечитывать сохранённый результат backend каждые 30 минут без запуска синхронизации T-Bank. Клиент MUST показывать freshness, полноту, время последнего успешного Snapshot, последнюю часовую попытку и доступность из `meta` текстом или доступно именованным элементом, а не только цветом. Клиент MUST NOT предоставлять действие ручной синхронизации Portfolio. При отсутствии ранее успешного ответа он MUST показывать структурный skeleton с доступным обозначением занятости без вымышленных сумм; skeleton MUST NOT быть интерактивным или получать клавиатурный фокус.
Истечение срока свежести MUST NOT скрывать Client Cached Snapshot. Во время фонового перечитывания и после его ошибки клиент MUST сохранять ранее полученные данные того же Portfolio, исходное `lastSuccessfulAt` и показывать неблокирующую индикацию либо предупреждение с действием повтора только HTTP-чтения. Сохранённые данные MUST NOT считаться новым успешным снимком или подтверждением текущей доступности Brokerage Account. При `isStale: true` клиент MUST показывать Snapshot, исходное время и нейтральное предупреждение без действия ручного обновления. Server-side stale и ошибка Client Cached Snapshot MUST оставаться раздельными состояниями.
При отсутствии Client Cached Snapshot и ответе `404` клиент MUST показать недоступный счёт с возвратом к списку без действия повтора. При отсутствии Snapshot и `TBANK_NOT_CONNECTED`, `INVALID_TBANK_TOKEN`, `TBANK_UNAVAILABLE` либо иной ошибке клиент MUST показать безопасное error-состояние; повтор HTTP-чтения разрешён только для временной и неизвестной ошибки.
#### Scenario: Показ устаревшего Portfolio
- **WHEN** клиент получает успешный ответ с `isStale: true`
- **THEN** клиент показывает Portfolio, исходное время и нейтральное предупреждение без ручной синхронизации
#### Scenario: Первичная загрузка Portfolio
- **WHEN** клиент ожидает Portfolio без ранее полученного успешного ответа
- **THEN** он показывает структурный skeleton без вымышленных или нулевых сумм и без фокусируемых placeholders
#### Scenario: Неуспешная последняя попытка сохраняет Portfolio
- **WHEN** клиент получает полный Portfolio и `meta` с неуспехом последней часовой попытки
- **THEN** он сохраняет Portfolio и исходное время видимыми и отдельно показывает текстовое либо доступно именованное предупреждение о неуспехе попытки
#### Scenario: Недоступный счёт на клиенте
- **WHEN** для текущего Portfolio отсутствует Client Cached Snapshot и клиент получает `404`
- **THEN** клиент не показывает Portfolio и предлагает вернуться к списку Brokerage Account без действия повтора
#### Scenario: Обновление после истечения срока свежести
- **WHEN** срок свежести показанного Portfolio истёк и началось фоновое HTTP-перечитывание
- **THEN** клиент сохраняет прежние валютные итоги и время, показывает неблокирующую индикацию и не заменяет данные skeleton-состоянием
#### Scenario: Ошибка обновления после успешного ответа
- **WHEN** Client Cached Snapshot Portfolio уже видим, а повторное HTTP-чтение завершается ошибкой
- **THEN** клиент сохраняет прежние данные и время, показывает неблокирующее предупреждение и действие повтора чтения
#### Scenario: Ошибка клиентского обновления при server-side stale
- **WHEN** клиент показывает Portfolio с `isStale: true`, а фоновое перечитывание завершается ошибкой
- **THEN** оба предупреждения остаются различимыми, а действие повтора относится только к HTTP-чтению
#### Scenario: Повторное обновление успешно
- **WHEN** после ошибки HTTP-чтения User запускает повторное чтение и получает успешный Portfolio Snapshot
- **THEN** клиент показывает значения и `lastSuccessfulAt` успешного ответа, убирает предупреждение об ошибке чтения и отображает server-side предупреждение только по новому `isStale`
#### Scenario: Штатный пустой Portfolio на клиенте
- **WHEN** успешно полученный Portfolio содержит пустые `positions` и `cashBalances`
- **THEN** клиент показывает empty-состояние без валютных карточек с вымышленными нулевыми итогами
@@ -0,0 +1,31 @@
## MODIFIED Requirements
### Requirement: Создание и замена подключения T-Bank
Система SHALL позволять авторизованному User создать или заменить единственное подключение через `PUT /api/v1/tbank-connection` с token. Система MUST удалить внешние пробелы token, отклонить пустое значение как ошибку валидации и проверить token запросом T-Bank открытых счетов до сохранения. Пустой список счетов MUST считаться успешной проверкой. Успех MUST вернуть `200` с текущим статусом подключения; успешная замена MUST установить новый `connectedAt`. После успешного подключения MUST начинаться первый часовой обход без команды frontend. История стоимости прежних счетов MUST сохраняться, но MUST быть доступна только для счетов, доступ к которым подтверждён новым подключением.
#### Scenario: Успешное создание подключения без счетов
- **WHEN** User отправляет непустой token и T-Bank успешно возвращает пустой список открытых счетов
- **THEN** система сохраняет подключение, отвечает `200` с `{ connected: true, connectedAt: <RFC 3339 UTC timestamp> }` и не создаёт фиктивные счета
#### Scenario: Успешная замена подключения
- **WHEN** User с подключением отправляет другой token и T-Bank успешно его проверяет
- **THEN** система заменяет token, обновляет `connectedAt`, отвечает текущим статусом без token и начинает первый часовой обход нового подключения
#### Scenario: Невалидный token не заменяет существующее подключение
- **WHEN** T-Bank отклоняет token как неаутентифицированный или не имеющий прав
- **THEN** система отвечает `400` Problem Details с code `INVALID_TBANK_TOKEN` и сохраняет прежнее подключение без изменений
#### Scenario: T-Bank временно недоступен при проверке token
- **WHEN** проверка token не завершается из-за временной или иной upstream-ошибки
- **THEN** система отвечает `503` Problem Details с code `TBANK_UNAVAILABLE` и не создаёт либо не изменяет подключение
### Requirement: Удаление подключения T-Bank
Система SHALL позволять авторизованному User удалить своё подключение через `DELETE /api/v1/tbank-connection`. Удаление MUST быть идемпотентным и отвечать `204`; после успешного удаления backend MUST прекратить использовать удалённый token и сделать относящиеся к User текущие личные данные недоступными. История стоимости счёта MUST сохраняться, но MUST NOT раскрываться до подтверждения нового доступа к тому же счёту.
#### Scenario: Удаление существующего подключения
- **WHEN** User удаляет существующее подключение
- **THEN** система удаляет token, скрывает текущие личные данные и историю стоимости и отвечает `204`
#### Scenario: Повторное удаление подключения
- **WHEN** User удаляет уже отсутствующее подключение
- **THEN** система отвечает `204`, не раскрывая сохранённую историю
@@ -0,0 +1,36 @@
## Purpose
Определяет единственного владельца обновления личных данных T-Bank и узкие
исключения первой загрузки данных, которых ещё нет в локальном результате.
## ADDED Requirements
### Requirement: Часовой обход владеет обновлением личных данных T-Bank
Система MUST обновлять личные инвестиционные данные T-Bank только часовым обходом для текущего подключения User. Обычные resource reads, действия повтора и открытие экранов MUST NOT запускать импорт, ручную личную синхронизацию или обращение к T-Bank. Единственными исключениями являются первая загрузка отсутствующего интервала Account Ledger и впервые открытого диапазона свечей: они MAY начать получение только этого отсутствующего интервала либо диапазона, когда для него нет подтверждённого локального результата. Эти исключения MUST NOT обновлять Accounts, Portfolio, иные интервалы операций, прогноз будущих событий или другой диапазон свечей. После подтверждения полноты запрошенного прошлого интервала или диапазона его повторное чтение MUST оставаться локальным.
#### Scenario: Обычное чтение не запускает импорт
- **WHEN** User открывает Accounts, Portfolio, Account Ledger, будущие события либо повторяет неуспешное HTTP-чтение
- **THEN** система читает локальный результат и не начинает личную синхронизацию T-Bank
#### Scenario: Первый отсутствующий интервал операций
- **WHEN** User впервые открывает интервал Account Ledger без подтверждённого локального результата
- **THEN** система MAY начинает получение только этого интервала и возвращает его локальное состояние неполноты до подтверждения результата
#### Scenario: Впервые открытый диапазон свечей
- **WHEN** User впервые открывает диапазон свечей, для которого нет подтверждённого локального результата
- **THEN** система MAY начинает получение только этого диапазона и не загружает другие диапазоны или полную историю свечей
### Requirement: Результат личного обновления остаётся наблюдаемым
Каждый response resource read личных данных текущего подключения MUST возвращать `{ data, meta }`. `meta` MUST отдельно делать наблюдаемыми freshness и полноту именно `data`, момент последнего успешного полного результата resource, момент и состояние последней часовой попытки, затронувшей resource, и доступность resource. Состояние последней попытки MUST различать отсутствие попытки, выполнение, полный успех, частичный успех и неуспех. Доступность MUST различать доступный полный результат, первую загрузку без полного результата, временную недоступность без полного результата и недоступность из-за отсутствующего либо недействительного подключения. Неполный, продолжающийся или неуспешный результат MUST NOT обозначаться полным или свежим. Успешный пустой Accounts либо иной пустой полный результат MUST обозначаться доступным и полным, а не отсутствующим. При неуспешном обновлении последний подтверждённый полный результат и его время MUST оставаться доступными, когда отдельное требование capability не предписывает недоступность данных; его freshness MUST определяться исходным временем, а `meta` MUST отдельно обозначать неуспех последней попытки. Наблюдение этих состояний MUST NOT предоставлять способ начать синхронизацию.
#### Scenario: Ошибка после полного результата
- **WHEN** очередное обновление набора личных данных завершается ошибкой после ранее подтверждённого полного результата
- **THEN** система возвращает предыдущий полный `data`, его исходное время и freshness, а `meta` отдельно обозначает неуспех последней попытки
#### Scenario: Первая загрузка ещё не завершена
- **WHEN** для набора личных данных ещё нет подтверждённого полного результата
- **THEN** система возвращает `data: null` и в `meta` обозначает выполнение первой загрузки либо её неуспех без частичного результата, выданного за полный
#### Scenario: Успешный пустой список счетов
- **WHEN** последний полный результат Accounts подтверждает отсутствие открытых поддерживаемых счетов
- **THEN** система возвращает `data: []`, а `meta` обозначает доступный полный результат, его время и состояние последней часовой попытки
@@ -0,0 +1,108 @@
# System Analysis
## Requirement Coverage
| Approved delta | System decision covered here |
| --- | --- |
| `hourly-investment-sync`: hourly walk and observable result | Per-connection durable hourly attempt, no catch-up or overlap, and `lastAttempt`/Accounts `sync` semantics. |
| `tbank-data-sync`: one update owner and observable resource state | Local resource reads, two narrow first-load exceptions, and the target `{ data, meta }` contract. |
| `brokerage-accounts` | Saved Accounts and per-account summaries, plus the connection-wide hourly result exposed as `sync`. |
| `portfolio-view` and `portfolio-value-history` | Saved complete snapshots, stale fallback, connection-revision publication fence, and private retained value history. |
| `brokerage-account-ledger` | First opening of an unconfirmed historical interval may load only that interval; complete history remains local. |
| `brokerage-account-upcoming-events` | Saved forecast query/window semantics, complete-source publication, and independent failed-update status. |
| `online-instrument-information` | Online reference lookup is separate from personal-resource reads and cannot delay or alter their status. |
| `frontend-data-resilience` | A retry reissues only HTTP GET; frontend cache, server fallback, and hourly-attempt failure remain distinct. |
| `tbank-connection` | PUT starts the new connection's first walk; replacement/deletion hides old personal data and old walk status. |
## Systems and Responsibilities
| System | Responsibility | Must not do |
| --- | --- | --- |
| Frontend | Read resources, render `data` and all observable `meta` fields, retain its own successful screen cache, and retry only the GET. | Start personal sync, infer freshness from a client error, or expose a manual personal-sync action. |
| Backend resource API | Authenticate, enforce the current connection boundary, read local persisted resources, and return the target envelope. | Wait for or call T-Bank while serving ordinary resource GETs. |
| Durable hourly scheduler | Reserve at most one current walk per connection; refresh membership, each available Portfolio, mutable operations tail, then events. | Replay missed terms, queue an overlapped term, or refresh complete historical ledger intervals/candle ranges. |
| Local store | Keep last confirmed complete data, coverage, resource timestamps, hourly attempt/result, and connection revision. | Publish data or status from a superseded connection revision. |
| T-Bank and public reference sources | Supply upstream data to connection PUT validation, the hourly walk, and explicitly online reference lookups. | Become a synchronous dependency of a normal personal resource GET. |
## Interaction Flows
`sequences/read-only-resource-get.mmd` defines the normal read and retry boundary. A `200` read uses only local state; a boundary error may occur before any data lookup.
`sequences/hourly-investment-walk.mmd` defines the sole normal personal-data updater. A successful connection starts its first attempt immediately. Thereafter a next attempt is eligible no earlier than 60 minutes after the preceding attempt started. Startup performs at most one current attempt, not a replay. If a term arrives while an attempt is running, it is dropped rather than queued; the next eligible term is measured from the running attempt's start.
The walk updates current membership, every available account's Portfolio, each account's mutable operations tail, and forecasts/events for instruments in the current Portfolio. Each successful section persists independently. A failed section preserves its earlier complete result and its original success time. Complete past operation intervals, completed candles, and full market history are not refreshed by the walk.
### First-load exceptions
1. Opening an unconfirmed historical Account Ledger interval may start retrieval of only that missing interval and immediately returns local rows plus incomplete coverage/status. It does not refresh membership, Accounts, Portfolio, events, another operations interval, or candles. A confirmed complete interval is never rechecked due to a GET or the hourly walk.
2. Opening a candle range with no confirmed local result may start retrieval of only that range. It does not retrieve other ranges or all history; a confirmed range is subsequently local.
These are acquisition exceptions, not variants of manual sync. HTTP retry of either resource rereads its local state; it does not broaden or restart work.
## Contracts
`contracts/investment-resource-envelope.schema.json` is the minimal target cross-system artifact. It describes the changed resource envelope rather than duplicating backend `openapi.json`.
All affected personal resource successes use `{ data, meta }`: Accounts; account and `all` Portfolio; account and `all` operations; account and `all` events. `data` is the last confirmed complete result, except that ledger first-load may return locally held rows with `meta.completeness: "incomplete"`. Where no complete result exists, `data` is `null`; a partial result must never be represented as complete.
`meta` has five independent meanings:
| Field | Target meaning |
| --- | --- |
| `freshness` | `fresh`/`stale` applies to the timestamp of the returned confirmed result; `unknown` applies when none exists. A failed or running attempt never makes data fresh. |
| `completeness` | `complete` only after the resource's required source result is confirmed. `incomplete` describes an in-progress/failed first-load or ledger coverage gap. |
| `lastSuccessfulAt` | Timestamp of the returned complete result, or `null` when none exists. It is not replaced by a later failed attempt. |
| `lastAttempt` | The resource-relevant hourly result: `none`, `running`, `succeeded`, `partial`, or `failed`, with nullable start/end timestamps and only safe failed section/account/error-code entries. `partial` means at least one section succeeded and at least one failed. |
| `availability` | `available` has a complete result, including `[]`; `initial-loading` and `temporarily-unavailable` have no complete result; `connection-unavailable` means the current connection cannot expose the resource. |
Accounts also returns top-level `sync`: `null` if the current connection has no hourly attempt, otherwise the complete connection-wide last-walk status. Its `problems` list is the safe, scoped failure list for `membership`, `portfolio`, `operations`, and `events`; an account ID is present only for account-scoped sections. `sync` is observation only and is not a command or progress API.
Events preserves the existing account and `all` scope. `from` and `to` are optional strict `YYYY-MM-DD` UTC dates with inclusive bounds; omitted `from` is the current UTC date and omitted `to` is `from + 30` calendar days. `from > to` returns `400 INVALID_EVENTS_PERIOD`, does not read a substitute range, and starts no source work. A saved forecast retains `calculatedAt` and its original inclusive calculation window. The response filters events to the intersection of that original window and the requested window. Empty intersection and no matching events are successful empty `data` with the same forecast metadata. `all` is complete only if every included account has its required complete forecast sources; it cannot hide a missing account contribution. Events' `meta.lastAttempt` reports the resource-relevant hourly events result independently of the saved forecast, so a failed update can coexist with an available forecast.
## Data and State Semantics
An empty confirmed Accounts list, empty Portfolio, empty event intersection, and empty event list are all available complete results, never missing data. A stale complete result remains available indefinitely until a connection boundary makes it unavailable. `Portfolio.isStale` retains its snapshot-specific meaning and is not a substitute for `meta.lastAttempt` or a frontend request error.
Forecast publication requires complete broker and public-source results for the Portfolio Snapshot used to calculate it. It stores the Portfolio quantity basis, `calculatedAt`, and source window together. A failed refresh cannot publish a partial forecast or relabel an older forecast as calculated from a newer Portfolio.
Successful complete Portfolio evaluations may append one exact, per-currency value-history point at the actual successful Portfolio time. Missing valuation must not be replaced with zero and cannot create a point. Retry/publication of one attempt is idempotent. History survives connection replacement/deletion but is readable only after the current connection confirms access to the same account; no public history-read endpoint is introduced.
The connection revision is the publication fence. PUT validates the candidate token, atomically establishes a new revision and membership boundary, then triggers its first walk. DELETE is idempotent, prevents further token use, and hides current personal data. Old attempts cannot publish into a replacement connection, and neither old `sync` nor retained value history is disclosed across an unconfirmed boundary.
## Error and Recovery Semantics
Authentication errors remain HTTP boundary errors. No connection is `409 TBANK_NOT_CONNECTED`. A token known invalid/unauthorized from an hourly attempt is `400 INVALID_TBANK_TOKEN`; it retains the connection record but blocks personal-data disclosure and must not be masked by fallback data. A requested account outside current confirmed membership is `404`; unknown membership does not authorize an arbitrary account ID. Connection PUT validation failures remain `400 INVALID_TBANK_TOKEN` or `503 TBANK_UNAVAILABLE` and leave the prior connection unchanged.
Temporary upstream failure during an hourly attempt is represented in `meta.lastAttempt` and, on Accounts, `sync`; it is not a synchronous GET failure. With a previous complete result, GET returns `200` and the preserved data. Without one, GET returns `{ data: null, meta }` with `initial-loading` or `temporarily-unavailable`. `503 TBANK_UNAVAILABLE` remains reserved for an HTTP request that itself cannot be served, not a completed background failure.
Frontend recovery is limited to rereading the resource. It retains its Client Cached Snapshot during a background HTTP failure, while separately showing server stale/fallback and the failed hourly attempt where supplied. Replacing/deleting a connection invalidates personal client queries so a late response cannot revive hidden data.
## Observability Semantics
The durable hourly attempt is the observable unit: start time, terminal state, finish time, and safe failed section/account/error-code list. Absence of an attempt is `none` in resource `lastAttempt` and `null` in Accounts `sync`; it is not success. A running attempt has `finishedAt: null`. The status is scoped to the current connection revision and contains no token, upstream payload, cursor, or internal revision.
Resource `lastSuccessfulAt`, freshness, completeness, and availability describe returned `data`; the hourly result describes the attempt. This separation permits a truthful response after partial/failed refresh. Frontend status text or accessible names must expose all five resource meanings and must not rely on color alone.
## Security and NFR at System Boundaries
All personal reads require the authenticated User and current-connection access check. T-Bank tokens remain server-only, encrypted, and absent from response metadata, OpenAPI examples, logs, and status failures. Online instrument reference failure cannot prove an instrument absent or alter personal snapshot freshness.
The scheduler is a single durable reservation owner for the deployed single backend process. The target explicitly does not add multi-process scheduling. Local read latency is isolated from upstream latency because ordinary GETs do not invoke upstream personal data calls. The existing upstream limiter remains the boundary for PUT validation and hourly work.
## Compatibility / Migration
The current backend-generated `openapi.json` and frontend snapshot are identical and already expose resource envelopes, Accounts top-level `sync`, and events `sync=observe`; they are evidence of AS-IS, not the target authority. Current generated `InvestmentResourceMetaDto` uses `availability: missing|partial|complete` plus per-work `sync: idle|running|failed`, while `HourlySyncStatusDto` adds `cancelled`; these do not fully express the approved target's first-load availability, `none|succeeded|partial` attempt distinction, and resource-scoped last attempt. Current `sync=observe` and `/api/v1/tbank-sync/{id}` preserve legacy screen-work observation and must not be used to reintroduce request-started personal synchronization.
Implementation must evolve backend Swagger DTOs/controllers and regenerate backend OpenAPI. The frontend must receive that published snapshot and regenerate its local transport types. The HTTP route identities and established Problem Details codes remain compatible unless the generated target contract explicitly changes them; no copy of the full OpenAPI belongs in this change. The target schema is intentionally additive/semantic and must be reconciled with generated field names before release.
## Durable Documentation Candidates
- `docs/adr/019-hourly-reads-and-online-instrument-reference.md`: clarify that hourly status is connection-scoped and that only the two first-load exceptions may acquire personal/market data.
- `docs/architecture/cross-cutting/data-freshness.md`: replace legacy request-driven freshness table and `sync=observe` language with target resource/attempt separation and `data: null` first-load states.
- `docs/domains/portfolio/flows/portfolio-loading.md`: replace screen-triggered refresh/status flow with local GET and hourly-walk flow.
- `docs/domains/income/upcoming-events.md`: describe saved forecast source-window intersection, `calculatedAt`, and independent failed hourly update state.
- `docs/adr/017-local-tbank-reads-and-background-sync.md`: mark its request-driven/manual-refresh decisions superseded by ADR 019 rather than leaving competing guidance.
## Open Questions
None. The approved Proposal and Delta Specs determine the target semantics; implementation will reconcile field naming and route annotations through backend-generated OpenAPI without treating current generated artifacts as a second authority.
@@ -0,0 +1,54 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "reconcile-hourly-investment-sync/investment-resource-envelope.schema.json",
"title": "Target investment resource envelope",
"description": "Target cross-system semantics only; the backend-generated OpenAPI remains the executable HTTP contract.",
"type": "object",
"required": ["data", "meta"],
"properties": {
"data": {
"description": "The last confirmed complete result, an allowed incomplete ledger first-load result, or null when no complete result is available."
},
"meta": { "$ref": "#/$defs/resourceMeta" },
"sync": { "$ref": "#/$defs/hourlyWalk" }
},
"$defs": {
"resourceMeta": {
"type": "object",
"required": ["freshness", "completeness", "lastSuccessfulAt", "lastAttempt", "availability"],
"properties": {
"freshness": { "enum": ["fresh", "stale", "unknown"] },
"completeness": { "enum": ["complete", "incomplete"] },
"lastSuccessfulAt": { "type": ["string", "null"], "format": "date-time" },
"lastAttempt": { "$ref": "#/$defs/attempt" },
"availability": { "enum": ["available", "initial-loading", "temporarily-unavailable", "connection-unavailable"] }
}
},
"attempt": {
"type": "object",
"required": ["state", "startedAt", "finishedAt", "problems"],
"properties": {
"state": { "enum": ["none", "running", "succeeded", "partial", "failed"] },
"startedAt": { "type": ["string", "null"], "format": "date-time" },
"finishedAt": { "type": ["string", "null"], "format": "date-time" },
"problems": { "type": "array", "items": { "$ref": "#/$defs/problemSection" } }
}
},
"hourlyWalk": {
"description": "Present only on Accounts; absent from other resource envelopes.",
"oneOf": [
{ "type": "null" },
{ "$ref": "#/$defs/attempt" }
]
},
"problemSection": {
"type": "object",
"required": ["section", "accountId", "errorCode"],
"properties": {
"section": { "enum": ["membership", "portfolio", "operations", "events"] },
"accountId": { "type": ["string", "null"] },
"errorCode": { "type": ["string", "null"] }
}
}
}
}
@@ -0,0 +1,18 @@
sequenceDiagram
participant Scheduler
participant DB as Durable reservation/status
participant TB as T-Bank and public sources
participant Store as Local resource store
participant API as Resource GET
Scheduler->>DB: Reserve current connection walk
alt Due and no walk running
DB-->>Scheduler: Attempt with connection revision
Scheduler->>TB: Refresh membership, portfolios, mutable operations, events
Scheduler->>Store: Publish each complete successful result for the same revision
Scheduler->>DB: Settle succeeded, partial, or failed with safe failed sections
else Not due or an attempt is running
DB-->>Scheduler: No work
end
API->>Store: Read persisted result and attempt status
Store-->>API: Last confirmed data and metadata
@@ -0,0 +1,13 @@
sequenceDiagram
actor User
participant UI as Frontend
participant API as Backend resource GET
participant DB as Local store
participant TB as T-Bank
User->>UI: Open screen or retry HTTP read
UI->>API: GET resource
API->>DB: Authorize current connection and read saved result/status
DB-->>API: data, resource state, last hourly attempt
API-->>UI: 200 { data, meta } or boundary Problem Details
Note over API,TB: No T-Bank call and no personal sync work from GET or retry
@@ -0,0 +1,67 @@
# Tasks
## 1. Backend runtime and published contract
- [ ] 1.1 [backend] Привести durable hourly walk, connection-revision fence и локальные resource readers к утверждённой модели: только hourly owner для обычного обновления, узкие first-load исключения, сохранение complete fallback и изолированное наблюдение resource/connection attempt.
- **Covers:** `tbank-data-sync` — `Часовой обход владеет обновлением личных данных T-Bank`; `hourly-investment-sync` — `Часовой обход личных инвестиционных данных`; `hourly-investment-sync` — `Последний результат обхода доступен только для наблюдения`; `brokerage-account-ledger` — `Отсутствующий исторический интервал загружается только впервые`; `portfolio-view` — `Свежесть и fallback полного Portfolio Snapshot`; `portfolio-value-history` — `Полные оценки Portfolio сохраняются как история`; `portfolio-value-history` — `История оценки скрыта вне подтверждённого доступа`; `online-instrument-information` — `Справочные сведения и поиск инструментов получают онлайн`; `tbank-connection` — `Создание и замена подключения T-Bank`; `tbank-connection` — `Удаление подключения T-Bank`
- **Source:** System Analysis §§ Interaction Flows, Data and State Semantics, Error and Recovery Semantics, Observability Semantics, Security and NFR at System Boundaries; Design §§ Backend: retain storage owners and publication fences, Data / Storage Changes, Developer Verification Strategy; QA Analysis §§ Coverage Matrix, Cross-system Verification.
- **Verification:** Run the backend's relevant runtime, SQLite/migration, controller, and E2E tests proving local ordinary GET/retry has no personal upstream call; hourly cadence and dropped overlaps; narrow first-load acquisition; persisted fallback/partial results; connection replacement/deletion fences; events windows; and no token or internal details in observable status.
- **Depends on:** none
- [ ] 1.2 [backend] Replace the shared investment resource DTO/mapper and endpoint responses with the target `{ data, meta }` projection, Accounts-only connection `sync`, then regenerate the backend OpenAPI from Swagger annotations.
- **Covers:** `tbank-data-sync` — `Результат личного обновления остаётся наблюдаемым`; `brokerage-accounts` — `Список активных брокерских счетов`; `brokerage-accounts` — `Состояния недоступности списка счетов`; `portfolio-view` — `Получение Portfolio выбранного Brokerage Account`; `portfolio-view` — `Свежесть и fallback полного Portfolio Snapshot`; `brokerage-account-upcoming-events` — `User can retrieve upcoming events for an account`; `brokerage-account-upcoming-events` — `Forecasts preserve event meaning and uncertainty`; `brokerage-account-upcoming-events` — `Upcoming events use complete source results`
- **Source:** System Analysis §§ Contracts, Compatibility / Migration; Design §§ Backend: one resource-meta projection and one connection-walk projection, Contract: Swagger is produced at the DTO boundary, Contract Implementation; QA Analysis §§ Cross-system Verification, Regression Impact.
- **Verification:** Regenerate backend `openapi.json` and run the backend's OpenAPI contract/type/lint tests plus runtime response tests. Confirm semantic compatibility with `system-analysis/contracts/investment-resource-envelope.schema.json`: nullable `data`; all five required `meta` meanings; `none|running|succeeded|partial|failed`; nullable attempt fields; Accounts-only nullable top-level `sync`; established routes and Problem Details; and `sync=observe` never starting source work.
- **Depends on:** 1.1
## 2. Frontend generated consumer and states
- [ ] 2.1 [frontend] Receive the published backend OpenAPI snapshot and regenerate the local transport types; project its target resource envelope directly through the existing resource-query seam without handwritten parallel types or compatibility normalization.
- **Covers:** `tbank-data-sync` — `Результат личного обновления остаётся наблюдаемым`; `brokerage-accounts` — `Список активных брокерских счетов`; `portfolio-view` — `Получение Portfolio выбранного Brokerage Account`; `brokerage-account-upcoming-events` — `User can retrieve upcoming events for an account`
- **Source:** System Analysis § Compatibility / Migration; Design §§ Contract: Swagger is produced at the DTO boundary, Frontend: one query cache and metadata-aware screen state; QA Analysis §§ Cross-system Verification, Regression Impact.
- **Verification:** Run `openapi:check` and the frontend typecheck; verify the committed generated snapshot is the backend-published artifact and generated types expose nullable `data`, required target metadata, and Accounts-only `sync`.
- **Depends on:** 1.2
- [ ] 2.2 [frontend] Update Accounts, Portfolio, operations, and events query owners, page VMs, and views to render the five metadata meanings and separate server attempt, server stale, and client HTTP-read states; retain successful snapshots, keep retry/reread HTTP-only, and preserve access invalidation.
- **Covers:** `frontend-data-resilience` — `Client Cached Snapshot остаётся доступным при обновлении Accounts и Portfolio`; `brokerage-accounts` — `Экран списка брокерских счетов`; `brokerage-accounts` — `Клиентские состояния ошибки списка счетов`; `portfolio-view` — `Состояния и ошибки Portfolio на клиенте`; `brokerage-account-upcoming-events` — `Account page presents an upcoming events list`; `brokerage-account-upcoming-events` — `Account overview includes upcoming events`
- **Source:** System Analysis §§ Error and Recovery Semantics, Observability Semantics; Design §§ Frontend: one query cache and metadata-aware screen state, UI/UX Package, Developer Verification Strategy; QA Analysis §§ Coverage Matrix: Frontend recovery and accessibility, Manual / Exploratory Scope.
- **Verification:** Run frontend unit/component tests and configured UI checks covering no automatic retry, retry as GET only, 30-minute reread, cache retention after background error, connection invalidation, no-data/empty/loaded states, distinct stale/attempt/transport warnings, keyboard-accessible status text, and narrow viewport wrapping.
- **Depends on:** 2.1
## 3. Durable AS-IS documentation
- [ ] 3.1 [docs] Update `architecture/cross-cutting/data-freshness.md` with the approved hourly-owner, local-read, first-load-exception, and independent resource/attempt/client-state model.
- **Covers:** `N/A` — approved documentation promotion; no additional product behavior.
- **Source:** Docs Impact § Documentation Changes: `architecture/cross-cutting/data-freshness.md`.
- **Verification:** Compare the page against delivered runtime responses and UI: ordinary GET/retry has no personal upstream call; target generated-contract meanings, stale fallback, no-result, client read failure, and hourly failure remain distinct.
- **Depends on:** 1.2, 2.2
- [ ] 3.2 [docs] Update `domains/portfolio/flows/portfolio-loading.md` with read-only saved snapshot loading, 30-minute local rereads, fallback semantics, and connection/revision disclosure fences.
- **Covers:** `N/A` — approved documentation promotion; no additional product behavior.
- **Source:** Docs Impact § Documentation Changes: `domains/portfolio/flows/portfolio-loading.md`.
- **Verification:** Compare the page with the delivered Portfolio route and screen: GET/retry does not call T-Bank or expose manual sync; no-data, empty, stale, failed hourly attempt, and failed HTTP reread remain distinct.
- **Depends on:** 1.2, 2.2
- [ ] 3.3 [docs] Update `domains/income/upcoming-events.md` with saved forecast window intersection, `calculatedAt`, independent failed-update state, and empty/no-result semantics.
- **Covers:** `N/A` — approved documentation promotion; no additional product behavior.
- **Source:** Docs Impact § Documentation Changes: `domains/income/upcoming-events.md`.
- **Verification:** Compare the page with delivered account and `all` events responses/screens: invalid periods return `INVALID_EVENTS_PERIOD` without source work, window intersection preserves forecast metadata, and empty intersection differs from failed-refresh fallback.
- **Depends on:** 1.2, 2.2
- [ ] 3.4 [docs] Update the investment section of `architecture/http-api.md` to identify backend-generated OpenAPI as the executable `{ data, meta }` contract and remove the request-started screen-sync model from AS-IS guidance.
- **Covers:** `N/A` — approved documentation promotion; no additional product behavior.
- **Source:** Docs Impact § Documentation Changes: `architecture/http-api.md`.
- **Verification:** Compare prose and examples with the backend-generated OpenAPI and runtime responses: target nullability/enums and required metadata match, only Accounts has top-level `sync`, and ordinary reads cannot trigger personal work.
- **Depends on:** 1.2
- [ ] 3.5 [docs] Update `adr/017-local-tbank-reads-and-background-sync.md` to preserve historical context while explicitly superseding its request-driven/manual-refresh decisions through ADR 019.
- **Covers:** `N/A` — approved documentation promotion; no additional product behavior.
- **Source:** Docs Impact § Documentation Changes: `adr/017-local-tbank-reads-and-background-sync.md`.
- **Verification:** Confirm ADR 017 explicitly identifies superseded decisions, links to ADR 019 and current freshness documentation, and no longer presents manual/request-driven sync as current architecture.
- **Depends on:** 3.1, 3.6
- [ ] 3.6 [docs] Update `adr/019-hourly-reads-and-online-instrument-reference.md` with the current-connection-scoped hourly result, ordinary read-only GET/retry boundary, two narrow acquisition exceptions, and observation-not-command rule.
- **Covers:** `N/A` — approved documentation promotion; no additional product behavior.
- **Source:** Docs Impact § Documentation Changes: `adr/019-hourly-reads-and-online-instrument-reference.md`.
- **Verification:** Confirm ADR 019 describes only the delivered hourly model and bounded exceptions, agrees with the updated freshness page, and does not introduce another HTTP contract source.
- **Depends on:** 1.2, 2.2
@@ -0,0 +1,2 @@
schema: wealth-ledger-sdd
created: 2026-10-07
@@ -0,0 +1,104 @@
## Context
`useScreenSync` уже является общим module для экранной синхронизации. Его
interface принимает scope и набор resource queries; реализация опрашивает
статусы выполняющейся работы и вызывает `observe()` для ресурсов того же
экрана. Один интервал в 1 секунду управляет обоими видами запросов, поэтому
страницы с несколькими ресурсами умножают фоновый трафик.
Нужна меньшая частота обновления при сохранении текущей cache identity,
автоматического восстановления, защиты доступа и итогового обновления сразу
после завершения работы.
## Goals / Non-Goals
**Goals:**
- Сократить периодический HTTP-трафик выполняющейся экранной синхронизации
примерно в пять раз.
- Сохранить небольшую и прежнюю interface `useScreenSync` для всех page VMs.
- Сразу обновлять ресурсы после terminal state синхронизации.
- Проверять поведение на HTTP seam, а не детали реализации TanStack Query.
**Non-Goals:**
- Изменять backend status contract, resource endpoints или модель синхронизации.
- Вводить отдельный polling module, второй cache или push-канал.
- Менять пользовательское представление загрузки и ошибок.
## Decisions
### Preserve one screen-sync module and lengthen its interval
`useScreenSync` остаётся единственным module, который владеет связью между
статусом экранной синхронизации и observe reads. Его implementation заменяет
интервал status polling с одной секунды на пять секунд. Поскольку effect уже
наблюдает ресурсы после нового status response, это одним изменением
ограничивает оба потока запросов без расширения interface для callers.
Alternative: добавить второй timer только для observe reads при status polling
раз в секунду. Отклонено: status traffic останется частым, а два независимых
ритма увеличат implementation и риск дублирующих наблюдений без требуемой
пользы.
Alternative: читать ресурсы только после terminal state. Отклонено: это
устранит промежуточную публикацию уже готовых данных и изменит существующее
поведение сильнее, чем требуется.
### Keep completion observation driven by the terminal status response
Текущий effect уже distinguishes running и completed status: для terminal state
он observes every resource, включая тот, который был fetching. Этот путь
сохраняется. Terminal response не ждёт отдельного timer, поэтому итоговый
результат появляется в обработке того же status response; максимальная
задержка возникает только до следующей пятиисекундной проверки.
### Test the public HTTP outcome at the screen-sync seam
Colocated tests `screen-sync` будут использовать MSW и fake timers, чтобы
проверить отсутствие повторного status/read traffic до пяти секунд, следующий
цикл после интервала и одно итоговое observe read при terminal status. Existing
tests продолжат доказывать, что уход с экрана останавливает polling и что
observe reads не создают новую импортирующую работу.
## UI/UX Applicability and Package
**Applicability and scope:** N/A. Изменяется только частота невидимых фоновых
запросов; экраны, состояния, interactions и responsive behavior не меняются.
## Delivery Map
| Area | Status | Architectural outcome or N/A reason |
| --- | --- | --- |
| frontend | affected | `useScreenSync` реже опрашивает статус и тем самым реже наблюдает ресурсы, сохраняя немедленное обновление после terminal state. |
| backend | N/A | Status и resource contracts не меняются. |
| specs | affected | Delta фиксирует пятиисекундную верхнюю частоту и итоговое наблюдение после terminal state. |
| docs | N/A | Пользовательские и эксплуатационные инструкции не меняются. |
## Risks / Trade-offs
- Промежуточно готовые данные могут появиться до четырёх секунд позже, чем
раньше; пользователь явно выбрал меньший трафик в обмен на эту задержку.
- Terminal state обнаруживается следующей status-проверкой, поэтому его
отображение также может ждать до пяти секунд после фактического завершения
backend work. После обнаружения дополнительной задержки нет.
- Смена только общей константы затрагивает все экраны-потребители; тесты
module защищают единое ожидаемое поведение.
## Migration Plan
1. Изменить policy interval у существующего screen-sync module.
2. Адаптировать и дополнить его seam tests для пятиисекундного ритма и
terminal refresh.
3. Выполнить локальные frontend quality gates перед поставкой.
### Rollback Contract
| Field | Decision |
| --- | --- |
| Status | `N/A` |
| Reason | Изменение локальной frontend polling policy не меняет данные, публичный contract, security или миграции; rollback состоит в возврате прежнего значения интервала. |
| Trigger | N/A |
| Actions | N/A |
| Data safety | N/A |
| Verification | N/A |
@@ -0,0 +1,50 @@
## Why
Пока экран ожидает синхронизацию T-Bank, клиент опрашивает её состояние каждую
секунду. Каждый новый ответ запускает наблюдение всех ресурсов открытого экрана.
На экранах с несколькими финансовыми выборками это создаёт непропорционально
много запросов при том, что пользователь готов увидеть промежуточный результат
с небольшой задержкой.
## What Changes
- Во время выполняющейся экранной синхронизации приложение реже проверяет её
состояние и обновляет наблюдаемые финансовые ресурсы не чаще одного раза за
пять секунд.
- После terminal state синхронизации приложение сразу наблюдает ресурсы
открытого экрана, чтобы отобразить итоговый результат без дополнительной
задержки.
- Существующие состояния ожидания, доступные частичные результаты,
автоматическое восстановление чтения и прекращение работы после ухода с
экрана сохраняются.
Ожидаемый результат: длительная синхронизация создаёт существенно меньше
фоновых HTTP-запросов, а пользователь получает промежуточные результаты с
задержкой не более пяти секунд и итоговые данные сразу после завершения.
Non-goals:
- Изменять запуск синхронизации, её scope, доступ к данным или финансовые
расчёты.
- Добавлять новые пользовательские элементы, ручное управление частотой или
отдельный канал доставки изменений.
- Гарантировать обновления финансовых данных в реальном времени.
## Capabilities
### New Capabilities
<!-- None. -->
### Modified Capabilities
- `specs/frontend-data-resilience/spec.md`: ограниченная частота наблюдения
ресурсов во время выполняющейся синхронизации и немедленное наблюдение после
terminal state.
## Impact
Изменение затрагивает финансовые экраны, использующие экранную синхронизацию:
счета, детали счёта, операции, доходы, события и страницы инструментов. Оно
сокращает фоновый HTTP-трафик при продолжающейся синхронизации, сохраняя
автоматическое отображение готовых данных и существующие правила устойчивости.
@@ -0,0 +1,85 @@
## MODIFIED Requirements
### Requirement: Временные сбои чтения восстанавливаются автоматически
Frontend SHALL автоматически повторять временно неуспешное чтение и
восстанавливать получение необходимых текущему экрану данных без ручного
обновления. Повторы одной попытки MUST иметь конечный предел и паузы,
исключающие непрерывную череду запросов. При исчерпании предела доступные данные
SHALL сохраняться, а при их отсутствии SHALL показываться локальная
недоступность. Возвращение сети, возвращение пользователя к открытому экрану и
повторное открытие экрана MUST возобновлять получение отсутствующих или
требующих обновления данных без перезагрузки страницы. Свежий полный результат
MUST NOT требовать обращения к источнику только из-за такого события.
Наблюдая выполняющуюся экранную синхронизацию, frontend MUST проверять её
состояние и повторно читать связанные ресурсы не чаще одного раза за пять
секунд. После получения terminal state этой синхронизации frontend MUST сразу
наблюдать ресурсы текущего экрана, не ожидая следующего пятиисекундного
интервала.
Восстановление MUST учитывать сбой чтения результата и сбой самого получения
данных из источника, даже если чтение состояния завершилось успешно. Наблюдение
уже выполняющейся работы MUST NOT запускать её повторно. Уход с экрана MUST
прекращать новые попытки его восстановления; это не требует отменять уже
начатую работу источника. Автоматическое восстановление MUST NOT повторять
запросы, требующие исправления ввода, входа или восстановления доступа, и MUST
NOT переносить старые параметры в новую пользовательскую выборку.
#### Scenario: Кратковременный сбой чтения
- **WHEN** чтение текущей выборки временно не удалось, а следующая разрешённая
автоматическая попытка успешна
- **THEN** экран самостоятельно показывает результат без ручного действия и без
сообщения о сбое или восстановлении
#### Scenario: Экранная синхронизация продолжается
- **WHEN** экран наблюдает синхронизацию, которая остаётся в состоянии running
после очередной проверки
- **THEN** frontend выполняет следующую проверку состояния и наблюдение
связанных ресурсов не ранее чем через пять секунд
#### Scenario: Экранная синхронизация завершилась
- **WHEN** очередная проверка экранной синхронизации возвращает terminal state
- **THEN** frontend сразу наблюдает связанные ресурсы текущего экрана, чтобы
показать итоговый результат без ожидания следующего интервала
#### Scenario: Источник завершил работу ошибкой
- **WHEN** чтение сохранённых данных успешно, но получение необходимых новых
данных из источника временно не удалось
- **THEN** приложение применяет ограниченное автоматическое восстановление к
неуспешной работе, а не считает успешное чтение доказательством её успеха
#### Scenario: Повторы исчерпаны
- **WHEN** временная недоступность сохраняется после конечного предела
автоматических попыток
- **THEN** приложение прекращает серию повторов, сохраняет доступный результат
либо локальную недоступность без бесконечной загрузки и ожидает следующего
события восстановления
#### Scenario: Сеть или пользователь вернулись
- **WHEN** после временного сбоя восстановилась сеть либо пользователь вернулся
к экрану с отсутствующим или требующим обновления результатом
- **THEN** получение данных возобновляется автоматически для текущей выборки и
при успехе заменяет состояние недоступности содержимым
#### Scenario: Несколько событий восстановления совпали
- **WHEN** сеть восстановилась одновременно с возвращением пользователя на экран
- **THEN** приложение не запускает дублирующую работу получения одного набора
данных
#### Scenario: Работа источника ещё выполняется
- **WHEN** приложение восстанавливает чтение состояния уже выполняющейся работы
- **THEN** оно продолжает получение её результата без повторного запуска той же
работы
#### Scenario: Пользователь покинул экран
- **WHEN** пользователь ушёл со страницы с ожидающим восстановлением
- **THEN** новые попытки для покинутого экрана прекращаются, а уже начатая работа
не изменяет содержимое другой выборки
#### Scenario: Ошибка требует действия пользователя
- **WHEN** подтверждены недействительное подключение, отсутствие доступа или
некорректный ввод
- **THEN** автоматическая серия повторов прекращается и появляется
соответствующее действие или локальная ошибка без технической кнопки повторной
загрузки
@@ -0,0 +1,30 @@
## 1. Screen synchronization policy
- [x] 1.1 [frontend] Change the screen-sync status polling policy from one
second to five seconds while keeping the existing `useScreenSync` interface
and terminal-state resource observation.
- **Covers:** `frontend-data-resilience` — `Временные сбои чтения восстанавливаются автоматически`
- **Verification:** Run the colocated screen-sync test file and confirm its
MSW assertions show no subsequent status or observe request before five
seconds, with resources observed on a terminal status response.
- [x] 1.2 [frontend] Update colocated screen-sync tests with fake timers to
cover the five-second running cadence, immediate terminal observation, and
existing stop-on-unmount behavior.
- **Covers:** `frontend-data-resilience` — `Временные сбои чтения восстанавливаются автоматически`
- **Verification:** `nvm use --silent && npm test -- screen-sync.test.tsx`
## 2. Frontend delivery
- [x] 2.1 [frontend] Run the frontend's canonical verification after the
polling-policy and regression-test changes.
- **Covers:** `N/A` — This task verifies the affected frontend delivery; it
introduces no separate observable behavior beyond the requirement covered
by tasks 1.1 and 1.2.
- **Verification:** `nvm use --silent && npm run verify`
- [x] 2.2 [specs] Verify the completed OpenSpec change against the
`wealth-ledger-specs` store before implementation review.
- **Covers:** `N/A` — This task validates the planning artifacts and their
traceability; it does not introduce runtime behavior.
- **Verification:** `openspec validate --change reduce-screen-sync-polling --store wealth-ledger-specs`
@@ -0,0 +1,2 @@
schema: wealth-ledger-sdd
created: 2026-09-20
@@ -0,0 +1,179 @@
## Context
Согласован отдельный репозиторий `deploy` и команда `./deploy.sh <tag>` по SSH.
Тег принадлежит комплекту, а не компоненту; соответствие пары хранится в
`release.env`. Решения приняты в обсуждении с владельцем и записаны в ADR 016
репозитория docs; эти артефакты готовят реализацию, не подтверждают её выполнение.
На сервере уже есть Compose project `wealthledger`, SQLite, постоянные ключи
и read-only mount `certs/tbank-ca.pem` через `TBANK_CA_CERT_PATH`. Внешний HTTPS
и Docker подготовлены. Образы публикуются в Nexus независимыми релизами.
`containerized-application-delivery` остаётся отдельным active change;
его runtime-контракт используется без редактирования или преждевременного archive.
## Goals / Non-Goals
**Goals:** небольшой комплект с воспроизводимой парой образов, одним entrypoint,
безопасным обновлением SQLite и понятным восстановлением существующего сервера.
**Non-Goals:** provisioning, CI/CD автодеплой, новый registry, Kubernetes,
Ansible-инфраструктура, blue-green, замена БД, backup daemon, автоматическая
ротация копий, собственная система миграций или управление HTTPS.
## Decisions
### Комплект и постоянные ресурсы
Минимальное содержимое `deploy`: `compose.yaml`, `release.env`, `.env.example`,
`deploy.sh`, README и одна исполняемая проверка сценариев. Compose использует
готовые frontend/backend images, migration запускается из backend image.
Production-комплект самодостаточен и не требует соседних checkout.
Существующие root Compose-файлы для development не удаляются этим change.
На сервере checkout располагается отдельно от `data/`, `certs/`, `backups/`
и защищённого server env. Например, checkout — `/opt/wealthledger/deploy`,
остальное — соседние пути в `/opt/wealthledger`. Существующий data path
не переименовывается; пути bind mounts абсолютные. Ключи не генерируются
повторно. Root filesystem остаётся read-only, процесс non-root, backend
не получает host port; сертификат доверия монтируется read-only без отключения TLS.
`release.env` содержит только полные versioned image references из Nexus;
`latest` исключён. Server env хранит настройки и секреты, но не выбирает пару.
Переменные окружения вызывающего shell не должны переопределять выбранные
images или Compose project. Env-файлы обрабатываются Compose как данные,
не выполняются shell через `source`. Полный config с секретами не печатается.
### Исполнение выбранной версии
Entrypoint получает тег только из настроенного доверенного `origin`, разрешает
его в commit и использует содержимое этого commit для всей операции, включая
версию deploy-логики. Полученный ранее тег не перезаписывается при расхождении.
Работа идёт из временного snapshot выбранного commit; запущенный скрипт и
операторские изменения checkout не перезаписываются переключением ветки.
Compose file, env files и project `wealthledger` задаются явно: временная
директория не должна создать второй project или изменить пути данных.
До остановки выполняются проверки входов, ресурсов, конфигурации и загрузка
образов. Поддерживаемый target — один Linux Docker host; Git, Bash и `flock`
входят в перечисленные README предпосылки. Архитектура опубликованных образов
должна соответствовать серверу; multi-platform поддержку этот change не добавляет.
### Обновление, повтор и состояние
Один `flock` на установку исключает конкурирующие запуски. Не добавляется
сервис блокировок. Файл блокировки и минимальные служебные записи находятся
вне checkout, с ограниченными правами, без секретов.
Нужны только запись последнего успешного релиза и маркер незавершённого
обновления: выбранный commit, фактические images, этап и расположение backup.
Маркер сохраняется до первого изменения runtime; успешная запись атомарно
обновляется только после готовности затронутых сервисов. При прерывании
маркер остаётся для диагностики; новый запуск требует ручного разбора по README.
Это не журнал всех deployments и не отдельная база состояний.
При первом переходе предыдущая версия описывается фактической парой images:
несуществующий тег прежнего комплекта не придумывается.
План изменений сверяет желаемые images и Compose service configuration
с фактическими контейнерами. Для сравнения использовать native Compose
service hash и Docker labels, если их соответствие подтверждено проверкой.
Секреты не записываются в служебные файлы. Отсутствие достоверного baseline
не разрешает frontend-only shortcut: первый переход с серверного Compose
выполняется как полное обновление с backup. Успешная запись сама по себе
не заменяет проверку фактически работающих контейнеров.
При обновлении API: stop backend → проверка отсутствия migration writer →
новый уникальный каталог backup → копия всего data directory, включая WAL/SHM →
одноразовая migration → backend readiness. Для существующей установки отсутствие
`wealthledger.db` — ошибка. Только явный первый запуск `--init` на пустом
каталоге без прежнего runtime допускает создание новой базы без backup.
Копия отмечается пригодной для восстановления только после успешного окончания
копирования; незавершённый каталог не выдаётся за готовый backup.
Неизменный frontend не пересоздаётся; обновление только frontend не запускает
зависимости. При обновлении обоих компонентов новый frontend запускается после
готовности backend. Healthchecks используются из готовых images через Compose;
новый HTTP endpoint не нужен. Browser smoke через HTTPS остаётся отдельной
проверкой выпуска: container health не доказывает login/refresh.
Backup failure и migration failure прекращают последовательность. При провале
readiness backend останавливается. Провал frontend не останавливает исправный
backend. Любая ошибка оставляет релиз неподтверждённым, текущую БД и backup
неизменёнными процедурой восстановления; диагностика не выводит secrets.
### Выпуск и инструкция
После публикации компонентов оператор меняет пару в `release.env`, проверяет
её на disposable установке и выпускает отдельный неизменяемый `vX.Y.Z` тег
`deploy`. Компонентные pipeline не связываются автоматическим publisher.
Первый комплект закрепляет реально доступные образы, не демонстрационные версии.
README описывает подготовку Docker/Git/registry/HTTPS, разрешения каталогов,
сохранение ключей, подключение действующего Compose project, install/update,
диагностику незавершённой операции, ручную очистку backups и recovery.
## UI/UX Applicability and Package
**Applicability and scope:** `N/A` — браузерный интерфейс и его состояния
не меняются; оператор использует терминал и README. Макеты не нужны.
## Delivery Map
| Area | Status | Architectural outcome or N/A reason |
| --- | --- | --- |
| frontend | `N/A` | Готовый image переиспользуется, UI и pipeline не меняются |
| backend | `N/A` | Готовые runtime, healthcheck и migrations переиспользуются |
| specs | `affected` | Контракт deployment-bundle и план его проверки |
| docs | `affected` | ADR 016 и обновлённый эксплуатационный маршрут после реализации |
| deploy | `affected` | Самодостаточный комплект, сценарии проверки и README |
| root | `affected` | Только навигация README к production-комплекту; development Compose не меняется |
Уточнение старого шаблона: обязательные четыре области сохранены; добавлен
реальный repository owner `deploy`, явно выбранный пользователем, и `root`
для необходимой поправки workspace-навигации. В Tasks используются `[deploy]`
и `[root]`, а не фиктивный `[backend]` или `[workspace]`.
Это локальное расширение перечня owners для данного change; глобальная
переработка SDD schema и process specs в scope не входит. Native structural
validation не доказывает соответствие этому уточнению или semantic approval.
## Solution Assessment
| Вариант | Fit, зрелость и безопасность | Стоимость и ограничения |
| --- | --- | --- |
| Только Docker Compose | Зрелый runtime, зависимости, healthchecks и выбор images уже покрыты | Не выполняет stop/copy/migrate и не исключает две операторские операции |
| Bash + Git + Compose + flock | Native инструменты владеют загрузкой, запуском и блокировкой; shell связывает существующий безопасный порядок | Небольшой branching требует проверок ошибок и прерываний; подходит одному Linux host |
| Ansible | Зрелый механизм управления серверами, но backup/migration порядок всё равно надо определить | Добавляет control environment, inventory и роль при уже подготовленном одном сервере |
Выбран второй вариант. Чистого Compose недостаточно для согласованной копии
при остановленной записи; отдельный provisioning stack не соответствует
согласованному отсутствию управления серверной инфраструктурой. Новый package
manager, framework или сторонняя runtime-зависимость не вводятся.
## Risks / Trade-offs
- Краткий простой и единственный writer сохраняются; внешние процессы записи
должны быть исключены оператором при подключении существующей установки.
- Локальный backup не спасает от потери диска; свободное место и ручная очистка
остаются эксплуатационной обязанностью. Ошибка копирования блокирует миграции.
- Native hash может измениться между версиями Compose: сомнение приводит
к полному безопасному обновлению, а не к пропуску backup.
- Прерванная migration требует разбора, а не слепого повторения команды.
Ни служебный маркер, ни последний успешный тег не являются копией базы.
- Доверие к Git origin и неизменяемым Nexus release tags остаётся существующей
границей поставки; собственная система подписей здесь не добавляется.
## Migration Plan
Подготовить самодостаточный комплект и проверить безопасный lifecycle на
disposable данных. Затем обновить docs, сохранив development-инструкцию.
Для действующего сервера сохранить старые настройки и соответствие images,
подключить существующие абсолютные пути и project, выполнить первый полный
переход с backup. Старый способ запуска не выполнять параллельно.
### Rollback Contract
| Field | Decision |
| --- | --- |
| Status | `required` |
| Trigger | Ошибка migration/readiness, прерывание либо выявленная регрессия |
| Actions | Остановить API при риске для данных; отдельно выбрать совместимый image либо explicit restore; незавершённый маркер снять только после проверки состояния |
| Data safety | Не восстанавливать БД автоматически; перед explicit restore сохранить текущий каталог, выбрать копию, подтвердить потерю новых записей и наличие прежнего ключа |
| Verification | На отдельной копии проверить целостность БД, совместимость схемы, auth/cards и расшифровку подключения; после восстановления повторить smoke через HTTPS |
@@ -0,0 +1,40 @@
## Why
После выпуска компонентов оператор вручную выбирает совместимые версии
и повторяет порядок обновления. Нужен один выбранный релиз всей установки
и одна команда, сохраняющая данные и настройки существующего сервера.
## What Changes
- Версионируемый комплект закрепляет проверенную пару версий интерфейса и API.
- Оператор явно выбирает релиз; публикация компонента не обновляет сервер.
- Обновление выполняется одной командой с предварительными проверками,
сохранением данных и диагностируемым результатом.
- Обновление только интерфейса не прерывает работу API и не затрагивает базу.
- Перед обновлением API создаётся согласованная резервная копия. Неудачное
обновление не запускает автоматическое восстановление или откат.
- Инструкция охватывает подготовку сервера, первую установку, подключение
существующей установки, выпуск комплекта, обновление и ручное восстановление.
Non-goals: настройка серверной инфраструктуры командой развёртывания,
автоматическое обновление по выходу компонента, непрерывная доступность,
несколько экземпляров API, автоматическая очистка копий и внешнее backup-хранилище.
## Capabilities
### New Capabilities
- `specs/deployment-bundle/spec.md`: выбор полного релиза, безопасная автоматизация
обновления и сохранение существующей установки.
### Modified Capabilities
Нет. Новый контракт дополняет контейнерную поставку; существующий active change
`containerized-application-delivery` и его capability `application-delivery`
не изменяются и не архивируются в рамках этой работы.
## Impact
Основной пользователь — оператор одной установки WealthLedger. Изменяются
комплект развёртывания, эксплуатационная документация и требования к обновлению.
Прикладные функции интерфейса и API остаются прежними.
@@ -0,0 +1,169 @@
## Purpose
Определяет установку выбранного релиза WealthLedger одной командой,
сохранение состояния сервера и безопасный результат успешного или неудачного обновления.
## ADDED Requirements
### Requirement: Релиз определяет полную пару компонентов
Комплект SHALL иметь отдельную неизменяемую версию, однозначно определяющую
проверенную пару версий интерфейса и API. Оператор MUST явно выбирать релиз
комплекта. Публикация компонента MUST NOT автоматически выпускать комплект
или обновлять сервер; локальные настройки MUST NOT незаметно подменять пару.
#### Scenario: Установка выбранного релиза
- **WHEN** оператор выбирает опубликованный релиз комплекта
- **THEN** устанавливается ровно закреплённая в нём пара, даже если опубликованы
более новые версии компонентов
#### Scenario: Отсутствующий или изменённый релиз
- **WHEN** выбранный релиз отсутствует либо ранее полученная версия теперь
указывает на другое содержимое
- **THEN** операция завершается ошибкой до изменения работающей установки
### Requirement: Проверки предшествуют изменению установки
Одна команда SHALL выполнять установку выбранного релиза на подготовленном
сервере. До остановки приложения она MUST проверить доступность комплекта,
его компонентов, обязательных настроек и постоянных ресурсов.
Отсутствующая база существующей установки MUST NOT трактоваться как новая
пустая установка. Секреты MUST NOT попадать в диагностический вывод.
#### Scenario: Компонент недоступен
- **WHEN** выбранный компонент нельзя получить или обязательный сертификат
либо каталог данных недоступен
- **THEN** обновление завершается с понятной ошибкой, не останавливая
работающие компоненты и не создавая замену отсутствующим данным
#### Scenario: Новая установка
- **WHEN** оператор явно подготовил новую установку с отдельным пустым
хранилищем и необходимыми настройками
- **THEN** команда подготавливает данные и запускает выбранный релиз,
не требуя предыдущей установленной версии
### Requirement: Существующая установка сохраняет состояние
Подключение существующей установки и последующие обновления MUST сохранять
её пользовательские данные, сессии, ключи шифрования и настройки доверия
внешнему провайдеру. Эти ресурсы MUST храниться независимо от обновляемого
комплекта. Команда MUST NOT запускать второй API на той же базе или генерировать
новые ключи вместо действующих.
#### Scenario: Переход с прежнего способа запуска
- **WHEN** оператор подключает существующую установку к комплекту
- **THEN** используются её прежние данные и ключи; после запуска доступны
существующие записи и расшифровка сохранённого подключения
### Requirement: Обновление затрагивает только изменённые компоненты
Если меняется только интерфейс, команда MUST сохранять работающий API,
не изменять данные и не запускать миграции или резервное копирование.
Изменение настроек API SHALL считаться изменением API, даже при той же версии.
Обновление только API MUST NOT пересоздавать неизменный интерфейс.
Повторное применение уже установленного исправного релиза с теми же настройками
MUST NOT выполнять ненужное обновление или резервное копирование.
#### Scenario: Новый интерфейс при прежнем API
- **WHEN** выбранный релиз меняет только интерфейс, а API и его настройки
подтверждённо совпадают с установленными
- **THEN** обновляется только интерфейс, API продолжает работу без миграций
и резервного копирования для этой операции
#### Scenario: Та же версия API с другими настройками
- **WHEN** версия API прежняя, но его настройки изменились
- **THEN** применяется безопасная процедура обновления API
#### Scenario: Повтор успешно установленного релиза
- **WHEN** выбран уже установленный исправный релиз без изменений настроек
- **THEN** команда сообщает, что обновление не требуется, без остановки
компонентов и без новой резервной копии
### Requirement: Обновление API сохраняет согласованную копию
Перед обновлением API существующей установки команда MUST остановить запись
и успешно сохранить согласованную копию всех постоянных данных до миграций.
Копии MUST находиться вне обновляемого комплекта; команда MUST NOT удалять
или перезаписывать прежние копии автоматически. Неуспешная копия MUST
блокировать миграции. Новая пустая установка не требует фиктивного backup.
#### Scenario: Подготовка обновления API
- **WHEN** выбран релиз с изменением API существующей установки
- **THEN** до миграций сохраняется отдельная согласованная копия при отсутствии
пишущих процессов, и оператор получает её расположение
#### Scenario: Копию сохранить не удалось
- **WHEN** резервное копирование завершилось ошибкой
- **THEN** миграции и запуск новой версии не выполняются, существующие данные
и прежние копии сохраняются, операция сообщает ошибку
### Requirement: Неудачное обновление не скрывает риск для данных
Ошибка миграции MUST блокировать запуск новой версии API. Если после миграции
API не готов, команда MUST остановить неисправный API и завершиться с ошибкой.
Любая неуспешная проверка обновляемого компонента MUST блокировать отметку
релиза как успешно установленного. Диагностика SHALL указывать этап сбоя,
предыдущую версию и доступный backup без раскрытия секретов.
Автоматический откат или восстановление данных MUST NOT выполняться.
#### Scenario: Миграция не прошла
- **WHEN** подготовка схемы завершается ошибкой
- **THEN** API остаётся остановленным, а результат содержит сведения для
отдельного решения о восстановлении
#### Scenario: Новый API не готов
- **WHEN** миграция завершилась успешно, но проверка готовности API не прошла
- **THEN** неисправный API останавливается без автоматического запуска старой
версии или восстановления базы
#### Scenario: Неудачное обновление интерфейса
- **WHEN** при обновлении только интерфейса его проверка запуска не прошла
- **THEN** операция завершается ошибкой, API продолжает работу, база не
восстанавливается, релиз не отмечается успешно установленным
### Requirement: Повтор и конкуренция не нарушают порядок обновления
Команда MUST исключать одновременные изменения одной установки несколькими
запусками. Прерванное или неуспешное обновление MUST отличаться от успешно
установленного релиза. Повтор команды после потенциального изменения данных
MUST NOT скрывать незавершённую операцию или автоматически восстанавливать данные.
#### Scenario: Второе обновление
- **WHEN** для установки уже выполняется обновление
- **THEN** второй запуск завершается без изменения её состояния
#### Scenario: Процесс прерван после начала обновления
- **WHEN** предыдущая операция прервана после остановки API или начала миграции
- **THEN** следующий запуск сообщает о незавершённой операции и необходимости
проверки состояния до нового обновления
### Requirement: Оператор располагает полным порядком эксплуатации
Инструкция комплекта SHALL описывать последовательность подготовки сервера,
первой установки и подключения существующей установки, подготовку проверенной
пары и выпуск релиза, обновление, диагностику, откат версии и отдельное
восстановление базы. Она MUST указывать сохранение ключей, ручную очистку
локальных копий и отсутствие защиты от потери диска только локальным backup.
Восстановление MUST требовать явного выбора копии и принятия потери более
новых данных; откат API — проверки совместимости с текущей схемой.
#### Scenario: Оператор восстанавливает установку
- **WHEN** оператор использует инструкцию после неудачного обновления
- **THEN** он может отдельно выбрать совместимую версию приложения или
восстановление конкретной копии с явным учётом потери новых данных
@@ -0,0 +1,68 @@
## 1. Самодостаточный комплект
- [x] 1.1 [deploy] Создать production Compose, release.env и пример server env, переиспользовав существующие images/migration/healthchecks и сохранив read-only CA mount и постоянные абсолютные пути.
- **Covers:** `deployment-bundle` — `Релиз определяет полную пару компонентов`; `deployment-bundle` — `Существующая установка сохраняет состояние`
- **Verification:** `docker compose --env-file <disposable-server.env> --env-file release.env -f compose.yaml config --quiet`; inspect подтверждает выбранную пару, один backend, общий data mount migration/backend, отсутствие host port API и read-only сертификат; exported image variables не подменяют релиз через entrypoint.
- [x] 1.2 [deploy] Реализовать получение выбранного тега, выполнение snapshot его commit, preflight, flock и минимальные записи состояния в одном deploy.sh согласно Solution Assessment.
- **Covers:** `deployment-bundle` — `Релиз определяет полную пару компонентов`; `deployment-bundle` — `Проверки предшествуют изменению установки`; `deployment-bundle` — `Повтор и конкуренция не нарушают порядок обновления`
- **Verification:** `bash -n deploy.sh`; сценарная проверка отсутствующего/переназначенного тега, недоступного image/CA/data, отказа второго запуска и отсутствия изменений runtime при preflight failure; snapshot использует deploy-логику выбранного commit.
## 2. Безопасный lifecycle и его доказательство
- [x] 2.1 [deploy] Реализовать first install/adoption, выбор изменённых сервисов, stop/backup/migration/readiness и остановку последовательности при ошибке без автоматического rollback.
- **Covers:** `deployment-bundle` — `Проверки предшествуют изменению установки`; `deployment-bundle` — `Существующая установка сохраняет состояние`; `deployment-bundle` — `Обновление затрагивает только изменённые компоненты`; `deployment-bundle` — `Обновление API сохраняет согласованную копию`; `deployment-bundle` — `Неудачное обновление не скрывает риск для данных`; `deployment-bundle` — `Повтор и конкуренция не нарушают порядок обновления`
- **Verification:** На disposable Compose project проверить явный `--init`, adoption существующей тестовой БД, обновление обоих компонентов и API отдельно, неизменность container ID API при frontend-only update, отсутствие повторного backup при healthy no-op; изменение backend config включает backup даже с прежним image.
- [x] 2.2 [deploy] Добавить одну исполняемую проверку tests/deploy.sh без нового test framework и закрепить локальный gate для shell и сценариев обновления.
- **Covers:** `deployment-bundle` — `Релиз определяет полную пару компонентов`; `deployment-bundle` — `Проверки предшествуют изменению установки`; `deployment-bundle` — `Существующая установка сохраняет состояние`; `deployment-bundle` — `Обновление затрагивает только изменённые компоненты`; `deployment-bundle` — `Обновление API сохраняет согласованную копию`; `deployment-bundle` — `Неудачное обновление не скрывает риск для данных`; `deployment-bundle` — `Повтор и конкуренция не нарушают порядок обновления`
- **Verification:** `bash -n deploy.sh tests/deploy.sh`, `bash tests/deploy.sh`, `git diff --check`; fault injection доказывает отсутствие migration после failed copy, отсутствие backend start после failed migration, stop после backend readiness failure, сохранение backend после frontend failure, неизменность данных/старых backups, блокировку конкуренции и незавершённой операции. Проверка использует временные данные; production paths недопустимы.
## 3. Эксплуатация и документация
- [x] 3.1 [deploy] Описать в README подготовку сервера, сохранение старых ключей и Compose project, install/update, выпуск тега с проверенной парой, диагностику, ручной rollback/restore и очистку backups; закрепить реально опубликованную первую пару перед выпуском.
- **Covers:** `deployment-bundle` — `Оператор располагает полным порядком эксплуатации`; `deployment-bundle` — `Релиз определяет полную пару компонентов`
- **Verification:** Пройти инструкцию на изолированной установке с тестовыми данными; проверить login/refresh через HTTPS, сохранение записей и расшифровку тестового подключения прежним ключом, `PRAGMA integrity_check` на копии и explicit restore; сравнить actual images с release.env. Первый тег не публиковать до проверки пары.
- [x] 3.2 [docs] Завершить ADR 016 и glossary, обновить production-раздел контейнерной поставки под deploy с сохранением development-инструкции и границ ручного recovery.
- **Covers:** `N/A` — docs объясняет поставленный механизм из Design и направляет оператора к единственному README комплекта; самостоятельного runtime behavior не добавляет.
- **Verification:** После `nvm use --silent` выполнить `npm run verify` в docs; проверить маршрут README → deployment → ADR и отсутствие обещаний ещё не реализованных операций или дублирующего production runbook.
- [x] 3.3 [root] Направить production-раздел root README к комплекту deploy, сохранив существующие development-команды.
- **Covers:** `N/A` — workspace-навигация должна вести к владельцу production lifecycle из Design; поведение приложения и root tooling не меняются.
- **Verification:** `git diff --check` в root; просмотр маршрута root README → deploy README и docs/architecture/deployment.md, проверка неизменности development-команд.
## 4. Сверка поставки
- [x] 4.1 [specs] Сверить артефакты с фактической реализацией, сохранить coverage всех требований и проверить canonical change без изменения чужого containerized-application-delivery.
- **Covers:** `N/A` — requirements и checklist должны оставаться контрактом реализованного комплекта; задача поддерживает точность SDD, а не вводит новое поведение приложения.
- **Verification:** После `nvm use --silent`: `openspec schema validate wealth-ledger-sdd --verbose`; `openspec validate versioned-deployment-bundle --type change --strict --store wealth-ledger-specs`; `openspec validate --specs --strict --store wealth-ledger-specs`; `npm run verify`; вручную проверить task coverage и соответствие дополнительному owner deploy из Delivery Map.
- [x] 4.2 [workspace] Выполнить итоговую сверку сценариев установки, ошибок и восстановления с готовыми образами и результатами локальных gates затронутых репозиториев.
- **Covers:** `N/A` — объединяет доказательства deploy/specs/docs для согласованной пары, не владеет реализацией какого-либо репозитория и не заменяет independent review.
- **Verification:** На disposable данных сверить успешный update, frontend-only, interrupted update и recovery с Delta Specs; зафиксировать фактические версии и результаты gates. Проверки реального сервера не подменять локальным smoke; production rollout не входит в эту задачу.
## 5. Implementation checkpoint
2026-09-20: implementation-owned задачи выполнены; готово к независимой Validation.
Change остаётся active. Первый тег deploy не опубликован, рабочий сервер не изменялся.
- Deploy: `bash -n deploy.sh tests/deploy.sh`, `bash tests/deploy.sh` — PASS.
Проверены selective update/no-op, config drift, сохранение ключей, symlink guard,
ошибки pull/preflight/copy/migration/backend/frontend, concurrent lock и pending.
- `DOCKER_DEFAULT_PLATFORM=linux/amd64 bash tests/deploy.sh --docker-restore` — PASS:
заменённый bind-каталог читается после force-recreate. Без пересоздания исходный
live repro читал старые данные; README recovery исправлен по этому результату.
- Disposable smoke с реальными Nexus images `0.2.3` — PASS: init, no-op по native
Compose hash, frontend-only/backend-only config update, stopped backup,
`PRAGMA integrity_check`, HTTPS deep link, registration, Secure-cookie и refresh.
- Explicit restore на синтетических данных — PASS: login, карта и её баланс,
отсутствие более новой карты и расшифровка тестового токена штатным TbankCrypto.
Оба тестовых Compose projects удалены; настоящие пользовательские данные
и реальное подключение к T-Bank не использовались.
- OCI revisions совпали с Git tags компонентов: frontend
`f486ae4fb2d06e71779901bde283950542f27ed2`, backend
`0862d56c4e4d9dd554556c3992cc073193020ff1`.
- Среда интеграционной проверки: Docker Desktop, Linux amd64 images через
эмуляцию на arm64 Mac, Compose 5.5.1; host flock в smoke заменён stdlib fcntl.
Это не проверка конфигурации целевого Linux-сервера или его registry login.
@@ -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
@@ -0,0 +1,2 @@
schema: wealth-ledger-sdd-v2
created: 2026-10-14

Some files were not shown because too many files have changed in this diff Show More