23 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
102 changed files with 6762 additions and 800 deletions
+1 -1
View File
@@ -2,7 +2,7 @@ name: Conventional commits
on:
pull_request:
types: [opened, synchronize, reopened]
types: [opened, synchronize, reopened, edited]
concurrency:
group: specs-commits-${{ gitea.event.pull_request.number }}
+1 -1
View File
@@ -1 +1 @@
24.21.0
24.22.1
+6 -1
View File
@@ -26,7 +26,12 @@ anchors и `git diff --check`; application gates принадлежат affected
## Ветки и релизы
Работа — в issue-linked ветке от `develop`, затем PR в `develop`; в `main` напрямую
не пушить. Release/hotfix и синхронизация веток — по [README](README.md#релизы).
не пушить. PR сливается squash или rebase-merge; merge-коммит не используется.
`main` обновляется только fast-forward из `develop`, поэтому `origin/main`
всегда предок `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; версии вручную не повышать.
+6
View File
@@ -16,8 +16,13 @@ types. CI каждого участника использует только е
| --- | --- |
| 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, 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) |
| 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) |
| Будущие выплаты | [Upcoming events](openspec/specs/brokerage-account-upcoming-events/spec.md) |
| Доходы и периоды | [Income analytics](openspec/specs/brokerage-account-income-analytics/spec.md) |
@@ -25,6 +30,7 @@ types. CI каждого участника использует только е
| Каталог компонентов | [UI catalog](openspec/specs/frontend-ui-catalog/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) |
| Тихое чтение и редкие уведомления | [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) |
| OpenAPI generation | [API contract generation](openspec/specs/api-contract-generation/spec.md) |
| Logging/request context | [Structured logging](openspec/specs/backend-structured-logging/spec.md) |
+29
View File
@@ -1,5 +1,34 @@
# 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
+43 -18
View File
@@ -18,6 +18,7 @@ OpenSpec применяется после явного решения Change Ow
Для native validation из этого репозитория:
```bash
npm run check:openspec
openspec schema validate wealth-ledger-sdd --verbose
openspec schema validate wealth-ledger-sdd-v2 --verbose
openspec status --change <change> --store wealth-ledger-specs
@@ -25,10 +26,11 @@ openspec validate <change> --type change --strict --store wealth-ledger-specs
openspec validate --specs --strict --store wealth-ledger-specs
```
Версия OpenSpec CLI не закреплена отдельным файлом: при обновлении проверяйте обе
schema, затронутые changes и canonical specs, фиксируя `openspec --version` вместе
с результатами проверок. 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 остаётся отдельной проверкой
@@ -39,35 +41,58 @@ Node задаётся собственной `.nvmrc`; после `nvm use --sil
Репозиторий выпускается независимо. `develop` — интеграция, `main` — принятые
релизы. Обычная работа: issue-linked ветка от актуального `develop`, PR в
`develop`; общие правила — в Engineering Workflow по ссылке из AGENTS.
Обычные PR допускают squash; release и обратная синхронизация сохраняют историю.
PR сливается squash или rebase-merge; merge-коммит не используется. `main`
обновляется только 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`. Убедись, что
`develop` содержит последний релиз. Создай `release/<name>` от проверенного
`develop`; `nvm use --silent`, `npm ci`. Hotfix начинается от `main` в
`release/hotfix-<name>` и проходит тот же процесс.
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 в `main` с заголовком `chore(release): X.Y.Z`. После зелёных
`verify`, `security`, `commits` выполни fast-forward-only merge. Если `main`
изменился, согласуй историю и повтори проверки кандидата.
4. Получи удалённый `main`, сравни его HEAD с проверенным release SHA и версией
открой 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`.
5. Верни release-коммит через PR `main → develop` без squash/rebase; проверь,
что `develop` содержит тег. Опубликованные теги не перемещай. При сбое после
merge создай отсутствующий тег без нового bump; до merge исправь и проверь
существующий кандидат без повторного повышения версии.
Обратная синхронизация не нужна: 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*` защищены.
администратором, включена блокировка устаревшей ветки, обязательны `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 не включены.
+1
View File
@@ -0,0 +1 @@
1.13.1
@@ -0,0 +1,2 @@
schema: wealth-ledger-sdd-v2
created: 2026-10-14
@@ -0,0 +1,63 @@
# Design: Upcoming events date filter
## Context and Constraints
The account-events page already has a URL-backed filter with draft, Apply,
Reset, and date validation. Its view model currently exposes a `maxDate` set to
today, which blocks forecast dates; the date picker has no lower bound. The
shared `defaultPeriod()` currently subtracts six elapsed 24-hour intervals and
formats both instants as Moscow calendar dates, which can collapse to fewer
than seven dates around local midnight.
The `useUrlPeriod()` hook already supplies a default when the URL has no range,
preserves valid URL dates (including past dates), and strips invalid or reversed
range parameters. Preserve these semantics. The API already supports requested
date windows; no API or persistence changes are needed.
## Technical Goals / Non-Goals
### Goals
- Compute the exact seven-date default using Moscow calendar-date boundaries.
- Let the date picker select today and future dates only.
- Preserve valid past URL periods and existing Apply/Reset behavior.
- Keep malformed, impossible, and reversed URL periods on the default path.
### Non-Goals
- Change backend event filtering, UTC inclusivity, or saved forecast window
intersection behavior.
- Remove the page filter or turn the page into a read-only forecast list.
- Reject valid past dates at the URL/API boundary.
## Design
Use the shared calendar utility as the owner of the default range. Derive the
current Moscow calendar date, then calculate the inclusive end as six calendar
days later using calendar arithmetic rather than elapsed milliseconds. Keep
the output as `YYYY-MM-DD`, matching existing URL and API parameter formats.
In the account-events page view model, expose today's date as the picker's
minimum selectable date and remove the upper date bound. The picker constraint
is presentation-only: do not add a future/past check to `isValidPeriod()` or
`useUrlPeriod()`. That keeps valid direct URLs independent of what can be newly
selected from the UI.
Keep URL normalization and Apply/Reset ownership where they are. Invalid URL
periods continue to be removed with replacement navigation and resolve to the
same default range; valid one-sided periods retain current behavior.
## Data and State
No new state or API fields are needed. Existing URL parameters remain the
applied state; the view model retains draft state until Apply. Today's picker
minimum is refreshed when opening the calendar, consistent with the existing
today-bound refresh behavior.
## Verification
Verify the default range spans seven distinct dates at ordinary times and
around Moscow midnight, picker bounds allow future dates and block past dates,
valid past URL periods are preserved, and malformed/impossible/reversed ranges
fall back to the default. Existing history, Apply, and Reset behavior must
remain intact. Use the frontend repository's local verification commands.
@@ -0,0 +1,24 @@
# Docs Impact: Upcoming events date filter
## Decision
Update the existing canonical page `docs/domains/income/upcoming-events.md`
after implementation. It already documents the date filter and URL behavior,
so no new documentation page is needed.
## Durable Information to Promote
Clarify the user-visible default as seven calendar dates inclusive of today,
and explain that the date picker permits today and future dates while valid
past ranges remain usable when supplied through a direct URL. Invalid date
ranges continue to fall back to the default.
## Not Required
No API reference, architecture page, glossary, or ADR change is expected: the
API contract and forecast-window semantics do not change.
## Verification
After implementation, confirm the existing upcoming-events domain page matches
the shipped default range, picker bounds, and URL compatibility behavior.
@@ -0,0 +1,40 @@
# Fix upcoming events date filter
## Why
Страница «Будущие события» предназначена для просмотра прогноза, но календарь
сейчас запрещает выбрать будущие даты и разрешает выбирать прошедшие. Значение
периода по умолчанию также должно соответствовать прогнозному горизонту:
семь календарных дат, начиная с сегодняшней.
## What Changes
- Оставить URL-backed фильтр периода и действия Apply/Reset.
- По умолчанию показывать семь календарных дат включительно: от сегодняшней даты
до шестого следующего календарного дня.
- В календаре разрешить даты начиная с сегодняшней; не ограничивать доступные
даты сверху.
- Сохранять валидные периоды из URL, включая прошедшие даты. Некорректный формат,
невозможную календарную дату или обратный диапазон нормализовать к периоду по
умолчанию.
## Capabilities
- `brokerage-account-upcoming-events`
## Impact
- `frontend/`: фильтр и URL-период страницы будущих событий.
- `specs/`: уточнение пользовательского поведения периода по умолчанию и
календарных ограничений существующей capability.
Изменение сохраняет выбор владельца из frontend Issue #63: фильтр остаётся.
Оно не переводит страницу в режим read-only списка исходного прогноза.
## Success Criteria
- При первом открытии без параметров период включает сегодня и следующие шесть
календарных дат.
- Пользователь может выбирать будущие даты и не может выбрать даты до сегодня.
- Прямой URL с валидными прошедшими датами сохраняет указанный период.
- Некорректный URL-период заменяется периодом по умолчанию.
@@ -0,0 +1,37 @@
# QA Analysis: Upcoming events date filter
## Verification Scope
In scope: `brokerage-account-upcoming-events` — **User can filter upcoming
events by a forecast date range** and its four scenarios in the delta spec.
Out of scope: backend date-window behavior, event calculations, forecast
freshness, and API error mapping; these are unchanged by this frontend date
filter adjustment.
## Coverage Matrix
| Requirement / Scenario | Risk | Verification |
| --- | --- | --- |
| Default range starts today and spans seven calendar dates | Off-by-one or local-midnight error yields the wrong period | Check that the inclusive range contains today and the next six distinct Moscow calendar dates, including when opened around Moscow midnight. |
| Future dates can be selected and past dates cannot | Incorrect picker bounds preserve the reported defect or block forecast dates | Open the date picker; confirm yesterday is unavailable and today plus later dates are selectable. Apply a future range and confirm the displayed period reflects it. |
| Valid past URL range is preserved | UI restriction is accidentally enforced on deep links | Open a valid past range directly and confirm the same dates remain applied and in the URL. |
| Invalid URL range falls back to the default | Malformed or impossible dates leak into requests or remain in navigation state | Open malformed, impossible, and reversed ranges; confirm the default range is applied and invalid range parameters are removed. |
## Test Data and Boundaries
- Use a controlled current date and include a case near the Europe/Moscow
calendar-date boundary.
- Cover exactly today, the sixth following calendar date, and a later future
date in the picker.
- For URL normalization, include invalid syntax, an impossible date such as
February 30, and `from` later than `to`.
- Include a valid past range to distinguish URL compatibility from picker
restrictions.
## Risks and Limitations
The calendar is a visual interaction, so system-level verification should use
the existing browser-capable UI validation available to the frontend project.
If only component-level verification is available, it can establish URL and
range state but cannot fully prove the rendered calendar's disabled dates.
@@ -0,0 +1,41 @@
# Delta for Brokerage Account Upcoming Events
## ADDED Requirements
### Requirement: User can filter upcoming events by a forecast date range
The events page MUST retain its URL-backed date-range filter. When the URL has
no `from` or `to` parameters, the applied range MUST include the current date
and the following six calendar dates. The page MUST allow selecting today and
future dates in its date picker and MUST prevent selecting dates before today.
The date-picker restriction MUST NOT invalidate or rewrite a valid past date
range supplied directly in the URL. A malformed date, impossible calendar date,
or reversed range MUST be normalized to the default range, removing invalid
`from` and `to` parameters from the URL. Applying and resetting the filter MUST
continue to use the existing URL-backed period behavior.
#### Scenario: Default range starts today and spans seven calendar dates
- **WHEN** the user opens the events page without `from` or `to` parameters
- **THEN** the applied range starts on the current calendar date
- **AND** the inclusive end date is six calendar dates after the start date
#### Scenario: Future dates can be selected and past dates cannot
- **WHEN** the user opens the date picker
- **THEN** today and later dates are selectable
- **AND** dates before today are not selectable
#### Scenario: Valid past URL range is preserved
- **WHEN** the user opens the events page with valid `from` and `to` dates in
the past and `from` is not after `to`
- **THEN** the page applies that URL range without clamping or replacing it
#### Scenario: Invalid URL range falls back to the default
- **WHEN** the URL contains a malformed date, an impossible calendar date, or
a range whose `from` date is after its `to` date
- **THEN** the page applies the default seven-calendar-date range
- **AND** invalid `from` and `to` parameters are removed from the URL
@@ -0,0 +1,61 @@
# System Analysis: Upcoming events date filter
## Scope and ownership
This change affects the frontend events page and its existing date-range
contract. The backend API already accepts optional `from` and `to` dates and
returns events for the requested range; the canonical
`brokerage-account-upcoming-events` capability defines inclusive UTC date
bounds and behavior for invalid request ranges. No backend/API contract change
is needed.
The frontend owns picker availability, URL normalization, and the default
range. The user-approved product decision is to retain the filter (rather than
replace it with an unfiltered saved-forecast list, as considered by frontend
Issue #63).
## Observable flow
```text
URL query parameters
|
v
validate calendar dates and ordering
| valid | invalid
v v
preserve supplied range remove invalid range
| |
+------------+--------------+
v
no range -> seven dates from today
|
v
GET events with inclusive range
```
The date picker prevents new selection of dates before today, but this UI
constraint is intentionally not a validation rule for a valid URL-provided
past range. API date serialization and existing inclusive-boundary semantics
remain unchanged.
## Time-zone and calendar semantics
The existing frontend date utilities represent calendar dates in the
`Europe/Moscow` zone. The default must be computed as calendar-date arithmetic
in that same zone, not by subtracting elapsed 24-hour durations from the
current instant; elapsed-time arithmetic can yield the wrong number of distinct
dates around a local midnight or daylight offset change. The current-date
boundary used by the picker and default must be consistent with this existing
frontend calendar convention.
## Failure and compatibility behavior
- Valid past query ranges remain accepted even though the picker will not
create them.
- Malformed, impossible, or reversed ranges follow the existing URL
normalization path and resolve to the default period.
- One-sided valid ranges keep their existing supported behavior; this change
does not require both query parameters to be present.
- Apply and Reset retain their existing URL/history semantics.
- Backend `INVALID_EVENTS_PERIOD`, request parameters, and saved forecast window
intersection behavior are outside this change.
@@ -0,0 +1,11 @@
# Tasks: Upcoming events date filter
- [x] 1.1 [frontend] Implement the seven-calendar-date default and date-picker bounds while preserving URL period normalization and Apply/Reset behavior.
- **Covers:** `brokerage-account-upcoming-events` — `User can filter upcoming events by a forecast date range`.
- **Source:** Proposal **What Changes**; Specs scenarios **Default range starts today and spans seven calendar dates**, **Future dates can be selected and past dates cannot**, **Valid past URL range is preserved**, and **Invalid URL range falls back to the default**; Design **Design**.
- **Verification:** Frontend owner runs relevant existing page/calendar tests and confirms Moscow-midnight default, future/past picker bounds, valid past URL preservation, and invalid URL fallback.
- [x] 1.2 [docs] Update the canonical upcoming-events page to document the shipped default range and picker/URL behavior.
- **Covers:** `brokerage-account-upcoming-events` — `User can filter upcoming events by a forecast date range`.
- **Source:** Docs Impact **Durable Information to Promote**.
- **Verification:** Confirm `docs/domains/income/upcoming-events.md` describes seven inclusive calendar dates from today, picker restrictions, valid past URL behavior, and invalid-range fallback.
@@ -0,0 +1,2 @@
schema: wealth-ledger-sdd-v2
created: 2026-10-13
@@ -0,0 +1,208 @@
# Engineering Design
## Context and Constraints
Аналитика дохода использует выбранный период, отдельные previous/year диапазоны
и публикует их через DTO и metadata; план синхронизации также запрашивает эти
диапазоны. Frontend строит блоки сравнений из ответа и показывает исполненные
операции через `LedgerEntryRow`. Полный журнал, недавние записи и личная история
бумаги используют этот же ряд; прогноз использует `BrokerageEventCard`;
календарные карточки акции и облигации реализованы внутри соответствующих
страниц. Все уже используют `EntryCard` либо существующие Mantine primitives,
но это не один предметный компонент выплаты.
Граница реализации — утверждённые Delta Specs и
[System Analysis](system-analysis/analysis.md), включая
[целевой контракт](system-analysis/contracts/income-analytics-response.schema.json).
Доступ, деньги, полнота периода, порядок записей и формы остальных API
сохраняются. Клиент получает публичный OpenAPI artifact явно.
## Technical Goals / Non-Goals
- Исключить сравнения из чтения, синхронизации, API и UI без изменения итогов
выбранного периода.
- Один компонент для купона/дивиденда в исполненной операции, прогнозе и
графике бумаги; режим определяется источником, а не знаком или датой.
- Не преобразовывать график в личный доход и не объединять чужие деньги,
прогноз и сумму на бумагу. Не удалять `buckets` или `instruments` из API.
## Implementation Impact Map
### main
N/A: нет изменений workspace, Compose, release orchestration или конфигурации.
### frontend
Affected: страница дохода и её display model, компонент записи операций,
компонент будущего события, календарные карточки акций/облигаций, локальная
копия публичного OpenAPI и генерируемые типы. Владельцы экранов сохраняют
свои списки, фильтры и локальные состояния.
### backend
Affected: расчёт и планирование income периода, чтение операций для
income-analytics, DTO ответа и план фонового получения выбранного интервала.
Источник исполненных операций и агрегация selected остаются владельцами денег.
### qa
N/A: самостоятельного репозитория QA нет; системные проверки описывает
`qa-analysis.md` без введения несуществующей инфраструктуры.
## Decisions
### main
Изменения не требуются: координация ссылками на Issue/Change, без workspace
автоматизации ради одного API перехода.
### frontend
Один предметный `PayoutCard` разместить на уровне `entities`, доступном
страницам и компонентам других slices через public export. Он использует
существующий `EntryCard` как визуальную основу; `EntryCard` не становится
владельцем финансовой семантики. Презентационные данные передаются явным
discriminated union по происхождению `executed | forecast | schedule`:
исполненная запись содержит signed payment и точный timestamp; прогноз —
nullable projected amount, dateKind и дату; график — nullable per-security
сумму, отдельную nullable оценку и доступные даты/параметры выпуска. Ветка
графика показывает дополнительные условия купона и дивиденда без размножения
карточек. У отсутствующих значений текстовый fallback, ноль форматируется
отдельно от неизвестного. Не вводить один универсальный произвольный набор
optional props, допускающий ложную комбинацию «исполнено + оценка».
Владельцы исходных данных адаптируют типизированный ответ непосредственно на
границе использования либо через узкий mapper сущности для повторных callers:
`LedgerEntryRow` передаёт точные купоны/дивиденды в `PayoutCard`, прочие
операции оставляет на прежнем пути; `BrokerageEventCard` делает то же для
coupon/dividend, сохраняя maturity/offer; страницы графиков заменяют только
содержимое календарной карточки. Доход продолжает вызывать `LedgerEntryRow`,
сохраняя `wrap`, HTML-список и occurrence key. Личная история инструмента
сохраняет quantity/price/account details там, где они доступны. Ключи списков
и порядок определяются caller, не самим компонентом.
Удалить comparison-only display model, компонент блока, подписи и стили,
относящиеся только к сравнениям. Не удалять предупреждение неполного
selected или сохранённого snapshot. Обновить текущие fixtures и регрессии
после передачи OpenAPI; генерация типов потребителя берёт только его
локальную копию artifact.
Альтернатива с одинаковым CSS для трёх существующих компонентов отвергнута:
не обеспечивает фактическое переиспользование. Альтернатива с общим DTO
backend отвергнута: источники и денежные смыслы различны, wire-контракт
остальных API не меняется.
### backend
Сократить план income-analytics до selected: сервис запрашивает один диапазон,
не конструирует comparison totals/availability; план экранной синхронизации
не включает предыдущий год и месяц. Сохранить независимый план current-month
summary и серверный выбор точных типов купона/дивиденда. В DTO удалить
`previous`, `yearComparison`, comparison DTO при отсутствии других callers;
`dependencies` сохранить в исходной форме с единственной ролью `selected`.
Текущий shared operations range reader использует индексированные имена
resource keys для трёх периодов; после одного диапазона оставить корректную
идентичность selected и убрать устаревший индексированный список. Сохраняем
status/coverage/freshness и публикацию подтверждённого периода. Дневные/
недельные/месячные buckets остаются, так как их удаление не согласовано.
Не вводить миграцию БД: убираются чтения и поля ответа, а не сохранённые
операции или подтверждённое покрытие. Никакой пересборки прошлого дохода.
## Cross-cutting Implementation
Существующая проверка пользователя/счёта и очистка личного query cache при
смене подключения остаются на местах. Изменение количества планируемых
диапазонов не добавляет work и не меняет поведение hourly attempt для других
экранов. Существующие request ID, metadata попытки и статусы дают достаточную
диагностику; новых событий или полей логирования не требуется. Пересчёта
денег, валютной конвертации и нового кэша нет.
## Data / Storage Changes
N/A: Prisma/SQLite schema и исторические записи не меняются; выбранное
покрытие продолжает подтверждаться до публикации окончательного результата.
## Contract Implementation
Backend обновляет DTO и исполняемый generated OpenAPI. Для обоих
income-analytics маршрутов проверить отсутствие `previous`/`yearComparison`
и наличие только `selected` в dependency role при сохранении nullable `data`,
остальных полей и Problem Details. Сверить обе схемы 200 с
[target JSON Schema](system-analysis/contracts/income-analytics-response.schema.json)
семантически, а не только с TypeScript shape; существующую проверку parity
runtime OpenAPI также сохранить. Передать созданный backend artifact frontend
явно; frontend обновляет snapshot и generated types штатной локальной
процедурой, без чтения соседнего checkout в сборке. Прежний клиент,
требующий удалённых полей, несовместим.
## Developer Verification Strategy
Backend: адресные проверки планирования только выбранного интервала, чтения
income summary/analytics, точных signed сумм и недоступного selected;
проверка HTTP контракта и OpenAPI parity. Frontend: наблюдаемое отсутствие
блоков сравнений в loaded/loading/error/empty; сохранение period URL,
порядка/повторов выплат и корректные режимы общего компонента на всех
потребителях. После локальных узких проверок выполнить обязательный verify
каждого изменённого implementation repo. Это developer-owned проверки;
системная матрица отдельно в `qa-analysis.md`.
## UI/UX Package
**Applicability and scope:** changed UI; пакет полностью inline, новых
изображений и assets не требуется. Один approval Design относится к этой
ревизии и приведённому ниже точному источнику переиспользования.
**Package revision:** `1`, change `unify-income-payment-cards`.
**Screens, states and interactions:** доход — фильтр, итоги, плоский список
исполненных выплат, без сравнений; прогноз на странице событий/превью —
пометка «Прогноз», неизвестная сумма отлична от нуля, дата реестра помечена;
графики акции/облигации — «Событие по графику», разные подписи per-share/
per-bond и личной оценки. В истории операций карточка сохраняет описание,
дату и доступную identity инструмента, signed Money и детали там, где были.
Loaded, empty, loading, error и обновление старого snapshot сохраняют текущие
локальные границы экранов. Оферта, погашение, сделка и налог не получают
режим выплаты. Компонент сам не переключает состояние и не инициирует запрос.
**Responsive behavior:** однотактный проход по карточкам без горизонтального
overflow при узкой ширине и длинных значениях; labels, значения и статусы
читаются текстом и клавиатурой, цвет остаётся дополнительным признаком.
Сохраняются доступные имена региона и фокус элементов фильтра.
**Reused source and differences:** immutable locator
`wealth-ledger-specs:openspec/changes/archive/2026-09-09-simplify-income-page/design.md`
(секция «Одна карточка соответствует одной выплате») — плоский income список,
одна карточка на запись, signed Money и сохранение порядка. Дополнить
только единым предметным presentation для прогнозов/графиков с явными
подписями происхождения; не переносить прогноз внутрь income страницы.
Визуальная основа реализации — существующий `EntryCard`; его геометрия
остаётся общим стилем, а не заменой `PayoutCard`. Пакет целиком inline.
## Risks / Trade-offs
- Разные даты и null-поля трёх источников провоцируют неверные labels при
слишком широком mapper: явные варианты данных и локальные проверки экранов
важнее экономии нескольких props.
- Для календаря бумаги нет логотипа/тикера в каждой записи: владелец страницы
передаёт известную identity, не создавая новый запрос.
- Меньше загружаемых диапазонов снижает объём работы, но отсутствие сравнения
может изменить порядок фонового покрытия старых периодов; цель сохранения
другой независимой истории остаётся прежней.
## Migration / Rollout / Rollback
Сначала обновить backend DTO/логику и получить опубликованный OpenAPI, затем
передать artifact frontend и обновить UI/типизацию; согласованно поставить
две стороны breaking ответа. Не выпускать старый frontend против нового
backend как поддерживаемую комбинацию. При rollback вернуть согласованную
предыдущую пару backend/frontend и прежний OpenAPI snapshot; сохранённые
операции и покрытие не требуют data rollback. Синхронизация canonical specs
и AS-IS docs выполняется по отдельным approved tasks после реализации.
## Solution Assessment
N/A: не создаётся новая инфраструктура, интеграция, storage, кэш или система
аутентификации. Существующие `EntryCard`, owner-slices и DTO сохраняются;
сопоставление альтернатив общего UI дано в решении frontend.
@@ -0,0 +1,50 @@
# Documentation Impact
## Summary
Обновление canonical AS-IS нужно: документы сейчас прямо описывают сравнения
дохода и три разных формы карточек выплат. После поставки описание должно
отражать только выбранный период и единое представление купонов/дивидендов
с разной степенью подтверждения. Целевые страницы уже существуют; новых
разделов навигации не требуется.
## Documentation Changes
| Action | Target | Category | Resulting AS-IS | Source | Verification |
| --- | --- | --- | --- | --- | --- |
| update | `docs/domains/income/overview.md` | domain | Удалены блоки и расчёт comparisons, а также сведения о comparison coverage и процентах; income API содержит только данные выбранного периода, buckets сохраняются; плоский список фактических выплат использует общее представление купона/дивиденда. | `specs/brokerage-account-income-analytics/spec.md`; `system-analysis/analysis.md` §§ Contracts, Data and State Semantics | Сверить текст о расчёте, полноте, пустоте и карточках с двумя income API ответами и фактической страницей. |
| update | `docs/domains/portfolio/all-accounts.md` | domain | Для `all` доход учитывает доступные счета, выбранный период, source currencies и принадлежность entries без годового/предыдущего сравнения. | `specs/brokerage-account-income-analytics/spec.md`; `system-analysis/analysis.md` §§ Contracts, Security and NFR at System Boundaries | В документе нет правил year comparison; данные `all` соответствуют ответу и интерфейсу. |
| update | `docs/domains/operations/overview.md` | domain | Купоны/дивиденды в полном журнале, пяти недавних операциях и истории инструмента показаны одним предметным представлением исполненной выплаты; не-доходные операции сохраняют прежнюю семантику и детали. | `specs/brokerage-account-ledger/spec.md`; `design.md` § Decisions/frontend | Сопоставить три экрана, знак, исходную валюту, описание и дополнительную историю инструмента. |
| update | `docs/domains/income/upcoming-events.md` | domain | Полный прогноз и превью используют общее представление купона/дивиденда с пометкой прогноза, nullable Money и датой реестра для dividend; offer/maturity сохраняют смысл. | `specs/brokerage-account-upcoming-events/spec.md`; `system-analysis/analysis.md` § Data and State Semantics | Сверить оба экрана с типизированным ответом, неизвестной суммой, dateKind и отсутствием фактического дохода в прогнозе. |
| update | `docs/domains/instruments/stocks.md` | domain | Будущий и прошедший дивидендный график используют общую карточку с пометкой события по графику; сумма на акцию, оценка и дата реестра не выдаются за личное зачисление. История личных операций остаётся отдельной. | `specs/instrument-income-schedules/spec.md`; `design.md` §§ Decisions/frontend, UI/UX Package | Сверить разные деньги/даты и границу между графиком и операцией на странице акции. |
| update | `docs/domains/instruments/bonds.md` | domain | Купонный календарь и прошлые события используют общую карточку с суммой на облигацию, отдельной оценкой и известными параметрами; исполнение подтверждает только личная история. | `specs/instrument-income-schedules/spec.md`; `design.md` §§ Decisions/frontend, UI/UX Package | Сверить ближайший/прошлый/недатированный купон и неизвестную сумму на странице облигации. |
| update | `docs/architecture/http-api.md` | architecture | Зафиксировать, что оба income-analytics ответа больше не содержат данных сравнения; публичный контракт остаётся backend-generated OpenAPI, frontend получает обновлённый snapshot отдельно. Не дублировать машинную схему. | `system-analysis/analysis.md` §§ Contracts, Compatibility / Migration | Сверить описание с опубликованным OpenAPI и отсутствием comparison fields/roles. |
## Diagram Promotion
Не переносить `system-analysis/sequences/income-period.mmd` и
`system-analysis/sequences/payment-sources.mmd`: устойчивые инварианты
помещаются в тематические страницы; подробный сценарный flow полезен в
архиве change, а не как параллельная AS-IS диаграмма.
## ADR Decision
N/A: изменяется продуктовый UI и ответ income API, не вводится новое
долгоживущее архитектурное правило или интеграция. Существующий
`docs/adr/017-local-tbank-reads-and-background-sync.md` — исторический
принятый документ о старом поведении; не переписывать старое решение
задним числом. Актуальный текст о выбранном периоде должен жить в
`docs/domains/income/overview.md` и cross-cutting AS-IS при необходимости.
## Glossary / Navigation Impact
Новых терминов или маршрутов нет. Существующие значения «выплата», «прогноз»,
«операция» и «событие графика» разъясняются в тематических страницах.
`docs/overview.md` сохраняет существующую ссылку на доход.
## No-Change Rationale
Не создавать новую страницу `docs/design/`, не переносить target JSON Schema
и не менять интеграционные описания T-Bank/MOEX: данные их источников и
вычисления не изменяются. Остальные docs/ и исторические ADR не требуют
обновления.
@@ -0,0 +1,90 @@
# Proposal
## Why
Сравнения с предыдущим периодом и годом ранее не помогают пользователю решить
основную задачу страницы «Пассивный доход»: увидеть фактически полученные купоны
и дивиденды за выбранный период. Они усложняют экран и требуют получения данных
за дополнительные периоды. Купоны и дивиденды в истории операций, прогнозах и
календарях инструментов представлены разными карточками; сходный вид при этом
не обеспечивает единого представления выплаты и ясной границы между полученными
деньгами и ожидаемым событием.
## What Changes
- **BREAKING**: сравнения дохода с предыдущим периодом и годом ранее перестают
рассчитываться, загружаться и предоставляться потребителям аналитики.
- Страница «Пассивный доход» показывает выбранный период, итоги по исходным
валютам и все включённые исполненные выплаты без блоков сравнений и их состояний.
- Купоны и дивиденды во всех существующих местах показа выплат используют одно
предметное представление. Оно явно различает исполненную выплату, прогноз и
запись графика инструмента, сохраняя смысл дат и денежных величин.
## Scope
### In Scope
- Аналитика дохода одного счёта и режима «Все счета»: удаление сравнения
периодов из результата и связанных чтений, при сохранении дохода выбранного
периода и самостоятельной сводки текущего месяца.
- Карточки купонов и дивидендов в доходе, журнале и недавних операциях,
личной истории инструмента, полном списке и превью будущих событий,
календарях и истории выплат акций и облигаций.
- Обновление нормативного поведения и согласование изменённого публичного
контракта с потребителями.
### Out of Scope / Non-Goals
- Возврат графика динамики либо прогноза на страницу дохода; удаление иных
исторических данных аналитики, не относящихся к сравнению периодов.
- Изменение отбора исполненных операций, расчёта итогов, налоговой трактовки,
прогноза или правил происхождения календарных событий.
- Унификация карточек сделок, налогов, комиссий, погашений и оферт.
## Capabilities
### New Capabilities
- `specs/instrument-income-schedules/spec.md` — различимое представление
будущих и прошедших событий графиков выплат акций и облигаций, не выдаваемых
за подтверждённое личное зачисление.
### Modified Capabilities
- `specs/brokerage-account-income-analytics/spec.md` — результат без сравнений
и единое представление исполненных выплат.
- `specs/brokerage-account-ledger/spec.md` — то же представление купонов и
дивидендов в полном журнале, недавних операциях и истории инструмента.
- `specs/brokerage-account-upcoming-events/spec.md` — то же представление
прогнозных купонов и дивидендов на странице событий и в превью.
## Impact
Пользователь видит более короткую страницу дохода и узнаваемую карточку
купона или дивиденда на смежных экранах. Аналитика прекращает запрос
дополнительных периодов для сравнения, меняется публичный ответ и его
потребители. Исполненные операции, будущие события и графики инструментов
сохраняют разные источники и статус достоверности. Затронуты поведение
интерфейса, инвестиционная аналитика, публикация контракта и документация
дохода и событий.
## Success Criteria
- На странице дохода нет сравнений и сообщений об их недоступности; выбранный
период, итоги, налоговое пояснение и отдельная карточка каждой фактической
выплаты доступны как прежде.
- Чтение и синхронизация дохода не требуют периодов сравнения, а ответ
аналитики их не предоставляет. Полнота выбранного периода и итоги сохраняются.
- Купон и дивиденд на перечисленных экранах представлены одним предметным
компонентом; полученная сумма имеет исходный знак и валюту, прогноз может
не иметь суммы, а календарное событие не называется полученным доходом.
- Остальные типы операций и будущих событий не теряют своего представления.
## Assumptions / Dependencies
- Изменение публичного ответа согласуется с его потребителями при поставке;
удаление сравнений намеренно несовместимо с прежней формой ответа.
- Одна карточка может показывать данные разной природы без ложного обещания
зачисления: исполненная операция, прогноз и график остаются различимыми.
- Планирование и реализация связаны с координирующей Issue
`WealthLedger/main#33`.
@@ -0,0 +1,99 @@
# QA Analysis
## Verification Scope
### In Scope
Удаление сравнения дохода по `brokerage-account-income-analytics` и
семантика выплаты на экранах дохода, журнала, истории инструмента,
будущих событий и графиков акций/облигаций по четырём delta capabilities.
Проверяется поведение пользователя и публичного API как системы.
### Out of Scope
Пересмотр правил расчёта купонов/дивидендов брокером, налоговая оценка,
удаление buckets, новые типы событий, компонентные и внутренние тесты
реализации, несуществующий QA репозиторий.
## Coverage Matrix
| Requirement / Scenario из Delta Specs | Наблюдаемая системная проверка |
| --- | --- |
| `brokerage-account-income-analytics` — removed `Period comparisons preserve calendar and currency meaning`; `Analytics summary remains readable per currency / Динамика отсутствует для периода любой длительности` | На коротком/многолетнем периоде нет comparison UI или состояний; 200 ответ обоих income-analytics маршрутов не содержит comparison fields/roles, и чтение не инициирует дополнительные диапазоны ради сравнения. |
| `User explicitly applies an analytics period represented in the URL / Черновик периода не применён`, `Применение периода`, `Восстановление применённого периода из URL`, `Сброс периода`, `Недопустимый черновик периода` | Итоги и карточки меняются только после применения; URL/back/reset, неверные даты и доступность ввода сохраняют поведение без блоков сравнений. |
| `Realized analytics never presents an incomplete result as complete / Все страницы операций успешно получены`, `Одна из страниц операций недоступна`, `В периоде нет купонов и дивидендов` | Выбранный период с полной историей показывает итог; отсутствие или ошибка покрытия не выглядит нулём; подтверждённый пустой период отличим. |
| `Income payments remain traceable to exact Ledger Entries / Каждая выплата показана отдельной карточкой`, `Отрицательная корректировка и повторяющиеся IDs сохраняются`, `Валюта видна в сумме каждой выплаты` | Порядок и количество карточек равны серверным entries; повторы и минус в исходной валюте сохранены; нет внешних групп. |
| `Income page excludes upcoming forecasts / Данные будущих событий недоступны` | Недоступный прогноз не скрывает полный полученный доход и не возвращает прогнозный блок. |
| `brokerage-account-ledger` — `Full ledger presents compact rows / Отображение купона`, `Отображение дивиденда`, `Подпись типа не дублируется`; `Account overview includes recent ledger events / Отображение последних событий` | Полный журнал, превью и история инструмента используют одну карточку выплаченного купона/дивиденда, сохраняют signed Money, детали и fallback; другие операции сохраняют ряд. |
| `brokerage-account-upcoming-events` — `Account page presents an upcoming events list / Просмотр ближайших событий`, `Прогнозный дивиденд с неизвестной суммой`; `Account overview includes upcoming events / Отображение ближайших событий` | Та же карточка для прогноза в списке и превью, «прогноз», дата реестра и отсутствие суммы не превращаются в исполнение/ноль; offer/maturity не становятся выплатой. |
| `instrument-income-schedules / Будущий купон с известными условиями`, `Прошедший дивиденд без подтверждённой операции`, `Неизвестные условия графика` | На страницах бумаг та же карточка, но пометка графика, отдельные сумма на бумагу и оценка; прошлое событие не названо полученным, null не превращён в ноль. |
## Cross-system Verification
Сравнить оба 200 ответа income-analytics с
`system-analysis/contracts/income-analytics-response.schema.json` и
опубликованным OpenAPI; проверить отсутствие comparison ranges в запросах
при чтении и планировании income, не запрещая независимый запрос прошлого
периода пользователем. Проверить неполный/ошибочный selected, недоступный счёт,
новое подключение и поздний ответ старого без утечки дохода. Порядок и
повторение ID проверить на ответе нескольких операций; нулевая сумма и
отсутствующая сумма должны иметь разные значения. Фоновые HTTP ошибки
прогноза и календаря не должны менять исполненную выплату. Отдельных
callback и новых timeout контрактов нет.
## Risk-based Priorities
1. Точность финансового смысла и граница доступа: исправления с отрицательным
знаком, мультивалютность, «прошлый график» против факта, нет ложного нуля.
2. Breaking ответ и избыточная загрузка: оба маршрута, отсутствие comparison
fields/dependencies, синхронизация только выбранного периода.
3. Регрессии соседних экранов: предложенные выплаты, maturity/offer, trades,
текущий месяц на обзоре, back/forward и сохранённый snapshot.
## Test Data and Environment Needs
Тестовый доступный и недоступный счёт, режим `all`, завершённый и неполный
периоды; coupon/dividend одной и нескольких валют, отрицательная корректировка,
повтор operation ID, подтверждённо пустой период. Прогноз с известной/нулевой/
неизвестной суммой, dividend с датой реестра; расписание с per-security Money,
отдельной оценкой и отсутствующими датами. Управляемая доступность источника
и возможность наблюдать только показанные запросы/ресурсные metadata.
## Manual / Exploratory Scope
Пройти страницу дохода с клавиатуры и узкого viewport, проверить доступные
подписи, перенос длинных сумм и отсутствие горизонтальной прокрутки; перейти
между income, журналом, личной историей инструмента, будущими событиями и
календарём выпуска. Сравнить смысл подписи в трёх режимах при одинаковой
дате и тикере. Отдельно проверить empty/loading/error/recovery и preview.
## Automation Candidates
Black-box API: сравнение фактического schema обоих маршрутов с target,
успешный/ошибочный/пустой selected и отсутствие запросов к comparison ranges.
Сквозные journeys: переход от аккаунта через income/ledger к событию прогноза
и календарю инструмента с проверкой подписей и денег. Repository automation:
`deferred until QA repo exists`; текущее подтверждение возможно вручную и
через existing system-level HTTP/browser runners без требования нового repo.
## Observability Verification
По metadata ответа различить complete, pending, failed selected; запрос
исторического периода не должен сопровождаться comparison-only dependencies.
На отказе проверить корреляцию HTTP request ID и безопасного ответа, не
ожидая новых событий журналирования или передачи личных данных в лог.
## Regression Impact
Полный журнал, недавние операции, страница акции/облигации и их фильтры,
превью/полный прогноз, summary текущего месяца, мультивалютность и
независимые loading/error states. Затронутые публичные потребители
income-analytics требуют обновления контракта как пары backend/frontend.
## Gaps / Blockers
Блокирующих semantic gaps после утверждения Specs/System Analysis нет.
Если опубликованный backend OpenAPI не удовлетворяет target либо картинка
«получено» появляется на графике, маршрутизировать несовпадение в System
Analysis/Delta Specs до принятия реализации; внутреннюю стратегию исправления
определяют владельцы реализации.
@@ -0,0 +1,138 @@
## REMOVED Requirements
### Requirement: Period comparisons preserve calendar and currency meaning
**Reason**: Сравнения с предыдущим периодом и годом ранее больше не входят в
назначение аналитики полученного дохода и требуют избыточной загрузки истории.
**Migration**: Пользователь видит только итоги и состав выплат применённого
периода; потребители ответа аналитики больше не получают данные сравнений и
состояния их доступности. Исторический период можно по-прежнему применить
самостоятельно, но система не сопоставляет его с другим периодом автоматически.
## MODIFIED Requirements
### Requirement: User explicitly applies an analytics period represented in the URL
Страница выбранного Brokerage Account «Пассивный доход» MUST позволять User редактировать черновик `from` и `to`, не изменяя видимую аналитику до явного применения. Верхний блок фильтров MUST предоставлять два подписанных календарных поля начала и окончания периода с тем же взаимодействием выбора даты, что в истории событий портфеля, и отображением дат в формате `ДД.ММ.ГГГГ`. Поля и действия применения и сброса MUST быть доступны с клавиатуры. Явное применение MUST заменить применённый период, загрузить соответствующий результат и представить применённые даты в URL страницы. Недопустимый черновик MUST показывать ошибку соответствующего поля и MUST NOT заменять последний применённый период. Сброс MUST немедленно вернуть применённый период к текущему месяцу по сегодняшний день и синхронизировать URL. Перезагрузка или открытие URL с допустимым периодом MUST восстановить тот же применённый период. Страница MUST NOT дублировать период отдельной подписью под блоком фильтров. Действие перехода на страницу MUST быть доступно из выбранного Brokerage Account, а не из глобальной навигации.
#### Scenario: Черновик периода не применён
- **WHEN** User изменяет черновик `from` или `to`, но не применяет его
- **THEN** видимые итоги и карточки выплат остаются основанными на последнем применённом периоде
#### Scenario: Выбор дат в верхнем блоке
- **WHEN** User выбирает начало и окончание периода через календарные поля
- **THEN** выбранные даты отображаются в подписанных полях в формате `ДД.ММ.ГГГГ`, а изменение остаётся черновиком до явного применения
#### Scenario: Управление фильтрами с клавиатуры
- **WHEN** User управляет верхним блоком фильтров с клавиатуры
- **THEN** он может последовательно перейти к обоим полям дат, изменить период и выполнить применение или сброс без мыши
#### Scenario: Применение периода
- **WHEN** User явно применяет допустимый черновик периода
- **THEN** страница загружает аналитику этого периода и отражает применённые `from` и `to` в URL
#### Scenario: Недопустимый черновик периода
- **WHEN** User пытается применить черновик с отсутствующей, некорректной или будущей датой либо с `from` после `to`
- **THEN** страница показывает ошибку соответствующего поля и сохраняет последний применённый период, его URL и видимую аналитику
#### Scenario: Восстановление применённого периода из URL
- **WHEN** User открывает URL страницы с допустимыми `from` и `to`
- **THEN** страница показывает аналитику за указанный применённый период
#### Scenario: Сброс периода
- **WHEN** User сбрасывает пользовательский период
- **THEN** страница немедленно применяет период от первого дня текущего месяца по Europe/Moscow до сегодняшнего дня и синхронизирует URL
### Requirement: Realized analytics never presents an incomplete result as complete
Система MUST формировать выбранный результат, его trend buckets, instrument breakdown и drill-down только после успешного получения всех необходимых cursor pages исполненных операций выбранного периода. Ошибка любой необходимой страницы MUST завершать расчёт восстанавливаемой ошибкой и MUST NOT возвращать частичные totals, buckets или breakdown как полный результат. Успешный период без подходящих выплат MUST возвращать нулевой результат и отображаться с явным объяснением пустого состояния. Система MUST NOT загружать другие периоды ради сравнения полученного дохода с выбранным периодом.
#### Scenario: Все страницы операций успешно получены
- **WHEN** для расчёта требуется несколько cursor pages и каждая страница успешно получена
- **THEN** система формирует результат по всем подходящим Ledger Entries со всех страниц выбранного периода без расчёта сравнений с другими периодами
#### Scenario: Одна из страниц операций недоступна
- **WHEN** получение любой необходимой cursor page завершается ошибкой
- **THEN** страница показывает восстанавливаемую ошибку и не показывает частичный результат как полный
#### Scenario: В периоде нет купонов и дивидендов
- **WHEN** все необходимые операции успешно получены, но среди них нет подходящих coupon или dividend Ledger Entries
- **THEN** страница показывает нулевой результат и объясняет, что за выбранный период купоны и дивиденды не найдены
### Requirement: Income payments remain traceable to exact Ledger Entries
В разделе «Выплаты» страница MUST показывать единый список карточек точных исполненных Ledger Entries применённого периода в исходном серверном порядке entries. Каждая выплата MUST иметь собственную сразу видимую карточку с описанием, датой исполнения, доступной identity Instrument и signed Money в исходной валюте. Купоны и дивиденды MUST использовать то же представление выплаты, что и на экранах журнала, прогноза и графиков инструментов, при этом карточка MUST обозначать эту сумму именно как фактически исполненную, а не прогнозную. Страница MUST NOT добавлять внешние группировочные карточки или заголовки по Instrument либо валюте, subtotals инструмента или количество выплат инструмента. Список MUST сохранять отрицательные корректировки и повторяющиеся operation IDs как отдельные записи и MUST NOT включать записи вне аналитического итога. Backend MUST обеспечивать согласованность entries, instrumentKey, валюты и receipt count при формировании ответа из одного набора записей. Frontend SHALL доверять типизированному contract без второго runtime consistency validator и MUST NOT пересчитывать денежные итоги.
#### Scenario: Каждая выплата показана отдельной карточкой
- **WHEN** выбранный период содержит несколько включённых Ledger Entries одного или разных Instrument
- **THEN** раздел «Выплаты» сразу показывает отдельную карточку каждой записи в исходном порядке без внешних группировочных блоков, раскрытия, итогов инструмента и receipt count на UI
#### Scenario: Список воспроизводит точный состав дохода
- **WHEN** User просматривает загруженный раздел «Выплаты»
- **THEN** он видит все и только включённые coupon и dividend Ledger Entries применённого периода с датой, описанием, доступной identity и исходной суммой, обозначенной как исполненная выплата
#### Scenario: Отрицательная корректировка и повторяющиеся IDs сохраняются
- **WHEN** доход содержит отрицательную корректировку либо несколько включённых entries с одинаковым operation ID
- **THEN** каждая запись остаётся отдельной карточкой, а корректировка отображается с исходным знаком
#### Scenario: Валюта видна в сумме каждой выплаты
- **WHEN** подходящие выплаты одного Instrument представлены в нескольких исходных валютах
- **THEN** каждая карточка показывает свою Money с исходной валютой, без отдельного заголовка валютной группы или объединённого мультивалютного итога
### Requirement: Analytics summary remains readable per currency
Страница MUST обозначать фактический результат как «Получено купонами и дивидендами» и для каждой исходной валюты показывать total, coupon subtotal и dividend subtotal выбранного периода. Она MUST объяснять, что tax operations не включены, не называя фактический результат gross, net, суммой до налогов, суммой после налогов или налоговым расчётом. Значения MUST оставаться доступными как читаемый текст, не только как графика, цвет или положение. Страница MUST NOT показывать блок «Динамика», его график, переключатель валюты графика или текстовый список временных buckets независимо от длительности выбранного периода. Страница MUST NOT показывать блоки «Предыдущий период» и «Годом ранее» или сообщения о недоступности сравнений.
#### Scenario: Итоги дохода доступны как текст
- **WHEN** выбранный период содержит исполненные купонные или дивидендные выплаты
- **THEN** страница показывает читаемые total, coupon subtotal и dividend subtotal с обозначением фактически полученного дохода и объяснением исключения tax operations
#### Scenario: Итоги нескольких валют остаются разделёнными
- **WHEN** результат содержит подходящие выплаты в нескольких исходных валютах
- **THEN** страница показывает отдельные читаемые итоги каждой валюты без конвертации и общего итога разных валют
#### Scenario: Динамика отсутствует для периода любой длительности
- **WHEN** User открывает аналитику или применяет допустимый период любой длительности
- **THEN** страница показывает итоги и выплаты без динамики, сравнений периодов и состояний их доступности
### Requirement: Income page excludes upcoming forecasts
Страница «Пассивный доход» MUST NOT показывать блок «Ближайший прогноз», его суммы, список будущих выплат, состояния загрузки, пустоты или ошибки. Forecast values MUST NOT входить в realized total, trend buckets или instrument breakdown. Доступность данных Future Cash Events MUST NOT влиять на отображение успешно загруженной realized analytics. Существующая самостоятельная страница «Будущие события» и её доступность из выбранного Brokerage Account MUST сохраняться.
#### Scenario: Известные будущие выплаты не появляются на странице дохода
- **WHEN** для выбранного Brokerage Account известны будущие coupon или dividend события с суммой или без неё либо maturity или offer события
- **THEN** страница дохода не показывает блок «Ближайший прогноз» и не включает эти будущие события в фактически полученный доход
#### Scenario: Изменение исторического периода не возвращает прогноз
- **WHEN** User применяет допустимый исторический период или сбрасывает фильтры
- **THEN** страница показывает соответствующую realized analytics без блока «Ближайший прогноз»
#### Scenario: Данные будущих событий недоступны
- **WHEN** realized analytics успешно загружена, а данные Future Cash Events недоступны
- **THEN** итоги и карточки выплат остаются доступными, а страница дохода не показывает состояние ошибки или загрузки прогноза
#### Scenario: Самостоятельная страница будущих событий сохраняется
- **WHEN** User переходит из выбранного Brokerage Account на существующую страницу «Будущие события»
- **THEN** эта страница остаётся доступной с прежним поведением независимо от отсутствия прогноза на странице дохода
@@ -0,0 +1,59 @@
## MODIFIED Requirements
### Requirement: Account overview includes recent ledger events
Обзор счёта MUST показывать пять последних исполненных записей журнала этого счёта и действие перехода к его полному журналу. Каждая показанная запись MUST иметь ту же визуальную идентичность инструмента и семантику денежного движения, что и запись полного журнала. Исполненные купоны и дивиденды MUST иметь то же представление выплаты, что и на других экранах с купонами и дивидендами; остальные операции сохраняют представление записи журнала. Неуспешный или пустой запрос последних записей MUST оставаться локальным состоянием блока последних событий и MUST NOT заменять успешно отображённый обзор Portfolio.
#### Scenario: Отображение последних событий
- **WHEN** загружается обзор счёта и его журнал содержит исполненные операции
- **THEN** обзор показывает не более пяти последних записей и ссылку на полный журнал, а купоны и дивиденды узнаваемы по тому же представлению выплаты
#### Scenario: Последние записи недоступны
- **WHEN** обзор счёта успешно загружается, но запрос последних записей завершается ошибкой
- **THEN** обзор Portfolio остаётся доступным, а блок последних событий отображает собственное состояние восстановления
#### Scenario: Последняя запись не содержит visual metadata инструмента
- **WHEN** одна из последних записей не имеет доступной presentation metadata инструмента
- **THEN** обзор показывает для неё понятный текстовый fallback и сохраняет дату, тип, инструмент и денежное изменение записи доступными для чтения
### Requirement: Full ledger presents compact rows
Страница полного журнала MUST показывать для каждой записи дату, тип операции, инструмент и денежное изменение в исходной валюте в компактной структурированной строке. При доступной presentation metadata инструмента строка MUST показывать его визуальную идентичность; при её отсутствии MUST использовать понятный текстовый fallback. Для купонных и дивидендных операций на странице полного журнала и в личной истории инструмента MUST использоваться то же представление выплаты, что в доходе, прогнозе и графиках инструментов, с обозначением исполненного движения. Для каждой записи денежное изменение в исходной валюте MUST сопровождаться подписью конкретного типа события и знаком: для купонной операции — «Купон», для дивидендной — «Дивиденд», для остальных записей — подпись их категории («Сделка», «Налог», «Комиссия», «Перевод», «Прочее»). Строка MUST NOT повторять подпись типа события во вторичной метадате записи. Знак «+» / «−» и семантический цвет MUST оставаться носителями направления и MUST NOT заменяться только текстом. Страница MUST обозначать записи без движения денег нейтрально и MUST NOT показывать цену или количество.
#### Scenario: Отображение записи только с инструментом
- **WHEN** полный журнал отображает запись без денежной выплаты
- **THEN** страница определяет её как запись без движения денег
#### Scenario: Отображение поступления
- **WHEN** полный журнал отображает запись с положительной денежной выплатой
- **THEN** строка явно показывает поступление знаком и подписью конкретного типа события («Купон», «Дивиденд» или категории) и его денежное изменение в исходной валюте, не полагаясь только на цвет
#### Scenario: Отображение списания
- **WHEN** полный журнал отображает запись с отрицательной денежной выплатой
- **THEN** строка явно показывает списание знаком и подписью конкретного типа события («Купон», «Дивиденд» или категории) и его денежное изменение в исходной валюте, не полагаясь только на цвет
#### Scenario: Отображение купона
- **WHEN** полный журнал или личная история инструмента отображает исполненную операцию с upstream-типом купона
- **THEN** запись показывает подпись «Купон» и исполненную сумму в том же представлении выплаты, что на других экранах
#### Scenario: Отображение дивиденда
- **WHEN** полный журнал или личная история инструмента отображает исполненную операцию с upstream-типом дивиденда
- **THEN** запись показывает подпись «Дивиденд» и исполненную сумму в том же представлении выплаты, что на других экранах
#### Scenario: Подпись типа не дублируется
- **WHEN** полный журнал отображает запись с типом события и вторичной метадатой
- **THEN** подпись типа события присутствует в строке один раз и не повторяется во вторичной метадате записи
#### Scenario: Отсутствует visual metadata инструмента
- **WHEN** полный журнал отображает запись без доступной presentation metadata инструмента
- **THEN** строка показывает понятный текстовый fallback без подмены или сокрытия доступных данных инструмента
@@ -0,0 +1,59 @@
## MODIFIED Requirements
### Requirement: Account page presents an upcoming events list
Страница Brokerage Account MUST предоставлять read-only действие перехода на отдельную страницу «Будущие события». Эта страница MUST показывать события сохранённого полного прогноза компактным списком без фильтров, пагинации, суммарного итога или действия ручной синхронизации, с датой, типом, инструментом и прогнозной суммой в исходной валюте, если она известна. Купоны и дивиденды MUST использовать то же представление выплаты, что и исполненные купоны, дивиденды и события графика инструмента, но MUST быть явно обозначены как прогноз, а не полученные деньги. Дата дивиденда 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** его карточка обозначает дату как дату реестра и сумму как неизвестную, не показывает нулевую или полученную выплату
#### 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 оставаться локальным состоянием блока, сохранять показанные события и их исходное окно и 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,25 @@
## Purpose
Позволяет отличать опубликованные календарные сведения о купонах и дивидендах
инструмента от фактически полученных личных выплат и прогноза по позиции.
## ADDED Requirements
### Requirement: Instrument payout schedules do not imply executed income
На страницах акций и облигаций известные будущие, прошедшие и недатированные события графика дивидендов или купонов MUST использовать то же представление выплаты, что и купоны/дивиденды других экранов, но MUST обозначать их как события графика, не подтверждающие фактическое зачисление. Доступные дата реестра, дата выплаты, сумма на одну бумагу и отдельная личная оценка MUST сохранять свои подписи и смысл; неизвестная сумма или дата MUST NOT подменяться нулём или вымышленной датой. Исполненная личная операция MUST оставаться отличимой от события графика, даже когда их даты совпадают.
#### Scenario: Будущий купон с известными условиями
- **WHEN** User просматривает будущий купон облигации с известными датой выплаты, суммой на облигацию и оценкой для позиции
- **THEN** единое представление показывает купон как событие графика, сохраняет обе разные суммы и дату и не называет оценку полученными деньгами
#### Scenario: Прошедший дивиденд без подтверждённой операции
- **WHEN** User просматривает в истории акции событие дивидендного графика с прошедшей датой реестра
- **THEN** представление показывает его как событие графика с суммой на акцию, а не как исполненную выплату пользователю
#### Scenario: Неизвестные условия графика
- **WHEN** в купонном или дивидендном графике отсутствует дата выплаты либо денежное значение
- **THEN** представление сохраняет известные сведения и явно различает неизвестное значение и ноль без вымышленной даты зачисления
@@ -0,0 +1,123 @@
# System Analysis
## Requirement Coverage
| Нормативная область | Поток и контракт |
| --- | --- |
| `brokerage-account-income-analytics`: удаление `Period comparisons preserve calendar and currency meaning`, выбранный период, полнота, читаемые итоги, отсутствие прогноза | [income-period.mmd](sequences/income-period.mmd), [target schema](contracts/income-analytics-response.schema.json) |
| `brokerage-account-income-analytics`: точные карточки выплат | [payment-sources.mmd](sequences/payment-sources.mmd), существующая форма entries |
| `brokerage-account-ledger`: журнал, недавние операции и история инструмента | [payment-sources.mmd](sequences/payment-sources.mmd), существующий ответ операций |
| `brokerage-account-upcoming-events`: полный список и превью | [payment-sources.mmd](sequences/payment-sources.mmd), существующий ответ событий |
| `instrument-income-schedules`: будущее, прошлое, неопределённая дата/сумма | [payment-sources.mmd](sequences/payment-sources.mmd), существующие ответы календарей |
## Systems and Responsibilities
- Пользователь выбирает применённый период и отличает исполненные выплаты от
возможных поступлений и условий выпуска.
- Frontend хранит применённые даты в URL, показывает только финансовые значения
их собственного источника и использует один предметный компонент для
купонов/дивидендов всех трёх режимов. Он не выводит исполнение из календаря.
- Backend проверяет доступ, период и покрытие операций; формирует точные
source-currency итоги и записи выбранного периода. Для income-analytics не
запрашивает предыдущий или годовой период ради сравнения.
- Сохранённые операции T-Bank остаются источником фактических выплат; прогноз
основан на позиции, календарь бумаги — на опубликованных условиях T-Bank/MOEX.
Они не становятся взаимозаменяемыми при объединении представления.
## Interaction Flows
- [Доход выбранного периода](sequences/income-period.mmd): доступ, полнота,
успешный/пустой результат и неполное покрытие без запросов сравнения.
- [Три источника карточки выплаты](sequences/payment-sources.mmd): подтверждённая
операция, будущий прогноз и событие календаря; остальные типы сохраняют
существующее представление.
## Contracts
| Производитель → потребитель | Транспорт / операция | Целевой контракт |
| --- | --- | --- |
| Backend → frontend | `GET /api/v1/brokerage-accounts/{accountId}/income-analytics`, `GET /api/v1/brokerage-accounts/all/income-analytics`, `application/json` 200 | [JSON Schema ограничений ответа](contracts/income-analytics-response.schema.json) поверх текущего backend-generated OpenAPI: убрать `data.previous`, `data.yearComparison`, comparison roles в `meta.dependencies`; оставить выбранный период и все прочие поля и ошибки в исходном смысле. |
| Backend → frontend | Income summary, операции, события, календари акций/облигаций | Без изменений wire-формы; типизированные исходные данные адаптируются к общему представлению купона/дивиденда на границе UI. |
Целевой schema-файл описывает *изменение* ответа и не подменяет полную схему
неизменённых полей; при согласовании используется вместе с предоставленным
OpenAPI snapshot. `meta.dependencies` содержит только зависимость выбранного
периода с ролью `selected`; это не новая причина загрузки другого периода.
Существующие auth, invalid-period и resource errors не меняются. Отсутствуют
новые операции/версии и callback-протоколы. Порядок entries и повторяющиеся ID
сохраняются; повторный HTTP read не должен создавать второе зачисление.
## Data and State Semantics
- `data: null` с незавершённым покрытием выбранного периода не становится
нулевым доходом. Успешный полный период без qualifying entries — доступный
нулевой результат. Свежесть и попытка обновления относятся к выбранному
результату, не к удалённым comparison-периодам.
- Купоны/дивиденды фактического дохода — точные исполненные операции с
исходным знаком, валютой и timestamp; календарное событие с прошедшей датой
не доказывает их наличие.
- Forecast projected amount может быть `null`; для dividend `dateKind=registry`
означает дату реестра, не зачисления. График хранит per-share/per-bond Money,
опциональную оценку по позиции, известные даты и время источника отдельно от
`payment` исполненной операции. Неизвестное не становится нулём.
- Общий предметный компонент получает явный режим происхождения данных;
адаптация не пересчитывает backend totals и не меняет source identity.
## Error and Recovery Semantics
- Некорректный черновик остаётся на клиенте с ошибкой поля; серверный invalid
period и отказ доступа сохраняют действующие ответы без fallback на другой
период или чужой счёт.
- Ошибка получения страницы исполненных операций не завершает покрытие
выбранного периода и не публикует частичный итог как полный. Последний
подтверждённый результат и его исходное время остаются независимыми от
неуспеха последней попытки.
- Ошибка прогноза не скрывает уже загруженный доход; отсутствие оценки,
логотипа либо даты в календаре не создаёт ложного исполнения или нуля.
- Поздний ответ прежнего подключения не должен восстановить личные данные
нового/недоступного подключения; граница доступа сохраняется.
## Observability Semantics
- Существующие status результата, `meta` полноты/попытки и request correlation
позволяют отличить полный выбранный период, pending и failure.
- Событие синхронизации выбранного периода не включает comparison-only
dependencies; получение прошлого/годового интервала для иных самостоятельных
запросов не считается сравнением дохода.
- Новых аналитических событий или чувствительных атрибутов не требуется.
## Security and NFR at System Boundaries
- Bearer-сессия, текущее подключение и доступ к выбранному счёту проверяются
до выдачи личных операций; `all` ограничен доступными счетами.
- Даты остаются включительными календарными датами Europe/Moscow, без
максимальной длительности; валюты не конвертируются. Не вводится новый SLA.
## Compatibility / Migration
Удаление `previous` и `yearComparison` из ответа намеренно breaking для
потребителя старой формы. Backend публикует обновлённый исполняемый OpenAPI;
frontend получает его как явный artifact и обновляет локальные типы прежде,
чем интеграция начнёт опираться на новую форму. Строгая semantic compatibility
между новым OpenAPI и target schema проверяется до поставки. При поставке
backend и frontend согласуются как пара, без обещания одновременной
совместимости со старым потребителем. Существующие поля buckets/instruments,
запросы сводки/журнала/прогноза и их доступность не меняются.
## Durable Documentation Candidates
- `docs/domains/income/overview.md`: отсутствие сравнений и загрузки чужих
периодов, только полнота выбранного периода.
- `docs/domains/portfolio/all-accounts.md`,
`docs/adr/017-local-tbank-reads-and-background-sync.md`: проверить
упоминания приоритета и смысла удалённых comparisons; ADR хранить как
историческое решение, не переписывать как AS-IS.
- `docs/domains/income/upcoming-events.md`, `docs/domains/instruments/stocks.md`,
`docs/domains/instruments/bonds.md`, `docs/domains/operations/overview.md`:
общая карточка при сохранении границы между исполнением, прогнозом и графиком.
## Open Questions
Нет блокирующих вопросов в утверждённом scope. Детали визуальной композиции
общего компонента остаются за Engineering Design при сохранении нормативных
различий денежного и календарного смысла.
@@ -0,0 +1,46 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://wealthledger.local/contracts/unify-income-payment-cards/income-analytics-response.schema.json",
"title": "Целевое изменение ответа income-analytics",
"description": "Ограничения поверх неизменённых свойств текущего backend-generated OpenAPI: точный контракт остальных полей остаётся у опубликованного OpenAPI. Применяется к 200 application/json обоих маршрутов income-analytics.",
"type": "object",
"required": ["data", "meta"],
"properties": {
"data": {
"anyOf": [
{ "type": "null" },
{
"type": "object",
"required": [
"accountId",
"period",
"totalsByCurrency",
"calculatedAt",
"bucketGranularity",
"buckets",
"instruments",
"entries"
],
"properties": {
"previous": false,
"yearComparison": false
}
}
]
},
"meta": {
"type": "object",
"required": ["dependencies"],
"properties": {
"dependencies": {
"type": "array",
"items": {
"type": "object",
"required": ["role"],
"properties": { "role": { "const": "selected" } }
}
}
}
}
}
}
@@ -0,0 +1,26 @@
sequenceDiagram
actor User
participant UI as WealthLedger frontend
participant API as WealthLedger backend
participant Store as Saved investment data
participant Broker as T-Bank
User->>UI: Open income page / apply from,to
UI->>API: GET income-analytics(accountId or all, from, to)
API->>Store: Verify session, membership, selected-period coverage and operations
alt Access denied or invalid period
API-->>UI: Problem Details (access/input error)
UI-->>User: Account/period error; preserve last applied data on invalid draft
else Selected period complete
Store-->>API: Confirmed selected operations and source state
API-->>UI: data with selected totals/entries + meta(selected)
UI-->>User: Totals and executed payment cards, no comparisons
else Selected period missing or incomplete
Store-->>API: Incomplete coverage + resource state
API-->>UI: data null + meta(selected)
UI-->>User: Loading or recoverable error, no partial final totals
opt First read of missing historical interval
API->>Broker: Begin bounded acquisition of missing selected interval
Broker-->>Store: Publish only confirmed complete result
end
end
Note over API,Store: No previous/year-ago interval is requested for comparison
@@ -0,0 +1,22 @@
sequenceDiagram
actor User
participant UI as WealthLedger frontend
participant API as WealthLedger backend
participant Source as Broker / market source
alt Executed coupon or dividend
Source-->>API: Confirmed operation with signed payment and timestamp
API-->>UI: Ledger/income entry with original Money and instrument identity
UI-->>User: Shared payout card - executed, original signed amount
else Upcoming coupon or dividend
Source-->>API: Forecast event based on position at calculatedAt
API-->>UI: Future event, date kind and nullable projected Money
UI-->>User: Same payout card - forecast, registry date where applicable
else Instrument schedule event
Source-->>API: Published coupon/dividend terms, possibly undated/unknown
API-->>UI: Per-security amount, optional estimate and dates
UI-->>User: Same payout card - schedule, not a confirmed receipt
else Other operation or future event
API-->>UI: Trade/tax/fee/maturity/offer data
UI-->>User: Existing non-payout representation
end
Note over UI,User: Same payout presentation; source and certainty remain explicit
@@ -0,0 +1,67 @@
# Tasks
## 1. Удаление сравнений и поставка контракта дохода
- [x] 1.1 [backend] Оставить для дохода только выбранный период в чтении и планировании фоновой загрузки; сохранить точные итоги, signed entries, полноту, текущую месячную сводку и существующие buckets.
- **Covers:** `brokerage-account-income-analytics` — `Period comparisons preserve calendar and currency meaning` (удаление); `brokerage-account-income-analytics` — `Realized analytics never presents an incomplete result as complete`; `brokerage-account-income-analytics` — `Realized income analytics is account-scoped and period-bound`; `brokerage-account-income-analytics` — `Account overview has an independent current-month income summary`.
- **Source:** `design.md` §§ Decisions/backend, Data / Storage Changes; `system-analysis/analysis.md` §§ Interaction Flows, Data and State Semantics.
- **Verification:** адресные backend tests: один выбранный диапазон в аналитике и screen sync, без previous/year; полные, неполные, пустые, многостраничные и signed-мультивалютные результаты; неизменная current-month summary.
- **Depends on:** none
- [x] 1.2 [backend] Удалить comparison-only поля и роли из обоих публичных income-analytics ответов и опубликовать обновлённый backend-generated OpenAPI как явный контракт для frontend.
- **Covers:** `brokerage-account-income-analytics` — `Period comparisons preserve calendar and currency meaning` (удаление); `brokerage-account-income-analytics` — `Realized analytics never presents an incomplete result as complete`.
- **Source:** `system-analysis/analysis.md` §§ Contracts, Compatibility / Migration; `system-analysis/contracts/income-analytics-response.schema.json`; `design.md` § Contract Implementation.
- **Verification:** `npm run openapi:generate`, `npm run test:openapi-contract` и сопоставление 200 response schema обоих маршрутов с утверждённым target: отсутствуют `previous`, `yearComparison` и роли `previous`/`year`, остальные данные/ошибки сохранены; релевантные HTTP checks и `npm run verify` в backend.
- **Depends on:** 1.1
- [x] 1.3 [frontend] Обновить полученный OpenAPI snapshot и generated types, удалить UI, display-only расчёты и состояния сравнений на странице дохода, сохранив фильтры, итоги, налоговое пояснение, плоские выплаты и состояния выбранного периода.
- **Covers:** `brokerage-account-income-analytics` — `Period comparisons preserve calendar and currency meaning` (удаление); `brokerage-account-income-analytics` — `User explicitly applies an analytics period represented in the URL`; `brokerage-account-income-analytics` — `Analytics summary remains readable per currency`; `brokerage-account-income-analytics` — `Income page excludes upcoming forecasts`; `brokerage-account-income-analytics` — `Realized analytics never presents an incomplete result as complete`.
- **Source:** `design.md` §§ Decisions/frontend, Contract Implementation, UI/UX Package; `system-analysis/analysis.md` § Contracts.
- **Verification:** frontend behavior checks для applied/draft периода, URL/back/reset, неполного/пустого/ошибочного selected и отсутствия comparison headings/notices во всех состояниях; локальные typecheck и `npm run verify` на итоговом frontend diff.
- **Depends on:** 1.2
## 2. Одна предметная карточка купона и дивиденда
- [x] 2.1 [frontend] Предоставить единый публичный компонент выплаты с явными режимами исполнения, прогноза и графика, сохранив исходную валюту, знак, отсутствие данных и различимые подписи суммы/дат.
- **Covers:** `brokerage-account-income-analytics` — `Income payments remain traceable to exact Ledger Entries`; `brokerage-account-ledger` — `Full ledger presents compact rows`; `brokerage-account-upcoming-events` — `Account page presents an upcoming events list`; `instrument-income-schedules` — `Instrument payout schedules do not imply executed income`.
- **Source:** `design.md` §§ Decisions/frontend, UI/UX Package; `system-analysis/analysis.md` § Data and State Semantics.
- **Verification:** frontend checks разных source modes с signed Money, нулём против неизвестного, registry date и separate per-security/position amounts; `npm run deps`, `npm run typecheck`.
- **Depends on:** none
- [x] 2.2 [frontend] Использовать единый компонент для исполненных купонов/дивидендов в доходе, полном журнале, недавних операциях и личной истории инструментов; сохранить порядок, повторы и представление остальных операций.
- **Covers:** `brokerage-account-income-analytics` — `Income payments remain traceable to exact Ledger Entries`; `brokerage-account-ledger` — `Account overview includes recent ledger events`; `brokerage-account-ledger` — `Full ledger presents compact rows`.
- **Source:** `design.md` §§ Decisions/frontend, UI/UX Package; `qa-analysis.md` § Coverage Matrix.
- **Verification:** frontend checks для одного и того же типа карточки во всех перечисленных местах, повторов IDs, исправлений с минусом, fallback identity, деталей личной истории и неизменённых trade/tax rows.
- **Depends on:** 2.1
- [x] 2.3 [frontend] Использовать тот же компонент в полном прогнозе и его превью только для купонов/дивидендов, обозначить прогноз и дату реестра дивиденда, сохранить offer/maturity.
- **Covers:** `brokerage-account-upcoming-events` — `Account page presents an upcoming events list`; `brokerage-account-upcoming-events` — `Account overview includes upcoming events`.
- **Source:** `design.md` §§ Decisions/frontend, UI/UX Package; `system-analysis/analysis.md` § Data and State Semantics.
- **Verification:** frontend checks для известной, нулевой и неизвестной projected amount, registry date, preview/full list, offer/maturity и локальных ошибок источника.
- **Depends on:** 2.1
- [x] 2.4 [frontend] Использовать тот же компонент в будущих и прошедших графиках акций/облигаций с отдельными подписями условий на бумагу и оценки без приписывания личного зачисления.
- **Covers:** `instrument-income-schedules` — `Instrument payout schedules do not imply executed income`.
- **Source:** `design.md` §§ Decisions/frontend, UI/UX Package; `system-analysis/analysis.md` § Data and State Semantics.
- **Verification:** frontend checks для будущего купона, прошлого дивиденда, недатированного/неизвестного условия, разных сумм на бумагу и позицию; проверка узкого viewport и клавиатуры, `npm run verify` на итоговом frontend diff.
- **Depends on:** 2.1
## 3. Актуальная документация после подтверждения целевого поведения
- [x] 3.1 [docs] Привести страницы дохода одного и всех счетов к состоянию без сравнений и описать прежнюю полноту выбранного периода.
- **Covers:** `brokerage-account-income-analytics` — `Period comparisons preserve calendar and currency meaning` (удаление); `brokerage-account-income-analytics` — `Analytics summary remains readable per currency`; `brokerage-account-income-analytics` — `Realized analytics never presents an incomplete result as complete`.
- **Source:** `docs-impact.md` § Documentation Changes — `docs/domains/income/overview.md`, `docs/domains/portfolio/all-accounts.md`.
- **Verification:** сопоставление двух страниц с согласованными ответами income API, UI и проверка ссылок/Markdown средствами docs repo.
- **Depends on:** 1.2, 1.3
- [x] 3.2 [docs] Зафиксировать одинаковую карточку исполненной выплаты, прогнозного события и графика с разными финансовыми смыслами на существующих тематических страницах.
- **Covers:** `brokerage-account-income-analytics` — `Income payments remain traceable to exact Ledger Entries`; `brokerage-account-ledger` — `Full ledger presents compact rows`; `brokerage-account-ledger` — `Account overview includes recent ledger events`; `brokerage-account-upcoming-events` — `Account page presents an upcoming events list`; `brokerage-account-upcoming-events` — `Account overview includes upcoming events`; `instrument-income-schedules` — `Instrument payout schedules do not imply executed income`.
- **Source:** `docs-impact.md` § Documentation Changes — `docs/domains/operations/overview.md`, `docs/domains/income/upcoming-events.md`, `docs/domains/instruments/stocks.md`, `docs/domains/instruments/bonds.md`.
- **Verification:** сверить текст четырёх страниц с экранами и публичными исходными данными, проверить ссылки/Markdown средствами docs repo.
- **Depends on:** 2.2, 2.3, 2.4
- [x] 3.3 [docs] Обновить описание HTTP income analytics без дублирования машинной схемы.
- **Covers:** `brokerage-account-income-analytics` — `Period comparisons preserve calendar and currency meaning` (удаление).
- **Source:** `docs-impact.md` § Documentation Changes — `docs/architecture/http-api.md`; `system-analysis/analysis.md` § Contracts.
- **Verification:** сопоставить текст с опубликованным backend OpenAPI обоих маршрутов и target schema, проверить ссылки/Markdown средствами docs repo.
- **Depends on:** 1.2
@@ -0,0 +1,2 @@
schema: wealth-ledger-sdd
created: 2026-10-04
@@ -0,0 +1,70 @@
# Completion Evidence — quiet-user-feedback
Факты поставки, наблюдённые 2026-10-17 перед native archive. Все 31 task
отмечены выполненными; реализация и документация проверены по состоянию
`origin/develop` соответствующих репозиториев, а не по отметкам в `tasks.md`.
Change координировался между тремя репозиториями. Delivery шла не в одном
потоке: `frontend` поставлен 2026-10-06, `specs` и `docs` отстали и были
завершены 2026-10-17 по `WealthLedger/specs#50` и `WealthLedger/docs#51`.
## Delivery Map
| Область | Поставленный scope | Коммит |
| --- | --- | --- |
| frontend | Тихое чтение: удалены ручное обновление и повтор на экранах просмотра, индикатор фоновой работы и предупреждение о синхронизации. Добавлены ограниченный retry GET и модель первичного ожидания. | `5b30bb8` |
| specs | Delta-спецификации согласованы с canonical specs после часовой синхронизации; новый spec `frontend-user-feedback`. | `6682f0c`, merge `5a7e319` |
| docs | ADR 021 о тихом чтении; `ui-resilience` и затронутые `domains/**` приведены к AS-IS. | merge `e8e3ef6` |
## Нормативное поведение
Решение определено по фактической реализации. Два независимых изменения
оказались совместимы и вместе образуют принятое поведение:
- `frontend 5b30bb8` (2026-10-06) убрал индикацию собственного чтения клиента
и ручные действия повтора.
- `frontend 3c226f8` (2026-10-09, `WealthLedger/main#22`) добавил показ
состояния источника из `meta`: время последнего успеха, устаревание, исход
последней часовой попытки и неполноту.
Запрет change относится к индикации собственного чтения клиента и ручному
повтору. Состояние источника — свойство данных, а не отчёт о техническом
процессе, и остаётся видимым. Ошибки явных действий пользователя сохраняются.
## Проверки
| Проверка | Результат |
| --- | --- |
| `npx openspec validate quiet-user-feedback --strict` | valid |
| `npx openspec archive quiet-user-feedback --yes` | `Totals: + 9, ~ 12, - 0, → 0` |
| Сохранность 10 сценариев, которые удалили бы исходные `MODIFIED`-блоки | 10/10 присутствуют в canonical specs после archive |
| specs `npm run verify`, `npm run check:openspec` | pass; `openspec 1.13.1 on Node 24.21.0` |
| docs `npm run verify` | exit 0, новый ADR охвачен gate |
| specs PR #51, docs PR #52 | по 3/3 required checks |
Код-основание нормативного поведения, снятое с `frontend/origin/develop`:
```
retry actions в src/pages/** 0
Button/onClick в SourceSyncStatus.tsx 0
sourceTime + attempt renders 3
GET_DEADLINE_MS 15_000
RETRY_DELAYS_MS [1000, 2000]
INITIAL_WAIT_BUDGET_MS 10_000
```
## Ограничения
Отметки `[x]` задач 8.1–8.4 были проставлены 2026-10-06 без фактической работы
в `docs`: ADR отсутствовал, а `ui-resilience.md` описывал отменённое ручное
обновление. Расхождение обнаружено при подготовке archive и устранено в
`WealthLedger/docs#51`; archive до этого не выполнялся. `[x]` в `tasks.md`
этого change не является доказательством поставки.
Исходные delta-блоки устарели относительно canonical specs и при archive
удалили бы 10 принятых сценариев. Они переписаны в `6682f0c` до archive.
`frontend` gates в рамках завершения не перезапускались: реализация принята
в `develop` 2026-10-06 и работами 2026-10-17 не изменялась. Независимая QA
по schema `wealth-ledger-sdd` не требуется; semantic review выполнен по
фактическому коду и состоянию документации.
@@ -0,0 +1,424 @@
## Context
Frontend уже разделяет получение данных, page VM и presentation. TanStack
Query хранит успешные результаты; инвестиционные ресурсы дополнительно
возвращают `data` и `meta` с полнотой, свежестью и состоянием источника.
`useResourceQuery` сохраняет общую query identity обычного чтения и наблюдения,
а `useScreenSync` наблюдает известную работу и предоставляет ручные действия.
На счетах и портфеле используются `InvestmentRefreshState` и
`SourceSyncStatus`; сходные элементы встроены в остальные finance views.
Сейчас query retries и focus refetch отключены, ошибка status GET прекращает
наблюдение, а `observe()` также запрещает retry. В банковских формах отдельно
существуют технические повторы загрузки справочников и перечитывание записи
после конфликта версий. Это разные случаи: удаление всех обработчиков с именем
`retry` или `onReload` без сохранения их ответственности недопустимо.
Предоставленный frontend OpenAPI уже содержит обычное чтение ресурсов,
`sync=observe`, status GET и принудительный screen refresh. Дополнительная
сверка реализации показала: обычное чтение запрашивает missing/stale источники,
свежие полные источники переиспользуются, существующая running work объединяется.
Принудительный refresh обходит freshness и охватывает зависимости экрана.
Поэтому он не подходит для автоматического восстановления обычного чтения.
Основные specs текущего store отстают от части реализованной синхронизации.
Есть пересечение с active change `persist-tbank-data`; его старые UI-требования
не используются как новая норма поверх согласованных quiet Delta Specs.
Это различие учитывается в порядке согласования требований ниже.
## Goals / Non-Goals
**Goals:**
- Сосредоточить transport recovery в существующем query layer, а восстановление
инвестиционной работы — у существующего владельца `useScreenSync`.
- Упростить presentation interface: view получает готовое содержимое,
недоступность или необходимое пользовательское действие, а не набор
технических статусов и callbacks повторной загрузки.
- Сохранить один финансовый кэш, действующую отмену запросов, границы доступа
и точные серверные финансовые результаты.
- Встроить редкие уведомления в существующие Mantine theme и i18n.
**Non-Goals:**
- Новый query client, offline-хранилище, event bus или универсальный scheduler.
- Автоматическое повторение мутаций, изменение auth refresh или правил
разрешения конфликтов пользовательских данных.
- Новые endpoints, удаление существующего refresh endpoint, изменение DTO,
серверных TTL, алгоритмов расчёта или схемы хранения.
- Переработка композиции финансовых страниц и диаграмм.
## Decisions
### 1. Два владельца восстановления вместо одного глобального retry
`shared/api` владеет классификацией transport errors, ограниченным retry и
deadline application GET. Существующие authenticated query hooks применяют
общую политику к чтению карт, справочников и статуса подключения. Auth bootstrap
и пользовательские mutations сохраняют текущую отдельную политику.
`useResourceQuery` использует тот же transport recovery, но жизненный цикл
инвестиционной работы остаётся в `features/tbank-connection`. `useScreenSync`
становится автоматической orchestration текущего экрана: его callers больше
не передают `refresh`/`retry` в UI. Page VM продолжает владеть смыслом готовности
конкретного результата, выбранными параметрами и доступными действиями.
Альтернативы: глобальный retry всех запросов и mutations нарушает границы
записи и доступа; независимые timers каждой страницы размножают политику.
Собственный универсальный recovery service дублирует Query lifecycle.
### 2. Конечная transport policy
Для application GET принимаются следующие параметры:
| Параметр | Решение |
| --- | --- |
| HTTP deadline | 15 секунд на попытку, включая чтение тела ответа |
| Автоматические повторы | Не более двух после исходного запроса |
| Паузы | 1 и 2 секунды; серия не перезапускается от render |
| Временные ошибки | Подтверждённая transport/network failure, timeout, HTTP 408, 500, 502, 503, 504, `TBANK_UNAVAILABLE` |
| Не повторять | Отмена, ошибки ввода, 401/403/404, известные access/domain failures, 409, неизвестный постоянный исход |
| Rate limiting | 429 не повторяется короткой серией без доступного корректного `Retry-After`; наличие серверной отсрочки исключает более ранний повтор |
| Новая серия | Новый актуальный запрос, возвращение сети или возвращение пользователя на экран |
Классификация использует существующие `ApiErrorCode` и структурированный
Problem Details status, с приоритетом отказа доступа над общим HTTP status.
Отсутствие известного code само по себе не означает временную ошибку.
Transport failure маркируется там, где выполняется GET: ошибка render,
`select` или преобразования данных не превращается в network failure.
Если `Retry-After` используется, его transport metadata сохраняется локально
в ошибке клиента, без нового поля публичного DTO.
Deadline применяется только к application GET, не к auth endpoints и не к
mutations. Сигнал Query объединяется с сигналом timeout; исходная отмена
сохраняет причину и не повторяется. Существующий единственный auth refresh
не дублируется query retry. Восстановление GET после исчерпанного auth refresh
не допускается до восстановления сессии.
Для обычных authenticated reads применяются штатные Query focus/reconnect
events и текущие entity `staleTime`. На инвестиционных ресурсах обработка этих
событий координируется `useScreenSync`: параллельный автоматический refetch
тех же ресурсов из второго владельца выключен. Lifecycle события берутся
из Query focus/online managers, без второго набора browser listeners.
Уход последнего consumer отменяет запрос и его retries через существующий
AbortSignal. Imperative чтение, начатое screen owner, также прекращается
при его cleanup, если у запроса не осталось других активных consumers.
### 3. Восстановление источника — обычным GET, наблюдение — только observe
Для инвестиционного ресурса разделяются три ситуации:
| Ситуация | Действие владельца |
| --- | --- |
| Не получен HTTP-ответ | Ограниченный transport retry того же режима чтения |
| Известна running work | Status GET и чтение проекции с `sync=observe` |
| Необходимые данные missing/stale, а work failed/cancelled либо отсутствует | Ограниченный повтор обычного GET ресурса без `sync=observe` |
Успешный HTTP `200` с `data: null` или `sync.state: failed` не считается
успешным восстановлением источника. Источники определяются из `meta` и
`meta.dependencies`; `unavailableReason` и предметные ограничения не считаются
временным сбоем. Неудачная прежняя принудительная сверка уже полного свежего
результата не запускается заново только ради удаления её внутреннего статуса.
После terminal source failure допускаются две повторные обычные загрузки
нуждающегося в данных ресурса: через 5 и 15 секунд после соответствующего
неуспеха. Бюджет относится к активации текущей выборки, а не к ID очередной
серверной попытки. Новый ID работы, успешное чтение failed metadata и повторный
render не обнуляют бюджет. После его исчерпания следующий запуск возможен
при новом lifecycle событии восстановления или выборе новой выборки.
Transport retry и source recovery не вкладываются бесконтрольно: следующая
source attempt начинается только после завершения предыдущего GET и его
transport retries. Для одной серии это не более трёх обычных загрузок ресурса,
каждая не более чем с тремя HTTP-попытками. Наблюдение running work не является
новой source attempt и никогда не вызывает принудительный POST refresh.
Status observation сохраняет существующий интервал 1 секунда при активном
экране и известной running work. Повторный tick не отменяет выполняющееся
чтение или его retry. После terminal status выполняется заключительное
observe-чтение; если другое чтение ещё идёт, заключительное выполняется после
него, а не теряется. После ухода, потери сессии или terminal status периодический
опрос соответствующей работы прекращается. В скрытой вкладке и offline
новые циклы приостанавливаются; возвращение возобновляет актуальную работу.
`404` status ID не означает автоматически потерю доступа ко всем счетам:
ресурс перечитывается по действующему адресу, где проверяется текущий доступ.
Подтверждённые access errors ресурса либо статуса подключения сохраняют
существующую invalidation личных данных. Коды отказа из metadata также не
должны маскироваться успешным HTTP-ответом.
Обычное чтение и observe остаются в одной generated query identity. Внутри
владельца режим запроса задаётся явно; observe не оставляет постоянный
observe-only queryFn для последующего обычного восстановления. При чтении
проверяется фактическое in-flight состояние Query, а не только снимок props.
Повторный event не создаёт второй запрос того же key. Переход на новую выборку
отменяет старые timers и не позволяет late response менять новую область.
### 4. UI readiness отдельно от query status
Page VM выбирает presentation в следующем порядке:
1. Подтверждённый access failure: скрытые личные данные и необходимое действие.
2. Доступный результат текущей выборки: loaded, partial или подтверждённый empty.
3. Отсутствие результата: первоначальный skeleton либо unavailable.
Loaded view не получает `isFetching`, время получения и callbacks технического
повтора. Технические metadata остаются у data owner для восстановления и
определения полноты. Не вводится второй кэш успешных финансовых payload.
Первичное ожидание области ограничивается 10 секундами. Ошибка завершившегося
получения или истечение этого срока переводит её в unavailable. Это UI budget,
а не отмена полезной source work: поздний успешный результат заменяет состояние
автоматически. Одна общая небольшая presentation-функция/hook в `shared/lib`
владеет временем ожидания и latch unavailable для identity области. Она
не выполняет запросы, не знает T-Bank и не копирует финансовые данные.
Page VM передаёт readiness и identity; смена выборки сбрасывает ожидание,
очередная попытка той же выборки не вызывает мигание skeleton.
Доступность проверяется явно, а не через truthiness суммы. `data: []` и полный
пустой Portfolio — успешные результаты; `data: null` не равен пустому результату.
Для неполных totals/comparisons выводится `—` с доступной подписью
«Итог пока недоступен»/«Сравнение пока недоступно». Для неполного списка
допустима подпись «Показана часть операций», без статуса синхронизации.
Ключи loaded containers не зависят от `isFetching`, revision или времени ответа.
Фильтры, локальные раскрытия, формы и focus сохраняют существующих владельцев.
В банковской пагинации прежняя страница не используется как placeholder новой:
текущий `placeholderData`, допускающий иной page, убирается для этого перехода.
Сохранение данных при refetch той же страницы обеспечивается самим Query.
`InvestmentRefreshState` и `SourceSyncStatus` удаляются вместе с их callers
и ненужными presentation props. Общие `StateView` и page skeleton переиспользуются;
нейтральная недоступность не требует нового компонента-обёртки. Ошибки форм,
валидация и сообщения, объясняющие финансовый смысл, не удаляются поиском строк.
### 5. Формы и конфликты сохраняют самостоятельное подтверждение
Запросы категорий, исходной покупки возврата и текущей версии записи получают
обычную политику чтения. Временный refetch не очищает ранее доступные варианты
и ввод. Когда необходимые данные отсутствуют, соответствующее сохранение
недоступно; отмена и выход из формы остаются доступны.
После `BANK_VERSION_CONFLICT` или аналогичного предметного конфликта существующая
invalidation автоматически получает актуальную запись. VM использует этот
результат вместо отдельной кнопки перечитывания. Пользователь по-прежнему
видит конфликт, сохраняет ввод и явно принимает новую версию перед повторным
сохранением или удалением. Фоновый ответ не принимает версию за него.
Пока новая версия неизвестна, повтор опасной записи заблокирован, но форма
не находится в бесконечном submit и допускает закрытие.
Создание и правка карт/операций, удаление, категории и изменение подключения
сохраняют `retry: false`, version и существующие intent IDs. Общая read policy
не означает автоматического повторения неуспешной записи.
### 6. Mantine Notifications с явным владельцем сообщения
Используется `@mantine/notifications` **9.6.1**, согласованный с установленными
`@mantine/core` и `@mantine/hooks` 9.6.1. Registry подтверждает эту версию и
peer requirements React/React DOM `^19.2.0`, совместимые с manifest проекта.
Стили пакета подключаются после Mantine core; один `Notifications` host
находится в application composition под MantineProvider.
Параметры: `limit=2`, `autoClose=4000`, desktop `bottom-right`, narrow viewport
`bottom-center`; видимая кнопка закрытия имеет русское доступное имя. Сообщение
не получает отдельный заголовок «Успех» и не переносит focus. Текст принадлежит
существующему i18n; имя пользователя, token и необработанный error в него не входят.
Очередь очищается при завершении или смене пользовательской сессии.
Уведомление вызывается из одного обработчика успешного действия в его VM,
не из `useEffect` по `isSuccess` и не из глобального query/mutation interceptor.
ID относится к выполнению действия, а не к названию операции вообще. Повторное
завершение того же выполнения подавляется у владельца и получает тот же ID;
следующее отдельное действие имеет новый ID. Не создаётся постоянный журнал
показанных сообщений.
Решение о необходимости подтверждения принадлежит VM, знающей оставшийся
экран. Критерий проверяется по фактически видимой выборке, а не по одному
наличию успешного HTTP-ответа:
| Действие и оставшийся контекст | Обратная связь |
| --- | --- |
| Созданная/изменённая карта или операция видна в обновлённом списке | Содержимое подтверждает результат, toast отсутствует |
| Успешная операция не видна из-за периода, страницы, фильтра или другого счёта | Одно «Операция сохранена» после закрытия формы |
| Удалённая запись исчезла из видимого списка | Toast отсутствует |
| Запись удалена, но оставшийся экран не отражает её исчезновение | Одно «Операция удалена» |
| Создание/удаление подключения меняет видимый статус | Toast отсутствует |
| Замена token при прежнем статусе «Подключён» | Одно «Подключение обновлено» |
| Категория появилась или изменена в открытом списке/поле | Toast отсутствует |
| Сохранение успешно, но связанное чтение не подтвердило результат на экране | Краткое подтверждение сохранения без сообщения о сбое чтения |
VM получает outcome связанного обновления от существующего владельца
invalidation; её решение о подтверждении выполняется один раз для action ID.
Успех записи отделён от успеха последующего GET: ошибка чтения не становится
ошибкой сохранения и не предлагает повторить уже выполненную запись.
Получение видимого подтверждения ограничено тем же 10-секундным UI budget;
после него при отсутствии подтверждения используется одно уведомление.
Отдельный глобальный наблюдатель mutation results для этого не вводится.
Альтернатива `react-toastify` добавляет вторую визуальную систему без нужной
дополнительной функции. Собственный toast из Mantine `Notification` потребовал
бы обслуживать очередь, timers и доступность; готовый пакет дешевле.
## UI/UX Applicability and Package
**Applicability and scope:** `new/changed` — меняются feedback и состояния
существующих экранов, а не их финансовая композиция.
**Package revision:** `quiet-user-feedback-ui-v1`.
Пакет полностью inline. Отдельные PNG/SVG не нужны: новая геометрия страниц,
иллюстрации и визуализация финансовых данных не вводятся. Следующие описания
и схемы входят в единое Design approval.
**Screens, states and interactions:**
| Область | Loaded / background failure | Нет результата | Действия |
| --- | --- | --- | --- |
| Счета, портфель, режим всех счетов | Заголовок сразу переходит в карточки и содержимое; строки синхронизации отсутствуют | Page-owned skeleton → нейтральная недоступность | Открыть счёт, перейти в раздел, настроить отсутствующее подключение |
| Операции, доход, события | Фильтры и доступный результат сохраняются; даты событий остаются | Недоступность только текущей выборки или расчёта | Применить/сбросить фильтры, сменить страницу, навигация |
| Поиск и детали инструментов | Поиск, график и доступные секции без технических controls | Незагруженная секция сохраняет свой заголовок и локальное состояние | Поиск, выбор инструмента и диапазона |
| Карты, сводки и история | Данные и пользовательские controls сохраняются | Локальная недоступность без ложного нулевого остатка | Добавить/изменить, фильтры, страницы |
| Настройки подключения | Статус и введённый token сохраняются при временном сбое чтения | Нет формы до первого подтверждённого статуса | Подключить/заменить/отключить после получения статуса |
| Формы и подтверждения удаления | Локальное ожидание явного действия; ошибки у владельца | Недоступность нужного справочника или актуальной версии | Сохранить/отменить; явное принятие новой версии при конфликте |
Схема loaded портфеля:
```text
Счета / Основной счёт
Основной счёт
[ ₽ 125 000,50 ] [ $ 320,00 ]
Структура портфеля
Позиции / Деньги / Последние операции / Доход
```
Между заголовком и содержимым нет зарезервированного места под удалённые
кнопки, даты и баннеры. Temporary refetch failure имеет тот же вид, что loaded.
Схема локальной недоступности:
```text
Получено купонами и дивидендами
Пока недоступно
Открыть пассивный доход →
```
Ссылка присутствует только там, где у блока уже есть предметная навигация.
Нет дополнительного «Попробуйте позже», countdown или обещания завершения.
Для основной страницы сохраняются shell и существующие пути навигации.
Нейтральное состояние — читаемый текст, не warning/error Alert. Символ `—`
для отсутствующей суммы всегда имеет доступное объяснение.
Схема редкого подтверждения:
```text
Операция сохранена ×
```
Нотификация объявляется ненавязчиво, не закрывает modal, не переводит фокус
и не является единственным носителем ошибки или необходимого действия.
При отображении над формой учитывается её focus trap; закрытие уведомления
не отбирает доступ к кнопкам формы. Повторные фоновые попытки не создают
live-announcements. Skeleton busy остаётся только у реально ожидающей области.
**Responsive behavior:** существующие layout и semantic tokens сохраняются.
При ширине 320–767 px нейтральные состояния занимают ширину области; toast
располагается снизу по центру с отступами и учётом safe area, без горизонтального
скролла. От 768 px — справа снизу. Сообщения переносятся, кнопка закрытия
остаётся доступной. Контейнер уведомлений не блокирует pointer events вне
самих сообщений и не перекрывает единственный путь к основным controls.
Появление учитывает reduced motion.
**Reused source and differences:** формального reuse чужого SDD UI package нет.
Применяются текущие Mantine theme, `StateView` и page-owned skeleton; их
ответственность сохраняется. Отдельная новая theme или макет неизменяемых
финансовых секций не создаются.
## Delivery Map
| Area | Status | Architectural outcome or N/A reason |
| --- | --- | --- |
| frontend | `affected` | Query recovery, автоматическое наблюдение ресурсов, упрощённые VM/views, сохранение form conflict flow, Mantine Notifications, согласованные UI/data conventions и каталог состояний |
| backend | `N/A` | Существующие обычные GET, observe и server-side work deduplication достаточны; DTO, force endpoint, TTL, хранение и серверные повторы не меняются |
| specs | `affected` | Согласованные Delta Specs заменяют старые UI/retry obligations; новое правило feedback входит в canonical specs и индекс; пересечение с persist lifecycle разрешается без переписывания чужого active change |
| docs | `affected` | As-built объяснение тихой загрузки, восстановления и уведомлений заменяет описание ручного refresh в UI resilience, data freshness и затронутых предметных страницах; принятое сквозное решение фиксируется в журнале ADR |
## Solution Assessment
Обязательный trigger — изменение общей политики caching/recovery и
координации concurrent reads; дополнительно выбирается общий механизм уведомлений.
| Вариант | Fit и зрелость | Security и ограничения | Operating / migration cost | Решение |
| --- | --- | --- | --- | --- |
| Существующий TanStack Query + текущие resource/screen owners | Уже владеет cache, retry, cancellation, focus/online events; domain metadata остаётся отдельной | Сохраняет session cancellation; требует отличать read failure от failed work и запретить retries mutations | Без новой runtime-инфраструктуры; изменения сосредоточены в существующих owners | Выбран |
| Новый scheduler или state-machine runtime | Потребовал бы повторить текущий query lifecycle | Добавляет второе состояние и риск late-response/cache рассогласования | Новая dependency и миграция всех consumers без текущей необходимости | Отклонён |
| Периодический force refresh | Не соответствует ограниченному recovery текущих missing/stale данных | Повторяет внешнюю работу и игнорирует freshness | Лишние RPC и нагрузка; усложняет rate limiting | Отклонён |
| Mantine Notifications 9.6.1 | Зрелый companion текущей UI-системы; очередь, lifecycle, responsive position | Только доверенные локализованные тексты, без HTML/error payload | Один согласованный пакет; нет нового сервиса или отдельной темы | Выбран |
| React-Toastify | Зрелый самостоятельный механизм, но дополнительная функция не нужна | Потребует отдельно согласовывать styling и accessibility | Вторая UI-интеграция и её поддержка | Отклонён |
| Собственная очередь над Mantine Notification | Встроенный primitive есть, готовой очереди в core нет | Собственные timers, focus/live-region и cleanup повышают риск | Собственный код дороже готового companion | Отклонён |
Выбор опирается на текущие исходники и публичную документацию
[Query retries](https://tanstack.com/query/latest/docs/framework/react/guides/query-retries),
[Query cancellation](https://tanstack.com/query/latest/docs/framework/react/guides/query-cancellation),
[focus refetch](https://tanstack.com/query/latest/docs/framework/react/guides/window-focus-refetching)
и [Mantine Notifications](https://mantine.dev/x/notifications/).
Документация объясняет возможности библиотек; domain recovery и UI-пакет
полностью определены здесь и не зависят от доступа к этим ссылкам.
## Risks / Trade-offs
- **Тишина не означает актуальность в реальном времени.** TTL сам не создаёт
расписание; после исчерпания recovery budget новая серия ждёт lifecycle
event. Это ограничивает нагрузку ценой возможного ожидания следующего
возвращения на экран при долгой недоступности источника.
- **Короткий UI budget и долгая история.** Через 10 секунд может появиться
«Пока недоступно», хотя работа продолжается. Готовый результат заменяет
это состояние; уже доступные области не ждут чужой истории.
- **Умножение повторов.** Transport, status polling и source recovery имеют
разных владельцев и конечные бюджеты. Новые source IDs и failed metadata
не запускают неограниченную серию.
- **Access loss важнее cached content.** Ошибки доступа проверяются до loaded
presentation. Status ID not found не смешивается с account not found.
- **Конфликт версии не является ошибкой чтения.** Автоматическое получение
новой версии не подтверждает её принятие и не повторяет запись.
- **Уведомления могут дублировать содержимое.** Решение остаётся у action VM,
где известна видимая выборка; глобальный toast на каждый success исключён.
- **Исходные ветки не равны новой canonical норме.** Существующие local
изменения переноса PortfolioStructure сохраняются; design не требует их
отката. Удалённая актуальность specs ранее не была подтверждена из-за DNS;
фактическая база поставки требует сверки перед её объединением.
## Migration Plan
1. Сохранить текущую canonical базу quiet change и порядок требований:
утверждённое quiet-поведение заменяет старые обязательства показывать
статусы и ручной retry. Технические возможности уже предоставленного API
используются без автоматического переноса всех artifacts `persist-tbank-data`.
Его последующий sync/archive не должен восстанавливать отменённые UI-правила:
пересекающийся change требует отдельного rebase и approval до своего archive.
Если он попадает в canonical specs раньше quiet, quiet deltas адаптируются
к новой базе с сохранением согласованного поведения и повторным approval
затронутых artifacts. Чужие active artifacts сейчас не переписываются.
2. Ввести recovery у существующих владельцев чтения и отделить presentation
readiness от transport status. Сохранить текущие auth/access/invalidation
semantics и различие обычного чтения и observe.
3. Перевести экраны и формы на тихую presentation, убрать ручные технические
controls и сохранить предметные conflict actions. Подключить единый host
уведомлений и явные action outcomes. Эти части выпускаются одним frontend
изменением: удаление recovery-кнопок не поставляется отдельно от замены.
4. Согласовать новые conventions и presentation states, затем обновить
as-built docs и canonical specs по принятому lifecycle. Серверный rollout
и миграция данных для этого решения не требуются.
### Rollback Contract
| Field | Decision |
| --- | --- |
| Status | `required` |
| Reason | Recovery касается сохранения личных снимков, access invalidation и взаимодействия с финансовыми mutations |
| Trigger | Новая политика показывает данные прежней границы, создаёт неконтролируемые запросы, повторяет запись или ломает восстановление без ручных controls |
| Actions | Вернуть предыдущую frontend-поставку целиком: старые query policies вместе с прежними UI recovery controls; не откатывать только автоматическое восстановление под уже тихим UI |
| Data safety | Не менять БД, серверные данные, connection revision, intent IDs и versions; не повторять пользовательские mutations для компенсации; при подозрении на неправильный показ завершить локальную сессию и очистить её кэш |
| Verification | После отката данные разделены по сессии и подключению, временный сбой имеет прежний доступный recovery path, чтение ограничено прежней политикой, сохранение и удаление требуют явного действия |
@@ -0,0 +1,102 @@
## Why
Пользователь WealthLedger хочет видеть свои финансы, а не обслуживать получение
данных. Сейчас сообщения «Источник не удалось обновить», «Сохранённые данные
не изменены», время последней загрузки и действия «Обновить»/«Повторить»
перекладывают на него контроль технической работы приложения. Эти элементы
повторяются на разных экранах и отвлекают от содержимого.
Простого удаления надписей недостаточно: восстановление после временного сбоя
не должно зависеть от исчезнувшей кнопки. Нужен единый принцип:
тихое чтение и обновление, самостоятельное восстановление и понятный результат
явных действий пользователя.
## What Changes
- На экранах просмотра приложение сохраняет доступные данные при фоновом
обновлении и временном сбое, не показывает технические предупреждения,
индикаторы фоновой работы, отметки времени получения и элементы ручного
обновления или повторной загрузки. Это относится к счетам, портфелю, операциям,
доходам, событиям, инструментам, поиску, картам и чтению состояния подключения.
Те же сообщения не переносятся во всплывающие уведомления или подсказки.
- После временных сбоев приложение самостоятельно возвращается к нормальной
работе, не требуя от пользователя повторять загрузку или перезагружать страницу.
- Первичное ожидание сохраняет понятную структуру будущего содержимого. При
затянувшейся недоступности вместо бесконечной загрузки показывается спокойное
локальное состояние «Пока недоступно» без технического повтора. После
восстановления содержимое появляется автоматически. Сбой одного блока не
скрывает остальные доступные блоки.
- Отсутствие данных, подтверждённая пустота и неполный финансовый результат
остаются различимыми. Неизвестные суммы не становятся нулями, незавершённый
расчёт не выдаётся за окончательный, а незагруженный список — за отсутствие
операций. Неполнота передаётся состоянием конкретного результата без отчёта
о процессе синхронизации. Даты операций, выплат и выбранных периодов сохраняются.
- Редкие краткие уведомления подтверждают завершённые действия пользователя,
когда результат иначе легко пропустить. Очевидные изменения экрана не
сопровождаются дублирующим уведомлением. Фоновая загрузка не порождает
уведомлений об успехе или ошибке, а повтор одного события — каскад сообщений.
- Ошибки ввода и неуспешное сохранение остаются у соответствующего поля или
формы с сохранением ввода. Действия, необходимые от пользователя, например
вход или восстановление подключения, остаются понятными и доступными.
Исчезающее уведомление не является единственным сообщением о таком препятствии.
- Обновление данных не сбрасывает выбранные счёт, инструмент, период, фильтры
и не прерывает работу пользователя. Личные данные остаются доступны только
тому, кто имеет право их просматривать.
Ожидаемый результат: пользователь видит устойчивое финансовое содержимое,
не управляет загрузкой и получает обратную связь только там, где она помогает
понять результат его действия или необходимый следующий шаг.
Non-goals:
- Скрывать неуспешные изменения данных или выдавать их за сохранённые.
- Менять финансовые расчёты, валютный смысл, даты событий и правила доступа.
- Добавлять центр уведомлений, историю сообщений или уведомления вне приложения.
- Обещать актуальность данных в реальном времени.
## Capabilities
### New Capabilities
- `specs/frontend-user-feedback/spec.md`: единая политика редких уведомлений
о действиях пользователя, локальных ошибок и отсутствия технического шума
на всех экранах приложения, включая карты и инструменты.
### Modified Capabilities
- `specs/frontend-data-resilience/spec.md`: тихое сохранение доступного
результата и самостоятельное восстановление вместо предупреждений
и ручного повтора; защита границ личных данных сохраняется.
- `specs/frontend-loading-presentation/spec.md`: отсутствие индикации фонового
обновления при доступных данных и переход от первичного ожидания к устойчивому
локальному состоянию недоступности; явные действия сохраняют обратную связь.
- `specs/frontend-ui-resilience/spec.md`: ожидаемые состояния не требуют
предупреждения об устаревании и ручного повтора; навигация после
аварийной ошибки и действия восстановления доступа остаются доступными.
- `specs/brokerage-accounts/spec.md`: тихое отображение сохранённого списка,
автоматическое восстановление временных ошибок и отсутствие ручной загрузки.
- `specs/portfolio-view/spec.md`: показ портфеля без времени получения,
предупреждений об обновлении и кнопок повтора, с автоматическим восстановлением
и сохранением корректного смысла доступных финансовых значений.
- `specs/brokerage-account-ledger/spec.md`: локальное тихое восстановление
последних операций и журнала без технических действий пользователя.
- `specs/brokerage-account-upcoming-events/spec.md`: сохранение доступных событий
при временной ошибке и локальное автоматическое восстановление вместо ручного
повтора и технической ошибки блока.
- `specs/brokerage-account-income-analytics/spec.md`: локальная недоступность
незавершённого расчёта и автоматическое восстановление дохода и его сводки
без выдачи неполного результата за окончательный.
- `specs/tbank-connection/spec.md`: автоматическое восстановление чтения статуса
без кнопки повтора; ошибки создания, замены и удаления подключения сохраняют
явную обратную связь и введённые данные.
## Impact
Изменение охватывает просмотр счетов, портфеля, операций, доходов, будущих
событий, инструментов и карт, поиск и настройки подключения.
Пользователю больше не нужно следить за обновлением данных и устранять
временные сбои загрузки вручную. Интерфейс становится спокойнее, а сообщения
помогают понять результат собственного действия или необходимый следующий шаг.
Финансовый смысл данных, понятность ошибок сохранения и защита личной
информации сохраняются.
@@ -0,0 +1,82 @@
## MODIFIED Requirements
### Requirement: Realized analytics never presents an incomplete result as complete
Система MUST формировать итог выбранного периода, его trend buckets,
instrument breakdown и drill-down только после успешного получения всех
необходимых cursor pages исполненных операций этого периода. Каждое comparison
MUST дополнительно требовать полного результата соответствующего периода
сравнения. Ошибка любой необходимой страницы MUST NOT возвращать частичные
totals, comparisons, buckets или
breakdown как полный результат. При временной недоступности клиент MUST
сохранять предыдущий полный результат той же выборки либо показывать
нейтральную недоступность соответствующего расчёта без технических сообщений
и ручного повтора. Восстановление SHALL выполняться автоматически по
`frontend-data-resilience`.
Доступные независимые части MUST оставаться видимыми; неполное сравнение MUST
обозначаться как недоступное без нулевой базы и окончательной разницы. Успешный
полный период без подходящих выплат MUST возвращать нулевой результат и
отображаться с объяснением пустого состояния. Неизвестный результат MUST NOT
подменяться таким нулевым результатом.
#### Scenario: Все страницы операций успешно получены
- **WHEN** для расчёта требуется несколько cursor pages и каждая страница успешно получена
- **THEN** система формирует результат по всем подходящим Ledger Entries со всех страниц
#### Scenario: Одна из страниц операций недоступна
- **WHEN** получение любой необходимой cursor page завершается временной ошибкой
- **THEN** страница сохраняет предыдущий полный результат той же выборки либо показывает нейтральную недоступность расчёта без ручного повтора и не выдаёт частичный результат за полный
#### Scenario: В периоде нет купонов и дивидендов
- **WHEN** все необходимые операции успешно получены, но среди них нет подходящих coupon или dividend Ledger Entries
- **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 наследовать последний пользовательский период страницы аналитики.
Пустой или неуспешный расчёт MUST оставаться локальным состоянием блока и
MUST NOT заменять успешно отображённый Portfolio или другие блоки обзора.
Временная ошибка обновления MUST сохранять доступную сводку той же выборки без
предупреждения и ручного повтора. При отсутствии результата SHALL показываться
нейтральная недоступность после первичного ожидания с автоматическим восстановлением.
#### 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 сохраняет предыдущий результат той же выборки либо показывает нейтральную недоступность без ручного повтора
## ADDED Requirements
### Requirement: Доход обновляется без технических сообщений
Страница дохода MUST сохранять применённый период, черновик фильтров и
доступный результат при фоновом обновлении. Временная ошибка SHALL
восстанавливаться автоматически без кнопок обновления и повтора, времени
получения данных и уведомлений об их загрузке. Изменение периода MUST NOT
подписывать прежний результат новыми датами. Ошибки пользовательского периода
MUST оставаться у полей и MUST NOT запускать автоматический повтор ошибочного
ввода. Подтверждённая потеря доступа MUST скрывать личные результаты.
#### Scenario: Доход обновляется при редактировании периода
- **WHEN** пользователь редактирует черновик дат, а фоновое обновление применённого периода завершилось
- **THEN** черновик и фокус сохраняются, результат относится к прежнему применённому периоду и техническое уведомление не появляется
#### Scenario: Новый период пока недоступен
- **WHEN** пользователь применил другой допустимый период, но его расчёт ещё не получен
- **THEN** страница показывает ожидание или недоступность нового результата, не выдаёт прежние суммы за доход нового периода и восстанавливается автоматически
@@ -0,0 +1,46 @@
## MODIFIED Requirements
### Requirement: Account overview includes recent ledger events
Обзор счёта MUST показывать пять последних исполненных записей журнала этого
счёта и действие перехода к его полному журналу. Каждая запись MUST иметь ту же
визуальную идентичность инструмента и семантику денежного движения, что и запись
полного журнала. Неуспешный или пустой запрос MUST оставаться локальным состоянием
блока и MUST NOT заменять успешно отображённый Portfolio. При временном сбое
обновления блок MUST сохранять доступные записи без технического предупреждения
или ручного повтора. При отсутствии результата он SHALL показывать первичное
ожидание либо нейтральную недоступность, автоматически восстанавливая чтение.
Неподтверждённая пустота MUST NOT представляться отсутствием операций.
#### Scenario: Отображение последних событий
- **WHEN** загружается обзор счёта и его журнал содержит исполненные операции
- **THEN** обзор показывает не более пяти последних записей и ссылку на полный журнал
#### Scenario: Последние записи недоступны
- **WHEN** обзор счёта успешно загружается, но запрос последних записей завершается временной ошибкой
- **THEN** Portfolio остаётся доступным, а блок сохраняет прежние записи либо показывает нейтральную недоступность без технического повтора и восстанавливается автоматически
#### Scenario: Последняя запись не содержит visual metadata инструмента
- **WHEN** одна из последних записей не имеет доступной presentation metadata инструмента
- **THEN** обзор показывает для неё понятный текстовый fallback и сохраняет дату, тип, инструмент и денежное изменение записи доступными для чтения
## ADDED Requirements
### Requirement: Журнал восстанавливает чтение без ручного обновления
Страница журнала SHALL сохранять записи текущей выборки при временном сбое
обновления и восстанавливать чтение автоматически по `frontend-data-resilience`.
Период, категории и выбранная страница MUST сохраняться; результат другой
выборки MUST NOT подменять текущую. Страница MUST NOT показывать время получения,
технические статусы или действия ручного повтора. Даты операций, фильтры,
действия их применения и переходы по страницам MUST оставаться доступными.
При отсутствии результата SHALL применяться первичное ожидание и нейтральная
недоступность, а не сообщение об отсутствии операций до подтверждения полноты.
#### Scenario: Обновление журнала временно не удалось
- **WHEN** для применённого периода и страницы уже есть записи, а обновление завершается временной ошибкой
- **THEN** записи и параметры просмотра сохраняются без предупреждения и кнопки повтора до автоматического восстановления
#### Scenario: Следующая страница ещё не получена
- **WHEN** пользователь перешёл к следующей странице, но её загрузка временно не удалась
- **THEN** область новой страницы показывает ожидание или нейтральную недоступность, не выдаёт прежние записи за новую страницу и восстанавливает чтение автоматически
@@ -0,0 +1,73 @@
## MODIFIED Requirements
### Requirement: Account page presents an upcoming events list
Страница Brokerage Account MUST предоставлять действие перехода на отдельную
страницу «Будущие события». Эта страница MUST показывать все полученные события
компактным списком без фильтров, пагинации и суммарного итога, с датой, типом,
инструментом и прогнозной суммой в исходной валюте, если она известна. Страница
MUST объяснять, что прогноз основан на позиции счёта, указан до налогов и может
быть неполным из-за доступности данных источника. Это предметное ограничение
прогноза MUST NOT превращаться в отчёт о технической синхронизации.
При временном сбое обновления страница MUST сохранять ранее доступный результат
той же выборки с его исходными `calculatedAt` и окном, без индикации
собственного чтения и ручного повтора. Состояние источника из `meta`, включая
неуспех последней часовой попытки и неполноту, MUST представляться по
`frontend-data-resilience`. При отсутствии результата SHALL показываться
первичное ожидание либо нейтральная недоступность с автоматическим
восстановлением по `frontend-data-resilience`. Подтверждённая потеря доступа
MUST скрывать личные данные независимо от наличия сохранённого результата.
#### Scenario: Просмотр ближайших событий
- **WHEN** User открывает «Будущие события» из обзора Brokerage Account
- **THEN** он видит отсортированный список событий ближайших 30 дней, где offer обозначен без суммы
#### Scenario: Пустое состояние страницы
- **WHEN** запрос будущих событий успешно возвращает полный пустой список
- **THEN** страница сообщает, что на ближайшие 30 дней событий не найдено, и объясняет предметные ограничения покрытия источника
#### Scenario: Ошибка страницы
- **WHEN** запрос будущих событий завершается временной ошибкой
- **THEN** страница сохраняет доступный результат той же выборки либо показывает нейтральную недоступность без ручного повтора, восстанавливая чтение автоматически и не выдавая частичные данные за полный прогноз
#### Scenario: Прогнозный дивиденд с неизвестной суммой
- **WHEN** прогноз содержит дивиденд с известной датой реестра, но без суммы
- **THEN** его карточка обозначает дату как дату реестра и сумму как неизвестную, не показывает нулевую или полученную выплату
#### Scenario: Ошибка обновления при сохранённом прогнозе на странице
- **WHEN** запрос будущих событий возвращает последний полный прогноз вместе с ошибкой его обновления
- **THEN** страница показывает этот прогноз с его исходными `calculatedAt` и окном и отдельно обозначает неуспех обновления источника, не предлагая ручной повтор
#### Scenario: Первая загрузка либо ошибка без сохранённого прогноза
- **WHEN** запрос будущих событий возвращает `data: null`, а `meta` обозначает выполнение первой загрузки либо её неуспех
- **THEN** страница показывает доступное состояние первичного ожидания либо нейтральной недоступности без частичного списка и восстанавливает чтение автоматически
### Requirement: Account overview includes upcoming events
Обзор Brokerage Account MUST показывать не более пяти ближайших Future Cash
Event и действие перехода на полную страницу «Будущие события». Неуспешный
или пустой запрос этого блока MUST оставаться локальным состоянием блока и
не заменять успешно отображённый Portfolio. При временном сбое обновления
блок MUST сохранять доступные события сохранённого окна без индикации
собственного чтения; при отсутствии результата SHALL показываться нейтральная
недоступность после первичного ожидания. Состояние источника из `meta` MUST
представляться по `frontend-data-resilience`. Восстановление MUST выполняться
автоматически без ручного повтора и без подмены неизвестного результата
подтверждённой пустотой.
#### Scenario: Отображение ближайших событий
- **WHEN** обзор счёта успешно загружен и календарь содержит будущие события
- **THEN** обзор показывает первые пять событий в порядке даты и действие перехода к полной странице
#### Scenario: Ближайшие события недоступны
- **WHEN** обзор счёта успешно загружен, но запрос будущих событий завершается временной ошибкой
- **THEN** Portfolio остаётся доступным, а блок сохраняет прежние события либо показывает нейтральную недоступность без технической ошибки и ручного повтора
#### Scenario: Ближайшие события недоступны без сохранённого прогноза
- **WHEN** обзор счёта успешно загружен, а события возвращают `data: null` и `meta` обозначает выполнение первой загрузки либо её неуспех
- **THEN** Portfolio остаётся доступным, а блок будущих событий отображает собственное доступное состояние первичного ожидания либо нейтральной недоступности без частичного списка
#### Scenario: Неуспешное обновление не скрывает сохранённый прогноз
- **WHEN** обзор счёта получает сохранённый полный прогноз вместе с ошибкой его обновления
- **THEN** Portfolio и события сохранённого окна остаются доступными, а блок отдельно обозначает неуспех обновления источника, не предлагая ручной повтор
@@ -0,0 +1,116 @@
## MODIFIED Requirements
### Requirement: Экран списка брокерских счетов
Клиент SHALL загружать список при открытии защищённого маршрута `/accounts`
без ручного обновления. Клиент MUST использовать успешный frontend-кэш списка
в течение 60 секунд. После истечения этого срока открытие или возвращение
на экран SHALL инициировать получение актуального списка; имеющийся Client
Cached Snapshot MUST оставаться видимым во время обновления и после временной
ошибки без индикации собственного чтения и действий повтора.
Автоматическое восстановление SHALL соответствовать `frontend-data-resilience`.
Открытый экран MAY периодически перечитывать backend-результат, сохраняя
видимый snapshot, и MUST NOT создавать импорт T-Bank. Состояние источника из
`meta` MUST представляться по `frontend-data-resilience`.
При успешном непустом ответе клиент 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: Открытый экран перечитывает список
- **WHEN** экран Accounts остаётся открытым 30 минут после последнего успешного HTTP-чтения
- **THEN** клиент выполняет фоновое HTTP-перечитывание backend-результата, сохраняет видимый snapshot и не запускает импорт T-Bank
#### Scenario: Последняя попытка неуспешна после полного списка
- **WHEN** клиент получает полный `data` списка и `meta` с неуспехом последней часовой попытки
- **THEN** клиент сохраняет список видимым, показывает его исходное время и текстовое либо доступно именованное предупреждение о неуспешной попытке, не предлагая ручной повтор
#### Scenario: Первая загрузка списка выполняется или неуспешна
- **WHEN** клиент получает `data: null`, а `meta` обозначает выполнение первой загрузки либо её неуспех
- **THEN** клиент показывает соответственно доступное состояние загрузки либо локальной недоступности без карточек счетов и без представления частичного списка как полного
#### Scenario: Ошибка фонового обновления не скрывает snapshot
- **WHEN** обновление устаревшего списка завершается временной ошибкой при наличии последнего успешного Client Cached Snapshot
- **THEN** клиент сохраняет snapshot видимым без предупреждения и ручного повтора и восстанавливает получение данных автоматически
#### Scenario: Карточка ограничивает валютные итоги
- **WHEN** доступная сводка счёта содержит итоги в трёх или более валютах
- **THEN** карточка показывает не более двух исходных валютных итогов и обозначает наличие остальных без общего сконвертированного значения
#### Scenario: Недоступность сводки локальна карточке
- **WHEN** клиент получает список, в котором `portfolioSummary` одного счёта равна `null`
- **THEN** он показывает нейтральную недоступность только этой сводки, не скрывает остальные карточки и сохраняет переход на детали счёта
#### Scenario: Пустая сводка не является ошибкой
- **WHEN** клиент получает доступную `portfolioSummary` с пустым `totalsByCurrency`
- **THEN** он показывает нейтральное состояние отсутствия финансовых итогов, а не состояние временной ошибки
### Requirement: Клиентские состояния ошибки списка счетов
Клиент SHALL автоматически восстанавливать временно неуспешное чтение списка
по правилам `frontend-data-resilience` без кнопок обновления и повтора. При
`TBANK_NOT_CONNECTED` клиент MUST показать необходимость подключения и действие
открытия настроек T-Bank. При `INVALID_TBANK_TOKEN` клиент MUST показать
необходимость восстановить подключение и то же действие. Эти подтверждённые
состояния MUST скрывать личный список и MUST NOT запускать автоматические повторы.
При временной недоступности, включая `TBANK_UNAVAILABLE`, клиент MUST сохранять
успешный результат текущей границы доступа без предупреждений. Если результата
нет, после первичного ожидания клиент SHALL показать спокойную локальную
недоступность без технических подробностей. Неизвестная ошибка MUST получать
безопасное локальное представление; отсутствие известного кода MUST NOT
разрешать бесконечные повторы или обход доступа. Временный сбой MUST NOT
предлагать перенастройку действующего подключения как способ повторить загрузку.
#### Scenario: Подключение требуется
- **WHEN** клиент получает `TBANK_NOT_CONNECTED`
- **THEN** клиент не показывает список и предлагает открыть настройки T-Bank
#### Scenario: Известная ошибка не повторяется автоматически
- **WHEN** запрос списка завершается подтверждённой ошибкой подключения или доступа, требующей действия пользователя
- **THEN** клиент не выполняет автоматических повторов и предлагает соответствующее действие вместо технической повторной загрузки
#### Scenario: Token требует внимания
- **WHEN** клиент получает `INVALID_TBANK_TOKEN`
- **THEN** клиент скрывает личный список и предлагает восстановить подключение в настройках T-Bank
#### Scenario: Временная ошибка T-Bank
- **WHEN** клиент получает `TBANK_UNAVAILABLE` без доступного результата списка
- **THEN** клиент автоматически восстанавливает получение и после первичного ожидания показывает нейтральную недоступность без повторной загрузки и предложения перенастроить подключение
#### Scenario: Неизвестная ошибка загрузки
- **WHEN** клиент получает ошибку без известного кода списка счетов и без доступного результата
- **THEN** клиент показывает безопасное нейтральное состояние без технического сообщения или кнопки повтора, применяя только допустимое ограниченное восстановление
#### Scenario: Известная ошибка при сохранённом snapshot
- **WHEN** фоновое обновление списка с последним успешным Client Cached Snapshot получает `TBANK_UNAVAILABLE`
- **THEN** клиент сохраняет snapshot видимым без предупреждения, времени получения и ручных действий восстановления
@@ -0,0 +1,138 @@
## MODIFIED Requirements
### Requirement: Client Cached Snapshot остаётся доступным при обновлении Accounts и Portfolio
Frontend MUST сохранять последний успешный результат списка Brokerage Accounts,
Portfolio и других экранов чтения при фоновом обновлении и временной ошибке
для того же пользователя, подключения и набора данных. Подтверждённый пустой
результат MUST также считаться успешным. Смена счёта, инструмента, поискового
запроса, страницы или применённого периода MUST NOT представлять данные прежней
выборки как результат новой.
Доступный результат MUST оставаться видимым без индикаторов фонового чтения
клиента, технических предупреждений об этом чтении и действий ручного повтора.
Frontend MUST NOT подменять внутренние признаки свежести и полноты, успешное
время источника или ошибку обновления только ради отсутствия их отображения.
Client Cached Snapshot MUST оставаться отличным от server-side fallback;
ошибка чтения MUST NOT изменять `Portfolio.isStale`.
Состояние источника из `meta` — время последнего успешного результата, признак
устаревания, исход последней часовой попытки и неполнота — MUST представляться
текстом либо доступно именованным элементом, а не только цветом, и MUST
различать отсутствие попытки, выполнение, полный успех, частичный успех и
неуспех. Это состояние принадлежит источнику данных, а не собственному чтению
клиента, и MUST NOT требовать от пользователя действия.
Обновление MUST сохранять выбранные фильтры, период, открытые разделы, ввод,
фокус и положение просмотра, пока соответствующее содержимое существует.
При logout, смене подключения или подтверждённом отказе доступа личные данные
прежней границы MUST перестать отображаться; поздний результат прежней загрузки
MUST NOT возвращать их на экран.
#### Scenario: Список Brokerage Accounts обновляется в фоне
- **WHEN** экран Accounts уже показывает Client Cached Snapshot и начинается фоновое обновление того же списка
- **THEN** frontend оставляет список видимым без индикации собственного чтения и технических действий
#### Scenario: Resource сохраняется после неуспешной часовой попытки
- **WHEN** response содержит полный `data` и `meta`, обозначающий неуспех последней часовой попытки
- **THEN** frontend сохраняет `data` и его исходное время видимыми и отдельно обозначает неуспех попытки, не предлагая ручной повтор
#### 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 сохраняет доступные данные без технических сообщений и не подменяет один внутренний вид состояния другим
#### Scenario: Подтверждённая пустота переживает фоновую ошибку
- **WHEN** текущая выборка ранее успешно вернула полный пустой результат, а её обновление временно не удалось
- **THEN** подтверждённое пустое состояние сохраняется без предупреждения и не заменяется первичной загрузкой
#### Scenario: Изменились параметры выборки
- **WHEN** пользователь применил другой период или открыл другой счёт, а данных новой выборки ещё нет
- **THEN** экран показывает ожидание или недоступность новой выборки и не подписывает прежние суммы новым периодом или счётом
#### Scenario: Обновление не сбрасывает работу пользователя
- **WHEN** пользователь раскрыл раздел, выбрал фильтры или вводит данные, а фоновое чтение завершилось
- **THEN** обновление не сбрасывает эти действия, не переносит фокус и не возвращает пользователя в начало сохранившегося содержимого
#### Scenario: Изменился доступ к личным данным
- **WHEN** пользователь вышел, успешно сменил подключение либо получил подтверждённый отказ доступа к текущим данным
- **THEN** прежние личные данные скрываются, а позднее завершение старой загрузки не восстанавливает их
## ADDED Requirements
### Requirement: Временные сбои чтения восстанавливаются автоматически
Frontend SHALL автоматически повторять временно неуспешное чтение и восстанавливать
получение необходимых текущему экрану данных без ручного обновления. Повторы
одной попытки MUST иметь конечный предел и паузы, исключающие непрерывную
череду запросов. При исчерпании предела доступные данные SHALL сохраняться,
а при их отсутствии SHALL показываться локальная недоступность. Возвращение сети,
возвращение пользователя к открытому экрану и повторное открытие экрана MUST
возобновлять получение отсутствующих или требующих обновления данных без
перезагрузки страницы. Свежий полный результат MUST NOT требовать обращения
к источнику только из-за такого события.
Восстановление MUST учитывать сбой чтения результата и сбой самого получения
данных из источника, даже если чтение состояния завершилось успешно. Наблюдение
уже выполняющейся работы MUST NOT запускать её повторно. Уход с экрана MUST
прекращать новые попытки его восстановления; это не требует отменять уже начатую
работу источника. Автоматическое восстановление MUST NOT повторять запросы,
требующие исправления ввода, входа или восстановления доступа, и MUST NOT
переносить старые параметры в новую пользовательскую выборку.
#### Scenario: Кратковременный сбой чтения
- **WHEN** чтение текущей выборки временно не удалось, а следующая разрешённая автоматическая попытка успешна
- **THEN** экран самостоятельно показывает результат без ручного действия и без сообщения о сбое или восстановлении
#### Scenario: Источник завершил работу ошибкой
- **WHEN** чтение сохранённых данных успешно, но получение необходимых новых данных из источника временно не удалось
- **THEN** приложение применяет ограниченное автоматическое восстановление к неуспешной работе, а не считает успешное чтение доказательством её успеха
#### Scenario: Повторы исчерпаны
- **WHEN** временная недоступность сохраняется после конечного предела автоматических попыток
- **THEN** приложение прекращает серию повторов, сохраняет доступный результат либо локальную недоступность без бесконечной загрузки и ожидает следующего события восстановления
#### Scenario: Сеть или пользователь вернулись
- **WHEN** после временного сбоя восстановилась сеть либо пользователь вернулся к экрану с отсутствующим или требующим обновления результатом
- **THEN** получение данных возобновляется автоматически для текущей выборки и при успехе заменяет состояние недоступности содержимым
#### Scenario: Несколько событий восстановления совпали
- **WHEN** сеть восстановилась одновременно с возвращением пользователя на экран
- **THEN** приложение не запускает дублирующую работу получения одного набора данных
#### Scenario: Работа источника ещё выполняется
- **WHEN** приложение восстанавливает чтение состояния уже выполняющейся работы
- **THEN** оно продолжает получение её результата без повторного запуска той же работы
#### Scenario: Пользователь покинул экран
- **WHEN** пользователь ушёл со страницы с ожидающим восстановлением
- **THEN** новые попытки для покинутого экрана прекращаются, а уже начатая работа не изменяет содержимое другой выборки
#### Scenario: Ошибка требует действия пользователя
- **WHEN** подтверждены недействительное подключение, отсутствие доступа или некорректный ввод
- **THEN** автоматическая серия повторов прекращается и появляется соответствующее действие или локальная ошибка без технической кнопки повторной загрузки
### Requirement: Неполный результат не выдаётся за окончательный
Frontend MUST различать неизвестный результат, доступную часть данных и
подтверждённую пустоту. Неполные суммы и сравнения MUST NOT показываться как
окончательные или заменяться нулями. Доступные записи MAY оставаться видимыми,
но их список MUST NOT представляться полным при неподтверждённой полноте.
Состояние SHALL относиться к содержимому конкретной области, например
недоступному итогу, без отчёта о прогрессе синхронизации. Готовая область MUST
NOT ждать догрузки истории, которая не влияет на её результат.
#### Scenario: Итог ещё не определён
- **WHEN** часть данных, необходимых для суммы или сравнения, отсутствует
- **THEN** соответствующее значение обозначается как недоступное без вымышленного нуля или окончательного частичного итога
#### Scenario: Пустота ещё не подтверждена
- **WHEN** записи не получены и полнота выбранного периода неизвестна
- **THEN** экран не сообщает об отсутствии операций или выплат за этот период
#### Scenario: Период готов раньше остальной истории
- **WHEN** данные показанного периода полны, а не влияющая на него история ещё загружается
- **THEN** пользователь видит готовый результат без сообщения о догрузке остальной истории
@@ -0,0 +1,52 @@
## MODIFIED Requirements
### Requirement: Контекстная индикация действий и фонового обновления
Frontend MUST сохранять уже доступное содержимое или элемент управления
основным контекстом, когда выполняется пользовательское действие либо фоновое
обновление Client Cached Snapshot. Явное действие пользователя SHALL иметь
компактную индикацию ожидания рядом с соответствующим элементом управления.
Фоновое обновление при доступном результате MUST проходить без индикаторов,
технических сообщений и объявлений о ходе работы. В обоих случаях frontend
MUST NOT заменять уже доступный контекст skeleton placeholders.
#### Scenario: Выполняется пользовательское действие
- **WHEN** User запустил действие и его результат ещё ожидается
- **THEN** frontend сохраняет соответствующий элемент управления или окружающий контекст видимым, показывает компактную индикацию процесса и не заменяет интерфейс skeleton placeholders
#### Scenario: Обновляется уже показанный snapshot
- **WHEN** frontend обновляет данные при наличии видимого Client Cached Snapshot
- **THEN** frontend оставляет snapshot видимым без индикации фонового обновления и без замены snapshot skeleton placeholders
## ADDED Requirements
### Requirement: Первичное ожидание не становится бесконечной загрузкой
При отсутствии результата frontend SHALL сначала показывать структурное
ожидание соответствующей области. После неуспешного завершения получения
либо истечения ограниченного времени первичного ожидания область MUST переходить
в спокойное локализованное состояние недоступности без технических подробностей
и ручного повтора. Это состояние MUST NOT утверждать, что результат пустой,
или требовать перезагрузки страницы. Продолжающееся автоматическое восстановление
MUST NOT заставлять область постоянно переключаться между недоступностью и
skeleton. После успешного получения содержимое SHALL появляться автоматически.
Отсутствие результата одной области MUST NOT скрывать остальные доступные
области. Первичная загрузка MUST сохранять доступное обозначение занятости,
а состояние недоступности MUST NOT продолжать объявляться бесконечно занятым.
#### Scenario: Первый результат успешно получен
- **WHEN** экран ожидает первый результат и получает его в пределах первоначального ожидания
- **THEN** структурные placeholders сменяются содержимым либо подтверждённым пустым состоянием
#### Scenario: Первое получение данных не удалось
- **WHEN** у области нет результата и первоначальная загрузка завершилась ошибкой или превысила ограниченное время ожидания
- **THEN** область показывает нейтральную недоступность без кнопки повтора, вымышленных данных и бесконечного индикатора занятости
#### Scenario: Восстановление продолжается после недоступности
- **WHEN** область уже показывает недоступность и выполняется очередная разрешённая попытка восстановления
- **THEN** область сохраняет устойчивое состояние без мигания skeleton и автоматически показывает содержимое после успеха
#### Scenario: Один блок недоступен
- **WHEN** основной результат загружен, а дополнительный блок не получил данных
- **THEN** состояние недоступности ограничено этим блоком и не заменяет доступное содержимое страницы
@@ -0,0 +1,20 @@
## MODIFIED Requirements
### Requirement: Ожидаемые finance states не становятся route failures
Frontend SHALL сохранять локальное отображение известных состояний Brokerage
Account, Portfolio и T-Bank connection. Эти состояния MUST NOT заменяться
глобальным fallback. Временная ошибка фонового чтения MUST сохранять доступный
результат без предупреждения об устаревании, технической ошибки и ручного
повтора. При отсутствии результата SHALL показываться локальная недоступность
с автоматическим восстановлением. Подтверждённая необходимость входа,
восстановления подключения или выбора доступного счёта MUST сохранять понятное
объяснение и соответствующее действие, не маскируясь сохранёнными личными данными.
#### Scenario: Portfolio Snapshot устарел
- **WHEN** frontend получает устаревший Portfolio Snapshot в действующей границе доступа
- **THEN** frontend отображает Portfolio без предупреждения об устаревании и без глобального fallback
#### Scenario: Ошибка T-Bank connection
- **WHEN** запрос T-Bank connection завершается известной ошибкой
- **THEN** frontend сохраняет локальное состояние: временный сбой чтения восстанавливается автоматически, а подтверждённая необходимость действия пользователя сопровождается этим действием вместо глобального fallback
@@ -0,0 +1,112 @@
## Purpose
Определяет спокойную и понятную обратную связь во всех разделах WealthLedger:
пользователь видит финансовые данные и результат своих действий, а не состояние
внутреннего процесса получения данных.
## ADDED Requirements
### Requirement: Чтение данных не требует внимания к техническим состояниям
На экранах счетов, портфеля, операций, доходов, событий, инструментов, поиска,
карт и настроек подключения приложение MUST NOT показывать время получения
данных, признаки их технического устаревания, ход фонового обновления и сообщения
об успехе либо временной ошибке такого обновления. Приложение MUST NOT предоставлять
кнопки или иные действия ручного обновления и повторной загрузки, включая их
иконные варианты. Эти сведения MUST NOT переноситься в уведомления, подсказки
или объявления вспомогательных технологий. Даты операций, выплат, открытия
счёта и выбранных периодов MUST сохранять свой предметный смысл.
Запрет технических действий MUST NOT удалять применение фильтров, переходы между
страницами, поиск, отправку формы, вход, подключение банка или безопасную навигацию.
После неуспешного сохранения пользователь SHALL иметь возможность вновь отправить
сохранённый ввод обычным действием формы.
#### Scenario: Фоновая загрузка завершилась ошибкой
- **WHEN** на любом из перечисленных экранов уже есть доступный результат и его фоновое обновление временно не удалось
- **THEN** результат остаётся видимым без предупреждения, времени получения, индикатора, уведомления и действия ручного повтора
#### Scenario: Фоновая загрузка завершилась успешно
- **WHEN** приложение получает обновлённые данные без явного действия сохранения со стороны пользователя
- **THEN** содержимое обновляется без сообщения об успешной загрузке и без переноса фокуса
#### Scenario: Просмотр списка карт и операций карты
- **WHEN** временная ошибка возникает при обновлении уже показанных карт, сводки или операций выбранной карты
- **THEN** соответствующий результат остаётся доступным без технического предупреждения и ручного повтора, а действия работы с картой сохраняются
#### Scenario: Поиск или новый диапазон инструмента пока недоступен
- **WHEN** пользователь изменил поисковый запрос либо диапазон графика, а соответствующий результат ещё не получен
- **THEN** экран показывает ожидание или локальную недоступность нового результата без кнопки повтора и не выдаёт результат прежнего запроса или диапазона за новый
#### Scenario: Предметные даты остаются видимыми
- **WHEN** экран показывает дату операции, выплаты или выбранный период
- **THEN** эти даты остаются доступны независимо от отсутствия времени технического получения данных
### Requirement: Краткое уведомление подтверждает неочевидный результат действия
Приложение SHALL показывать краткое локализованное уведомление об успешном
явном действии пользователя, если после завершения на экране отсутствует
однозначное подтверждение результата. Если результат уже очевиден по состоянию
формы или содержимого, приложение MUST NOT дублировать его уведомлением.
Уведомление MUST описывать выполненное действие, а не внутренний процесс.
Ожидание действия MUST NOT представляться его успешным завершением.
#### Scenario: Результат действия иначе легко пропустить
- **WHEN** действие успешно завершилось, его форма закрылась, а оставшийся экран не подтверждает результат однозначно
- **THEN** пользователь получает одно краткое уведомление о выполненном действии
#### Scenario: Результат уже очевиден
- **WHEN** успешное сохранение оставляет на экране явное подтверждение результата
- **THEN** приложение не добавляет дублирующее уведомление
#### Scenario: Приём действия ещё не означает успех
- **WHEN** приложение только начало выполнение действия и окончательный результат неизвестен
- **THEN** оно не показывает уведомление об успешном завершении
### Requirement: Уведомления не мешают работе и не дублируются
Уведомления MUST быть доступны как текст, не полагаться только на цвет и не
перехватывать клавиатурный фокус. Подтверждение успеха SHALL автоматически
закрываться и позволять доступное ручное закрытие. Повторная обработка одного
результата MUST NOT создавать повторное уведомление. Приложение MUST ограничивать
число одновременно видимых уведомлений и MUST NOT создавать каскад сообщений
из-за фонового восстановления данных.
#### Scenario: Один результат обработан повторно
- **WHEN** приложение повторно обрабатывает результат того же пользовательского действия
- **THEN** пользователь не получает второе уведомление об этом результате
#### Scenario: Работа с клавиатурой продолжается
- **WHEN** уведомление появляется во время ввода или навигации с клавиатуры
- **THEN** текущий фокус сохраняется, текст уведомления доступен вспомогательным технологиям, а действие закрытия имеет понятное доступное имя
#### Scenario: Несколько действий завершились рядом по времени
- **WHEN** несколько различных действий успешно завершаются за короткое время
- **THEN** уведомления соблюдают конечный предел одновременного показа и не блокируют доступ к содержимому и действиям страницы
### Requirement: Ошибки действий и препятствия доступа остаются понятными
Ошибка ввода MUST показываться у соответствующего поля. При неуспешном
сохранении приложение MUST сохранять ввод и показывать понятную локальную ошибку
в контексте действия, не объявляя его успешным. Если продолжение требует входа,
подключения банка или другого действия пользователя, объяснение и применимое
действие MUST оставаться доступны в соответствующей области. Исчезающее
уведомление MUST NOT быть единственным способом сообщить такое препятствие.
Приложение MUST NOT самостоятельно повторять изменение пользовательских данных
только потому, что включено автоматическое восстановление чтения.
#### Scenario: Операция не сохранилась
- **WHEN** пользователь отправил форму операции, а сохранение завершилось ошибкой
- **THEN** введённые значения остаются в форме вместе с локальной ошибкой, обычное действие сохранения снова доступно и подтверждение успеха отсутствует
#### Scenario: Поле содержит недопустимое значение
- **WHEN** пользователь пытается отправить форму с недопустимым значением
- **THEN** ошибка относится к этому полю и не заменяется общим всплывающим уведомлением
#### Scenario: Требуется восстановить доступ
- **WHEN** приложение подтвердило необходимость входа или восстановления подключения
- **THEN** соответствующая область предлагает необходимое действие и сохраняет объяснение до устранения препятствия или ухода пользователя
#### Scenario: Ответ на изменение данных не получен
- **WHEN** соединение прервалось после отправки пользовательского изменения и результат неизвестен
- **THEN** приложение не объявляет успех и не повторяет изменение автоматически как обычную загрузку данных
@@ -0,0 +1,83 @@
## MODIFIED Requirements
### Requirement: Состояния и ошибки Portfolio на клиенте
Клиент SHALL загружать Portfolio при открытии защищённого маршрута
`/accounts/:accountId` и использовать успешный ответ в течение 30 секунд.
Временные сбои и возвращение к экрану с требующими обновления данными SHALL
обрабатываться автоматическим восстановлением по `frontend-data-resilience`.
Клиент MUST NOT предоставлять ручное обновление или повторную загрузку.
При первичной загрузке без успешного результата того же Brokerage Account
клиент MUST показывать структурный skeleton ожидаемого содержимого, включая
сводку, без вымышленных сумм. Область MUST иметь доступное обозначение занятости;
skeleton MUST NOT быть интерактивным или получать фокус. Неуспешное или
затянувшееся первичное получение SHALL переходить в локальную недоступность
по `frontend-loading-presentation`, а не в бесконечную загрузку.
Истечение срока свежести MUST NOT скрывать доступный Client Cached Snapshot.
При обновлении и временной ошибке клиент MUST сохранять данные того же
Portfolio, включая валютные итоги, без индикации фоновой работы собственного
чтения, технических предупреждений об этом чтении и уведомлений. Исходное
`lastSuccessfulAt` и `isStale` MUST сохранять внутренний смысл: их показ или
скрытие MUST NOT делать сохранённый снимок новым успешным результатом или
подтверждением текущего доступа. Server-side stale и ошибка клиентского чтения
MUST NOT подменять друг друга.
Состояние источника из `meta` — время последнего успешного результата, признак
устаревания и исход последней часовой попытки — не относится к собственному
чтению клиента: клиент MUST представлять его текстом либо доступно именованным
элементом по `frontend-data-resilience`, не превращая в действие пользователя.
Клиент MUST показывать штатное empty-состояние только при подтверждённом полном
результате с пустыми `positions` и `cashBalances`, без вымышленных нулевых карточек.
Неполный итог MUST NOT выдаваться за окончательный; доступные независимые данные
MUST сохраняться на экране. При подтверждённом `404` клиент MUST скрыть личный
Portfolio и предложить возврат к списку счетов. При `TBANK_NOT_CONNECTED` или
`INVALID_TBANK_TOKEN` он MUST скрыть личные данные и предложить восстановление
подключения. Эти состояния MUST NOT маскироваться сохранённым снимком или
вызывать техническую повторную загрузку.
#### Scenario: Показ устаревшего Portfolio
- **WHEN** клиент получает успешный ответ с `isStale: true` в действующей границе доступа
- **THEN** клиент показывает позиции, остатки и доступные валютные итоги и обозначает устаревание источника текстом либо доступно именованным элементом, без ручного обновления
#### Scenario: Неуспешная последняя попытка сохраняет Portfolio
- **WHEN** клиент получает полный Portfolio и `meta` с неуспехом последней часовой попытки
- **THEN** он сохраняет Portfolio и исходное время видимыми и отдельно показывает текстовое либо доступно именованное предупреждение о неуспехе попытки, не предлагая ручной повтор
#### Scenario: Недоступный счёт на клиенте
- **WHEN** клиент получает подтверждённый `404` текущего Portfolio
- **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, а обновление этих же данных завершается временной ошибкой
- **THEN** клиент сохраняет данные и суммы без предупреждения и действия повтора; ошибка не делает снимок свежим и не изменяет `isStale`
#### Scenario: Ошибка клиентского обновления при server-side stale
- **WHEN** клиент показывает Portfolio с `isStale: true`, а фоновое обновление этого же Portfolio завершается временной ошибкой
- **THEN** прежние суммы остаются видимыми без технических предупреждений и ручных действий, сохраняя различие внутренних состояний
#### Scenario: Повторное обновление успешно
- **WHEN** после временной ошибки автоматическое восстановление получает успешный Portfolio Snapshot
- **THEN** до ответа прежние суммы остаются видимыми, затем клиент показывает новый результат без уведомления об обновлении и без переноса пользовательского фокуса
#### Scenario: Штатный пустой Portfolio на клиенте
- **WHEN** успешно полученный полный Portfolio содержит пустые `positions` и `cashBalances`
- **THEN** клиент показывает существующее empty-состояние без валютных карточек с вымышленными нулевыми итогами
#### Scenario: Первичный Portfolio пока недоступен
- **WHEN** данных Portfolio нет, а первичное получение не удалось или превысило ограниченное время ожидания
- **THEN** клиент показывает нейтральную недоступность без кнопки повтора и автоматически заменяет её содержимым после успешного восстановления
#### Scenario: Требуется восстановить подключение
- **WHEN** клиент получает `TBANK_NOT_CONNECTED` или `INVALID_TBANK_TOKEN` при наличии сохранённого личного Portfolio
- **THEN** личные данные скрываются, а клиент предлагает необходимое действие подключения без технической повторной загрузки
@@ -0,0 +1,45 @@
## MODIFIED Requirements
### Requirement: Управление подключением в настройках
Клиент SHALL показывать в настройках статус T-Bank connection, текст о
необходимости read-only T-Bank Invest token, поле token и действие подключения
либо замены. Для существующего подключения клиент MUST показывать действие
удаления с modal-подтверждением. Во время создания, замены или удаления все
действия секции подключения и действия modal MUST быть отключены. После
успешного создания или замены клиент MUST очистить поле token и обновить статус;
при ошибке token MUST остаться в поле.
Временный сбой чтения статуса SHALL восстанавливаться автоматически без кнопки
повтора и технического предупреждения. Пока первый статус неизвестен, клиент
MUST NOT показывать форму как подтверждённое отсутствие подключения: после
первичного ожидания SHALL отображаться нейтральная недоступность секции. Если
статус уже известен для текущей сессии, временный сбой его фонового чтения MUST
NOT скрывать доступную секцию или очищать ввод. Подтверждённое изменение
сессии или доступа MUST NOT маскироваться старым статусом.
Ошибки создания, замены и удаления MUST оставаться локальными, сохранять ввод
и возвращать применимые действия формы. Эти действия MUST NOT повторяться
автоматически как чтение статуса. Подтверждения успеха SHALL следовать
`frontend-user-feedback`: очевидное изменение статуса не требует дублирующего
уведомления, неочевидный результат получает одно краткое подтверждение.
#### Scenario: Пользователь подтверждает удаление
- **WHEN** User подтверждает удаление в modal
- **THEN** клиент блокирует действия секции до результата и после успешного ответа показывает состояние без подключения
#### Scenario: Ошибка загрузки статуса
- **WHEN** клиент временно не может получить статус подключения
- **THEN** он автоматически восстанавливает чтение, сохраняет известный статус текущей сессии либо показывает нейтральную недоступность после первичного ожидания без формы и кнопки повтора
#### Scenario: Ошибка проверки token в клиенте
- **WHEN** создание или замена получает `INVALID_TBANK_TOKEN` либо `TBANK_UNAVAILABLE`
- **THEN** клиент показывает соответственно локализованное сообщение о token и правах либо о временной недоступности T-Bank в контексте формы, возвращает интерактивность и сохраняет введённый token
#### Scenario: Фоновое чтение не сбрасывает ввод
- **WHEN** пользователь редактирует token в доступной секции, а фоновое чтение статуса временно не удалось
- **THEN** ввод и доступная секция сохраняются без технического предупреждения или уведомления
#### Scenario: Удаление не удалось
- **WHEN** пользователь подтвердил удаление подключения, а действие завершилось ошибкой
- **THEN** клиент не показывает успех, сохраняет известный статус и локальную ошибку, возвращает применимые действия и не повторяет удаление автоматически
@@ -0,0 +1,159 @@
# Implementation tasks
Основание: согласованные Delta Specs и [Design](design.md), включая inline
UI/UX package `quiet-user-feedback-ui-v1` и Solution Assessment.
Задачи выполняются по порядку групп и пунктов. Каждая задача имеет одного
владельца репозитория; технические проверки выполняет implementer. Проверки
сети используют synthetic fixtures и существующий MSW, а не реальный T-Bank.
Перед Node-командами в каждом репозитории выполняется `nvm use --silent`.
Указанные ниже проверки — план, а не свидетельство выполненной реализации.
Перед изменениями сохраняются чужие staged/unstaged/new files, в частности
перенос PortfolioStructure. База требований сверяется по Migration Plan Design:
изменение порядка интеграции `persist-tbank-data` требует согласования
затронутых artifacts, а не молчаливого перезаписывания active change.
Recovery и удаление ручных controls поставляются вместе. Backend и workspace
не получают implementation tasks: они не входят в affected Delivery Map.
## 1. Принятые решения и зависимости
- [x] 1.1 [frontend] Применить Solution Assessment: оставить TanStack Query и существующих resource/screen owners, добавить exact `@mantine/notifications@9.6.1` с согласованным lockfile без второго query runtime или toast framework.
- **Covers:** `N/A` — dependency preparation не вводит отдельного пользовательского поведения; реализует выбранные средства из Design §1 и §6 до custom recovery work.
- **Verification:** `npm ci`; `npm ls @mantine/core @mantine/hooks @mantine/notifications react react-dom`; сверить exact versions, peer compatibility и отсутствие посторонних dependency changes.
## 2. Ограниченное восстановление чтения
- [x] 2.1 [frontend] В существующем `shared/api` реализовать классификацию временных GET failures, 15-секундный deadline с отменой чтения тела и до двух повторов с паузами 1/2 секунды; исключить auth endpoints, mutations, access/domain errors и отмену.
- **Covers:** `frontend-data-resilience` — `Временные сбои чтения восстанавливаются автоматически`; `frontend-user-feedback` — `Ошибки действий и препятствия доступа остаются понятными`.
- **Verification:** MSW-проверки через public client/query interface: recovery после network/timeout/503, конечное число запросов, прерванное чтение тела, отсутствие retries для 401/403/404/409 и malformed result, поведение 429 и серверной отсрочки по Design; `npm run test:run -- src/shared/api`.
- [x] 2.2 [frontend] Подключить read policy к authenticated query hooks и `useResourceQuery`, сохранив одну generated cache identity, независимые ordinary/observe режимы и существующие session/access fences; убрать локальные запреты retry только там, где они заменяются принятой политикой.
- **Covers:** `frontend-data-resilience` — `Client Cached Snapshot остаётся доступным при обновлении Accounts и Portfolio`; `frontend-data-resilience` — `Временные сбои чтения восстанавливаются автоматически`.
- **Verification:** Расширить resource lifecycle MSW-сценарии: cached snapshot при ошибке, `data: null` при HTTP success, ordinary GET после observe, unmount/cancel и late response после logout/connection replacement; `npm run test:run -- src/shared/api src/features/tbank-connection/model/connection-boundary`.
- [x] 2.3 [frontend] Перевести `useScreenSync` на ограниченное восстановление failed/cancelled missing/stale источников обычным GET: две дополнительные source attempts с паузами 5/15 секунд, бюджет на текущую выборку и отсутствие автоматического force POST.
- **Covers:** `frontend-data-resilience` — `Временные сбои чтения восстанавливаются автоматически`; `frontend-data-resilience` — `Неполный результат не выдаётся за окончательный`.
- **Verification:** MSW + управляемое время: failed source при HTTP 200, новая work ID без сброса бюджета, исчерпание серии, fresh complete после неудачного force, permanent unavailableReason; проверить число ordinary GET и отсутствие POST `/api/v1/tbank-sync`; `npm run test:run -- src/features/tbank-connection/model`.
- [x] 2.4 [frontend] Согласовать status observation и заключительное observe-чтение с in-flight запросами, сохранив интервал 1 секунда только для running work; различить status ID not found и подтверждённый отказ доступа к ресурсу.
- **Covers:** `frontend-data-resilience` — `Временные сбои чтения восстанавливаются автоматически`; `frontend-data-resilience` — `Client Cached Snapshot остаётся доступным при обновлении Accounts и Portfolio`.
- **Verification:** MSW-сценарии: медленное observe не отменяется каждым tick, terminal status не теряет заключительное чтение, HTTP/status failure не запускает force sync, `404` work ID перечитывает ресурс, access loss очищает личный snapshot; `npm run test:run -- src/features/tbank-connection/model/screen-sync.test.tsx src/shared/api/resource-query.test.tsx`.
- [x] 2.5 [frontend] Подключить единый lifecycle recovery на focus/reconnect и смену выборки: обычные reads используют штатную Query policy, инвестиционные — screen owner; отменять неактуальные timers и не дублировать восстановление при совпадении событий.
- **Covers:** `frontend-data-resilience` — `Временные сбои чтения восстанавливаются автоматически`; `frontend-data-resilience` — `Client Cached Snapshot остаётся доступным при обновлении Accounts и Portfolio`.
- **Verification:** Через Query focus/online managers и MSW проверить fresh/stale/missing results, offline/hidden pause, одновременный reconnect/focus, повторное открытие, переход на другой счёт/период и уход последнего consumer; `npm run test:run -- src/shared/api src/features/tbank-connection/model`.
## 3. Состояния ожидания и доступного результата
- [x] 3.1 [frontend] Реализовать общую presentation-only модель ограниченного первичного ожидания в `shared/lib`: 10-секундный budget, устойчивый unavailable latch для identity области и автоматический переход к результату; применить её к существующим loading/state presentation без второго кэша.
- **Covers:** `frontend-loading-presentation` — `Первичное ожидание не становится бесконечной загрузкой`; `frontend-loading-presentation` — `Контекстная индикация действий и фонового обновления`.
- **Verification:** Минимальная runnable проверка собственного state transition: skeleton → timeout/error unavailable → loaded, отсутствие мигания при retry, сброс только при новой выборке, cleanup и доступное busy-состояние; `npm run test:run -- src/shared/lib` и `npm run typecheck`.
- [x] 3.2 [frontend] В общих investment section models разделить readiness, полноту и access failure: успешные пустые данные не теряются, missing/partial не становятся нулевым итогом, а технические metadata остаются у владельца данных.
- **Covers:** `frontend-data-resilience` — `Неполный результат не выдаётся за окончательный`; `frontend-ui-resilience` — `Ожидаемые finance states не становятся route failures`.
- **Verification:** Проверки через потребляющие views: `null` против полного `[]`, partial rows против полного empty, недоступный total/comparison, independent ready section и access failure при cached content; `npm run test:run -- src/features/instrument-details src/pages/account-income`.
## 4. Инвестиционные экраны без технических controls
- [x] 4.1 [frontend] Перевести Accounts и AccountPage, включая «Все счета», на новые состояния: убрать даты получения, refresh/retry и технические предупреждения, сохранить валютные итоги, доступные snapshots, контекст и предметную навигацию.
- **Covers:** `brokerage-accounts` — `Экран списка брокерских счетов`; `brokerage-accounts` — `Клиентские состояния ошибки списка счетов`; `portfolio-view` — `Состояния и ошибки Portfolio на клиенте`; `frontend-data-resilience` — `Client Cached Snapshot остаётся доступным при обновлении Accounts и Portfolio`; `frontend-ui-resilience` — `Ожидаемые finance states не становятся route failures`.
- **Verification:** RTL/MSW: initial/loaded/empty/partial/unavailable, quiet refetch failure, сохранение раскрытий и focus, отсутствие вымышленных нулей, `404` и invalid token скрывают снимок; `npm run test:run -- src/pages/accounts src/pages/account src/app/routes/AppRoutes/AllAccounts.test.tsx`.
- [x] 4.2 [frontend] Перевести полный журнал и recent operations на локальное тихое восстановление; сохранить применённые фильтры, cursor navigation, даты и смысл строк, не подменять новую страницу прежней.
- **Covers:** `brokerage-account-ledger` — `Account overview includes recent ledger events`; `brokerage-account-ledger` — `Журнал восстанавливает чтение без ручного обновления`.
- **Verification:** RTL/MSW: background failure с прежними записями, missing новой страницы, отсутствие false empty и retry, локальный сбой preview не скрывает Portfolio; `npm run test:run -- src/pages/account-operations src/pages/account`.
- [x] 4.3 [frontend] Перевести страницу будущих событий и preview на сохранение доступного результата и нейтральную локальную недоступность; убрать технические статусы, сохранив даты и ограничения финансового прогноза.
- **Covers:** `brokerage-account-upcoming-events` — `Account page presents an upcoming events list`; `brokerage-account-upcoming-events` — `Account overview includes upcoming events`.
- **Verification:** RTL/MSW: cached events при временной ошибке, неизвестный результат против полного empty, offer без суммы, потеря доступа и независимость preview; `npm run test:run -- src/pages/account-events src/entities/brokerage-event src/pages/account`.
- [x] 4.4 [frontend] Перевести доход и current-month summary на тихое обновление; оставить независимые готовые результаты, недоступные comparisons без нулевой базы и неизменный пользовательский черновик периода.
- **Covers:** `brokerage-account-income-analytics` — `Realized analytics never presents an incomplete result as complete`; `brokerage-account-income-analytics` — `Account overview has an independent current-month income summary`; `brokerage-account-income-analytics` — `Доход обновляется без технических сообщений`; `frontend-data-resilience` — `Неполный результат не выдаётся за окончательный`.
- **Verification:** RTL/MSW: полный старый результат при временном сбое, missing/partial текущего периода, готовый доход с недоступным сравнением, смена периода без переименования прежних сумм, сохранение черновика; `npm run test:run -- src/pages/account-income src/pages/account`.
- [x] 4.5 [frontend] Убрать refresh/retry из поиска акций и облигаций, связав VM с автоматическим recovery и ожиданием текущего debounced query; сохранить фильтры и отсутствие подмены результата прежним запросом.
- **Covers:** `frontend-user-feedback` — `Чтение данных не требует внимания к техническим состояниям`; `frontend-loading-presentation` — `Первичное ожидание не становится бесконечной загрузкой`.
- **Verification:** RTL/MSW: смена query во время запроса, delayed/failed поиск с последующим восстановлением, отсутствие false no-results и технических controls; `npm run test:run -- src/features/instrument-search src/pages/stocks src/pages/bonds`.
- [x] 4.6 [frontend] Перевести детали акции/облигации и их секции на readiness текущей выборки: график, личные позиции, история, сведения и выплаты сохраняют доступное содержимое без технических дат и действий.
- **Covers:** `frontend-user-feedback` — `Чтение данных не требует внимания к техническим состояниям`; `frontend-data-resilience` — `Неполный результат не выдаётся за окончательный`; `frontend-ui-resilience` — `Ожидаемые finance states не становятся route failures`.
- **Verification:** RTL/MSW: independent sections, новый диапазон свечей и instrument ID не показывают прежний результат как новый, partial history не является полной; `npm run test:run -- src/features/instrument-details src/pages/stock src/pages/bond`.
## 5. Карты, формы и настройки подключения
- [x] 5.1 [frontend] Перевести CardsPage, CardPage и BankSummary на тихое чтение, ограниченное ожидание и сохранение текущей выборки; удалить placeholder прежней страницы при переходе к другой странице банковских операций.
- **Covers:** `frontend-user-feedback` — `Чтение данных не требует внимания к техническим состояниям`; `frontend-data-resilience` — `Client Cached Snapshot остаётся доступным при обновлении Accounts и Portfolio`; `frontend-loading-presentation` — `Первичное ожидание не становится бесконечной загрузкой`.
- **Verification:** RTL/MSW: карты/summary/history при refetch failure, `page` и период меняются без подмены данных, полный empty отличается от missing, формы не размонтируются из-за фоновой ошибки; `npm run test:run -- src/pages/cards src/pages/card/ui/CardPage src/entities/bank-card/api`.
- [x] 5.2 [frontend] Сохранить ввод и предметные действия BankCardForm и CategoryManager, заменив техническое перечитывание автоматической загрузкой; актуальную версию после конфликта принимать только явно, не повторяя запись.
- **Covers:** `frontend-user-feedback` — `Ошибки действий и препятствия доступа остаются понятными`; `frontend-loading-presentation` — `Контекстная индикация действий и фонового обновления`.
- **Verification:** RTL/MSW: field errors, сохранённый ввод при network failure, version conflict → автоматическое чтение → явное принятие версии, доступная отмена при unavailable, отсутствие второго mutation request; `npm run test:run -- src/entities/bank-card/ui`.
- [x] 5.3 [frontend] Перевести чтение категорий, исходной покупки и версий OperationForm/DeleteOperation на автоматическое восстановление; сохранить intent ID, refund constraints, явное подтверждение версии и недопущение повторной финансовой записи.
- **Covers:** `frontend-user-feedback` — `Ошибки действий и препятствия доступа остаются понятными`; `frontend-loading-presentation` — `Контекстная индикация действий и фонового обновления`.
- **Verification:** RTL/MSW: сбой зависимого GET без потери ввода, отсутствие ложного empty для расходов, конфликт удаления/правки, network loss после submit без auto-resubmit, сохранность version/intent при ручной повторной отправке; `npm run test:run -- src/pages/card/ui/OperationForm src/pages/card/ui/DeleteOperation src/entities/bank-card/api/banking.test.tsx`.
- [x] 5.4 [frontend] Перевести чтение T-Bank connection status на тихое восстановление: не показывать форму до первого известного статуса, сохранять уже открытую секцию и token при временном GET failure, оставить явные локальные ошибки mutations.
- **Covers:** `tbank-connection` — `Управление подключением в настройках`; `frontend-user-feedback` — `Ошибки действий и препятствия доступа остаются понятными`; `frontend-ui-resilience` — `Ожидаемые finance states не становятся route failures`.
- **Verification:** RTL/MSW: initial unavailable → loaded, сохранность ввода при фоновом сбое, single mutation на submit/delete, ошибки token и удаления, новая cache boundary только после успешной mutation; `npm run test:run -- src/features/tbank-connection`.
## 6. Редкие подтверждения действий
- [x] 6.1 [frontend] Подключить один Mantine Notifications host с общей темой, styles и i18n: limit 2, autoClose 4000 ms, desktop/mobile positions, доступное закрытие, reduced motion и очистка очереди при смене/завершении сессии.
- **Covers:** `frontend-user-feedback` — `Уведомления не мешают работе и не дублируются`.
- **Verification:** Проверить application-owned cleanup при logout/смене сессии через RTL; выполнить `npm run typecheck`; конфигурацию third-party limit/timer не дублировать unit-тестами библиотеки, доступность и размещение проверить в задаче 7.3.
- [x] 6.2 [frontend] В action VMs карт, операций и категорий отделить успех записи от последующего чтения и реализовать однократное подтверждение по таблице Design: видимый результат без toast, неочевидный — краткое сообщение с action ID и ограниченным ожиданием подтверждения.
- **Covers:** `frontend-user-feedback` — `Краткое уведомление подтверждает неочевидный результат действия`; `frontend-user-feedback` — `Уведомления не мешают работе и не дублируются`; `frontend-user-feedback` — `Ошибки действий и препятствия доступа остаются понятными`.
- **Verification:** RTL/MSW: сохранённая запись внутри/вне видимой выборки, удаление видимой/невидимой записи, успешная запись с неуспешным GET, 10-секундный предел подтверждения, повторный callback того же action ID и отдельное следующее действие; `npm run test:run -- src/entities/bank-card/ui src/pages/card src/pages/cards`.
- [x] 6.3 [frontend] В VM подключения добавить одно подтверждение успешной замены token при неизменном видимом статусе; создание/удаление с очевидным изменением статуса и все фоновые чтения оставить без toast.
- **Covers:** `tbank-connection` — `Управление подключением в настройках`; `frontend-user-feedback` — `Краткое уведомление подтверждает неочевидный результат действия`; `frontend-user-feedback` — `Чтение данных не требует внимания к техническим состояниям`.
- **Verification:** RTL/MSW: create/delete без дублирующего подтверждения, replace с единственным toast, failure без success, background recovery без notifications, сообщение без token; `npm run test:run -- src/features/tbank-connection/ui`.
## 7. Завершение frontend и UI-проверки
- [x] 7.1 [frontend] После миграции consumers удалить InvestmentRefreshState, SourceSyncStatus, неиспользуемые props/exports/строки и их устаревшие stories; проверить оставшиеся технические actions во всём scope без удаления предметных form/access controls.
- **Covers:** `frontend-user-feedback` — `Чтение данных не требует внимания к техническим состояниям`; `frontend-loading-presentation` — `Контекстная индикация действий и фонового обновления`.
- **Verification:** Проверить callers и i18n keys, отсутствие видимых/иконных/tooltip/live-region замен удалённых статусов; `npm run knip`; `npm run deps`; `npm run test:run -- src/test/storybook.test.tsx`; убедиться, что внутренние metadata и серверный контракт не удалены.
- [x] 7.2 [frontend] Обновить UI/data/testing conventions и значимые presentation stories под UI package v1: тихий loaded, нейтральный unavailable, partial без ложного итога и редкое подтверждение; сохранить изоляцию Storybook от application API/auth/query.
- **Covers:** `N/A` — conventions и каталог закрепляют уже утверждённую архитектуру и UI-пакет, не вводя отдельного поведения; необходимы для согласованной frontend delivery из Design.
- **Verification:** Сверить локальные инструкции с новыми retry/focus policies, примеры и ссылки; `npm run deps:storybook`; `npm run build:catalog`; проверить достижимость stories и отсутствие устаревшего обещания ручного retry.
- [x] 7.3 [frontend] Выполнить implementation-owned browser review состояний и interactions UI package v1 на 320, 768 и 1440 px: страницы чтения, partial/unavailable, формы, notification host и восстановление контекста.
- **Covers:** `frontend-user-feedback` — `Уведомления не мешают работе и не дублируются`; `frontend-user-feedback` — `Чтение данных не требует внимания к техническим состояниям`; `frontend-loading-presentation` — `Первичное ожидание не становится бесконечной загрузкой`; `frontend-data-resilience` — `Client Cached Snapshot остаётся доступным при обновлении Accounts и Portfolio`.
- **Verification:** В браузере проверить Tab/focus, modal trap и доступное закрытие, busy/live announcements, reduced motion, длинный текст, safe area, отсутствие перекрытых controls/горизонтального скролла, сохранение scroll и раскрытий; presentation states воспроизвести synthetic stories, runtime lifecycle — приложением и RTL/MSW, без browser MSW worker.
- [x] 7.4 [frontend] Завершить Boy Scout review затронутого diff и выполнить полный canonical frontend gate после всех production, test, dependency и convention изменений.
- **Covers:** `N/A` — финальная проверка frontend delivery по Design, без нового observable behavior; targeted tests не заменяют canonical gate.
- **Verification:** `npm ci` и `npm run verify`; отдельно сверить сохранность чужого PortfolioStructure diff, отсутствие generated/OpenAPI drift, дублированных query owners и global toast interception; записать фактические результаты и ограничения.
## 8. As-built документация
- [x] 8.1 [docs] После frontend delivery оформить принятое решение о тихом чтении и редких уведомлениях в существующем журнале ADR, с причинами, альтернативами, последствиями и связью с предыдущим решением о background sync.
- **Covers:** `N/A` — ADR фиксирует rationale уже утверждённого Design и выполненной delivery, а не создаёт новую behavioral spec; docs отмечен affected в Delivery Map.
- **Verification:** Проверить номер по актуальному `adr/README.md`, самостоятельность текста, связь с ADR 017 и сохранность его исторического содержания; новый ADR описывает реализованный механизм и не обещает непоставленное поведение.
- [x] 8.2 [docs] Обновить объяснения UI resilience, data freshness и visual states, убрав описание обязательных предупреждений/ручного обновления и объяснив ограниченное автоматическое восстановление, первичную недоступность и сохранение ошибок действий.
- **Covers:** `N/A` — as-built тематические страницы объясняют согласованные требования и технические решения; это documentation outcome Design, без самостоятельного нового поведения.
- **Verification:** Сверить `architecture/cross-cutting/ui-resilience.md`, `architecture/cross-cutting/data-freshness.md` и `design/overview.md` с реализованным flow, исключить противоречия между freshness, completeness и UI; проверить локальные ссылки и предметные даты.
- [x] 8.3 [docs] Согласовать затронутые предметные описания счетов, портфеля, операций, дохода, событий, инструментов и карт с тихим UI; обновить только фрагменты, описывающие изменённые состояния и recovery.
- **Covers:** `N/A` — предметная документация устраняет устаревшие инструкции пользователю после frontend delivery; область и поведение уже заданы Design и Delta Specs.
- **Verification:** Пройти маршруты README к затронутым domains; сверить упоминания refresh/retry, времени получения и ошибок с приложением, сохранив полезные финансовые пояснения, ограничения прогнозов и входящие ссылки.
- [x] 8.4 [docs] Выполнить semantic/link review обновлённой документации и canonical docs gate с учётом новых файлов.
- **Covers:** `N/A` — проверка документационной delivery из Design; не является новой пользовательской capability.
- **Verification:** `npm ci` при отсутствии установленных dependencies, затем `npm run verify`; проверить ADR index, новые локальные anchors и пользовательские маршруты чтения; если затронуты диаграммы, отдельно проверить их рендер доступным средством.
## 9. Canonical specifications и итоговые gates
- [x] 9.1 [specs] После подтверждения реализации перенести согласованные quiet Delta Requirements в canonical specs через OpenSpec sync workflow и добавить `frontend-user-feedback` в AI_INDEX; учесть порядок пересечения с persist по Design, сохраняя несвязанные требования.
- **Covers:** `N/A` — canonical publication не вводит поведения сверх утверждённых Delta Specs; реализует обязательный specs outcome Delivery Map и сохраняет единственный источник требований.
- **Verification:** Сравнить перенесённые Requirement blocks со всеми десятью delta paths, проверить отсутствие восстановления старых UI/retry obligations и потери несвязанных scenarios; проверить новый target в AI_INDEX; при изменившейся базе вернуть затронутые artifacts на согласование до переноса.
- [x] 9.2 [specs] Выполнить canonical specs gate, native schema/change/spec validation и итоговую сверку трассировки с реально выполненными задачами.
- **Covers:** `N/A` — проверка полноты и согласованности specs delivery, обязательная по Design и местным правилам; нового observable behavior нет.
- **Verification:** `npm run verify`; `openspec schema validate wealth-ledger-sdd --verbose`; `openspec validate quiet-user-feedback --type change --strict --store wealth-ledger-specs`; `openspec validate --specs --strict --store wealth-ledger-specs`; проверить новые файлы и их diff отдельно, а checklist отмечать только по наблюдённым результатам.
@@ -0,0 +1,2 @@
schema: wealth-ledger-sdd-lab
created: 2026-10-29
@@ -0,0 +1,216 @@
# Engineering Design
## Inputs
- Change: `wealth-ledger-specs`, `openspec/changes/group-account-rows-by-instrument/`.
- Утверждённые входы: `proposal.md`, `ux-design.md`, `specs/brokerage-account-ledger/spec.md`, `specs/brokerage-account-upcoming-events/spec.md`, `specs/brokerage-account-income-analytics/spec.md` относительно change.
- Issue: `WealthLedger/main#47`; рабочая ветка specs: `feature/47-group-rows-by-instrument`. При различии формулировки ISIN в Issue и утверждённых входов используется identity Instrument из входов.
- Проверенные источники реализации: `frontend/src/shared/api/generated/schema.d.ts`, `frontend/AGENTS.md`, `frontend/.ai/architecture.md`, conventions `engineering.md`, `typescript-react.md`, `ui.md`, `data.md`, `testing.md`.
- Ниже пути `src/` и `packages/` относятся к `frontend/`; этот документ не изменяет код. UX остаётся владельцем flows, визуальных решений и a11y.
## Architecture Drivers
- `brokerage-account-ledger` — `All-accounts ledger groups same-day entries of one instrument`: точный ключ, минимум две записи, отсутствие изменений single-account.
- `brokerage-account-ledger` — `All-accounts ledger group shows each entry and an exact total`: сохранение каждого денежного движения, точность и запрет ложного итога.
- `brokerage-account-ledger` — `All-accounts ledger grouping preserves order and limits`: серверный порядок и только показанные записи.
- `brokerage-account-upcoming-events` — `All-accounts upcoming events group same-day events of one instrument`: дата вместе с `dateKind`, identity и точный тип.
- `brokerage-account-upcoming-events` — `All-accounts upcoming event group shows each event and a forecast total`: неизвестный прогноз и неподтверждённая полнота не превращаются в ноль.
- `brokerage-account-upcoming-events` — `All-accounts upcoming event grouping preserves order and limits`: ограничение превью до группировки.
- `brokerage-account-income-analytics` — `Income payments remain traceable to exact Ledger Entries`: визуальная агрегация не заменяет записи или серверные аналитические итоги.
- Ограничения: существующие API и query lifecycle, локальный FSD profile, без новых runtime dependencies и без хранения производного состояния.
## Technical Goals / Non-Goals
- Реализовать одну чистую операцию группировки и переиспользуемую точную decimal-арифметику; предметные ключи остаются у владельцев сущностей/страницы.
- Сохранить число исходных записей, знаки, валюты, порядок, provenance и прежние ветки одиночных карточек.
- Не менять backend, фильтры, cursor protocol, аналитические totals, доступ, синхронизацию, URL или часовые пояса текущего отображения.
- Не вводить межстраничное накопление, поиск identity через сеть, валютную конвертацию, новый feature/widget или универсальный aggregation framework.
## Implementation Impact Map
| Area | Affected | Outcome or N/A reason |
| --- | --- | --- |
| main | Нет | Issue уже существует; workspace и tooling не меняются. |
| frontend | Да | Shared grouping/decimal, entity presentation, четыре page slices и EntryCard workspace package. |
| backend | Нет | DTO и envelope достаточны; endpoints и вычисления сервера остаются прежними. |
| docs | Да, после реализации | Уточнить AS-IS представление списков и границу клиентских итогов; точные страницы ниже. |
| specs | Да | Этот design; утверждённые proposal/specs/UX не переписываются. |
## Decisions
### D1. Одна чистая группировка, предметные ключи снаружи
Драйвер: `brokerage-account-ledger` — `All-accounts ledger grouping preserves order and limits`.
В `src/shared/lib/group-items.ts` разместить `groupItems<T>(items: readonly T[], keyOf: (item: T) => string | null)`.
Результат — discriminated union: single с исходным item либо group с key и исходными items (не менее двух).
Функция не знает DTO, денег, счетов, React и режима маршрута; `null` всегда оставляет самостоятельный single.
Map сохраняет позицию первого появления ключа; последующие совпадения добавляются к его записям без сортировки.
Вход не мутируется; ссылки на записи сохраняются, записи одного счёта не схлопываются. Сложность O(n), память O(n).
Три независимых реализации отклонены из-за дублирования инварианта; framework с reducers/plugins — избыточен.
### D2. Ключи соответствуют реально отображаемому дню
Драйверы: `brokerage-account-ledger` — `All-accounts ledger groups same-day entries of one instrument`; `brokerage-account-upcoming-events` — `All-accounts upcoming events group same-day events of one instrument`.
| Список | День | Identity: первое непустое поле | Тип и дополнительные поля |
| --- | --- | --- | --- |
| Журнал и последние операции | Локальный календарный день `occurredAt` в timezone браузера | `instrumentUid`, затем `figi` | Исходный `type` без нормализации |
| Выплаты | Тот же локальный день `occurredAt` | `instrumentUid`, затем `figi` | Исходный `type` |
| События и их превью | `date`, отображаемая через `formatDate` в UTC | `instrumentUid`, затем `figi`, затем `isin` | Исходный `type` и `dateKind` |
`LedgerEntryRow` передаёт `formatSnapshotTime(occurredAt)` и в обычную карточку, и в executed PayoutCard; formatter содержит дату и время без `timeZone` (`src/shared/lib/display.ts:132–139`).
Добавить рядом day-only formatter с тем же timezone браузера: использовать календарные части, не полную строку с временем и не `occurredAt.slice(0, 10)`.
`formatDate` уже фиксирует UTC (`display.ts:19–38`); для date-only события его ISO-дата достаточна как ключ.
`moscowDate` из `src/shared/lib/calendar.ts` обслуживает периоды/границы фильтров; не подменяет день отображения выплат.
Identity кодируется парой namespace/value; весь ключ — сериализованный tuple дня, identity, type и, для события, dateKind.
`account.id`, валюта, category, ticker, description и logo не входят в ключ. Пустая identity возвращает null.
`IncomeAnalyticsEntryDto.instrumentKey` — opaque membership аналитики, не доказанная identity Instrument: не использовать как fallback и не парсить.
Это подтверждает existing test `AccountIncomePage.test.tsx`, «opaque membership rather than instrument identity»: один instrumentKey допускает разные UID.
Сопоставление UID одной записи с FIGI другой через словарь aliases не вводится: нет авторитетного локального соответствия; одинаковый ticker не доказывает одну бумагу.
### D3. Точное сложение и отдельная политика допустимости итога
Драйверы: `brokerage-account-ledger` — `All-accounts ledger group shows each entry and an exact total`; `brokerage-account-income-analytics` — `Income payments remain traceable to exact Ledger Entries`.
В `shared/lib` сейчас есть string-formatting, проверка нуля и BigInt-округление, но нет экспортированного exact add.
Однако `src/pages/account/model/positions.ts:41–64,170–179` уже содержит decimal parse/serialize и точную сумму scaled BigInt.
Выделить только эту арифметику в `src/shared/lib/decimal.ts`, перевести существующего owner на неё; расчёт процентов и округление портфеля оставить локальными.
Публичная минимальная операция `sumDecimalStrings(values)` возвращает decimal string; внутри выравниваются scales и складываются signed BigInt, без Number/parseFloat и без округления.
Предусмотреть пустой ввод → `"0"`, канонический ноль без минуса, дробную часть любой поддержанной контрактом длины; не вводить библиотеку decimal.
Чистая `sumMoneyByCurrency` в shared принимает структурные `{ currency, value } | null`, возвращает список сумм либо недоступность; Map сохраняет первое появление валюты.
Если хотя бы один исходный amount отсутствует (а для журнала и «Выплат» также если он численно равен нулю), скрывается весь итог, а не только проблемная валюта. Некорректная decimal-строка также не превращается в ноль.
Только `meta?.completeness === "complete"` разрешает показ результата; отсутствие meta и `incomplete` запрещают его. Это predicate владельца ресурса, не арифметики.
Для ledger/income источник — только `payment`, не price/quantity/commission; для событий — `projectedAmount`, null означает неизвестный прогноз, включая offer.
Компенсация ненулевых слагаемых до нуля допустима: `+100 + (-100)` даёт видимый нейтральный ноль, в отличие от нулевого исходного amount.
Финальное представление — `formatMoney` с `withSign: true`, без `withRoundedCents`; серверные `totalsByCurrency`, buckets и instruments не пересчитываются.
### D4. Граница группировки — текущая выборка, не query cache
Драйверы: `brokerage-account-ledger` — `All-accounts ledger grouping preserves order and limits`; `brokerage-account-upcoming-events` — `All-accounts upcoming event grouping preserves order and limits`.
`src/entities/broker-operation/model/useBrokerageOperationHistory.ts` хранит стек cursors и читает одну страницу: `onNext` добавляет cursor, `onPrevious` снимает его.
Это previous/next pagination, не infinite append: группировать только текущие `operations.data.items`; не объединять cached pages.
Тем самым «собирается только из показанных записей» выполняется буквально; `hasNext` не означает partial и не скрывает итог текущей группы.
Превью операций получает `limit: 5` в `AccountPage.vm.ts`; событий — `slice(0, RECENT_EVENTS_LIMIT)` в `pages/account/model/previews.ts`.
Группировать после этих ограничений; никогда не добирать шестую запись ради пары. При смене периода/счёта/cursor результат вычисляется заново.
### D5. Композиция вместо новых разновидностей каждой карточки
Драйверы: `brokerage-account-ledger` — `Full ledger presents compact rows`; `brokerage-account-income-analytics` — `Income payments remain traceable to exact Ledger Entries`.
Общий недоменный `EntryGroup` в `src/shared/ui/EntryGroup/` принимает готовый header, accessible label и children; он владеет только композицией группы.
Предметные `LedgerEntryGroup` и `LedgerAccountRow` принадлежат `entities/broker-operation/ui`; `BrokerageEventGroup` и `BrokerageEventAccountRow` — `entities/brokerage-event/ui`.
Account row лёгкий, без вложенного полноценного LedgerEntryRow/Avatar; у выплат его существующие данные отображаются в карточном варианте, у остальных — строковом.
В `packages/ui/src/EntryCard/EntryCard.tsx` расширить только value slot с string до ReactNode (включая null); сохранить string callers и прежний DOM для них.
Слот допускает несколько самостоятельно оформленных валютных значений и отсутствие видимого значения; общий tone карточки не должен перекрашивать все валюты одним знаком.
Для одной внешней рамки использовать существующий `details` slot EntryCard под содержимое группы, не вкладывать вторую Paper вокруг целой карточки.
Singleton продолжает вызывать `LedgerEntryRow` либо `BrokerageEventCard`, а через них существующий `PayoutCard`; его API не расширяется ради grouping.
Альтернативы — копировать EntryCard или добавлять group/account варианты в PayoutCard — увеличивают дублирование либо смешивают ответственности.
### D6. Явный all-mode на границе page
Драйвер: `brokerage-account-ledger` — `All-accounts ledger groups same-day entries of one instrument`.
Page VM читает route param и передаёт `isAllAccounts = accountId === "all"`; это существующий sentinel, например `pages/account/ui/AccountPage/components/AccountHeading.view.tsx`.
При false сохраняется прежний flat-render path без вызова группировки и новой политики сумм. Наличие account у записи не используется как определение режима.
Не извлекать режим из URL внутри entity, имени счёта, количества счетов или query result.
## Components and Responsibilities
| Owner / путь | Ответственность и направление зависимостей |
| --- | --- |
| `shared/lib/group-items.ts`, `decimal.ts` | Недоменная чистая механика; экспорт через `shared/lib/index.ts`. |
| `entities/broker-operation/model/display.ts` | Ключ операции и group presentation; reuse `paymentPresentation`, `isIncomePayment`, `operationLabelKey`, а не category как type. |
| `entities/brokerage-event/model/event-key.ts` | Добавить отдельный grouping key; `brokerageEventKey` оставить ключом исходного события, не заменять им группировку. |
| `entities/*/ui/*Group`, `*AccountRow` | Преобразование предметных данных в shared UI; без queries и зависимости сущностей друг от друга. |
| `pages/account-income/model/display.ts` | Сохранить occurrence keys каждой выплаты, сформировать grouped sequence только в all; income DTO структурно подходит operation renderer. |
| `pages/account-operations`, `pages/account-events` | VM передаёт all и подтверждение полноты; loaded view выбирает single/group. |
| `pages/account/model/previews.ts` | Передать all/completeness каждого собственного ресурса после ограничения записей. |
| `RecentOperations.view.tsx`, `BrokerageEventsPreview.tsx` | Композиция preview single/group; состояния блоков не подменяют основной портфель. |
Импорты через public `index.ts`; page не импортирует соседнюю page. Компонент с тестом/CSS живёт в `Component/Component.tsx`, `.test.tsx`, `.module.css` по UI conventions; чистые тесты colocated `.test.ts`.
## Data / Storage Changes
N/A: новые данные не сохраняются. Группы и BigInt существуют только при вычислении presentation; ни localStorage, ни второй query cache, ни миграции БД не нужны.
## Contract Implementation
N/A для изменения контракта: проверены следующие существующие поля `src/shared/api/generated/schema.d.ts`.
| DTO | Счёт / дата / тип | Identity | Деньги |
| --- | --- | --- | --- |
| `BrokerageOperationDto` (1087) | `account?`, `occurredAt`, `type` | nullable `instrumentUid`, `figi`; ISIN нет | nullable `payment`, `price`, `accruedInterest`, `commission` |
| `UpcomingEventDto` (1229) | `account?`, `date`, `dateKind`, `type` | nullable `instrumentUid`, `figi`, `isin` | nullable `projectedAmount` |
| `IncomeAnalyticsEntryDto` (1158) | `account?`, `occurredAt`, `type` | nullable `instrumentUid`, `figi`, opaque `instrumentKey` | nullable `payment` и остальные поля операции |
`MoneyDto` содержит string `value` и `currency`; resource envelopes несут meta. Полей достаточно для алгоритма без нового endpoint, enrichment и ручных правок generated types.
Optional `account` нужен для single-account DTO; all-response должен фактически содержать `{id,name}` согласно `docs/domains/portfolio/all-accounts.md`. Типы сами эту гарантию не доказывают: проверить fixture/response на consumer boundary.
При отсутствующем account не угадывать происхождение и не строить ссылку из `all`; это нарушение данных, а не повод менять API в данном change.
## Cross-cutting Implementation
`src/shared/api/resource-query.ts:128–147` раскрывает data и meta одного envelope. Entity hooks и useScreenSync остаются владельцами загрузки и обновления.
Operations/events VM уже передают `source: query.meta` и `partial`; для итога использовать source.completeness, не инверсию partial.
Income VM сейчас передаёт только `partial: analytics.meta?.completeness === "incomplete"`; добавить явный `totalsAllowed` от meta и провести через loaded view в IncomePayments.
`previews.ts` теряет подтверждение complete при сведении к loaded/partial: добавить `totalsAllowed` отдельно для operations/events; meta портфеля здесь не подходит.
Stale/refresh failure не равны incomplete: сохранённые данные продолжают отображаться по прежней policy; новая группа не хранит собственный snapshot.
React keys групп — tuple-key; строки — прежняя identity с account и occurrence для повторений. Не дедуплицировать совпадающие события; индекс допустим лишь как occurrence внутри идентичного ключа.
Переводы и озвучивание остаются в presentation; shared арифметика не импортирует i18n. Новых telemetry, запросов и зависимостей нет.
## UI Implementation
Композиция: route → existing query/VM → текущие ограниченные items + all + totalsAllowed → pure grouping → entity single/group → shared EntryCard/EntryGroup.
Производные группы не помещаются в useState/effect; существующие состояния запроса и pagination остаются источником истины.
Ниже привязка к состояниям `ux-design.md#ui-states`, без переопределения их представления:
| UX state | Как производится |
| --- | --- |
| Одиночная запись | Single ветка результата или прежний single-account path. |
| Группа с итогом / мультивалютный итог | Два и более items; totalsAllowed; успешная сумма по валютам, переданная value slot. |
| Итог скрыт | Неподтверждённая completeness либо недопустимый amount; null visual value и перевод для accessible name. |
| Нулевой итог | Все слагаемые допустимы, точная сумма равна нулю; neutral presentation без знака. |
| Offer без сумм | Null projectedAmount; event-row использует существующий `events.noAmount`. |
| Прогноз | Event presenter использует `payout.forecast`, независимо от знака и числа валют. |
| Строка без денег | Existing payment semantics, нулевой/null amount; group total недоступен. |
| Обзор, 5 записей / граница страницы | D4; состав определяется до группировки, никаких additional fetch. |
| Loading / empty / error / partial-Alert | Existing VM/view guards; группировка только доступного массива, прежние alerts и SourceSyncStatus сохраняются. |
В all-ветке контейнеры loaded lists получают list semantics из UX; account-row использует данные счёта, не делает lookup через другой entity.
Подписи/дата/лого заголовка берутся из предметного presenter первой записи; у событий dateKind общий благодаря ключу.
EntryCard value slot и CSS module EntryGroup реализуют responsive/a11y контракт UX; отдельный локальный state раскрытия не создаётся.
## Developer Verification Strategy
Test Intent: доказать сохранение записей и денежных значений при изменении только композиции; риск — ложный итог, смешение типов/дней, межстраничная агрегация.
- Unit grouping: пустой список, одиночный ключ, null identity, не соседние совпадения, один счёт дважды, порядок первого появления и children, отсутствие мутации.
- Unit key adapters: разные type внутри одной category; UID/FIGI/ISIN priority; отсутствие identity; разный dateKind; одинаковый день с разным временем, граница суток в разных timezone.
- Unit decimal/money: разные scales, отрицательные дроби, значения больше safe integer, `0.1 + 0.2`, несколько валют, точная компенсация до нуля, null/zero/negative-zero, malformed input, пустой ввод.
- Regression выделенной арифметики: существующие `pages/account/model/positions.test.ts` и display tests проходят без изменения портфельных результатов.
- Component/route tests: все четыре поверхности all, singleton и single-account, account provenance, partial и отсутствующая meta, complete zero, unknown forecast; серверные income totals неизменны.
- Pagination через MSW: next/previous, тот же ключ на двух страницах, reset фильтра и invalid cursor; после перехода нет строк предыдущей страницы.
- Preview test: пять исходных записей, шестая не подтягивается; UI group не считается одной записью лимита. React keys не теряют повторные выплаты.
- RTL semantic queries проверяют nested lists, доступные имена и отсутствие ложного итога; browser proof — переносы, 320 px, 200% zoom и focus согласно UX, не проверки CSS через JSDOM.
- После загрузки `$HOME/.nvm/nvm.sh` и `nvm use --silent` из frontend: адресный `npm run test:run -- <path>`, `npm run typecheck`, `npm run lint`; на итоговом code diff — один `npm run verify`.
- Verify уже включает offline `openapi:check`, coverage, Knip, dependency boundaries, app/catalog build; отдельный runner или CI pipeline не добавлять. Для одного этого Markdown — semantic review и `git diff --check`, без application build.
## Migration / Rollout / Rollback
Сначала выделить арифметику с regression checks; затем pure grouping и entity composition; затем подключить all-ветки и провести meta до потребителей.
Существующие EntryCard callers сохраняют совместимость; выпуск возможен одним frontend build без координации backend или миграции cache/data.
Откат — предыдущий frontend build либо возврат flat-render all-веток; серверные данные и API не затронуты, feature flag ради этого change не нужен.
## Solution Assessment
N/A: нет новых auth, storage, integrations, caching, concurrency или shared tooling; изменение локально и обратимо. Риски точной арифметики и UI описаны проверками выше.
## Risks / Trade-offs
- В UX есть неоднозначность: Accessibility требует account links в строках вообще, Presentation Decisions п. 4 исключает обзор/события. До реализации ссылок владелец UX должен подтвердить область применения; дизайн её молча не меняет.
- UTC календарь событий, browser timezone исполненных записей и Moscow период аналитики различаются уже сейчас; намеренно не исправляем их этим change.
- Отсутствие общей identity или account нельзя восстановить из названия; проверка фактических all fixtures обязательна. instrumentKey не устраняет этот риск.
- Canonical «Account page presents an upcoming events list» говорит «без фильтров», а страница уже фильтрует период. Этот drift существовал до change и записан в WealthLedger/specs#63; фильтры в рамках группировки не трогаются.
## ADR Candidates
Отдельный ADR не требуется: API, архитектурный стиль и технологии не меняются; локальные обратимые решения D1–D6 достаточно сохранить в этом change.
## Docs Impact
Цели и итоговый AS-IS перечислены в разделе `## Docs Impact` в tasks.md, docs-impact не выбран. `docs/design/overview.md` не меняется.
@@ -0,0 +1,64 @@
# Process Record
**Mode:** lab
**Class:** standard
**Issue:** https://git.personal.ksv741.ru/WealthLedger/main/issues/47
## Change Profile
| Artifact | Selected | Reason |
| --- | --- | --- |
| research | no | Это потребность самого Change Owner, а as-is уже разобран и записан в Proposal Evidence. Рынок и альтернативы изучать не нужно. |
| ux-design | yes | Trigger: на четырёх экранах режима «все счета» появляется новый паттерн представления (заголовок группы, итог, разбивка по счетам), и он должен уживаться с существующими карточками событий и операций. |
| system-analysis | no | Не trigger: межсистемный contract, data ownership и интеграции не меняются. Это представление уже полученных данных внутри frontend. |
| data-mapping | no | Модуль требует system-analysis, а она не выбрана. Ключ группы, правило итога и запрет смешивания валют задаются Requirements в specs и проверяются в test-cases. |
| failure-model | no | Нет внешней интеграции, фоновой синхронизации и многошаговой записи. Состояния загрузки и ошибок списков не меняются. |
| nfr | no | Группировка охватывает уже загруженные строки списка. Значимых ограничений по performance, availability или capacity нет. |
| observability | no | Клиентское представление без фоновых процессов. Оператору нечего отличать. |
| security-analysis | no | Модель доступа, trust boundaries и состав раскрываемых данных не меняются: показываются те же записи текущего User. |
| test-cases | yes | Trigger: денежные итоги групп, граничные случаи ключа (тип, дата, отсутствие Instrument, один счёт, разные валюты) и стык загруженных страниц. Для их проверки нужны конкретные данные и oracle. |
| docs-impact | no | Обновляются только AS-IS страницы доменов (4 файла в `docs/domains/`). ADR, architecture- и integration-страниц нет. Цели перечислены в `## Docs Impact` в tasks.md. |
## Gate Log
| Gate | Date | Scope | Validation | Findings | Decision |
| --- | --- | --- | --- | --- | --- |
| G1 | 2026-10-29 | proposal, change-profile | `check.cjs plan` OK; `openspec validate` — ожидаемая ошибка «no deltas» до specs; G1 review product-analyst | 2 blocker, 5 major, 1 minor — закрыты решениями владельца в Proposal | approved "все ок, идем дальше" |
| G2 | 2026-10-29 | specs | `openspec validate --strict` valid; `check.cjs plan` OK; G2 review qa-engineer + independent-reviewer | 2 blocker, 6 major, 8 minor — закрыты правками specs и решением владельца (итог скрыт при `partial`) | approved "да, все ок, идем дальше" |
| G2 | 2026-10-29 | specs | — | UX нашёл 2 пробела в specs: итог при неполном прогнозе; нулевая сумма | invalidated: решения владельца в текущей сессии («Скрывать, как в журнале», «Скрывает итог») изменили Requirements об итоге группы журнала и событий |
| G2 | 2026-10-29 | specs | `openspec validate --strict` valid; `check.cjs plan` OK | Правки по 2 UX findings внесены в specs | approved "да, все ок" |
| G3 | 2026-10-29 | design, ux-design, qa-analysis, test-cases, tasks | `openspec validate --strict` valid; `check.cjs plan` planning complete; `git diff --check` clean; G3 review independent-reviewer | 3 major, 5 minor — исправлены | approved "да, давай дальше" |
| G4 | 2026-10-29 | implementation, review, QA, docs | frontend `npm run verify` exit 0 (530 tests, coverage 92.35%); docs `npm run verify` OK; independent review; QA в браузере (18 TC/сценариев pass); openspec-verify-change: 0 critical, все warnings закрыты; `check.cjs apply` OK | Review 3 major + 5 minor, verify 2 warning + 2 suggestion; всё исправлено. Flaky `screen-sync.test.tsx` есть и до change, файл не менялся | approved "да, все ок, давай коммититься" |
## Descoped Work
| Task | Reason | Gate |
| --- | --- | --- |
## Lab Journal
| Stage | Role / subagent | Unique findings | Decisions | Risks found / excluded | Handoff issues | Skills | Cost | Transferable |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| proposal + change-profile | product-analyst (G1 review) | Режим «Все счета» потерян в canonical ledger/income; запрет frontend-пересчёта итогов в «Выплатах»; неоднозначности даты и типа, порядка, лимита превью | 7 решений Change Owner внесены в Proposal | blocker ×2, major ×5, minor ×1; все закрыты | Ответ сабагента дважды обрезался, пришлось запрашивать продолжение | openspec-new-change | 3 вызова сабагента | Делать G1 review до показа владельцу: он нашёл canonical drift |
| specs | qa-engineer, independent-reviewer (G2 review) | Затронутые canonical блоки (compact rows, обзор, список событий без итога) нужно было модифицировать; восстановление `all` было неполным относительно archive; конфликт итога с frontend-data-resilience; ключ «точный тип» неоднозначен для покупки и продажи | Итог скрывается при `partial` (решение владельца); граница страницы — явное исключение; блоки восстановлены из archive целиком | 2 blocker, 6 major, 8 minor; все закрыты правками. Вне scope: canonical drift persist-tbank-data шире двух восстановленных блоков | Ответы сабагентов трижды обрезались | openspec-new-change | 4 вызова сабагентов | Ревьюеру консистентности давать список canonical блоков, которые затрагивает UI-изменение |
| design + ux-design + qa-analysis + test-cases + tasks | ux-designer, solution-architect, qa-engineer, independent-reviewer (G3 review) | UX: 2 пробела в specs (неполный прогноз, нулевая сумма), из-за них G2 переутверждён. Design: `instrumentKey` не является identity, у журнала пагинация назад/вперёд, а не дописывание, BigInt-decimal уже есть в `positions.ts`. Review: test-cases ссылались на несуществующие сценарии, неверные oracle в TC-PAG | Ссылки на счёт оставлены текстом, как сейчас; лимит превью считается по записям; Docs Impact перенесён в tasks | G3 review: 3 major, 5 minor, все исправлены. Вне scope: drift с фильтрами событий записан в specs#63 | qa-engineer сначала выдал 46 TC вместо case-only; сабагенты обрезались | — | 7 вызовов сабагентов | Давать QA правило «case только сверх Scenario» сразу; проверять ссылки на сценарии через grep |
| apply + G4 checks | frontend-engineer, documentation-engineer, independent-reviewer, qa-engineer | Review: имя группы для скринридера неполное, у выплат терялись occurrence keys, выплаты в группе не были карточками, ссылки на счета появились вопреки UX. QA: 18 проверок pass в браузере со stubbed API | Все findings исправлены. QA D1 (заголовок «Дивиденд») — артефакт заглушки с `type: "Купон"` вместо `OPERATION_TYPE_COUPON`, не дефект. D2 опровергнут кодом: `aria-label` стоит на вложенном `ul` по UX | Flaky `screen-sync.test.tsx` есть и до change, файл не менялся; на baseline 3/3 pass | Исполнитель дважды молча отклонился от UX; поймано review | — | ~9 вызовов сабагентов | Давать исполнителю явный список Presentation Decisions; QA строить заглушки только из enum generated types |
| verify (openspec-verify-change) | основной агент | Строка записи в группе журнала показывала дату вместо подписи типа (расхождение с `Full ledger presents compact rows`); у сценариев BUY/SELL, «итог в нескольких валютах» для событий и «оферта» не было автотестов | Исправлено: подпись типа в строке, `aria-label` у карточки-строки, 4 новых теста | 0 critical, 2 warning, 2 suggestion; все закрыты | — | openspec-verify-change | 1 проход | Verify по сценариям находит то, что пропустили review и QA |
## Retrospective
**Факты**
- 4 gate, 1 инвалидация G2: UX нашёл 2 пробела в specs (итог при неполном прогнозе, нулевая сумма).
- Независимый review находил реальные дефекты на каждом gate:
- G1: в canonical specs потерян режим `all`; спека «Выплат» запрещала frontend считать итоги.
- G2: группировка нарушала canonical requirements, которые change не модифицировал.
- G3: test-cases ссылались на несуществующие сценарии, а oracle пагинации были неверны.
- G4: имя группы для скринридера, повторные ключи выплат, ссылки на счета вопреки UX.
- Verify по сценариям нашёл расхождение, которое пропустили review и QA: в строке группы не было подписи типа.
- Побочный результат — WealthLedger/specs#63: canonical drift после archive `persist-tbank-data`.
- Стоимость: около 25 вызовов сабагентов. Ответы часто обрезались, так что прятались продолжения.
**Оценка**
- Окупились: G1/G2 review (без них delta строилась бы на неполном каноне), ux-design (нашёл пробелы в specs), verify по сценариям.
- Избыточным для standard UI-change оказался первый вариант QA-артефактов: 46 TC вместо cases «сверх Scenario». Пришлось переписать.
- Изменить в schema: в instruction для qa-analysis/test-cases явно требовать ссылки на существующие `#### Scenario` и проверку их grep-ом. В brief для исполнителя передавать Presentation Decisions из ux-design как обязательный список.
@@ -0,0 +1,79 @@
# Proposal
Issue: WealthLedger/main#47
## Why
В режиме «Все счета» события и операции показываются плоским списком: одна строка — одна запись одного счёта. Одна и та же бумага часто есть сразу на брокерском счёте и на ИИС. В этом случае выплаты по ней в один день разбросаны по отдельным строкам, и пользователю приходится самому связывать их и сравнивать суммы по счетам.
## What Changes
- В режиме «Все счета» записи с одним днём, одним Instrument и одним точным типом события или операции объединяются в одну визуальную группу.
- В заголовке группы — бумага, тип и итог. Итог — точная сумма записей группы, отдельно по каждой валюте и со знаком. Под заголовком всегда раскрыта разбивка: каждая запись остаётся отдельной строкой со своим счётом и суммой (например, «Брокерский — 200 ₽», «ИИС — 100 ₽»).
- Группировка применяется к предстоящим событиям и к исполненным операциям любой категории, если у записи есть identity Instrument.
- Группа образуется из двух и более записей с одинаковым ключом, даже если все они относятся к одному счёту.
- Группа стоит на месте своей первой записи в серверном порядке. Записи внутри группы и остальные строки сохраняют относительный серверный порядок.
- Режим одного счёта, записи без Instrument и одиночные записи не меняются.
- В нормативные specs журнала и аналитики дохода возвращается уже существующий режим «Все счета». Он был утрачен при синхронизации canonical specs; поведение при этом не меняется.
- Для «Выплат» в режиме «Все счета» вводится исключение из действующих запретов: группировочные карточки по Instrument и вычисление итога группы на клиенте разрешены. Аналитические итоги дохода по-прежнему приходят с сервера и не пересчитываются.
## Scope
### In Scope
- Страница предстоящих событий в режиме «Все счета».
- Страница журнала операций в режиме «Все счета».
- Превью ближайших событий и последних операций на обзоре в режиме «Все счета». Лимит считается по записям, как сейчас.
- Раздел «Выплаты» на странице доходов в режиме «Все счета».
- Восстановление в canonical specs журнала и дохода нормативного описания режима «Все счета» в его текущем поведении.
### Out of Scope / Non-Goals
- Режим одного счёта.
- Группировка за пределами одного дня, по эмитенту или между разными типами: покупка ≠ продажа, купон ≠ налог с купона, налог с купона ≠ налог с дивиденда.
- Изменение состава, фильтрации, серверного порядка, пагинации, лимитов превью или денежных значений, которые возвращает сервер.
- Записи без identity Instrument.
- Сворачивание групп и пометка, что группа может быть неполной.
- Аналитические итоги дохода и их пересчёт.
## Capabilities
### New Capabilities
Нет.
### Modified Capabilities
- `specs/brokerage-account-upcoming-events/spec.md`: представление событий (страница и превью) в режиме «Все счета».
- `specs/brokerage-account-ledger/spec.md`: восстановление режима «Все счета», представление журнала и превью операций в этом режиме.
- `specs/brokerage-account-income-analytics/spec.md`: восстановление режима «Все счета», раздел «Выплаты» в этом режиме с исключением из запретов на группировку и клиентский итог.
## Impact
- Пользователи с несколькими счетами, которые смотрят режим «Все счета».
- Меняется только представление уже полученных данных. Источники данных, серверные ответы и интеграция с T-Bank остаются прежними.
- Вероятно затрагиваются клиентское представление списков и AS-IS документация экранов.
## Success Criteria
- Купоны по одной облигации за один день (200 ₽ на брокерском счёте и 100 ₽ на ИИС) в режиме «Все счета» видны одной группой с итогом 300 ₽ и отдельной строкой для каждого счёта.
- Записи разных типов или дней по одной бумаге не объединяются.
- Итог группы точен, показывается отдельно по каждой валюте и учитывает знак записей.
- В режиме одного счёта и для записей без Instrument списки выглядят как раньше.
## Evidence
- [F] В режиме «Все счета» страницы событий, операций, превью обзора и «Выплаты» показывают плоский список, по строке на запись, со счётом в каждой строке. Группировки по Instrument нет. Источник: AS-IS frontend, разбор кода 2026-10-29.
- [F] Canonical `brokerage-account-income-analytics` («Income payments remain traceable to exact Ledger Entries») запрещает группировочные карточки и subtotals по Instrument в «Выплатах» и пересчёт денежных итогов на frontend. Источник: canonical spec, archive `2026-10-14-unify-income-payment-cards`.
- [F] Режим «Все счета» для журнала и дохода описан в delta specs archive `2026-10-09-persist-tbank-data`, но отсутствует в текущих canonical specs `brokerage-account-ledger` и `brokerage-account-income-analytics`. В `brokerage-account-upcoming-events` он есть. Источник: specs store, G1 review 2026-10-29.
- [F] Потребность: одна облигация на брокерском счёте и ИИС, выплаты по ней в один день нужно видеть рядом с суммой по каждому счёту. Источник: запрос Change Owner, WealthLedger/main#47.
- [H] Сервер в режиме «Все счета» уже возвращает для каждой записи счёт, дату, тип, identity Instrument и точную сумму. Подтверждается в Design.
## Assumptions / Dependencies
- День записи берётся тот же, что строка уже показывает на этом экране. Часовой пояс отображения не меняется.
- Группа собирается только из записей, показанных на текущей странице журнала или в превью. При переходе на другую страницу группы собираются заново из её записей, поэтому группа на границе страницы может быть неполной.
## Open Decisions
Нет. Change Owner принял в текущей сессии: ключ (день как на экране, identity Instrument, точный тип), клиентский итог по валютам, позиция группы на месте первой записи, лимит превью по записям, неполная группа без пометки, строка на каждую запись, восстановление режима «Все счета» в этом change.
@@ -0,0 +1,89 @@
# QA Analysis
Объект проверки: frontend-only изменение группировки строк по Instrument в режиме «Все счета» (журнал, обзор, будущие события, «Выплаты»). Backend и API не меняются; контракт используется как есть.
## Risk Assessment
| Risk | Affected requirements | Likelihood | Impact | Verification depth |
| --- | --- | --- | --- | --- |
| Неверный ключ группы: объединение записей разных типов/дней или не-объединение одинаковых | ledger/upcoming `groups same-day…` | Средняя | Высокая | Полный перебор граничных комбинаций ключа (тип, день, Instrument, один счёт) |
| Неверный итог: конвертация/округление валют, потеря знака, итог при отсутствии/нулевой сумме, при `partial` | ledger/upcoming `group shows each entry and …` | Средняя | Высокая | Мультивалютные, знаковые, нулевые и запрещающие сценарии итога; сверка с серверными суммами |
| Нарушен серверный порядок или лимит (5 записей обзора, пагинация журнала) из-за группировки | `preserves order and limits` | Средняя | Средняя | Проверка позиций групп, границы страницы, состава «последних 5» |
| Группировка случайно применяется в режиме одного счёта или к записям без Instrument | `groups same-day…`, MODIFIED «Full ledger…» | Низкая | Высокая | Регрессионная проверка одиночного режима и записей без Instrument |
| «Выплаты»: группировка нарушает запреты одиночного режима или подменяет аналитические итоги | `Income payments remain traceable…` | Средняя | Высокая | Проверка обоих режимов, неизменности серверных аналитических итогов |
| Пропадание записей внутри группы (свернуто, скрыто) или потеря fallback при отсутствии metadata | `group shows each entry…`, MODIFIED `Full ledger…`, MODIFIED overview | Низкая | Высокая | Визуальная проверка разбивки без раскрытия, fallback-представление |
| Обзор «Все счета»: режим не открывается / принадлежность счёту потеряна (восстановление режима `all`) | MODIFIED `User can retrieve an account ledger`, MODIFIED overview, MODIFIED upcoming list | Средняя | Средняя | Проверка доступности и контента режима `all`, `partial`-поведение |
## Coverage Matrix
| Requirement | Check | Level | Test cases |
| --- | --- | --- | --- |
| `brokerage-account-ledger` — `User can retrieve an account ledger` | Режим `all` доступен; принадлежность записей; `partial`; пустая незагруженная часть | System / E2E | Scenario: Получение журнала с периодом по умолчанию; Scenario: Общий журнал сохраняет принадлежность записей; Scenario: Общий журнал содержит неполный счёт; Scenario: Пустая загруженная часть не доказывает отсутствие операций |
| `brokerage-account-ledger` — `Account overview includes recent ledger events` | Группы в обзоре «Все счета»; лимит 5; fallback без metadata | System / E2E | Scenario: Отображение последних событий; Scenario: Последняя запись не содержит visual metadata инструмента; Scenario: Группировка в последних записях обзора; TC-PAG-002, TC-PAG-003 |
| `brokerage-account-ledger` — `Full ledger presents compact rows` | Строка в группе: счёт, сумма, знак, подпись типа; заголовок группы; fallback | System / E2E | Scenario: Запись внутри группы режима «Все счета»; Scenario: Подпись типа не дублируется; Scenario: Отсутствует visual metadata инструмента; TC-GRP-001, TC-TOT-001 |
| `brokerage-account-ledger` — `All-accounts ledger groups same-day entries of one instrument` | Ключ группы: два счёта, один счёт, разные типы, покупка/продажа, разные дни, без Instrument, один счёт | System / E2E | Scenario: Купоны одной облигации на двух счетах; Scenario: Две операции одного типа на одном счёте; Scenario: Разные типы по одной бумаге не объединяются; Scenario: Покупка и продажа не объединяются; Scenario: Разные дни не объединяются; Scenario: Запись без Instrument; Scenario: Режим одного счёта не меняется; TC-GRP-001, TC-GRP-002, TC-GRP-003, TC-GRP-004 |
| `brokerage-account-ledger` — `All-accounts ledger group shows each entry and an exact total` | Точный итог: мультивалюта, знак, ноль, без изменения, `partial`; разбивка всегда видна | System / E2E | Scenario: Итог по нескольким валютам; Scenario: Итог со знаком; Scenario: Нулевой итог; Scenario: Группа с записью без денежного изменения; Scenario: Запись с нулевой суммой скрывает итог; Scenario: Итог скрыт при неполном периоде; TC-TOT-001, TC-TOT-002, TC-TOT-003, TC-TOT-004 |
| `brokerage-account-ledger` — `All-accounts ledger grouping preserves order and limits` | Позиция группы; граница страницы; лимит 5 обзора | System / E2E | Scenario: Позиция группы в списке; Scenario: Группа на границе страницы; TC-PAG-001, TC-GRP-002 |
| `brokerage-account-upcoming-events` — `Account page presents an upcoming events list` | Страница в `all`; групповой итог ≠ итог списка; дисклеймер | System / E2E | Scenario: Просмотр ближайших событий; Scenario: Пустое состояние страницы; Scenario: Прогнозный дивиденд с неизвестной суммой |
| `brokerage-account-upcoming-events` — `All-accounts upcoming events group same-day events of one instrument` | Группы событий: два счёта, разные типы/даты, без Instrument, один счёт | System / E2E | Scenario: Прогноз купонов одной облигации на двух счетах; Scenario: Разные типы или даты не объединяются; Scenario: Событие без Instrument; Scenario: Режим одного счёта не меняется; TC-GRP-005 |
| `brokerage-account-upcoming-events` — `All-accounts upcoming event group shows each event and a forecast total` | Прогнозный итог: неизвестная сумма, оферта, мультивалюта, неполный прогноз, дата реестра | System / E2E | Scenario: Событие с неизвестной суммой в группе; Scenario: Оферта без сумм; Scenario: Прогнозный итог по нескольким валютам; Scenario: Прогнозный итог скрыт при неполном прогнозе; Scenario: Группа дивидендов по дате реестра; TC-GRP-005, TC-TOT-005 |
| `brokerage-account-upcoming-events` — `All-accounts upcoming event grouping preserves order and limits` | Позиция группы событий; ближайшие события обзора | System / E2E | Scenario: Позиция группы событий; Scenario: Группировка в ближайших событиях обзора; TC-PAG-002 (по аналогии для событий обзора) |
| `brokerage-account-income-analytics` — `Realized income analytics is account-scoped and period-bound` | Режим `all` в аналитике; полнота всех счетов; периоды | System / E2E | Scenario: Общий доход требует полноты всех счетов; Scenario: Открыт ещё не загруженный период (изменение frontend не затрагивает — smoke) |
| `brokerage-account-income-analytics` — `Income payments remain traceable to exact Ledger Entries` | «Выплаты»: одиночный режим без групп; группы в `all`; итог при неполноте; аналитика не пересчитана; корректировки и дубли ID | System / E2E | Scenario: Каждая выплата показана отдельной карточкой; Scenario: Список воспроизводит точный состав дохода; Scenario: Отрицательная корректировка и повторяющиеся IDs сохраняются; Scenario: Выплаты одной бумаги на двух счетах в режиме «Все счета»; TC-GRP-006, TC-TOT-004 (правило скрытия итога) |
## Negative and Boundary Focus
- Ключ группы: разные точные upstream-типы, покупка vs продажа, разные экранные дни, отсутствие identity Instrument, одна запись (нет пары), все записи на одном счёте.
- Итог: мультивалюта без конвертации; копеечные суммы без округления (+150.10 −50.05); нулевой итог без знака; запись без денежного изменения; нулевая сумма; `partial`-период; неполный прогноз; событие с неизвестной суммой; оферта без сумм.
- Порядок и лимиты: группа из записей первой и следующей страницы журнала; лимит 5 записей обзора (считается по записям, не по группам); позиция группы по первой записи.
- Режимы: один счёт — полное отсутствие группировки; записи без Instrument не сливаются.
## Cross-system Verification
system-analysis не выбран, backend и API в рамках change не меняются — отдельная кросс-системная проверка не требуется. Существующие серверные данные проверяются как вход: реальные ответы API (`{ data, meta }`, принадлежность записей счёту, `partial`-состояние, серверные аналитические итоги, порядок entries) используются как эталон, с которым сверяется клиентское представление (состав групп, суммы, порядок). Ключевой инвариант: аналитические денежные итоги и receipt count приходят только с сервера и не пересчитываются; единственный клиентский расчёт — итог визуальной группы, который сверяется с суммой серверных записей группы.
## Test Data and Environment Needs
Локальный frontend + локальный backend с сохранёнными данными (или stubbed API в браузере через devtools/сервис-воркер-заглушку). Фикстуры:
- Два счёта (брокерский + ИИС) с одной и той же облигацией: исполненные купоны в один день (+200 RUB и +100 RUB) — базовая группа.
- Мультипанель: одна группа с записями в RUB и USD; выплаты одной бумаги в разных валютах.
- Граничные суммы: +150.10 / −50.05 / −150.10; запись с нулевой суммой; запись без денежного изменения; отрицательная корректировка.
- Разнообразие типов: купон + налог в один день, покупка + продажа одной бумаги в один день, две покупки на одном счёте, купоны в разные (экранные) дни.
- Записи без identity Instrument (например, переводы) — два в один день одного типа.
- Записи без presentation metadata инструмента — fallback.
- `partial`: счёт с незагруженным интервалом в запрошенном периоде; пустая сохранённая часть при неполном покрытии.
- Граница страницы: настройки пагинации журнала так, чтобы записи одной группы попали на разные страницы; подтверждённо полный период.
- Будущие события: купон на двух счетах в одну дату; дивиденды по дате реестра (известная и неизвестная суммы); оферта на двух счетах; события без суммы; неполный прогноз.
- Обзор: ровно 5 последних записей, среди которых пара с одинаковым ключом.
- Режим одного счёта с теми же данными (два купона одной бумаги в один день).
Источник: подготовленные фикстуры в локальной БД тестового backend; при отсутствии — заглушка API-ответов в браузере с теми же контрактными envelope.
Примечания по окружению (не блокеры): тестовые данные границы страницы строятся вокруг фактического размера страницы, который возвращает сервер (TC-PAG-001 фиксирует конкретное содержимое страниц в момент прогона); состояние `partial` и неполный прогноз воспроизводятся заглушкой API-ответа в браузере (MSW / Playwright route) с `meta` partial.
## Manual / Exploratory Scope
- Визуальное чтение группы без раскрытия: разбивка всегда видна, заголовок не дублирует подпись типа в строках, знак и семантический цвет не единственные носители направления.
- Устойчивость к обновлению: приход `partial` → подтверждённой полноты — итог появляется/исчезает корректно; фоновая догрузка не меняет состав и порядок показанных записей.
- Переходы между режимами «Все счета» ↔ один счёт на одних данных: группировка появляется/исчезает целиком.
- Прокрутка/пагинация журнала в `all`: стабильность позиций групп между страницами.
- Consent-контент: дивидендные группы с датой реестра, оферта без итога — читаемость при отсутствии сумм.
## Automation Candidates
Black-box E2E (Playwright, режим «Все счета»): TC-GRP-001, TC-GRP-002, TC-GRP-005, TC-GRP-006, TC-TOT-001, TC-TOT-002, TC-TOT-003, TC-TOT-004, TC-TOT-005, TC-PAG-001, TC-PAG-002, TC-PAG-003 — стабильные контрактные сценарии групп, итогов и порядка. TC-GRP-003 и TC-GRP-004 — ручные до уточнения Specs. Статус всех: **deferred until QA repo exists**.
## Regression Impact
- Режим одного счёта: журнал, обзор, «Будущие события», «Выплаты» — полное отсутствие группировки (Scenario-блоки требований; ручная регрессия).
- Существующие карточки и строки журнала вне групп: представление выплаты купона/дивиденда, подписи типов, знаки, fallback без metadata — без изменений.
- Состояния загрузки/пустоты/ошибки: первичное ожидание, нейтральная недоступность, сохранение прежнего результата при временном сбое, `partial` и пустая-но-неполная выборка — не деградируют из-за группировки.
- Восстановленный режим `all` в specs журнала и аналитики — поведение не меняется, только smoke доступности (Scenario: Получение журнала с периодом по умолчанию; Scenario: Общий доход требует полноты всех счетов).
- Аналитические итоги дохода и receipt count — только серверные, отсутствие клиентского пересчёта (TC-GRP-006).
## Gaps / Blockers
Нет. Два вопроса из test design закрыты без изменения specs:
- Identity-ключ: specs требуют одинаковую identity Instrument; design D2 определяет её как первый непустой идентификатор, а записи, равенство которых не доказано, не объединяются (TC-GRP-003).
- День: specs задают «день, как на экране»; oracle — показанная в строке дата (TC-GRP-004).
@@ -0,0 +1,64 @@
## 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: Income payments remain traceable to exact Ledger Entries
В разделе «Выплаты» страница MUST показывать единый список карточек точных исполненных Ledger Entries применённого периода в исходном серверном порядке entries. Каждая выплата MUST иметь собственную сразу видимую карточку с описанием, датой исполнения, доступной identity Instrument и signed Money в исходной валюте. Купоны и дивиденды MUST использовать то же представление выплаты, что и на экранах журнала, прогноза и графиков инструментов, при этом карточка MUST обозначать эту сумму именно как фактически исполненную, а не прогнозную. В режиме одного Brokerage Account страница MUST NOT добавлять внешние группировочные карточки или заголовки по Instrument либо валюте, subtotals инструмента или количество выплат инструмента. В режиме «Все счета» записи раздела «Выплаты» MUST группироваться по тем же правилам ключа, состава, порядка и итога, что и записи журнала по Requirements «All-accounts ledger groups same-day entries of one instrument», «All-accounts ledger group shows each entry and an exact total» и «All-accounts ledger grouping preserves order and limits» capability `brokerage-account-ledger`. Каждая запись при этом MUST оставаться отдельной сразу видимой карточкой внутри группы, а итог группы MUST NOT показываться, пока полнота периода не подтверждена; заголовки валютных групп и receipt count MUST NOT добавляться. Список MUST сохранять отрицательные корректировки и повторяющиеся operation IDs как отдельные записи и MUST NOT включать записи вне аналитического итога. Backend MUST обеспечивать согласованность entries, instrumentKey, валюты и receipt count при формировании ответа из одного набора записей. Frontend SHALL доверять типизированному contract без второго runtime consistency validator и MUST NOT пересчитывать аналитические денежные итоги. Единственное исключение — итог визуальной группы режима «Все счета»: это точная сумма показанных записей группы по каждой валюте, и он MUST NOT подменять аналитические итоги.
#### Scenario: Каждая выплата показана отдельной карточкой
- **WHEN** в режиме одного Brokerage Account выбранный период содержит несколько включённых Ledger Entries одного или разных Instrument
- **THEN** раздел «Выплаты» сразу показывает отдельную карточку каждой записи в исходном порядке без внешних группировочных блоков, раскрытия, итогов инструмента и receipt count на UI
#### Scenario: Список воспроизводит точный состав дохода
- **WHEN** User просматривает загруженный раздел «Выплаты»
- **THEN** он видит все и только включённые coupon и dividend Ledger Entries применённого периода с датой, описанием, доступной identity и исходной суммой, обозначенной как исполненная выплата
#### Scenario: Отрицательная корректировка и повторяющиеся IDs сохраняются
- **WHEN** доход содержит отрицательную корректировку либо несколько включённых entries с одинаковым operation ID
- **THEN** каждая запись остаётся отдельной карточкой, а корректировка отображается с исходным знаком
#### Scenario: Валюта видна в сумме каждой выплаты
- **WHEN** подходящие в режиме одного Brokerage Account выплаты одного Instrument представлены в нескольких исходных валютах
- **THEN** каждая карточка показывает свою Money с исходной валютой, без отдельного заголовка валютной группы или объединённого мультивалютного итога
#### Scenario: Выплаты одной бумаги на двух счетах в режиме «Все счета»
- **WHEN** в режиме «Все счета» за один день получены купон по облигации на брокерском счёте +200 RUB и купон по той же облигации на ИИС +100 RUB
- **THEN** раздел «Выплаты» показывает их одной группой с итогом +300 RUB, внутри которой видны отдельные карточки каждого счёта, а аналитические итоги страницы не меняются
@@ -0,0 +1,170 @@
## 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** User запрашивает режим `all` и у одного из разрешённых счетов есть незагруженный интервал периода
- **THEN** система возвращает только разрешённые сохранённые записи с принадлежностью счёту и обозначает общий период неполным, не подменяя отсутствующий вклад пустым счётом
#### Scenario: Пустая загруженная часть не доказывает отсутствие операций
- **WHEN** в сохранённой части периода нет записей, но покрытие периода не завершено
- **THEN** ответ обозначает неполноту и запрошенную загрузку, а интерфейс не сообщает, что операций за период нет
### Requirement: Account overview includes recent ledger events
Обзор счёта MUST показывать пять последних исполненных записей журнала этого
счёта, а в режиме «Все счета» — пять последних записей всех входящих счетов, и действие перехода к его полному журналу. Каждая запись MUST иметь ту же
визуальную идентичность инструмента и семантику денежного движения, что и запись
полного журнала; в группе режима «Все счета» визуальная идентичность, дата и тип
показываются в заголовке группы. Неуспешный или пустой запрос MUST оставаться локальным состоянием
блока и MUST NOT заменять успешно отображённый Portfolio. При временном сбое
обновления блок MUST сохранять доступные записи без технического предупреждения
или ручного повтора. При отсутствии результата он SHALL показывать первичное
ожидание либо нейтральную недоступность, автоматически восстанавливая чтение.
Неподтверждённая пустота MUST NOT представляться отсутствием операций.
#### Scenario: Отображение последних событий
- **WHEN** загружается обзор счёта и его журнал содержит исполненные операции
- **THEN** обзор показывает не более пяти последних записей и ссылку на полный журнал
#### Scenario: Последние записи недоступны
- **WHEN** обзор счёта успешно загружается, но запрос последних записей завершается временной ошибкой
- **THEN** Portfolio остаётся доступным, а блок сохраняет прежние записи либо показывает нейтральную недоступность без технического повтора и восстанавливается автоматически
#### Scenario: Последняя запись не содержит visual metadata инструмента
- **WHEN** одна из последних записей не имеет доступной presentation metadata инструмента
- **THEN** обзор показывает для неё понятный текстовый fallback и сохраняет дату, тип, инструмент и денежное изменение записи доступными для чтения
### Requirement: Full ledger presents compact rows
Страница полного журнала MUST показывать для каждой записи дату, тип операции, инструмент и денежное изменение в исходной валюте в компактной структурированной строке. При доступной presentation metadata инструмента строка MUST показывать его визуальную идентичность; при её отсутствии MUST использовать понятный текстовый fallback. Для купонных и дивидендных операций на странице полного журнала и в личной истории инструмента MUST использоваться то же представление выплаты, что в доходе, прогнозе и графиках инструментов, с обозначением исполненного движения. Для каждой записи денежное изменение в исходной валюте MUST сопровождаться подписью конкретного типа события и знаком: для купонной операции — «Купон», для дивидендной — «Дивиденд», для остальных записей — подпись их категории («Сделка», «Налог», «Комиссия», «Перевод», «Прочее»). Строка MUST NOT повторять подпись типа события во вторичной метадате записи. Знак «+» / «−» и семантический цвет MUST оставаться носителями направления и MUST NOT заменяться только текстом. Страница MUST обозначать записи без движения денег нейтрально и MUST NOT показывать цену или количество. В группе режима «Все счета» дата, тип операции, инструмент и его визуальная идентичность MUST показываться один раз в заголовке группы, а строка каждой записи группы MUST показывать её счёт и денежное изменение с подписью типа и знаком.
#### Scenario: Отображение записи только с инструментом
- **WHEN** полный журнал отображает запись без денежной выплаты
- **THEN** страница определяет её как запись без движения денег
#### Scenario: Отображение поступления
- **WHEN** полный журнал отображает запись с положительной денежной выплатой
- **THEN** строка явно показывает поступление знаком и подписью конкретного типа события («Купон», «Дивиденд» или категории) и его денежное изменение в исходной валюте, не полагаясь только на цвет
#### Scenario: Отображение списания
- **WHEN** полный журнал отображает запись с отрицательной денежной выплатой
- **THEN** строка явно показывает списание знаком и подписью конкретного типа события («Купон», «Дивиденд» или категории) и его денежное изменение в исходной валюте, не полагаясь только на цвет
#### Scenario: Отображение купона
- **WHEN** полный журнал или личная история инструмента отображает исполненную операцию с upstream-типом купона
- **THEN** запись показывает подпись «Купон» и исполненную сумму в том же представлении выплаты, что на других экранах
#### Scenario: Отображение дивиденда
- **WHEN** полный журнал или личная история инструмента отображает исполненную операцию с upstream-типом дивиденда
- **THEN** запись показывает подпись «Дивиденд» и исполненную сумму в том же представлении выплаты, что на других экранах
#### Scenario: Подпись типа не дублируется
- **WHEN** полный журнал отображает запись с типом события и вторичной метадатой
- **THEN** подпись типа события присутствует в строке один раз и не повторяется во вторичной метадате записи
#### Scenario: Отсутствует visual metadata инструмента
- **WHEN** полный журнал отображает запись без доступной presentation metadata инструмента
- **THEN** строка показывает понятный текстовый fallback без подмены или сокрытия доступных данных инструмента
#### Scenario: Запись внутри группы режима «Все счета»
- **WHEN** полный журнал в режиме «Все счета» показывает запись в составе группы
- **THEN** её дата, тип и инструмент читаются из заголовка группы, а строка записи показывает счёт и денежное изменение со знаком и подписью типа
## ADDED Requirements
### Requirement: All-accounts ledger groups same-day entries of one instrument
В режиме «Все счета» полный журнал и последние записи обзора MUST объединять в визуальную группу две и более показанных записи с одинаковыми днём (как на экране), identity Instrument и точным upstream-типом операции, даже если они на одном счёте. Записи без Instrument или без пары MUST показываться как раньше. В режиме одного Brokerage Account группировка MUST NOT применяться.
#### Scenario: Купоны одной облигации на двух счетах
- **WHEN** в режиме «Все счета» журнал показывает за один день купон по облигации на брокерском счёте +200 RUB и купон по той же облигации на ИИС +100 RUB
- **THEN** они показаны одной группой с облигацией, типом «Купон» и итогом +300 RUB, а под заголовком видны строки «брокерский счёт +200 RUB» и «ИИС +100 RUB»
#### Scenario: Две операции одного типа на одном счёте
- **WHEN** в режиме «Все счета» журнал показывает за один день две покупки одной бумаги на одном счёте
- **THEN** они показаны одной группой, где каждая покупка остаётся отдельной строкой со своим денежным изменением
#### Scenario: Разные типы по одной бумаге не объединяются
- **WHEN** в режиме «Все счета» за один день по одной облигации есть купон и налог с этого купона
- **THEN** купоны и налоги показаны раздельно, и ни одна группа не содержит записи разных типов
#### Scenario: Покупка и продажа не объединяются
- **WHEN** в режиме «Все счета» за один день показаны покупка и продажа одной бумаги
- **THEN** они не входят в одну группу
#### Scenario: Разные дни не объединяются
- **WHEN** в режиме «Все счета» купоны по одной облигации на двух счетах датированы на экране разными днями
- **THEN** они показаны отдельными записями без группы
#### Scenario: Запись без Instrument
- **WHEN** в режиме «Все счета» за один день показаны две записи одного типа без identity Instrument
- **THEN** они показаны отдельными записями так же, как без группировки
#### Scenario: Режим одного счёта не меняется
- **WHEN** User открывает журнал или обзор одного Brokerage Account, где есть два купона по одной облигации за один день
- **THEN** записи показаны отдельными строками без группы, как до изменения
### Requirement: All-accounts ledger group shows each entry and an exact total
Заголовок группы журнала MUST показывать дату, Instrument с визуальной идентичностью, подпись типа операции, как в строке журнала, и итог. Под ним MUST всегда быть видна, без раскрытия, отдельная строка каждой записи со счётом и денежным изменением. Итог SHALL быть точной суммой со знаком по каждой исходной валюте без конвертации и округления. Итог MUST NOT показываться, если у записи группы нет денежного изменения (включая нулевое) или полнота периода не подтверждена.
#### Scenario: Итог по нескольким валютам
- **WHEN** группа содержит записи с денежными изменениями в RUB и в USD
- **THEN** заголовок показывает отдельный точный итог в RUB и отдельный в USD, без конвертации
#### Scenario: Итог со знаком
- **WHEN** группа содержит поступление +150.10 RUB и отрицательную корректировку −50.05 RUB
- **THEN** итог группы равен +100.05 RUB, а обе записи видны отдельными строками со своими знаками
#### Scenario: Нулевой итог
- **WHEN** группа содержит +150.10 RUB и −150.10 RUB
- **THEN** заголовок показывает итог 0 RUB без знака направления, а обе записи видны со своими знаками
#### Scenario: Группа с записью без денежного изменения
- **WHEN** у одной из записей группы нет денежного изменения
- **THEN** заголовок группы не показывает итог, а записи остаются видны отдельными строками
#### Scenario: Запись с нулевой суммой скрывает итог
- **WHEN** группа содержит запись +100 RUB и запись с денежным изменением 0 RUB
- **THEN** заголовок группы не показывает итог, а запись с нулём обозначена как запись без движения денег
#### Scenario: Итог скрыт при неполном периоде
- **WHEN** журнал в режиме «Все счета» показывает группу, а период обозначен как `partial`
- **THEN** заголовок группы не показывает итог, а записи группы остаются видны со своими суммами
### Requirement: All-accounts ledger grouping preserves order and limits
Группа журнала MUST занимать позицию своей первой записи в серверном порядке; записи внутри группы и остальные строки MUST сохранять относительный серверный порядок. Группировка MUST NOT менять состав, фильтрацию, пагинацию или лимит обзора, который считается по записям. Группа собирается только из показанных записей; это осознанное исключение: её итог относится только к ним и не сопровождается пометкой о неполноте.
#### Scenario: Позиция группы в списке
- **WHEN** серверный порядок записей A, B, C, где A и C образуют группу, а B не входит ни в одну группу
- **THEN** список показывает группу (A, C) на месте A, затем B
#### Scenario: Группа на границе страницы
- **WHEN** записи одной группы попадают на текущую и на ещё не показанную страницу журнала при подтверждённой полноте периода
- **THEN** группа на текущей странице содержит только её показанные записи, итог равен их сумме, и заголовок не содержит пометки о неполноте
#### Scenario: Группировка в последних записях обзора
- **WHEN** в режиме «Все счета» среди пяти последних записей есть две записи с одинаковым ключом
- **THEN** блок обзора показывает те же пять записей, что и без группировки, две из них одной группой
@@ -0,0 +1,100 @@
## MODIFIED Requirements
### Requirement: Account page presents an upcoming events list
Страница Brokerage Account MUST предоставлять действие перехода на отдельную
страницу «Будущие события». Эта страница MUST показывать все полученные события
компактным списком без фильтров, пагинации и суммарного итога списка, с датой, типом,
инструментом и прогнозной суммой в исходной валюте, если она известна. Прогнозный итог
визуальной группы режима «Все счета» не является суммарным итогом списка. Страница
MUST объяснять, что прогноз основан на позиции счёта, указан до налогов и может
быть неполным из-за доступности данных источника. Это предметное ограничение
прогноза MUST NOT превращаться в отчёт о технической синхронизации.
При временном сбое обновления страница MUST сохранять ранее доступный результат
той же выборки с его исходными `calculatedAt` и окном, без индикации
собственного чтения и ручного повтора. Состояние источника из `meta`, включая
неуспех последней часовой попытки и неполноту, MUST представляться по
`frontend-data-resilience`. При отсутствии результата SHALL показываться
первичное ожидание либо нейтральная недоступность с автоматическим
восстановлением по `frontend-data-resilience`. Подтверждённая потеря доступа
MUST скрывать личные данные независимо от наличия сохранённого результата.
#### Scenario: Просмотр ближайших событий
- **WHEN** User открывает «Будущие события» из обзора Brokerage Account
- **THEN** он видит отсортированный список событий ближайших 30 дней, где offer обозначен без суммы
#### Scenario: Пустое состояние страницы
- **WHEN** запрос будущих событий успешно возвращает полный пустой список
- **THEN** страница сообщает, что на ближайшие 30 дней событий не найдено, и объясняет предметные ограничения покрытия источника
#### Scenario: Ошибка страницы
- **WHEN** запрос будущих событий завершается временной ошибкой
- **THEN** страница сохраняет доступный результат той же выборки либо показывает нейтральную недоступность без ручного повтора, восстанавливая чтение автоматически и не выдавая частичные данные за полный прогноз
#### Scenario: Прогнозный дивиденд с неизвестной суммой
- **WHEN** прогноз содержит дивиденд с известной датой реестра, но без суммы
- **THEN** его карточка обозначает дату как дату реестра и сумму как неизвестную, не показывает нулевую или полученную выплату
#### Scenario: Ошибка обновления при сохранённом прогнозе на странице
- **WHEN** запрос будущих событий возвращает последний полный прогноз вместе с ошибкой его обновления
- **THEN** страница показывает этот прогноз с его исходными `calculatedAt` и окном и отдельно обозначает неуспех обновления источника, не предлагая ручной повтор
#### Scenario: Первая загрузка либо ошибка без сохранённого прогноза
- **WHEN** запрос будущих событий возвращает `data: null`, а `meta` обозначает выполнение первой загрузки либо её неуспех
- **THEN** страница показывает доступное состояние первичного ожидания либо нейтральной недоступности без частичного списка и восстанавливает чтение автоматически
## ADDED Requirements
### Requirement: All-accounts upcoming events group same-day events of one instrument
В режиме «Все счета» страница «Будущие события» и ближайшие события обзора MUST объединять в визуальную группу два и более показанных Future Cash Event с одинаковыми датой и видом даты (как на экране), identity Instrument и типом события, даже если они на одном счёте. События без Instrument или без пары MUST показываться как раньше. В режиме одного Brokerage Account группировка MUST NOT применяться.
#### Scenario: Прогноз купонов одной облигации на двух счетах
- **WHEN** в режиме «Все счета» на одну дату ожидается купон по облигации на брокерском счёте 200 RUB и купон по той же облигации на ИИС 100 RUB
- **THEN** они показаны одной группой с облигацией, типом «Купон» и прогнозным итогом 300 RUB, а под заголовком видны строки «брокерский счёт 200 RUB» и «ИИС 100 RUB»
#### Scenario: Разные типы или даты не объединяются
- **WHEN** в режиме «Все счета» по одной облигации ожидаются купон и погашение на одну дату либо купоны на разные даты
- **THEN** ни одна группа не содержит события разных типов или разных дат
#### Scenario: Событие без Instrument
- **WHEN** в режиме «Все счета» на одну дату показаны два события одного типа без identity Instrument
- **THEN** они показаны отдельными строками без группы
#### Scenario: Режим одного счёта не меняется
- **WHEN** User открывает «Будущие события» одного Brokerage Account
- **THEN** события показаны отдельными строками без группы, как до изменения
### Requirement: All-accounts upcoming event group shows each event and a forecast total
Заголовок группы событий MUST показывать дату (для дивиденда — как дату реестра, если она такая), Instrument, тип и прогнозный итог. Под ним MUST всегда быть видна, без раскрытия, отдельная строка каждого события со счётом и прогнозной суммой (или пометкой о неизвестной сумме). Итог SHALL быть точной суммой со знаком по каждой валюте без конвертации и округления. Итог MUST NOT показываться, если у события группы нет суммы или полнота прогноза не подтверждена.
#### Scenario: Событие с неизвестной суммой в группе
- **WHEN** в режиме «Все счета» группа содержит дивиденд на одном счёте с известной суммой и дивиденд по той же бумаге на ту же дату на другом счёте без суммы
- **THEN** заголовок группы не показывает итог, а строка второго счёта обозначает сумму как неизвестную
#### Scenario: Оферта без сумм
- **WHEN** в режиме «Все счета» на одну дату ожидается оферта по одной облигации на двух счетах
- **THEN** они показаны одной группой без итога, и каждая строка показывает свой счёт
#### Scenario: Прогнозный итог по нескольким валютам
- **WHEN** группа содержит дивиденд 10 USD на одном счёте и дивиденд 200 RUB по той же бумаге на ту же дату на другом счёте
- **THEN** заголовок показывает отдельные прогнозные итоги 10 USD и 200 RUB без конвертации
#### Scenario: Прогнозный итог скрыт при неполном прогнозе
- **WHEN** в режиме «Все счета» прогноз обозначен неполным, и на странице есть группа событий с известными суммами
- **THEN** заголовок группы не показывает итог, а строки событий остаются видны со своими суммами
#### Scenario: Группа дивидендов по дате реестра
- **WHEN** группа содержит дивиденды, датированные датой реестра
- **THEN** заголовок обозначает дату как дату реестра
### Requirement: All-accounts upcoming event grouping preserves order and limits
Группа событий MUST занимать позицию своего первого события в порядке ответа; события внутри группы и остальные строки MUST сохранять относительный порядок ответа. Группировка MUST NOT менять состав, окно, порядок по дате или лимит обзора, который считается по событиям; группы собираются только из показанных событий.
#### Scenario: Группировка в ближайших событиях обзора
- **WHEN** в режиме «Все счета» среди пяти ближайших событий есть два события с одинаковым ключом
- **THEN** блок обзора показывает те же пять событий, что и без группировки, два из них одной группой
#### Scenario: Позиция группы событий
- **WHEN** порядок ответа содержит события A, B, C, где A и C образуют группу, а B не входит ни в одну группу
- **THEN** список показывает группу (A, C) на месте A, затем B
@@ -0,0 +1,73 @@
# Tasks
## 1. Общая механика и ключи
- [x] 1.1 [frontend] Implementation: чистая группировка `groupItems` и точное десятичное сложение по валютам в `shared/lib` с unit-тестами
- **Covers:** `brokerage-account-ledger` — `All-accounts ledger grouping preserves order and limits`, `brokerage-account-ledger` — `All-accounts ledger group shows each entry and an exact total`
- **Source:** design.md — D1, D3; Developer Verification Strategy (unit grouping, unit decimal/money)
- **Verification:** `npm run test:run -- src/shared/lib src/pages/account/model`; `npm run typecheck`
- **Depends on:** none
- [x] 1.2 [frontend] Implementation: ключи группы и правило показа итога для операций и событий (день как на экране, identity, точный тип, `dateKind`), unit-тесты key adapters
- **Covers:** `brokerage-account-ledger` — `All-accounts ledger groups same-day entries of one instrument`, `brokerage-account-upcoming-events` — `All-accounts upcoming events group same-day events of one instrument`, `brokerage-account-upcoming-events` — `All-accounts upcoming event group shows each event and a forecast total`
- **Source:** design.md — D2, D3; Components and Responsibilities (`entities/broker-operation/model/display.ts`, `entities/brokerage-event/model/event-key.ts`)
- **Verification:** `npm run test:run -- src/entities/broker-operation src/entities/brokerage-event`
- **Depends on:** 1.1
## 2. Представление групп
- [x] 2.1 [frontend] Implementation: компоненты группы и строки счёта для операций и событий по ux-design (заголовок, вложенный список, скрытый итог, доступные имена), с component-тестами
- **Covers:** `brokerage-account-ledger` — `Full ledger presents compact rows`, `brokerage-account-ledger` — `All-accounts ledger group shows each entry and an exact total`, `brokerage-account-upcoming-events` — `All-accounts upcoming event group shows each event and a forecast total`
- **Source:** design.md — UI Implementation; ux-design.md — Information Architecture, UI States, Accessibility
- **Verification:** `npm run test:run -- src/entities packages/ui`; проверка в браузере на 320 px и при zoom 200%
- **Depends on:** 1.2
- [x] 2.2 [frontend] Implementation: группировка в режиме `all` на страницах журнала и «Будущие события». Полнота берётся из `meta`, путь одного счёта не меняется. Route-тесты, включая pagination через MSW
- **Covers:** `brokerage-account-ledger` — `User can retrieve an account ledger`, `brokerage-account-ledger` — `All-accounts ledger grouping preserves order and limits`, `brokerage-account-upcoming-events` — `Account page presents an upcoming events list`, `brokerage-account-upcoming-events` — `All-accounts upcoming event grouping preserves order and limits`
- **Source:** design.md — D4, Components and Responsibilities (`pages/account-operations`, `pages/account-events`)
- **Verification:** `npm run test:run -- src/pages/account-operations src/pages/account-events`
- **Depends on:** 2.1
- [x] 2.3 [frontend] Implementation: группировка в превью обзора `/accounts/all` после лимита в 5 записей, с preview-тестами
- **Covers:** `brokerage-account-ledger` — `Account overview includes recent ledger events`, `brokerage-account-ledger` — `All-accounts ledger grouping preserves order and limits`, `brokerage-account-upcoming-events` — `All-accounts upcoming event grouping preserves order and limits`
- **Source:** design.md — Components and Responsibilities (`pages/account/model/previews.ts`, `RecentOperations.view.tsx`, `BrokerageEventsPreview.tsx`)
- **Verification:** `npm run test:run -- src/pages/account`
- **Depends on:** 2.1
- [x] 2.4 [frontend] Implementation: группировка «Выплат» в режиме `all`. Ключи повторных выплат сохраняются, серверные аналитические итоги не меняются. Route-тесты
- **Covers:** `brokerage-account-income-analytics` — `Income payments remain traceable to exact Ledger Entries`, `brokerage-account-income-analytics` — `Realized income analytics is account-scoped and period-bound`
- **Source:** design.md — Components and Responsibilities (`pages/account-income/model/display.ts`)
- **Verification:** `npm run test:run -- src/pages/account-income`
- **Depends on:** 2.1
- [x] 2.5 [frontend] Verification: полный набор frontend gates на итоговом diff
- **Covers:** N/A — сводная проверка задач 1.1–2.4
- **Source:** design.md — Developer Verification Strategy
- **Verification:** `npm run verify` (после `nvm use --silent`)
- **Depends on:** 2.2, 2.3, 2.4
## 3. Документация
- [x] 3.1 [docs] Docs: обновить AS-IS страниц операций, будущих событий, дохода и режима «Все счета»
- **Covers:** N/A — docs отражают реализованное поведение
- **Source:** tasks.md — Docs Impact
- **Verification:** semantic review против реализации; локальные проверки docs-репозитория по его AGENTS
- **Depends on:** 2.5
## 4. Приёмка
- [x] 4.1 [frontend] Independent Review: read-only review кода против specs, ux-design и design
- **Covers:** N/A — проверка реализации всех ADDED и MODIFIED Requirements
- **Source:** process-record.md — протокол G4
- **Verification:** отчёт independent-reviewer без открытых blocker и major
- **Depends on:** 2.5
- [x] 4.2 [frontend] QA Verification: системная проверка по qa-analysis и test-cases на локальном стенде
- **Covers:** N/A — проверка всех ADDED и MODIFIED Requirements по Coverage Matrix
- **Source:** qa-analysis.md — Coverage Matrix; test-cases/*.md
- **Verification:** отчёт qa-engineer по каждому TC и Scenario из матрицы
- **Depends on:** 2.5
## Docs Impact
Обновляются AS-IS страницы в `docs` (по design.md — Docs Impact):
- `docs/domains/operations/overview.md`: группы журнала и превью; точный клиентский итог по показанным записям.
- `docs/domains/income/upcoming-events.md`: группы полного списка и превью, `dateKind`, когда показывается прогнозный итог.
- `docs/domains/income/overview.md`: группировка «Выплат» в режиме `all`; визуальный итог группы отдельно от серверной аналитики.
- `docs/domains/portfolio/all-accounts.md`: группировка в общих списках только в пределах страницы, cursor protocol прежний.
`docs/design/overview.md` не меняется: граница между UI foundation и предметным представлением остаётся прежней. ADR не требуется.
@@ -0,0 +1,69 @@
# Test Cases — grouping
## TC-GRP-001 — Интерливинг трёх записей одной группы в серверном порядке
**Verifies:** `brokerage-account-ledger` — `All-accounts ledger groups same-day entries of one instrument` (new combination: порядок внутри группы из 3+ записей с интерливингом чужих записей)
**Risk / Priority:** Высокий / High
**Preconditions:** Режим «Все счета»; брокерский счёт и ИИС доступны; период подтверждённо полный.
**Test Data:** Серверный порядок записей одного дня: A1 (купон X, Брокерский, +200 RUB), B (перевод, без Instrument), A2 (купон X, ИИС, +100 RUB), C (купон Y, Брокерский, +50 RUB), A3 (купон X, Брокерский, +150 RUB).
**Steps:**
1. Открыть полный журнал в режиме «Все счета» за этот день.
2. Зафиксировать порядок групп и записей.
**Expected Results:** Группа X (A1+A2+A3) стоит на месте A1 — до B; внутри группы порядок A1, A2, A3; затем B, затем C. Относительный порядок B и C сохранён. Итог группы +450 RUB.
**Automation Candidate:** yes (E2E)
## TC-GRP-002 — Две разные группы, записи которых чередуются
**Verifies:** `brokerage-account-ledger` — `All-accounts ledger groups same-day entries of one instrument` / `All-accounts ledger grouping preserves order and limits` (new combination)
**Risk / Priority:** Высокий / High
**Preconditions:** Режим «Все счета», полный период.
**Test Data:** Порядок: X1 (купон X), Y1 (дивиденд Y), X2, Y2, X3. X — облигация, Y — акция; купоны X по 100 RUB (Брокерский, ИИС, Брокерский), дивиденды Y по 50 RUB (ИИС, Брокерский).
**Steps:**
1. Открыть журнал «Все счета» за день.
**Expected Results:** Две группы: группа X на месте X1 с итогом +300 RUB, группа Y на месте Y1 с итогом +100 RUB; порядок групп X → Y; внутри каждой — серверный порядок записей.
**Automation Candidate:** yes (E2E)
## TC-GRP-003 — Одна и та же бумага: figi-only identity против instrumentUid — не смешивать разные идентичности
**Verifies:** `brokerage-account-ledger` — `All-accounts ledger groups same-day entries of one instrument` (new combination: строгость identity-ключа)
**Risk / Priority:** Средний / High
**Preconditions:** Режим «Все счета», полный период.
**Test Data:** В один день: купон облигации Z на счёте 1 (+100 RUB, identity по figi), купон облигации Z на счёте 2 (+100 RUB, identity по instrumentUid без figi); и купон облигации W на двух счетах (+100 RUB и +50 RUB), где обе записи имеют одинаковый identity.
**Steps:**
1. Открыть журнал «Все счета» за день.
**Expected Results:** Облигация W образует группу с итогом +150 RUB. Две записи облигации Z показаны отдельно: равенство identity не доказано, ключ строится по первому непустому идентификатору (design D2), а группировка без доказанного равенства не выполняется.
**Automation Candidate:** yes (E2E со stubbed API, отложено до появления QA repo)
## TC-GRP-004 — Граница дня около полуночи местного времени
**Verifies:** `brokerage-account-ledger` — `All-accounts ledger groups same-day entries of one instrument` (new combination: экранный день в местном времени)
**Risk / Priority:** Средний / Medium
**Preconditions:** Режим «Все счета», полный период.
**Test Data:** Купон X +100 RUB с execution time 23:59:30 Europe/Moscow и купон X +100 RUB с execution time 00:00:30 следующего календарного дня Europe/Moscow.
**Steps:**
1. Открыть журнал «Все счета», охватывающий оба момента.
**Expected Results:** Экранный день определяется так же, как дата, которую строка показывает на этом экране (spec: «как на экране», design D2 — локальная timezone браузера). При timezone браузера Europe/Moscow записи показаны в разных днях и группу не образуют. Oracle — дата, показанная в строке, а не UTC-дата `occurredAt`.
**Automation Candidate:** yes (E2E с фиксированной timezone браузера, отложено до появления QA repo)
## TC-GRP-005 — Прогноз: неизвестная сумма + известные суммы в двух валютах в одной группе
**Verifies:** `brokerage-account-upcoming-events` — `All-accounts upcoming event group shows each event and a forecast total` (new combination)
**Risk / Priority:** Высокий / High
**Preconditions:** Режим «Все счета», страница «Будущие события».
**Test Data:** Дивиденд D: событие на Брокерском 10 USD (известно), событие на ИИС 200 RUB (известно), событие на третьем счёте без суммы; все — одна дата реестра, один точный тип.
**Steps:**
1. Открыть «Будущие события» в режиме «Все счета».
**Expected Results:** Одна группа из трёх событий; строка без суммы показывает «неизвестно»; итог группы скрыт, потому что у одного события сумма неизвестна (прогноз полный, но не полностью известен по суммам); известные 10 USD и 200 RUB показаны отдельными строками, без конвертации и без общего итога. Дата в заголовке обозначена как дата реестра.
**Automation Candidate:** yes (E2E)
## TC-GRP-006 — «Выплаты» в `all`: отрицательная корректировка и дубли operation IDs внутри одной группы, аналитические итоги не меняются
**Verifies:** `brokerage-account-income-analytics` — `Income payments remain traceable to exact Ledger Entries` / Scenario: Отрицательная корректировка и повторяющиеся IDs сохраняются (new combination: то же внутри группы `all`)
**Risk / Priority:** Высокий / High
**Preconditions:** Режим «Все счета», раздел «Выплаты», период подтверждённо полный.
**Test Data:** За один день по облигации X: купон +200 RUB (Брокерский, opId=77), купон +100 RUB (ИИС, opId=77 — дубликат), корректировка −50 RUB (Брокерский, тот же тип). Аналитические итоги страницы зафиксированы до.
**Steps:**
1. Открыть «Выплаты» в режиме «Все счета» за период.
2. Сравнить аналитические итоги с зафиксированными.
**Expected Results:** Одна группа из трёх карточек; обе записи opId=77 — отдельные карточки; корректировка со знаком «−»; итог группы +250 RUB; валютные заголовки групп и receipt count отсутствуют; аналитические итоги страницы не пересчитаны и не изменились.
**Automation Candidate:** yes (E2E)
@@ -0,0 +1,38 @@
# Test Cases — pagination
## TC-PAG-001 — Группа, разрезанная границей страницы журнала: конкретное содержимое страниц и переходы
**Verifies:** `brokerage-account-ledger` — `All-accounts ledger grouping preserves order and limits` / Scenario: Группа на границе страницы (new combination: конкретное содержимое страниц, next/prev)
**Risk / Priority:** Высокий / High
**Preconditions:** Режим «Все счета»; полный период; тестовые данные строятся вокруг фактического размера страницы N, который возвращает сервер (фиксируется в момент прогона).
**Test Data:** Серверный порядок: p1..p5 (прочие записи), X1 (купон X, +100 RUB), X2 (купон X, +150 RUB), X3 (купон X, +50 RUB), p9..p12. Размер страницы N — как у сервера; данные готовятся так, чтобы X1 был последней записью страницы 1, а X2, X3 — началом страницы 2.
**Steps:**
1. Открыть страницу 1 журнала; зафиксировать состав.
2. Перейти на страницу 2.
3. Вернуться на страницу 1.
**Expected Results:** Страница 1 заканчивается записью X1 как обычной строкой: группы нет (для группы нужно ≥2 показанных записей), итог не показывается. Страница 2 начинается группой X2+X3 (итог +200 RUB, разбивка X2/X3), затем p9…. Возврат на страницу 1 воспроизводит тот же состав. Серверный относительный порядок нигде не нарушен; записи одной группы не «стягиваются» через границу страницы.
**Automation Candidate:** yes (E2E)
## TC-PAG-002 — Обзор «Все счета»: пять записей, шестая подходящая существует, но не показана
**Verifies:** `brokerage-account-ledger` — `Account overview includes recent ledger events` / Scenario: Отображение последних событий (new combination: группа на границе лимита 5)
**Risk / Priority:** Высокий / High
**Preconditions:** Обзор «Все счета».
**Test Data:** Последние 6 записей в серверном порядке (свежая → старая): X1 (купон X, ИИС, +100 RUB), p2, p3, p4, p5, X0 (купон X, Брокерский, +200 RUB — шестая, за пределами выборки). X1 и X0 приходятся на один и тот же экранный день и один точный тип — по ключу они образовали бы группу, но X0 не входит в выборку обзора.
**Steps:**
1. Открыть обзор «Все счета».
2. Перейти в полный журнал.
**Expected Results:** В обзоре ровно 5 записей; X1 не образует группу (второй записи с тем же ключом в выборке нет) и показан отдельной строкой; X0 в обзоре отсутствует. Полный журнал показывает группу X0+X1 с итогом +300 RUB на месте своей первой записи.
**Automation Candidate:** yes (E2E)
## TC-PAG-003 — Обзор «Все счета»: лимит 5 считается по записям, группа занимает одну визуальную позицию
**Verifies:** `brokerage-account-ledger` — `Account overview includes recent ledger events` / Scenario: Группировка в последних записях обзора (new combination: счётчик лимита)
**Risk / Priority:** Средний / Medium
**Preconditions:** Обзор «Все счета».
**Test Data:** Последние записи (свежая → старая): p1, X1 (купон X, +100 RUB), X0 (купон X, +200 RUB, тот же экранный день и точный тип), p4, p5, p6.
**Steps:**
1. Открыть обзор «Все счета».
2. Посчитать визуальные позиции и проверить отсутствие p6.
**Expected Results:** Лимит 5 считается по записям: показаны записи p1, X1, X0, p4, p5 — это 4 визуальные позиции: p1, группа X (на месте X1, итог +300 RUB, разбивка X1/X0), p4, p5. Запись p6 не показана: она шестая по счётчику записей, свободной позиции от лимита нет.
**Automation Candidate:** yes (E2E)
@@ -0,0 +1,58 @@
# Test Cases — totals
## TC-TOT-001 — Мультивалютная группа со смешанными знаками и большой разрядностью: точный десятичный оракул
**Verifies:** `brokerage-account-ledger` — `All-accounts ledger group shows each entry and an exact total` (new combination: calculated oracle)
**Risk / Priority:** Высокий / High
**Preconditions:** Режим «Все счета», полный период.
**Test Data:** Один день, облигация X, точный тип «купон»: +0.1 RUB (Брокерский), +0.2 RUB (ИИС), −1500000.45 RUB (Брокерский, корректировка того же типа), +200 RUB (ИИС); плюс в той же группе день выплат USD: +0.01 USD и −0.02 USD.
**Steps:**
1. Открыть журнал «Все счета» за день.
2. Прочитать итог группы по каждой валюте.
**Expected Results:** Итог RUB = −1499800.15 (точная десятичная сумма: 0.1+0.2−1500000.45+200), без плавающей точки артефактов (не −1499800.1499999); итог USD = −0.01; два отдельных итога, без конвертации и без общего итога; каждая строка показывает свою сумму со знаком.
**Automation Candidate:** yes (E2E)
## TC-TOT-002 — Разное число знаков после запятой у записей группы
**Verifies:** `brokerage-account-ledger` — `All-accounts ledger group shows each entry and an exact total` (new combination)
**Risk / Priority:** Средний / High
**Preconditions:** Режим «Все счета», полный период.
**Test Data:** Группа купонов X: +100.5 RUB, +49.50 RUB, +0.005 RUB (если сервер отдаёт такую точность).
**Steps:**
1. Открыть журнал, прочитать итог.
**Expected Results:** Итог = 150.005 RUB ровно, без округления до 2 знаков и без потери младшего разряда; строки показывают суммы как отданы сервером.
**Automation Candidate:** yes (E2E)
## TC-TOT-003 — Нулевой итог и запись без денежного изменения в одной группе
**Verifies:** `brokerage-account-ledger` — `All-accounts ledger group shows each entry and an exact total` / Scenario: Нулевой итог; Scenario: Группа с записью без денежного изменения (new combination: обе записи в одной группе)
**Risk / Priority:** Средний / Medium
**Preconditions:** Режим «Все счета», полный период.
**Test Data:** Группа X: +150.10 RUB, −150.10 RUB, запись без денежного изменения (например, только количество бумаг).
**Steps:**
1. Открыть журнал, прочитать группу.
**Expected Results:** Итог группы скрыт; запись без изменения обозначена как «без движения», а не как 0 RUB; строки остаются видимыми.
**Automation Candidate:** yes (E2E)
## TC-TOT-004 — `partial` → подтверждённая полнота: итог появляется
**Verifies:** `brokerage-account-ledger` — `All-accounts ledger group shows each entry and an exact total` / Scenario: Итог скрыт при неполном периоде (new combination: смена состояния)
**Risk / Priority:** Высокий / High
**Preconditions:** Режим «Все счета»; заглушка API (MSW/Playwright route) с управляемым `meta`.
**Test Data:** Группа X: +200 RUB и +100 RUB. Первый ответ — `meta` с `partial` для одного из счетов; второй ответ — подтверждённая полнота.
**Steps:**
1. Открыть журнал с partial-ответом; зафиксировать группу.
2. Подтвердить полноту (повторный запрос/фоновая догрузка с полной `meta`).
**Expected Results:** При `partial`: строки видны, итог скрыт. После полноты: появляется точный итог +300 RUB; состав и порядок строк не изменились.
**Automation Candidate:** yes (E2E)
## TC-TOT-005 — Прогнозный итог при частично неизвестных суммах скрыт, при полной известности — показан по валютам
**Verifies:** `brokerage-account-upcoming-events` — `All-accounts upcoming event group shows each event and a forecast total` (new combination)
**Risk / Priority:** Средний / High
**Preconditions:** Режим «Все счета», «Будущие события».
**Test Data:** Два соседних дня: день 1 — купоны X на двух счетах по 100 RUB (обе суммы известны); день 2 — купоны X на двух счетах, у одной суммы нет.
**Steps:**
1. Открыть «Будущие события».
**Expected Results:** Группа дня 1: итог +200 RUB. Группа дня 2: итог скрыт, строка без суммы обозначена как неизвестная. Итоги списком не сводятся.
**Automation Candidate:** yes (E2E)
@@ -0,0 +1,211 @@
# UX Design
Задача пользователя: в режиме «Все счета» (`/accounts/all/...`) быстро увидеть, что
несколько записей — это один и тот же факт (один день, одна бумага, один тип), понять
общую сумму и при этом сразу видеть вклад каждого счёта. Нормативное поведение
(ключ группы, состав, порядок, правила итога) задано в `proposal.md` и трёх spec;
этот документ его не повторяет и не меняет, а только определяет представление.
Допущение: design system — Mantine + `@wealthledger/ui` по `docs/design/overview.md`
(семантические роли, направление несут знак и текст, не только цвет).
## Requirements Covered
- `brokerage-account-ledger` — `Full ledger presents compact rows` (MODIFIED, группа в полном журнале)
- `brokerage-account-ledger` — `Account overview includes recent ledger events` (MODIFIED, группа в обзоре)
- `brokerage-account-ledger` — `User can retrieve an account ledger` (MODIFIED, режим `all`, `partial`)
- `brokerage-account-ledger` — `All-accounts ledger groups same-day entries of one instrument`
- `brokerage-account-ledger` — `All-accounts ledger group shows each entry and an exact total`
- `brokerage-account-ledger` — `All-accounts ledger grouping preserves order and limits`
- `brokerage-account-upcoming-events` — `Account page presents an upcoming events list` (MODIFIED, прогнозный итог группы)
- `brokerage-account-upcoming-events` — `All-accounts upcoming events group same-day events of one instrument`
- `brokerage-account-upcoming-events` — `All-accounts upcoming event group shows each event and a forecast total`
- `brokerage-account-upcoming-events` — `All-accounts upcoming event grouping preserves order and limits`
- `brokerage-account-income-analytics` — `Realized income analytics is account-scoped and period-bound` (MODIFIED, режим `all`)
- `brokerage-account-income-analytics` — `Income payments remain traceable to exact Ledger Entries` (MODIFIED, «Выплаты» в режиме `all`)
## User Flows
Цель пользователя — «что и сколько пришло/придёт по бумаге X и с каких счетов».
1. **Просмотр списка.** Пользователь открывает любой из 4 списков в режиме «Все счета» →
видит заголовок группы (дата, бумага, тип, итог) → читает строки счетов под ним,
раскрывать ничего не нужно.
2. **Переход к счёту.** Из строки группы (журнал, выплаты) пользователь открывает
`/accounts/{id}` по имени счёта; возврат — кнопкой «Назад» браузера, список и позиция
восстанавливаются как сейчас.
3. **Листание журнала.** «Назад/Вперёд» меняют страницу; группа собирается заново из показанных
записей страницы. Пользователь не получает пометок о неполноте на границе страницы.
4. **Обзор.** На `/accounts/all` в блоках «Ближайшие события» и «Последние операции» группа
занимает место первой записи; блок по-прежнему содержит не более 5 записей/событий, как сейчас.
5. **Одиночная запись / одиночный счёт.** Запись без пары, без Instrument или любой список в
режиме одного счёта выглядят как сейчас; пользовательский путь не меняется.
Ветвление по итогу: итог известен → показан в заголовке; итог скрыт (см. UI States) →
пользователь складывает строки сам или смотрит на состояние страницы (`partial`-Alert).
## Information Architecture
Группа — один визуальный блок (рамка `Paper withBorder`) внутри существующего списка, на
месте первой записи. Состав блока: заголовок + вложенный список строк счетов. Заголовок
повторяет композицию текущей карточки (иконка/логотип | название и метаданные | значение),
строка счёта — компактнее, без логотипа.
Лейблы заголовка: заголовок = «`<тип>` · `<инструмент>`» (как в карточках событий/выплат),
метаданные = дата (+ «дата реестра» для дивиденда по реестру), значение = итог,
подпись значения = подпись типа (в событиях — «Прогноз»).
### Журнал (`/accounts/all/operations`, обзор «Последние операции»)
```
┌──────────────────────────────────────────────────────────┐
│ [logo] Купон · X +300 RUB │ заголовок
│ 12 мая 2026 Купон │
│ ──────────────────────────────────────────────────────── │
│ Брокерский +200 RUB Купон │ строка
│ ИИС +100 RUB Купон │ строка
└──────────────────────────────────────────────────────────┘
```
Строка: имя счёта слева; справа денежное изменение со знаком и подпись типа
(по `Full ledger presents compact rows`). Количество и цена в строках группы не показываются (Presentation Decisions, п. 3).
Имя счёта — обычный текст во всех списках, как в текущей метадате строки (`withDetails` в этих списках не используется).
### События (`/accounts/all/events`, обзор «Ближайшие события»)
```
┌──────────────────────────────────────────────────────────┐
│ [logo] Дивиденд · X 10 USD │
│ 12 мая 2026 · дата реестра 200 RUB │ итоги по валютам
│ Прогноз │
│ ──────────────────────────────────────────────────────── │
│ Брокерский 10 USD │
│ ИИС Сумма неизвестна│
└──────────────────────────────────────────────────────────┘
```
Пример выше показывает два разных состояния сразу; на практике итог при строке
«Сумма неизвестна» скрыт (см. UI States). Строки счёта в событиях — только текст (ссылки
на счёт в текущей карточке события нет).
### Выплаты (`/accounts/all/income`)
```
┌──────────────────────────────────────────────────────────┐
│ [logo] Купон · X +300 RUB │
│ Исполнено 12 мая 2026 Купон │
│ ──────────────────────────────────────────────────────── │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Брокерский +200 RUB │ │ отдельная карточка
│ └──────────────────────────────────────────────────────┘ │ внутри группы
│ ┌──────────────────────────────────────────────────────┐ │
│ │ ИИС +100 RUB │ │
│ └──────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘
```
В «Выплатах» spec требует «отдельную сразу видимую карточку» записи внутри группы:
вложенные карточки без логотипа и с границей, но визуально легче внешней. Подпись «Исполнено»
сохраняет различие факта и прогноза.
Размещение в 4 списках: страница журнала и «Выплаты» — вертикальный стек с зазором `sm`;
страница событий — тот же стек; блоки обзора — тот же компонент группы без дополнительной
рамки блока. Позиция — по правилам порядка из spec (группа на месте первой записи).
## UI States
| Состояние | Представление |
| --- | --- |
| Одиночная запись | Как сейчас: текущая карточка, без группы и вложенного списка |
| Группа с итогом | Заголовок с итогом `+300 RUB` (тон по знаку), строки счетов |
| Мультивалютный итог | В заголовке столбец значений: по строке на валюту, `+150 RUB` над `+10 USD`; порядок — по первому появлению валюты в записях группы (Presentation Decisions, п. 2); без конвертации и общей суммы |
| Итог скрыт | Значение в заголовке отсутствует; строки счетов видны. Причина: запись без суммы / событие с неизвестной суммой / период `partial`. Для экранных дикторов — скрытый текст «Итог недоступен» (без различения причин) |
| Нулевой итог | `0 RUB` без знака, нейтральный тон; строки с их знаками |
| Offer без сумм | Группа без итога; строки: имя счёта + «Сумма не определена» (существующий текст `events.noAmount`) |
| Прогноз | Итог и суммы строк в событиях несут подпись «Прогноз» (существующий `payout.forecast`); тон нейтральный, как у текущей карточки прогноза |
| Строка без денег (ledger) | Нейтральный текст `payout.noMoney`/«без движения денег» как в одиночной записи; итог группы скрыт |
| Обзор, 5 записей | Не более 5 записей/событий, как сейчас; группа может занимать часть из них, например 2 из 5; кнопка «Все …» ведёт на полную страницу |
| Граница страницы журнала | Группа из показанных записей, без пометки о неполноте и без «ещё N на другой странице» |
| Loading / empty / error / partial-Alert | Не меняются: `LoadingSkeleton`, `StateView`, `Alert` и `SourceSyncStatus` из существующих страниц; группы строятся только для загруженных данных |
Знак: «+» и «−» всегда в тексте; цвет (positive/negative/neutral) — дополнение.
## Visual Package
Решение: **reuse + один новый тонкий контейнер**. Изображения (PNG/SVG) не создаются:
изменение — повторная композиция существующих карточек, а структура и тексты описаны
текстовыми макетами выше; отдельный макет дал бы расхождение с живыми компонентами и
токенами Mantine.
Переиспользуется:
- `EntryCard` (`@wealthledger/ui`) — заголовок группы: логотип/инициал, `title`, `metadata`
(дата), `value` (итог), `valueLabel` (тип/«Прогноз»), `tone`. Отличие: в `value` нужно
выводить несколько значений для нескольких валют (сейчас одна строка) и допустить
отсутствие значения.
- `LedgerEntryRow` / `PayoutCard` (executed) — единичные записи и карточки внутри группы
выплат; в группе опускаются логотип, дата и название инструмента (они в заголовке).
- `BrokerageEventCard` / `PayoutCard` (forecast) — единичные события; тексты
`events.type.*`, `events.noAmount`, `payout.forecast`.
- Форматирование: `formatMoney`, `formatDate`, `instrumentLogoUrl`; тексты `ledger.noInstrument`,
`payout.noMoney`.
- Токены: `--wl-color-border`, `--wl-color-subtle`, `--wl-color-surface`, `Paper withBorder`.
Новое (предложение, с обоснованием «нужно показать иерархию заголовок → строки»):
- Контейнер группы (рамка, заголовок, вложенный список) и компактная строка счёта
(имя счёта слева, сумма справа, без `Avatar`). `EntryCard` всегда рисует `Avatar`, поэтому
для строки счёта нужна его облегчённая форма либо отдельная разметка; выбор — за реализацией.
Отступ строк слева — под текстовым блоком заголовка (≈ 40 px аватар + `sm`), либо без
отступа на узкой ширине.
## Responsive
- Базовый макет — mobile-first: заголовок — сетка из трёх колонок как `EntryCard`
(`auto | 1fr | auto`).
- **≥ 768 px**: заголовок в одну строку; строки счетов — имя слева, сумма и подпись справа.
- **< 576 px (узкий мобильный)**: колонка значения заголовка переносится под название
(название и метаданные — вверху, итог — следующей строкой справа/слева), мультивалютный итог
— столбцом; название инструмента и имя счёта переносятся (`wrap`, без `lineClamp`),
отступ строк слева убирается. Строка счёта: имя счёта сверху, сумма с подписью типа ниже, если
вместе не помещаются; длинное имя счёта не обрезается.
- Никакого горизонтального скролла; минимальная ширина — 320 px.
- Ввод: touch и мышь; в группе нет новых целей нажатия.
## Accessibility
Ориентир — WCAG 2.2 AA.
- **Семантика списка.** Внешний список — `<ul>`; каждая одиночная запись и каждая группа — `<li>`.
Внутри группы-`<li>` заголовок (обычный блок, не `<h*>`, чтобы не засорять навигацию по
заголовкам страницы) и вложенный `<ul>` со строками-`<li>`. Группы в обзоре — внутри уже
существующих секций с их заголовками.
- **Доступное имя группы.** Вложенный `<ul>` получает `aria-label` вида: «Купон, X, 12 мая 2026,
итог +300 RUB» (дата, инструмент, тип, итог, мультивалютный итог — через запятую). При скрытом
итоге — «… итог недоступен». Для знаков в озвучке: «плюс 300 рублей», «минус …» — через
`aria-label` значения либо скрытый текст; сам знак в тексте остаётся.
- **Итог скрыт.** Визуально значения нет; в DOM есть `visually-hidden` текст «Итог недоступен» и
он включён в имя группы. Причина скрытия `partial` уже объявляется существующим `Alert role="status"`.
- **Фокус.** Заголовок и строки группы не фокусируемы: новых интерактивных элементов нет. Если одиночная карточка содержит ссылку, она остаётся как есть, а порядок Tab совпадает с порядком DOM.
- **Не только цвет.** Направление передаёт знак «+»/«−» (минус как «−»), подпись типа и слова
«Прогноз»/«Исполнено»; цвет только усиливает. Контраст текста ≥ 4.5:1 (dimmed `gray.6` на `subtle`
проверить), границы групп ≥ 3:1 не требуются — это декор, смысл не теряется без границы.
- **Динамика.** Смена страницы журнала не перемещает фокус неожиданно; пагинация — как сейчас.
- **Масштаб.** Текст масштабируется до 200 % без потери информации (перенос строк).
## Presentation Decisions
Решения, которые относятся только к представлению и не меняют поведение по specs:
1. **Видимая причина скрытого итога.** Видимого маркера нет. Скринридер получает текст «Итог недоступен». Строки с суммами остаются видны, так что отсутствие итога не выглядит пустотой.
2. **Несколько валют в заголовке.** Каждая валюта на отдельной строке, в порядке первого появления в записях группы.
3. **Количество и цена в строках сделок.** Не показываются, как и в несгруппированном журнале. Specs требуют только счёт и денежное изменение.
4. **Ссылка на счёт.** Сохраняется текущее поведение: имя счёта — текст во всех четырёх списках.
5. **Подпись «Исполнено» / «Прогноз».** Ставится у итога в заголовке. В строках событий подпись остаётся у суммы, как в одиночной карточке.
6. **Дата реестра.** Применяется только к группам прогнозных событий. Исполненные выплаты датируются датой исполнения.
## Findings → Specs
Нет. Два пробела, найденных при проектировании, закрыты решением Change Owner и внесены в specs, G2 инвалидирован:
- прогнозный итог скрывается, если полнота прогноза не подтверждена;
- запись с нулевой суммой считается записью без денежного изменения и скрывает итог.
@@ -0,0 +1,294 @@
#!/usr/bin/env node
// Deterministic checks for wealth-ledger-sdd-lab changes. OpenSpec CLI 1.13.1 has no conditional
// artifacts, gate records or archive policy, so this file covers only what the CLI cannot.
// It is the single source for profile, ordering, gate and readiness rules; schema.yaml points here.
// Semantic quality (contradictions, invented facts, design fit) stays with reviewers.
//
// Usage: node check.cjs <plan|apply|archive> <change-name> [--root <openspec-root>]
// plan — artifact consistency while planning; missing gates are notes, not errors.
// apply — plan + every apply blocker is an error.
// archive — apply + all tasks done, G4 approved, lab retrospective.
// Exit: 0 ok, 1 violations, 2 invocation/environment error.
"use strict";
const fs = require("node:fs");
const path = require("node:path");
const SA_MODULES = ["data-mapping", "failure-model", "nfr", "observability"];
// Primary file of each conditional artifact, relative to the change directory.
const PRIMARY = {
research: "research.md",
"ux-design": "ux-design.md",
"system-analysis": "system-analysis/system-analysis.md",
"data-mapping": "system-analysis/data-mapping.md",
"failure-model": "system-analysis/failure-model.md",
nfr: "system-analysis/nfr.md",
observability: "system-analysis/observability.md",
"security-analysis": "security-analysis.md",
"test-cases": "test-cases",
"docs-impact": "docs-impact.md",
};
const CONDITIONAL = Object.keys(PRIMARY);
// A selected conditional input must exist before any of these files is written.
const BEFORE_SOLUTION = ["design.md", "qa-analysis.md", "tasks.md"];
const DOWNSTREAM = {
research: ["specs", ...BEFORE_SOLUTION],
"ux-design": BEFORE_SOLUTION,
"system-analysis": BEFORE_SOLUTION,
...Object.fromEntries(SA_MODULES.map((id) => [id, BEFORE_SOLUTION])),
"security-analysis": BEFORE_SOLUTION,
"test-cases": ["tasks.md"],
"docs-impact": ["tasks.md"],
};
// Gate that may approve each artifact. Complex changes approve system analysis at G2 (contract-first).
const SA_IDS = ["system-analysis", ...SA_MODULES];
const allowedGates = (id, complex, hasG2) => {
if (id === "proposal") return ["G1"];
if (id === "change-profile") return ["G1", "G2", "G3"]; // later profile changes need a recorded decision
if (id === "specs") return hasG2 ? ["G2"] : ["G3"];
if (SA_IDS.includes(id)) return complex ? ["G2"] : hasG2 ? ["G2", "G3"] : ["G3"];
return ["G3"];
};
const NOT_APPROVED = new Set(["research", "process-record"]); // input context / the log itself
const REF = /`([a-z0-9][a-z0-9/-]*)`\s*[—–-]\s*`([^`]+)`/g;
const fail = (msg) => { console.error(msg); process.exit(2); };
const args = process.argv.slice(2);
const [mode, changeName] = args;
if (!["plan", "apply", "archive"].includes(mode) || !changeName) {
fail("Usage: node check.cjs <plan|apply|archive> <change-name> [--root <openspec-root>]");
}
const rootIdx = args.indexOf("--root");
if (rootIdx > 0 && !args[rootIdx + 1]) fail("--root needs a path");
const root = path.resolve(rootIdx > 0 ? args[rootIdx + 1] : path.join(__dirname, "..", ".."));
const changeDir = path.join(root, "changes", changeName);
if (!fs.existsSync(changeDir)) fail(`Change directory not found: ${changeDir}`);
const errors = [];
const warnings = [];
const notes = [];
const rel = (p) => path.relative(changeDir, p);
const read = (p) => fs.readFileSync(p, "utf8");
const at = (p) => path.join(changeDir, p);
const walk = (dir) => fs.existsSync(dir)
? fs.readdirSync(dir, { withFileTypes: true }).flatMap((e) =>
e.isDirectory() ? walk(path.join(dir, e.name)) : [path.join(dir, e.name)])
: [];
const present = (p) => p === "test-cases" || p === "specs"
? walk(at(p)).some((f) => f.endsWith(".md"))
: fs.existsSync(at(p));
const mdFiles = walk(changeDir).filter((f) => f.endsWith(".md"));
const strip = (s) => (s ?? "").replace(/`/g, "").trim();
// Columns: fixed count, the last cell keeps any `|` it contains.
const tableRows = (text, heading, columns) => {
const section = text.split(/^## /m).find((s) => s.startsWith(heading));
if (!section) return null;
return section.split("\n").filter((l) => /^\|/.test(l)).slice(2).map((l) => {
const parts = l.replace(/^\|/, "").replace(/\|\s*$/, "").split("|");
return [...parts.slice(0, columns - 1), parts.slice(columns - 1).join("|")].map((c) => c.trim());
});
};
finishIfMissing();
const record = read(at("process-record.md"));
// 1. Unfilled templates.
for (const f of mdFiles) {
if (read(f).includes("<!-- guide:")) errors.push(`${rel(f)}: unfilled template guide comment remains`);
}
// 2. Mode, class, Change Profile.
const labMode = (record.match(/\*\*Mode:\*\*\s*(lab|delivery)\b/i) || [])[1]?.toLowerCase();
if (!labMode) errors.push("process-record.md: **Mode:** must be `lab` or `delivery`");
const klass = (record.match(/\*\*Class:\*\*\s*(standard|complex)\b/i) || [])[1]?.toLowerCase();
if (!klass) errors.push("process-record.md: **Class:** must be `standard` or `complex`");
const complex = klass === "complex";
const hasG2 = labMode === "lab" || complex;
const profile = new Map();
for (const [rawId, selected, reason] of tableRows(record, "Change Profile", 3) ?? []) {
const id = strip(rawId);
if (!CONDITIONAL.includes(id)) { errors.push(`Change Profile: unknown artifact '${id}'`); continue; }
if (!/^(yes|no)$/i.test(strip(selected))) errors.push(`Change Profile: '${id}' Selected must be yes|no`);
if (!reason) errors.push(`Change Profile: '${id}' needs a trigger or reason`);
profile.set(id, /^yes$/i.test(strip(selected)));
}
const pending = [];
for (const id of CONDITIONAL) {
if (!profile.has(id)) { errors.push(`Change Profile: no decision for '${id}'`); continue; }
const has = present(PRIMARY[id]);
if (profile.get(id) && !has) {
const consumer = DOWNSTREAM[id].find(present);
if (consumer) errors.push(`'${id}' is selected but ${PRIMARY[id]} does not exist while ${consumer} was already written`);
else pending.push(id);
}
if (!profile.get(id) && has) errors.push(`'${id}' is not selected but ${PRIMARY[id]} exists; select it through a gate decision or remove the file`);
}
for (const id of SA_MODULES) {
if (profile.get(id) && !profile.get("system-analysis")) errors.push(`'${id}' requires system-analysis to be selected`);
}
if (fs.existsSync(at("system-analysis")) && !present(PRIMARY["system-analysis"])) {
errors.push("system-analysis/ exists without system-analysis/system-analysis.md");
}
// 3. Requirement references resolve; delta requirements are covered.
const skipSpecs = present(".openspec.yaml") && /^skip_specs:\s*true/m.test(read(at(".openspec.yaml")));
if (skipSpecs && present("specs")) errors.push("skip_specs: true but specs/ contains delta files");
const deltaReqs = [];
const known = new Map();
const removed = new Set();
const addKnown = (cap, h) => (known.get(cap) ?? known.set(cap, new Set()).get(cap)).add(h);
const key = (cap, req) => `${cap}\u0000${req}`;
for (const f of walk(at("specs")).filter((x) => x.endsWith("spec.md"))) {
const cap = path.relative(at("specs"), path.dirname(f)).split(path.sep).join("/");
let op = null;
const seen = new Set();
for (const line of read(f).split("\n")) {
op = (line.match(/^## (ADDED|MODIFIED|REMOVED|RENAMED) Requirements/) || [])[1] ?? op;
const req = (line.match(/^### Requirement:\s*(.+?)\s*$/) || [])[1];
if (req) {
if (seen.has(req)) errors.push(`specs/${cap}/spec.md: duplicate Requirement '${req}'`);
seen.add(req);
if (op === "REMOVED") removed.add(key(cap, req));
else addKnown(cap, req);
if (op === "ADDED" || op === "MODIFIED") deltaReqs.push(key(cap, req));
}
const to = (line.match(/^-?\s*TO:\s*`?###\s*Requirement:\s*(.+?)`?\s*$/) || [])[1];
if (to) addKnown(cap, to);
}
}
for (const f of walk(path.join(root, "specs")).filter((x) => x.endsWith("spec.md"))) {
const cap = path.relative(path.join(root, "specs"), path.dirname(f)).split(path.sep).join("/");
for (const m of read(f).matchAll(/^### Requirement:\s*(.+?)\s*$/gm)) {
if (!removed.has(key(cap, m[1]))) addKnown(cap, m[1]);
}
}
const refs = (text) => [...text.matchAll(REF)].map((m) => ({ cap: m[1], req: m[2] }));
for (const f of mdFiles.filter((x) => !rel(x).startsWith(`specs${path.sep}`))) {
for (const { cap, req } of refs(read(f))) {
if (!known.get(cap)?.has(req)) errors.push(`${rel(f)}: unknown requirement reference \`${cap}\` — \`${req}\``);
}
}
const tasksPath = at("tasks.md");
const tasksText = fs.existsSync(tasksPath) ? read(tasksPath) : "";
const coversLines = tasksText.split("\n").filter((l) => /\*\*Covers:\*\*/.test(l)).join("\n");
const taskCover = new Set(refs(coversLines).map((r) => key(r.cap, r.req)));
const qaText = [at("qa-analysis.md"), ...walk(at("test-cases"))].filter(fs.existsSync).map(read).join("\n");
const qaCover = new Set(refs(qaText).map((r) => key(r.cap, r.req)));
for (const k of deltaReqs) {
const label = k.replace("\u0000", "` — `");
if (tasksText && !taskCover.has(k)) errors.push(`tasks.md: delta requirement \`${label}\` has no task Covers`);
if (present("qa-analysis.md") && !qaCover.has(k)) warnings.push(`qa-analysis: delta requirement \`${label}\` has no verification reference (allowed only with a stated risk reason)`);
}
if (!skipSpecs && present("specs") && deltaReqs.length === 0) warnings.push("no ADDED/MODIFIED requirements found in delta specs");
// 4. Test case identity and traceability.
const tcIds = new Set();
for (const f of walk(at("test-cases")).filter((x) => x.endsWith(".md"))) {
for (const block of read(f).split(/^###\s+(?=TC-)/m).slice(1)) {
const id = block.split(/\s/)[0];
if (tcIds.has(id)) errors.push(`${rel(f)}: duplicate test case id ${id}`);
tcIds.add(id);
if (!refs(block).length) errors.push(`${rel(f)}: test case ${id} has no requirement reference`);
}
}
// 5. Contracts and diagrams are referenced, JSON parses, required task kinds exist.
const saText = walk(at("system-analysis")).filter((f) => f.endsWith(".md")).map(read).join("\n");
const contracts = walk(at("system-analysis/contracts"));
for (const f of [...contracts, ...walk(at("system-analysis/diagrams"))]) {
if (!saText.includes(path.basename(f))) errors.push(`${rel(f)}: not referenced from any system-analysis/*.md`);
if (f.endsWith(".json")) {
try { JSON.parse(read(f)); } catch (e) { errors.push(`${rel(f)}: invalid JSON (${e.message})`); }
}
}
const taskLines = tasksText.split("\n").filter((l) => /^\s*[-*+]\s*\[.?\]/.test(l));
if (tasksText) {
if (!taskLines.some((l) => /Independent Review:/.test(l))) errors.push("tasks.md: no `Independent Review:` task");
if (contracts.length && !taskLines.some((l) => /Contract Parity:/.test(l))) errors.push("tasks.md: contracts exist but no `Contract Parity:` task");
if (profile.get("docs-impact") === false && !/^## Docs Impact/m.test(tasksText)) {
errors.push("tasks.md: docs-impact not selected, so a `## Docs Impact` section (changes or no-change reason) is required");
}
}
// 6. Gate Log: chronological; invalidating Gn withdraws Gn and every later gate.
const gates = new Map();
const order = [];
for (const [rawGate, , scope, validation, , decision] of tableRows(record, "Gate Log", 6) ?? []) {
const id = (strip(rawGate).match(/^G[1-4]\b/) || [])[0];
if (!id) continue;
if (/^invalidated\b/i.test(decision)) {
for (const g of [...gates.keys()]) if (g >= id) gates.delete(g);
continue;
}
if (!/^approved\b/i.test(decision)) continue;
if (!/"[^"]+"|«[^»]+»/.test(decision)) errors.push(`Gate ${id}: approval must quote the Change Owner decision`);
if (!validation) errors.push(`Gate ${id}: approval has no validation evidence`);
const prior = ["G1", "G2", "G3", "G4"].filter((g) => g < id && (g !== "G2" || hasG2));
if (prior.some((g) => !gates.has(g))) errors.push(`Gate ${id}: approved before ${prior.filter((g) => !gates.has(g)).join(", ")}`);
const ids = strip(scope).split(/[,\s]+/).filter(Boolean);
for (const a of ids) {
if (id !== "G4" && !allowedGates(a, complex, hasG2).includes(id)) errors.push(`Gate ${id}: '${a}' must be approved at ${allowedGates(a, complex, hasG2).join(" or ")}`);
}
gates.set(id, ids);
order.push(id);
}
if (gates.has("G1") && !gates.get("G1").includes("change-profile")) errors.push("Gate G1: scope must include change-profile");
// 7. Readiness.
const artifactPaths = [
["proposal", "proposal.md"], ["specs", "specs"], ["design", "design.md"],
["qa-analysis", "qa-analysis.md"], ["tasks", "tasks.md"],
...CONDITIONAL.map((id) => [id, PRIMARY[id]]),
];
const approvedScope = new Set([...gates.values()].flat());
const required = hasG2 ? ["G1", "G2", "G3"] : ["G1", "G3"];
const missingGates = required.filter((g) => !gates.has(g));
const unapproved = artifactPaths.filter(([id, p]) => !NOT_APPROVED.has(id) && present(p) && !approvedScope.has(id)).map(([id]) => id);
const blockers = [
...(tasksText ? [] : ["tasks.md missing"]),
...(missingGates.length ? [`missing gates: ${missingGates.join(", ")}`] : []),
...(unapproved.length ? [`not in any approved gate scope: ${unapproved.join(", ")}`] : []),
...(pending.length ? [`selected but missing: ${pending.join(", ")}`] : []),
];
notes.push(`apply-ready: ${blockers.length || errors.length ? `no (${[...blockers, ...(errors.length ? ["errors"] : [])].join("; ")})` : "yes"}`);
if (mode !== "plan") blockers.forEach((b) => errors.push(`apply blocker: ${b}`));
const sequence = ["proposal", "process-record", "research", "specs", "ux-design", "system-analysis",
...SA_MODULES, "security-analysis", "design", "qa-analysis", "test-cases", "docs-impact", "tasks"];
const pathOf = Object.fromEntries([...artifactPaths, ["process-record", "process-record.md"]]);
const next = sequence.find((id) => {
if (CONDITIONAL.includes(id) && !profile.get(id)) return false;
if (id === "specs" && skipSpecs) return false;
return !present(pathOf[id]);
});
notes.push(`next artifact: ${next ?? "none (planning complete for this profile)"}`);
// 8. Archive readiness. Native archive treats any non-[x] marker as open, so descoped work is
// removed from tasks.md by a gate decision and recorded in "Descoped Work".
if (mode === "archive") {
for (const l of taskLines) if (!/\[x\]/i.test(l)) errors.push(`tasks.md: open task: ${l.trim()}`);
for (const [task, reason, gate] of tableRows(record, "Descoped Work", 3) ?? []) {
if (task && (!reason || !/^G[1-4]$/.test(strip(gate)))) errors.push(`Descoped Work '${task}': needs reason and approving gate`);
}
if (!gates.has("G4")) errors.push("Gate G4 Delivery Acceptance is not approved");
if (labMode === "lab") {
const retro = record.split(/^## /m).find((s) => s.startsWith("Retrospective")) ?? "";
if (!retro.replace(/^Retrospective.*\n/, "").trim()) errors.push("process-record.md: Retrospective is empty (lab mode)");
if ((tableRows(record, "Lab Journal", 9) ?? []).length < gates.size) warnings.push("Lab Journal has fewer rows than approved gates");
}
}
finish();
function finishIfMissing() {
if (fs.existsSync(at("process-record.md"))) return;
errors.push("process-record.md is missing: Change Profile and Gate Log are required");
finish();
}
function finish() {
for (const n of notes) console.log(`note: ${n}`);
for (const w of warnings) console.log(`warning: ${w}`);
for (const e of errors) console.log(`error: ${e}`);
console.log(errors.length ? `FAIL ${mode} ${changeName}: ${errors.length} error(s)` : `OK ${mode} ${changeName}`);
process.exit(errors.length ? 1 : 0);
}
@@ -0,0 +1,671 @@
name: wealth-ledger-sdd-lab
version: 1
description: Experimental Engineering Lab workflow - proposal + change profile → specs → selected analysis modules → design ∥ qa-analysis → tasks → apply, with 4 decision gates
# CLI contract (OpenSpec 1.13.1): an artifact is complete when its `generates` path exists;
# `requires` is a hard edge; there is no conditional artifact. Conditional artifacts below are
# therefore never required by a mandatory artifact or by apply. Selection, ordering of selected
# artifacts, gates and archive readiness are checked by `check.cjs` in this schema directory.
artifacts:
- id: proposal
generates: proposal.md
description: Change Owner intent, scope, capabilities, evidence and open decisions
template: proposal.md
instruction: |
Owner: Change Owner; draft — product-analyst. Gate: G1 Product Decision.
Purpose:
Proposal фиксирует, зачем нужно изменение, какой результат ожидается, где
проходит scope и какие решения ещё не приняты. Это не requirements, не system
analysis и не design.
Start of the lab lifecycle (обязательно до записи):
- Change создаётся явно: `openspec new change <name> --schema wealth-ledger-sdd-lab
--store wealth-ledger-specs`. Schema не становится default и не применяется к
существующим changes.
- Small (локальный fix, известное behavior) не идёт в OpenSpec: правка canonical spec
в том же Issue. Lab schema принимает только Standard и Complex.
- Если существует research (`research.md` в change, Issue, внешний отчёт), прочитай
его выводы; Proposal ссылается на research в **Evidence**, а не копирует его.
Research может закончиться решением не делать change — тогда change не создаётся.
- Общий протокол (Change Profile, gates, делегирование сабагентам, `check.cjs`) —
instruction artifact `process-record`; он действует на каждой стадии.
Read First:
- Релевантные canonical specs из `specs/openspec/specs/` и AS-IS pages `docs/`
(`overview.md`, `architecture/`, `domains/`, `integrations/`, `adr/`, `design/`).
- Code-level реализацию не исследуй глубже, чем нужно для scope/impact.
Critical Rules:
- Один Proposal = одно связное намерение; независимо поставляемое разделяй.
- Capabilities перечисляют каждую новую/изменяемую behavior spec точным path.
Если observable behavior не меняется — `.openspec.yaml: skip_specs: true`.
- Breaking observable behavior помечай **BREAKING**.
- Evidence разделяет факты (с источником) и гипотезы.
- Open Decisions: вопрос, кто решает, что блокирует. Вопрос, меняющий scope или
capabilities, решается до G1.
MUST NOT Include:
- files/classes/functions/tables/ORM entities; libraries/frameworks/transport;
endpoint/payload/error-code contracts; diagrams; test plans; tasks/PR plan;
пиксельные UI-детали.
Feedback Routing: research gap → research; всё остальное возвращается сюда.
Validation:
- `openspec validate <change> --store wealth-ledger-specs` (Why/What Changes);
structural success не является approval.
- Затем создай `process-record.md` (Change Profile) и предъяви G1 оба файла вместе.
requires: []
- id: research
generates: research.md
description: Optional product research used as input context; never approved as a requirement source
template: research.md
instruction: |
Owner: product-researcher. Conditional: создавай только если Change Profile выбрал
`research`. Не требуется ни одним artifact и не блокирует apply. Не входит в gates:
это входной контекст, не утверждаемый artifact.
Trigger: новый продуктовый функционал, незнакомая предметная область, конкуренты и
альтернативы, проверка гипотезы, высокая продуктовая неопределённость.
Если исследование уже существует вне change (Issue, `audit-results/`, отчёт),
не копируй его: Proposal ссылается на источник, Change Profile ставит `no` + ссылку.
Исследование, начатое до решения о change, живёт вне OpenSpec до G1.
Critical Rules:
- Каждое утверждение: [F] факт с источником, [I] интерпретация или [H] гипотеза.
- Не выдумывай источники, цифры и пользователей; нет данных — так и напиши.
- Recommendation допускает «не делать» и «сузить scope».
MUST NOT Include: окончательные requirements, обязательные архитектурные решения,
contracts, tasks.
Feedback Routing: выводы потребляет Proposal (Evidence, Scope, Open Decisions).
requires: []
- id: process-record
generates: process-record.md
description: Change Profile, gate decisions, descoped work and lab journal for the change
template: process-record.md
instruction: |
Owner: основной агент (оркестратор). Change Profile утверждает Change Owner на G1.
Это журнал процесса, а не planning authority: он не содержит requirements,
contracts или design.
## Общий протокол lab schema (действует на каждой стадии)
Modes and Classes:
- **Mode** `lab` — целенаправленная практика: G2 обязателен, независимая проверка
на каждом gate, Lab Journal после каждой стадии, Retrospective перед archive,
условные artifacts можно выбирать ради названной практики.
- **Mode** `delivery` — минимально достаточный процесс: artifacts только по
risk-триггеру, qa-analysis компактный, Lab Journal необязателен.
- **Class** `standard` | `complex`. Complex: межсистемно, деньги/синхронизация/
данные, необратимые миграции или высокая неопределённость. Mode и Class
независимы.
Change Profile (обязательная таблица, все 10 строк):
- `yes` только при конкретном trigger из instruction artifact; в lab допустим
`yes` с названной практикой («lab: data mapping practice»).
- `no` с конкретной причиной. Пустой документ с «N/A» вместо `no` запрещён.
- Profile меняется только решением Change Owner, записанным строкой Gate Log
со scope `change-profile`; уже утверждённые gates при этом инвалидируются.
- Выбранный условный input пишется до artifact, который его потребляет
(research, ux-design, system-analysis и modules, security-analysis — до design,
qa-analysis и tasks; test-cases, docs-impact — до tasks).
Deterministic checks — `check.cjs` (из `specs/`):
`node openspec/schemas/wealth-ledger-sdd-lab/check.cjs <plan|apply|archive> <change>`.
Он единственный владелец правил profile/порядка/gates/readiness; не дублируй их
проверку вручную, а читай его вывод. `plan` — после каждого artifact; `apply` —
перед apply; `archive` — перед native archive. Exit 1 = нарушение artifact,
exit 2 = ошибка вызова/окружения (не исправляй artifact из-за неё).
Next artifact:
- `openspec status` показывает невыбранные условные artifacts как `ready`, а
`isPlanningComplete` остаётся `false`, пока они не созданы. Это ограничение CLI:
не создавай artifact только потому, что он `ready`.
- Следующий artifact = `note: next artifact` из `check.cjs plan`.
Decision Gates (Gate Log, append-only):
| Gate | Scope | Когда |
| --- | --- | --- |
| G1 Product Decision | proposal, change-profile | всегда |
| G2 Requirements Baseline | specs (+ system-analysis и модули для complex) | lab или complex |
| G3 Solution Approval | всё остальное planning, включая tasks | всегда |
| G4 Delivery Acceptance | implementation, review, QA, parity, docs | перед archive |
- Без G2 (delivery standard) specs входят в scope G3.
- Complex: system-analysis и выбранные модули входят в G2, чтобы системные
обязательства были согласованы до design (contract-first).
- Решение gate явно разрешает draft следующего пакета artifacts. Approval
перечисляет каждый artifact id в Scope и дословно цитирует слова Change Owner
из текущей сессии или комментария Issue (укажи ссылку). Никогда не записывай
решение, которого владелец не давал; существование файла или строки Gate Log
не является approval — `check.cjs` проверяет только форму записи.
- Каждый gate различает: Artifact Validation (CLI + `check.cjs`), Independent
Review (findings) и Decision Approval (владелец). Validation и Findings
заполняются до запроса решения.
- Семантическое изменение утверждённого artifact: строка `invalidated: <reason>`
для gate, покрывающего самый ранний затронутый artifact (это снимает и все
последующие gates), затем повторное approval по порядку. Механические правки
(опечатка, формат) не инвалидируют.
- Release Approval находится вне OpenSpec (release-it, Gitea); G4 его не заменяет.
Independent Review (lab — на каждом gate; delivery — минимум G4):
- G1: product-analyst проверяет проблему/scope на противоречия и скрытые решения.
- G2: qa-engineer — testability; independent-reviewer — противоречия с canonical
specs и пропущенные error/boundary cases.
- G3: independent-reviewer — согласованность specs ↔ system-analysis ↔ design ↔
qa-analysis ↔ tasks; security-reviewer, если выбран security-analysis.
- G4: independent-reviewer — код против approved artifacts; qa-engineer — system
verification по qa-analysis.
- Reviewer не исправляет artifact; findings маршрутизируются по Feedback Routing.
Delegation to subagents:
- Сабагент — самостоятельный профессионал: не знает OpenSpec, gates и других
сабагентов. Основной агент выбирает роль, пишет Task Brief, получает результат,
размещает его в artifact, проверяет согласованность и выносит вопросы человеку.
- Task Brief: профессиональная цель («спроектируй межсистемные взаимодействия
для …», не «создай artifact system-analysis»); точные входные файлы/разделы;
ограничения из MUST NOT Include artifact, сформулированные профессионально;
ожидаемые разделы ответа; правило «неизвестное — вернуть вопросом, не
выдумывать».
- Сабагент не принимает решений за другие роли; конфликт с upstream возвращается
как finding.
Lab Journal (lab): строка после каждой стадии — уникальные находки (что нашла
только эта стадия), решения, риски, проблемы передачи контекста, skills, стоимость
(вызовы, токены/время), что переносимо на крупные проекты. Не пиши ожидаемые
результаты заранее.
requires:
- proposal
- id: specs
generates: "specs/**/*.md"
description: Normative black-box behavioral deltas, business rules and invariants
template: spec.md
instruction: |
Owner: product-analyst (requirements engineering). Gate: G2 (lab/complex) или G3.
Reviewers: qa-engineer (testability), independent-reviewer (consistency).
Purpose:
Delta Specs — нормативный black-box contract: что система обязана делать для
пользователя или внешнего наблюдателя. После archive становятся canonical specs.
Preconditions:
- G1 approved; используются только capabilities из Proposal.
- Для existing capability прочитай canonical spec целиком.
- `skip_specs: true` — spec files не создаются.
Critical Rules:
- Один Requirement = одно устойчивое observable behavior/invariant.
- BCP 14 по policy `config.yaml`: uppercase SHALL/MUST только для обязательств.
- Каждый Requirement имеет ≥1 `#### Scenario:` с конкретными WHEN/THEN.
- Бизнес-правила, инварианты (например, сохранение суммы, однократный учёт денег,
знак и точка округления) и граничные случаи — явные Requirements/Scenarios.
- Допущение не оформляется как факт: неизвестное правило → вопрос в Proposal
Open Decisions, а не выдуманный Requirement.
- Не дели Requirements по repositories; system names/transport/endpoints —
System Analysis, если сама их identity не является внешним обязательством.
Delta Operations:
- `ADDED`; `MODIFIED` — ПОЛНЫЙ existing block со всеми scenarios, heading точно;
`REMOVED` — **Reason** и **Migration**; `RENAMED` — `FROM:`/`TO:`.
- Новая capability: `## Purpose` 1–2 предложения (≥50 символов).
- Не оставляй пустые operation sections.
Reference Format (используется всеми downstream artifacts и `check.cjs`):
`` `capability-path` — `Requirement heading` `` — точный heading.
MUST NOT Include: implementation details, поведение вне Proposal, «корректно»/
«быстро»/«удобно» без критерия, противоречия canonical specs.
Feedback Routing: intent/scope/capabilities → Proposal; systems/contracts/
mappings → позже System Analysis; interaction details → UX Design.
Validation: `openspec validate <change> --strict --store wealth-ledger-specs`.
requires:
- process-record
- id: ux-design
generates: ux-design.md
description: Optional interaction design of approved behavior; part of the Design package
template: ux-design.md
instruction: |
Owner: ux-designer. Conditional: только если Change Profile выбрал `ux-design`.
Gate: G3 вместе с design (единый Design package, отдельного UX approval нет).
Trigger: новые экраны, существенное изменение пользовательского сценария,
редизайн, сложные формы, многосостоянийный интерфейс. Иначе UI описывается в
разделе UI Implementation `design.md` (reuse или N/A с причиной).
Authority:
- ux-design.md — UX-часть единого Design package (не отдельный planning
authority и не отдельный approval): внутри пакета он один владеет flows, IA,
состояниями, feedback, responsive и a11y. design.md ссылается на него и
фиксирует только инженерную реализацию UI.
- Observable behavior остаётся в Specs; UX не вводит новое behavior. Состояние или
ошибка, которых нет в Specs, — finding → Specs.
Critical Rules:
- Ссылайся на Requirements, не пересказывай их.
- UI States покрывает loading/empty/error/success/partial только там, где они
реально возможны.
- Visual materials следуют правилу Visual Package из instruction `design`.
- Используй существующую design system (`docs/design/`, `frontend` UI kit) прежде
нового паттерна; отклонение обоснуй.
MUST NOT Include: серверная архитектура, contracts, component tree, state
management, data fetching.
Feedback Routing: behavior → Specs; реализуемость → Design; данные/ошибки
интеграции → System Analysis.
requires:
- specs
- id: system-analysis
generates: system-analysis/system-analysis.md
description: Optional hub of cross-system context, boundaries, flows, data ownership and contracts
template: system-analysis.md
instruction: |
Owner: system-analyst. Conditional: только если Change Profile выбрал
`system-analysis`. Gate: G2 для complex, иначе G3.
Trigger: новый/изменённый межсистемный contract (HTTP API, внешняя интеграция,
event), изменение data ownership, несколько систем в одном flow, сложная
миграция/coexistence. Изменение внутри одного repo без contract — не trigger.
Structure (hub + modules; каждый module — отдельный выбор Change Profile):
- `system-analysis/system-analysis.md` — context, boundaries, As-Is/To-Be, impact,
responsibilities, data ownership, flows overview, contract semantics, error
semantics на границах, compatibility, ссылки на modules.
- `system-analysis/contracts/` — machine-readable target contracts
(OpenAPI/AsyncAPI/JSON Schema/Proto). Не отдельный artifact: принадлежит hub.
- `system-analysis/diagrams/*.mmd` — Mermaid sequence/state/data-flow, только
если помогают понять поведение. Каждый файл упоминается в hub или module.
- Modules: `data-mapping.md`, `failure-model.md`, `nfr.md`, `observability.md`.
- Расширения (audit, fraud, feature toggles, CMS, внешние системы) добавляются
новым module только при реальном requirement; schema их не предопределяет.
Contract Authority (сохранено из v2):
- Новый/изменённый contract определяется в `contracts/` до implementation.
- Backend-generated OpenAPI остаётся executable/runtime source после реализации
и MUST быть semantic-compatible с approved target contract до G4.
- Не создавай второй вечный canonical OpenAPI в `specs/`; target contract остаётся
в archived change. Mismatch generated ↔ target возвращает change сюда.
- Для existing HTTP API прочитай текущий backend-generated OpenAPI
(`frontend/src/shared/api/spec/openapi.json` — опубликованный snapshot).
- Каждый contract помечает compatibility: additive | breaking (+ migration).
Critical Rules:
- Не вводи observable behavior молча: gap → Specs и повторный G2/G3.
- Hub не копирует содержимое modules; module не копирует hub.
- Нет internal classes/modules/DB tables/developer tests.
Feedback Routing: behavior → Specs; internal implementation → Design;
verification → QA Analysis.
requires:
- specs
- id: data-mapping
generates: system-analysis/data-mapping.md
description: Optional field-level mapping, transformations and conservation invariants between systems
template: data-mapping.md
instruction: |
Owner: system-analyst; reviewer — database-specialist при изменении хранения.
Conditional: только если выбран `data-mapping` (требует `system-analysis`).
Trigger: преобразование данных между системами или моделями (внешний API →
domain, domain → API, агрегаты), денежные суммы, валюты, знаки, агрегаты,
классификация записей по группам.
Critical Rules:
- Каждое target-поле: source, transformation, unit/currency/scale, nullability,
default, validation, поведение при ошибке.
- Invariants: правила сохранения (сумма частей = итог, каждая денежная единица
учитывается ровно в одном агрегате), знаковые соглашения, точка округления —
со ссылкой на защищаемый Requirement. Нет Requirement — finding → Specs.
- Double-counting: каждый тип исходной записи, который может попасть в два
агрегата, и правило, относящее его ровно к одному.
- Большой mapping — machine-checkable файл в `contracts/` со ссылкой отсюда.
MUST NOT Include: ORM/SQL/код mappers, retry/idempotency (failure-model).
Feedback Routing: инвариант без Requirement → Specs; ownership → hub;
реализация → Design.
requires:
- system-analysis
- id: failure-model
generates: system-analysis/failure-model.md
description: Optional boundary failure semantics - timeouts, retries, idempotency, partial failure and recovery
template: failure-model.md
instruction: |
Owner: system-analyst; reviewer — reliability-specialist.
Conditional: только если выбран `failure-model` (требует `system-analysis`).
Trigger: внешняя интеграция, фоновая синхронизация, повторяемые операции,
многошаговая запись, возможны дубликаты/out-of-order/частичный отказ.
Critical Rules:
- Каждый failure scenario: граница, отказ, обнаружение, требуемое поведение
системы, observable результат, Requirement.
- Idempotency: ключ, scope, срок хранения, результат повторного запроса/события.
- Partial failure: что остаётся согласованным, compensation или recovery.
- Обязательства формулируются на границе систем; механизм (lock, queue,
transaction) — Design.
MUST NOT Include: численные SLO (nfr), выбор библиотек, код.
Feedback Routing: новое user-visible поведение при отказе → Specs; механизм →
Design; проверка → QA Analysis.
requires:
- system-analysis
- id: nfr
generates: system-analysis/nfr.md
description: Optional measurable non-functional requirements with thresholds and evidence
template: nfr.md
instruction: |
Owner: system-analyst; reviewer — reliability-specialist.
Conditional: только если выбран `nfr` (требует `system-analysis`).
Trigger: значимые performance/availability/capacity/retention/security
constraints, которые меняют design или verification.
Critical Rules:
- Каждый NFR: id, threshold, способ измерения, rationale/источник.
- Без threshold — раздел Constraints Without Thresholds с владельцем решения;
не выдумывай числа.
- Security-NFR живут здесь; security-analysis ссылается на них по id.
MUST NOT Include: механизмы достижения (Design), failure semantics
(failure-model), события наблюдаемости (observability).
requires:
- system-analysis
- id: observability
generates: system-analysis/observability.md
description: Optional semantic observability obligations - events, signals, correlation, SLI and alerts
template: observability.md
instruction: |
Owner: system-analyst; reviewer — reliability-specialist.
Conditional: только если выбран `observability` (требует `system-analysis`).
Trigger: фоновые/асинхронные процессы, финансовые расчёты или синхронизация,
где оператор должен отличать успех, частичный успех и отказ; новые SLI.
Critical Rules:
- Semantic events с trigger и обязательными атрибутами (например,
`code=1 → NO_MONEY`); correlation через границы систем.
- Diagnostic scenarios: «когда X, оператор определяет Y по Z».
- SLI/alerts — только с определением и целевым значением; цель из nfr — ссылкой.
MUST NOT Include: SDK/exporter/logger/инфраструктура (Design), секреты и
персональные данные в атрибутах.
requires:
- system-analysis
- id: security-analysis
generates: security-analysis.md
description: Optional change-scoped threat model with mitigations and residual risks
template: security-analysis.md
instruction: |
Owner: security-reviewer. Conditional: только если выбран `security-analysis`.
Gate: G3; validation requirements проверяются на G4.
Trigger: authentication/authorization, токены и секреты, новая trust boundary,
sensitive/financial personal data, внешняя интеграция, изменение модели доступа.
Не создавай для изменений без этих признаков.
Inputs: Specs; system-analysis (hub, nfr) если выбран; существующие ADR и
`docs/architecture/cross-cutting/`.
Critical Rules:
- Threat model только этого change (STRIDE как способ поиска, не как чек-лист).
- Каждая mitigation назначена artifact-владельцу: observable требование → Specs,
межсистемное обязательство → System Analysis, механизм → Design.
- Security-NFR не дублируй: ссылайся на id из `nfr.md`.
- Residual risks явно принимает Change Owner на G3.
MUST NOT Include: универсальный security checklist, эксплуатация внешних систем,
секреты.
requires:
- specs
- id: design
generates: design.md
description: Engineering design of how affected repositories implement approved behavior and system obligations
template: design.md
instruction: |
Owner: solution-architect; reviewers — affected frontend/backend engineers,
database-specialist при миграциях. Gate: G3.
Purpose:
Как affected repositories реализуют свою сторону approved Specs и выбранных
system-analysis/ux-design/security-analysis. Не повторный system analysis.
Preconditions:
- Все выбранные research, ux-design, system-analysis и modules,
security-analysis уже существуют (`check.cjs plan` без errors). Complex:
system-analysis утверждён на G2.
- Upstream contracts/sequences/mappings — входные ограничения.
Critical Rules:
- Не добавляй observable behavior; не меняй system contracts. Нереализуемый
contract → System Analysis, а не private contract здесь.
- Каждое существенное решение ссылается на driver: Requirement, NFR id, failure
id, threat id или constraint. Новая технология/компонент без driver запрещены.
- UI: при ux-design.md — только инженерная реализация (composition, state, data
fetching, производство каждого UI state); без ux-design.md — раздел UI
Implementation даёт new/changed/reuse/N/A с причиной и нужные экраны/состояния.
Неизменяемый UI и N/A не требуют макетов.
- Visual Package (единые правила для ux-design.md и UI Implementation): версия
пакета; для reuse — immutable locator (store ID + path от OpenSpec root),
применимая часть и отличия; локальные PNG/SVG под `visuals/` с relative path,
purpose и SHA-256; SVG самодостаточен (без scripts, handlers, внешних
fonts/assets); mutable внешняя ссылка не является утверждённой версией.
ux-design.md и design.md — один Design package с одним approval на G3.
- Solution Assessment обязателен для нового auth, security, storage, external
integration, observability, caching, concurrency, shared tooling или иного
дорогого необратимого решения; иначе N/A + reason.
- Rollback contract обязателен для security, destructive migration, public
contract и труднообратимых решений.
- Developer verification определяют implementation owners; QA matrix не копируй.
- ADR Candidates: только долгоживущие архитектурные решения с alternatives.
MUST NOT Include: approved sequences/contracts/mappings (ссылки, не копии);
UX flows/states при наличии ux-design.md; QA coverage; docs plan; tasks/PR plan;
длинный production code.
Feedback Routing: intent → Proposal; behavior → Specs; interaction → UX Design;
contracts/mappings/failure semantics/semantic observability → System Analysis;
threats → Security Analysis.
requires:
- specs
- id: qa-analysis
generates: qa-analysis.md
description: Risk-based system verification strategy independent from developer-owned tests
template: qa-analysis.md
instruction: |
Owner: qa-engineer. Gate: G3. Может выполняться параллельно с design.
Purpose:
Что нужно проверить на уровне системы, чтобы доказать соответствие Specs и
выбранным system-analysis modules, и с какой глубиной — по риску.
Depth (условные секции внутри обязательного artifact):
- Всегда: Risk Assessment, Coverage Matrix (строка на каждый delta Requirement),
Gaps / Blockers.
- Delivery standard: остальные разделы только при наличии содержания; удаляй
пустые разделы, не пиши N/A.
- Lab или complex: все разделы; Cross-system Verification обязателен при
выбранном system-analysis, failure-model или data-mapping (каждый failure id
и invariant имеет проверку или обоснованный risk-based отказ).
Critical Rules:
- QA проверяет system behavior, не устройство кода; не предписывай unit/component
tests, mocks, coverage percentage.
- Не создавай behavior: неопределённый expected result → gap в Specs;
неопределённый flow/error/contract → gap в System Analysis.
- Ссылайся на Scenarios, не переписывай их. Отдельные test cases — только через
artifact `test-cases`.
- Планы проверок отделены от результатов: результаты фиксируются на G4, не здесь.
- Automation: black-box API/E2E кандидаты; QA repo пока нет — `deferred until QA
repo exists`.
Feedback Routing: behavior → Specs; contracts/failures → System Analysis;
testability hooks → Design (как need, без проектирования).
requires:
- specs
- id: test-cases
generates: "test-cases/*.md"
description: Optional explicit test design for high-risk scenarios that need concrete data, steps or oracles
template: test-cases.md
instruction: |
Owner: qa-engineer. Conditional: только если выбран `test-cases`. Gate: G3.
Trigger: финансовые расчёты, сложные бизнес-сценарии, значимые интеграции,
много ошибок и граничных случаев, критичное пользовательское поведение — когда
проверка требует конкретных данных, шагов или oracle, которых нет в Scenario.
Critical Rules:
- Один файл на область: `test-cases/<area>.md`; id `TC-<AREA>-<NNN>` уникален.
- Каждый case: Verifies (reference format), Risk/Priority, Preconditions, Test
Data, Steps, Expected Results, Automation Candidate.
- Case добавляет информацию сверх Scenario (числа, комбинации, порядок событий,
расчётный oracle). Если не добавляет — не пиши case; coverage matrix ссылается
на Scenario.
- Не копируй acceptance criteria дословно; expected result выводится из
Requirement, а не придумывается. Неопределённость → gap в Specs.
- qa-analysis Coverage Matrix ссылается на TC ids.
requires:
- qa-analysis
- id: docs-impact
generates: docs-impact.md
description: Optional promotion plan from change knowledge to canonical AS-IS documentation
template: docs-impact.md
instruction: |
Owner: documentation-engineer. Conditional: только если выбран `docs-impact`.
Gate: G3.
Trigger: несколько docs targets, новый ADR, перенос diagrams, изменение
architecture/integration pages, lab-практика Documentation Impact Analysis.
Иначе — раздел `## Docs Impact` в tasks.md (targets или конкретная no-change
причина).
Canonical Documentation Model: `docs/` — AS-IS (`overview.md`, `glossary.md`,
`architecture/`, `architecture/cross-cutting/`, `domains/`, `integrations/`,
`adr/`, `design/`). Accepted ADR не переписывается.
Critical Rules:
- Promote только знание, полезное после завершения change; без migration diary.
- Не копируй machine-readable contract в docs как второй source of truth.
- Каждая entry: Action, exact Target (существующий path проверен), Category,
Resulting AS-IS, Source, Verification.
- Diagram promotion по умолчанию — нет.
- docs-impact не редактирует `docs/`; Tasks создаёт `[docs]` items.
requires:
- design
- id: tasks
generates: tasks.md
description: Dependency-ordered, repository-owned delivery checklist with verification, review and parity tasks
template: tasks.md
instruction: |
Owner: основной агент (delivery coordination); reviewers — affected engineers.
Gate: G3.
Preconditions:
- design и qa-analysis существуют; все выбранные условные artifacts существуют
(`check.cjs plan` без errors).
- Для complex/lab: G2 approved до записи tasks.
Task Format:
`- [ ] X.Y [main|frontend|backend|docs|specs] <Kind>: <action + outcome>`
с полями **Covers**, **Source**, **Verification**, **Depends on**.
Kinds: Implementation, Migration, Automated Testing, Contract Parity, Docs,
Independent Review, QA Verification, Verification.
Required Tasks (только релевантные; `check.cjs` проверяет отмеченные *):
- Implementation/Automated Testing per repository по Design.
- Migration: при изменении хранения — backup/rollback и проверка на копии данных.
- * Contract Parity: при наличии `system-analysis/contracts/` — сравнение
backend-generated OpenAPI/runtime с target contract.
- Docs: из docs-impact или раздела `## Docs Impact`.
- * Independent Review: read-only review реализации против approved artifacts.
- QA Verification: system checks по qa-analysis (и test-cases).
- Security: проверки из security-analysis Validation Requirements, если выбран.
- Native sync/archive не является task; canonical specs не правятся task'ом.
Critical Rules:
- Одна task = один repository + один independently reviewable outcome.
- * Каждый ADDED/MODIFIED Requirement появляется в Covers хотя бы одной task.
- Covers использует reference format; неизвестный heading — ошибка `check.cjs`.
- `[qa]` запрещён, пока QA repo не существует.
- Не создавай ceremonial tasks; нерелевантная Kind не нужна.
- Descope после G3: удали task из tasks.md и запиши в process-record
`Descoped Work` с причиной и gate решения владельца.
MUST NOT Include: новое behavior/contract/design/QA/docs decision (→ upstream
artifact), commits/branches, будущие результаты.
Feedback Routing: semantic change → самый ранний затронутый artifact,
инвалидируя его gate и последующие.
requires:
- design
- qa-analysis
apply:
requires:
- tasks
tracks: tasks.md
instruction: |
Apply реализует approved plan и не переоткрывает upstream decisions без feedback cycle.
Preconditions (CLI проверяет только наличие tasks.md, остальное — `check.cjs`):
- `check.cjs apply <change>` = OK (G1, G2 для lab/complex, G3; каждый
существующий artifact утверждён на своём gate; выбранные artifacts существуют).
- Перед зависимой UI-работой проверь наличие и SHA-256 материалов из таблицы
Materials (ux-design.md или UI Implementation design.md).
- Store `wealth-ledger-specs`; protocol — instruction `process-record`.
Context Loading: `contextFiles` из apply instructions, затем `AGENTS.md` только
affected repositories, затем relevant upstream sections для каждой task.
Execution:
- Pending tasks по dependency order; один repository на task; delegate по роли
(frontend-engineer, backend-engineer, database-specialist) с Task Brief.
- Checkbox отмечается только после observed success Verification.
- Contract Parity: backend-generated OpenAPI semantic-compatible с target contract.
- Independent Review выполняет independent-reviewer (read-only) по полному diff
против approved artifacts; Major findings исправляются или возвращаются в gate.
- QA Verification выполняет qa-engineer по qa-analysis; результаты — в Gate Log G4.
- Lab: строка Lab Journal после implementation, review и QA.
Before archive (G4 Delivery Acceptance):
- Все tasks `[x]`; descoped — только через `Descoped Work`.
- Validation G4: developer checks, parity, review verdict, QA results, docs.
- Lab: Retrospective заполнен.
- `check.cjs archive <change>` = OK (включает все apply-проверки), затем `openspec archive <change>
--store wealth-ledger-specs`. Никогда `--no-validate`. `--yes` подавляет и
подтверждение spec updates, и предупреждение о незакрытых tasks, поэтому
допустим только сразу после успешного `check.cjs archive` (в non-TTY агентской
среде без него archive не выполняется).
Sync / Archive Semantics: native archive обновляет canonical specs;
system-analysis, contracts и process-record остаются в archived change; `docs/`
меняются только `[docs]` tasks; значимые решения — immutable ADR.
Feedback Routing:
- intent/scope → Proposal; behavior/invariants → Specs; interaction → UX Design;
systems/contracts/mappings/failures/NFR/semantic observability → System Analysis
и modules; threats → Security Analysis; internal implementation → Design;
verification → QA Analysis/Test Cases; docs → Docs Impact; decomposition → Tasks.
- При semantic conflict останови downstream work, сообщи evidence, инвалидируй
gates от самого раннего затронутого artifact.
@@ -0,0 +1,29 @@
# Data Mapping
<!-- guide: Field-level transformation between systems. Large mappings go to a
machine-checkable file in contracts/ and are referenced here. Remove guides. -->
## Source and Target
| Mapping | Source (owner) | Target (owner) | Direction | Trigger |
| --- | --- | --- | --- | --- |
## Field Mapping
| Target field | Source field(s) | Transformation | Unit / currency / scale | Nullability | Default | Validation | On failure |
| --- | --- | --- | --- | --- | --- | --- | --- |
## Invariants
<!-- guide: Conservation rules that must hold after mapping, e.g. "total = sum of
positions + cash; each money unit counted once", sign conventions, rounding
point. Each invariant references the spec requirement it protects. -->
## Double-counting and Classification Risks
<!-- guide: Every source record type that could fall into two target aggregates,
and the rule that assigns it to exactly one. -->
## Transformation Errors
## Open Questions
@@ -0,0 +1,58 @@
# Engineering Design
<!-- guide: How affected repositories implement approved specs and system
obligations. Reference upstream ids; do not restate them. Remove guides. -->
## Inputs
<!-- guide: Exact upstream artifacts this design implements (specs, selected
system-analysis modules, ux-design, security-analysis). -->
## Architecture Drivers
<!-- guide: Requirements, NFR ids, constraints and risks that force decisions. -->
## Technical Goals / Non-Goals
## Implementation Impact Map
| Area | Affected | Outcome or N/A reason |
| --- | --- | --- |
| main | | |
| frontend | | |
| backend | | |
## Decisions
<!-- guide: Per decision: context, chosen option, rejected alternatives, driver it
satisfies. Group by repository when that helps. -->
## Components and Responsibilities
## Data / Storage Changes
## Contract Implementation
<!-- guide: How each contract is produced/consumed and how published OpenAPI is
compared with the target contract. N/A when no contracts changed. -->
## Cross-cutting Implementation
## UI Implementation
<!-- guide: With ux-design.md: component composition, state, data fetching, how
each UI state is produced — no restated UX. Without ux-design.md: state
new/changed, reuse or N/A with reason, plus the needed screen/state detail. -->
## Developer Verification Strategy
## Migration / Rollout / Rollback
## Solution Assessment
<!-- guide: Required for new auth, security, storage, integration, observability,
caching, concurrency or shared tooling; otherwise N/A with reason. -->
## Risks / Trade-offs
## ADR Candidates
@@ -0,0 +1,18 @@
# Documentation Impact
<!-- guide: Plan only; docs/ is edited by [docs] tasks. Remove guides. -->
## Summary
## Documentation Changes
| Action | Target | Category | Resulting AS-IS | Source | Verification |
| --- | --- | --- | --- | --- | --- |
## Diagram Promotion
## ADR Decision
## Glossary / Navigation Impact
## No-Change Rationale
@@ -0,0 +1,26 @@
# Failure Model
<!-- guide: Boundary-level failure semantics. Implementation of mechanisms (queues,
locks, libraries) belongs to design.md. Remove guides before G3. -->
## Failure Scenarios
| ID | Boundary / step | Failure | Detection | Required system behavior | Observable result | Requirement |
| --- | --- | --- | --- | --- | --- | --- |
## Timeouts and Retries
## Idempotency and Duplicate Handling
<!-- guide: Idempotency key, scope, retention; what a duplicate request/event
yields. -->
## Ordering and Concurrency
## Partial Failure and Compensation
## Recovery Behavior
## Compatibility During Failure
## Open Questions
@@ -0,0 +1,14 @@
# Non-Functional Requirements
<!-- guide: Only measurable constraints that change design or verification.
Each row needs a threshold and a way to measure it. Remove guides. -->
| ID | Category | Requirement | Threshold | Measurement / evidence | Rationale / source |
| --- | --- | --- | --- | --- | --- |
## Constraints Without Thresholds
<!-- guide: Constraints that are real but not measurable yet; state why and who
decides the threshold. -->
## Open Questions
@@ -0,0 +1,24 @@
# Observability
<!-- guide: What must be observable and why; not which SDK, exporter or logger
(design.md). Remove guides before G3. -->
## Semantic Events
| Event | Trigger | Required attributes | Consumer / purpose |
| --- | --- | --- | --- |
## Logs, Metrics and Traces
## Correlation
## SLI and Alert Conditions
| SLI | Definition | Target | Alert condition |
| --- | --- | --- | --- |
## Diagnostic Scenarios
<!-- guide: "When X happens, an operator can determine Y from Z." -->
## Open Questions
@@ -0,0 +1,57 @@
# Process Record
**Mode:** <!-- guide: lab | delivery -->
**Class:** <!-- guide: standard | complex -->
**Issue:** <!-- guide: Gitea Issue link -->
## Change Profile
<!-- guide: One row per conditional artifact, all ten rows required. Selected =
yes|no. Reason cites the concrete trigger from the instruction table or why
it does not apply; "lab practice" is a valid reason only in lab mode and
must name the practice. -->
| Artifact | Selected | Reason |
| --- | --- | --- |
| research | | |
| ux-design | | |
| system-analysis | | |
| data-mapping | | |
| failure-model | | |
| nfr | | |
| observability | | |
| security-analysis | | |
| test-cases | | |
| docs-impact | | |
## Gate Log
<!-- guide: Append-only, chronological. Scope = artifact ids covered by the
decision. Validation = commands run and their results, review verdicts.
Decision = `approved "<quoted owner words>"`, `rejected`, `changes requested`
or `invalidated: <reason>`. Never write a decision the owner did not give. -->
| Gate | Date | Scope | Validation | Findings | Decision |
| --- | --- | --- | --- | --- | --- |
## Descoped Work
<!-- guide: Tasks removed from tasks.md after G3. Reason and approving gate are
required. Leave the header row only when nothing was descoped. -->
| Task | Reason | Gate |
| --- | --- | --- |
## Lab Journal
<!-- guide: lab mode: one row per completed stage, written right after it.
Unique findings = what this stage found that no other stage did.
Cost = subagent calls, approximate tokens or time. delivery mode: optional. -->
| Stage | Role / subagent | Unique findings | Decisions | Risks found / excluded | Handoff issues | Skills | Cost | Transferable |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
## Retrospective
<!-- guide: Written after G4. Which stages paid off, which were redundant for this
class of change, what to change in the schema. Facts first, then opinion. -->
@@ -0,0 +1,36 @@
# Proposal
<!-- guide: Language of the Change Owner. No files, APIs, tables, libraries,
diagrams or test plans. Remove every guide comment before G1. -->
## Why
## What Changes
## Scope
### In Scope
### Out of Scope / Non-Goals
## Capabilities
### New Capabilities
### Modified Capabilities
## Impact
## Success Criteria
## Evidence
<!-- guide: Facts that justify the change, each with a source: Issue, research
(link or `research.md`), incident, measurement. Mark hypotheses as such. -->
## Assumptions / Dependencies
## Open Decisions
<!-- guide: Decisions not yet taken that may change scope. Each item: question,
who decides, what blocks until it is answered. Empty only if none exist. -->
@@ -0,0 +1,34 @@
# QA Analysis
<!-- guide: Risk-based system verification strategy. Reference scenarios; do not
restate them. No developer unit/component test plans. Remove guides. -->
## Risk Assessment
| Risk | Affected requirements | Likelihood | Impact | Verification depth |
| --- | --- | --- | --- | --- |
## Coverage Matrix
<!-- guide: One row per delta requirement. Check = system-level way of proving
it; test case ids when test-cases is selected; or "not verified: <risk
reason>". -->
| Requirement | Check | Level | Test cases |
| --- | --- | --- | --- |
## Negative and Boundary Focus
## Cross-system Verification
## Test Data and Environment Needs
## Manual / Exploratory Scope
## Automation Candidates
## Regression Impact
## Gaps / Blockers
<!-- guide: Each gap names its routing destination artifact. -->
@@ -0,0 +1,28 @@
# Product Research
<!-- guide: Input context, not a requirement source. Every claim is marked
[F] fact with source, [I] interpretation or [H] hypothesis. Remove guides. -->
## Problem Statement
## Target Audience
## Research Questions
## Existing Solutions and Alternatives
| Option | What it does | Strengths | Weaknesses | Source |
| --- | --- | --- | --- | --- |
## Evidence & Sources
## Assumptions
## Risks
## Conclusions
## Recommendation
<!-- guide: build / do not build / narrow scope / research more, with reasons.
"Do not build" is a valid outcome. -->
@@ -0,0 +1,26 @@
# Security Analysis
<!-- guide: Threat model of this change only. Reference security NFR rows by id;
do not restate them. No generic checklist. Remove guides before G3. -->
## Scope
## Assets
## Trust Boundaries
## Threats
| ID | Threat (STRIDE) | Attack scenario | Asset / boundary | Existing control | Likelihood / impact |
| --- | --- | --- | --- | --- | --- |
## Recommended Mitigations
| Threat | Mitigation | Owner artifact (specs / system-analysis / design) |
| --- | --- | --- |
## Residual Risks
## Validation Requirements
<!-- guide: Evidence needed before G4: review focus, negative tests, tooling. -->
@@ -0,0 +1,18 @@
<!--
Choose only the sections this delta needs; delete this guide before approval.
New capability only: add `## Purpose` with 1–2 sentences (50+ characters).
Existing capability: do not add Purpose.
Supported operations:
- `## ADDED Requirements`: new complete Requirement blocks.
- `## MODIFIED Requirements`: full existing Requirement blocks with every
surviving Scenario, then edited behavior; heading matches exactly.
- `## REMOVED Requirements`: Requirement heading plus **Reason** and **Migration**.
- `## RENAMED Requirements`: `FROM:` and `TO:` heading pairs only.
Every normative Requirement uses BCP 14 (RFC 2119 + RFC 8174) uppercase
SHALL/MUST deliberately, and at least one `#### Scenario:` with `- **WHEN**`
and `- **THEN**`. Lowercase must/should/may are ordinary language.
Do not leave empty operation sections or a sample Requirement.
-->
@@ -0,0 +1,50 @@
# System Analysis
<!-- guide: Hub document. Owns context, boundaries, flows overview and contract
semantics; details live in selected modules and are referenced, not copied.
Remove guides before G3. -->
## Requirement Coverage
| Requirement | Flow / contract | Module |
| --- | --- | --- |
## System Context and Boundaries
## As-Is / To-Be
## Impact Analysis
## Systems and Responsibilities
## Data Ownership
| Data | Source of truth | Writers | Readers |
| --- | --- | --- | --- |
## Interaction Flows
<!-- guide: Reference diagrams/*.mmd; text only for what a diagram cannot carry. -->
## Contracts
<!-- guide: Per contract: producer/consumer, operation/event, version, file in
contracts/, compatibility class (additive / breaking), published source it
must match after implementation. -->
| Contract | Producer → Consumer | File | Compatibility | Published source |
| --- | --- | --- | --- | --- |
## Error Semantics
<!-- guide: Business vs technical failures at boundaries. Detailed retry/
idempotency/recovery belongs to failure-model.md when selected. -->
## Compatibility / Migration
## Detailed Modules
<!-- guide: Link each selected module (data-mapping, failure-model, nfr,
observability) with one line on what it decides. -->
## Open Questions
@@ -0,0 +1,18 @@
# Tasks
<!-- guide: Task line format:
- [ ] X.Y [main|frontend|backend|docs|specs] <Kind>: <action + outcome>
- **Covers:** `capability` — `Requirement heading` pairs, or `N/A` — <reason>
- **Source:** exact upstream artifact section / contract / docs-impact entry
- **Verification:** owner-controlled command, observable check or artifact
- **Depends on:** task ids or none
Kinds: Implementation, Migration, Automated Testing, Contract Parity, Docs,
Independent Review, QA Verification, Verification. Remove guides before G3. -->
## 1. <!-- guide: delivery phase -->
## Docs Impact
<!-- guide: Only when docs-impact is not selected: list docs/ targets with
resulting AS-IS, or one concrete no-change reason. Delete when docs-impact.md
exists. -->
@@ -0,0 +1,16 @@
# Test Cases — <!-- guide: area -->
<!-- guide: One file per area under test-cases/. A case adds concrete data, steps
or an oracle that the scenario does not already state; otherwise reference the
scenario from qa-analysis instead of writing a case. Remove guides. -->
### TC-<!-- guide: AREA-NNN --> <!-- guide: title -->
- **Verifies:** `capability` — `Requirement heading` / Scenario
- **Risk / Priority:**
- **Preconditions:**
- **Test Data:**
- **Steps:**
1.
- **Expected Results:**
- **Automation Candidate:** <!-- guide: yes (level) | no (reason) -->
@@ -0,0 +1,40 @@
# UX Design
<!-- guide: Interaction design of observable behavior from Delta Specs. Reference
requirements as `capability` — `Requirement heading`; do not restate them.
No server architecture, contracts or component/state-management choices.
Remove guides before G3. -->
## User Goals
## User Flows
<!-- guide: Mermaid flowchart or numbered steps per flow, with entry and exit points. -->
## Information Architecture and Screen Structure
## Interaction Behavior
## UI States
| Screen / region | Loading | Empty | Error | Success | Partial / stale |
| --- | --- | --- | --- | --- | --- |
## Validation Feedback
## Responsive Behavior
## Accessibility
<!-- guide: Keyboard path, focus order, names/roles, contrast, non-color cues. -->
## Design System Reuse
## Materials
<!-- guide: Inline Markdown by default; local files follow the Visual Package rule. -->
| Material ID | Relative path / reference | Screen/state and purpose | SHA-256 |
| --- | --- | --- | --- |
## Open UX Questions
@@ -7,7 +7,9 @@
### Requirement: Realized income analytics is account-scoped and period-bound
Система MUST предоставлять аутентифицированному User аналитику фактически полученного дохода только для одного открытого поддерживаемого Brokerage Account, доступного через текущий T-Bank connection User. Применённый период MUST задаваться включительными календарными датами `from` и `to` в Europe/Moscow, MUST NOT начинаться после своего окончания, MUST NOT содержать будущую дату. Система MUST NOT ограничивать максимальную длительность периода. При отсутствии применённого периода система MUST использовать интервал от первого дня текущего месяца до сегодняшнего дня включительно.
Система 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: Аналитика за период по умолчанию
@@ -29,6 +31,14 @@
- **WHEN** User запрашивает аналитику Brokerage Account, который не является открытым поддерживаемым счётом, доступным через его текущий T-Bank connection
- **THEN** система возвращает существующий ответ о недоступности счёта и не раскрывает его доход или операции
#### Scenario: Общий доход требует полноты всех счетов
- **WHEN** User запрашивает режим `all` и выбранный период одного доступного счёта ещё не загружен полностью
- **THEN** система не возвращает окончательные общие суммы, сохраняет раздельное состояние покрытия счетов и запрашивает недостающую разрешённую историю
#### Scenario: Открыт ещё не загруженный период
- **WHEN** запрошенный допустимый период ещё не имеет сохранённых операций или подтверждённого полного пустого покрытия
- **THEN** система отвечает `200` со статусом первичной загрузки или частичного покрытия, запрашивает нужные интервалы и не выдаёт нулевой окончательный доход
### Requirement: Realized income uses exact executed coupon and dividend payments
Система MUST рассчитывать фактически полученный доход только по исполненным Ledger Entries с точным upstream-типом coupon или dividend. Для каждой исходной валюты система MUST отдельно возвращать общий итог, купонный subtotal и дивидендный subtotal как точные суммы исходных broker-reported payments. Знак каждой выплаты MUST сохраняться, поэтому отрицательная корректировка MUST уменьшать соответствующий итог и оставаться доступной в составе результата. Dividend transfer, погашение principal, offer, trade, transfer, fee, tax, overnight income, иной тип операции, нереализованный результат и изменение рыночной цены MUST NOT входить в фактически полученный доход. Система MUST NOT выполнять валютную конвертацию или формировать общий итог по разным валютам.
@@ -53,30 +63,6 @@
- **WHEN** выбранный период содержит подходящие выплаты в нескольких исходных валютах
- **THEN** система возвращает отдельные итоги по каждой валюте без сконвертированного grand total
### Requirement: Period comparisons preserve calendar and currency meaning
Система MUST сопоставлять выбранный период с непосредственно предшествующим периодом. Для полного календарного месяца предыдущим периодом MUST быть предыдущий полный календарный месяц; для любого другого периода предыдущий период MUST иметь ту же включительную продолжительность и непосредственно предшествовать выбранному. Когда соответствующие календарные даты годом ранее доступны, система MUST также показывать year-over-year comparison; когда они недоступны, система MUST обозначать сравнение как недоступное, а не подменять его нулём. Каждое сравнение MUST показывать абсолютные coupon, dividend и total значения и их денежные разницы отдельно по исходным валютам и MUST NOT характеризовать их как инвестиционную эффективность.
#### Scenario: Сравнение полного календарного месяца
- **WHEN** User применяет период от первого до последнего дня календарного месяца
- **THEN** система сравнивает его с полным непосредственно предшествующим календарным месяцем
#### Scenario: Сравнение произвольного периода
- **WHEN** User применяет период, не являющийся полным календарным месяцем
- **THEN** система сравнивает его с непосредственно предшествующим включительным периодом той же продолжительности
#### Scenario: Доступно сравнение год к году
- **WHEN** для соответствующих календарных дат годом ранее доступна полная история подходящих операций
- **THEN** система показывает отдельные source-currency значения и денежные разницы относительно этого периода
#### Scenario: Годовая история недоступна
- **WHEN** полная история для соответствующих календарных дат годом ранее недоступна
- **THEN** система обозначает year-over-year comparison как недоступное и не показывает отсутствующие данные как нулевой доход
### Requirement: User explicitly applies an analytics period represented in the URL
Страница выбранного Brokerage Account «Пассивный доход» MUST позволять User редактировать черновик `from` и `to`, не изменяя видимую аналитику до явного применения. Верхний блок фильтров MUST предоставлять два подписанных календарных поля начала и окончания периода с тем же взаимодействием выбора даты, что в истории событий портфеля, и отображением дат в формате `ДД.ММ.ГГГГ`. Поля и действия применения и сброса MUST быть доступны с клавиатуры. Явное применение MUST заменить применённый период, загрузить соответствующий результат и представить применённые даты в URL страницы. Недопустимый черновик MUST показывать ошибку соответствующего поля и MUST NOT заменять последний применённый период. Сброс MUST немедленно вернуть применённый период к текущему месяцу по сегодняшний день и синхронизировать URL. Перезагрузка или открытие URL с допустимым периодом MUST восстановить тот же применённый период. Страница MUST NOT дублировать период отдельной подписью под блоком фильтров. Действие перехода на страницу MUST быть доступно из выбранного Brokerage Account, а не из глобальной навигации.
@@ -84,7 +70,7 @@
#### Scenario: Черновик периода не применён
- **WHEN** User изменяет черновик `from` или `to`, но не применяет его
- **THEN** видимые итоги, сравнения и карточки выплат остаются основанными на последнем применённом периоде
- **THEN** видимые итоги и карточки выплат остаются основанными на последнем применённом периоде
#### Scenario: Выбор дат в верхнем блоке
@@ -118,55 +104,78 @@
### Requirement: Realized analytics never presents an incomplete result as complete
Система MUST формировать выбранный результат, его comparisons, trend buckets, instrument breakdown и drill-down только после успешного получения всех необходимых cursor pages исполненных операций. Ошибка любой необходимой страницы MUST завершать расчёт восстанавливаемой ошибкой и MUST NOT возвращать частичные totals, comparisons, buckets или breakdown как полный результат. Успешный период без подходящих выплат MUST возвращать нулевой результат и отображаться с явным объяснением пустого состояния.
Система MUST формировать итог выбранного периода, его trend buckets,
instrument breakdown и drill-down только после успешного получения всех
необходимых cursor pages исполненных операций этого периода. Каждое comparison
MUST дополнительно требовать полного результата соответствующего периода
сравнения. Ошибка любой необходимой страницы MUST NOT возвращать частичные
totals, comparisons, buckets или
breakdown как полный результат. При временной недоступности клиент MUST
сохранять предыдущий полный результат той же выборки либо показывать
нейтральную недоступность соответствующего расчёта без технических сообщений
и ручного повтора. Восстановление SHALL выполняться автоматически по
`frontend-data-resilience`.
Доступные независимые части MUST оставаться видимыми; неполное сравнение MUST
обозначаться как недоступное без нулевой базы и окончательной разницы. Успешный
полный период без подходящих выплат MUST возвращать нулевой результат и
отображаться с объяснением пустого состояния. Неизвестный результат MUST NOT
подменяться таким нулевым результатом.
#### Scenario: Все страницы операций успешно получены
- **WHEN** для расчёта требуется несколько cursor pages и каждая страница успешно получена
- **THEN** система формирует результат по всем подходящим Ledger Entries со всех страниц
#### Scenario: Одна из страниц операций недоступна
- **WHEN** получение любой необходимой cursor page завершается ошибкой
- **THEN** страница показывает восстанавливаемую ошибку и не показывает частичный результат как полный
- **WHEN** получение любой необходимой cursor page завершается временной ошибкой
- **THEN** страница сохраняет предыдущий полный результат той же выборки либо показывает нейтральную недоступность расчёта без ручного повтора и не выдаёт частичный результат за полный
#### Scenario: В периоде нет купонов и дивидендов
- **WHEN** все необходимые операции успешно получены, но среди них нет подходящих coupon или dividend Ledger Entries
- **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 или другие блоки обзора.
Обзор выбранного Brokerage Account MUST показывать компактный source-currency
summary «Получено купонами и дивидендами» за период от первого дня текущего
месяца по Europe/Moscow до сегодняшнего дня включительно и действие перехода
на страницу «Пассивный доход». Summary MUST всегда использовать этот current-month
период и MUST NOT наследовать последний пользовательский период страницы аналитики.
Пустой или неуспешный расчёт MUST оставаться локальным состоянием блока и
MUST NOT заменять успешно отображённый Portfolio или другие блоки обзора.
Временная ошибка обновления MUST сохранять доступную сводку той же выборки без
предупреждения и ручного повтора. При отсутствии результата SHALL показываться
нейтральная недоступность после первичного ожидания с автоматическим восстановлением.
#### 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 показывает собственное состояние восстановления
- **WHEN** Portfolio обзора успешно загружен, но расчёт current-month summary завершается временной ошибкой
- **THEN** Portfolio и остальные блоки остаются доступными, а summary сохраняет предыдущий результат той же выборки либо показывает нейтральную недоступность без ручного повтора
### Requirement: Income payments remain traceable to exact Ledger Entries
В разделе «Выплаты» страница MUST показывать единый список карточек точных исполненных Ledger Entries применённого периода в исходном серверном порядке entries. Каждая выплата MUST иметь собственную сразу видимую карточку с описанием, датой, доступной identity Instrument и signed Money в исходной валюте. Страница MUST NOT добавлять внешние группировочные карточки или заголовки по Instrument либо валюте, subtotals инструмента или количество выплат инструмента. Список MUST сохранять отрицательные корректировки и повторяющиеся operation IDs как отдельные записи и MUST NOT включать записи вне аналитического итога. Backend MUST обеспечивать согласованность entries, instrumentKey, валюты и receipt count при формировании ответа из одного набора записей. Frontend SHALL доверять типизированному contract без второго runtime consistency validator и MUST NOT пересчитывать денежные итоги.
В разделе «Выплаты» страница MUST показывать единый список карточек точных исполненных Ledger Entries применённого периода в исходном серверном порядке entries. Каждая выплата MUST иметь собственную сразу видимую карточку с описанием, датой исполнения, доступной identity Instrument и signed Money в исходной валюте. Купоны и дивиденды MUST использовать то же представление выплаты, что и на экранах журнала, прогноза и графиков инструментов, при этом карточка MUST обозначать эту сумму именно как фактически исполненную, а не прогнозную. В режиме одного Brokerage Account страница MUST NOT добавлять внешние группировочные карточки или заголовки по Instrument либо валюте, subtotals инструмента или количество выплат инструмента. В режиме «Все счета» записи раздела «Выплаты» MUST группироваться по тем же правилам ключа, состава, порядка и итога, что и записи журнала по Requirements «All-accounts ledger groups same-day entries of one instrument», «All-accounts ledger group shows each entry and an exact total» и «All-accounts ledger grouping preserves order and limits» capability `brokerage-account-ledger`. Каждая запись при этом MUST оставаться отдельной сразу видимой карточкой внутри группы, а итог группы MUST NOT показываться, пока полнота периода не подтверждена; заголовки валютных групп и receipt count MUST NOT добавляться. Список MUST сохранять отрицательные корректировки и повторяющиеся operation IDs как отдельные записи и MUST NOT включать записи вне аналитического итога. Backend MUST обеспечивать согласованность entries, instrumentKey, валюты и receipt count при формировании ответа из одного набора записей. Frontend SHALL доверять типизированному contract без второго runtime consistency validator и MUST NOT пересчитывать аналитические денежные итоги. Единственное исключение — итог визуальной группы режима «Все счета»: это точная сумма показанных записей группы по каждой валюте, и он MUST NOT подменять аналитические итоги.
#### Scenario: Каждая выплата показана отдельной карточкой
- **WHEN** выбранный период содержит несколько включённых Ledger Entries одного или разных Instrument
- **WHEN** в режиме одного Brokerage Account выбранный период содержит несколько включённых Ledger Entries одного или разных Instrument
- **THEN** раздел «Выплаты» сразу показывает отдельную карточку каждой записи в исходном порядке без внешних группировочных блоков, раскрытия, итогов инструмента и receipt count на UI
#### Scenario: Список воспроизводит точный состав дохода
- **WHEN** User просматривает загруженный раздел «Выплаты»
- **THEN** он видит все и только включённые coupon и dividend Ledger Entries применённого периода с датой, описанием, доступной identity и исходной суммой
- **THEN** он видит все и только включённые coupon и dividend Ledger Entries применённого периода с датой, описанием, доступной identity и исходной суммой, обозначенной как исполненная выплата
#### Scenario: Отрицательная корректировка и повторяющиеся IDs сохраняются
@@ -175,12 +184,17 @@
#### Scenario: Валюта видна в сумме каждой выплаты
- **WHEN** подходящие выплаты одного Instrument представлены в нескольких исходных валютах
- **WHEN** подходящие в режиме одного Brokerage Account выплаты одного Instrument представлены в нескольких исходных валютах
- **THEN** каждая карточка показывает свою Money с исходной валютой, без отдельного заголовка валютной группы или объединённого мультивалютного итога
#### Scenario: Выплаты одной бумаги на двух счетах в режиме «Все счета»
- **WHEN** в режиме «Все счета» за один день получены купон по облигации на брокерском счёте +200 RUB и купон по той же облигации на ИИС +100 RUB
- **THEN** раздел «Выплаты» показывает их одной группой с итогом +300 RUB, внутри которой видны отдельные карточки каждого счёта, а аналитические итоги страницы не меняются
### Requirement: Analytics summary remains readable per currency
Страница MUST обозначать фактический результат как «Получено купонами и дивидендами» и для каждой исходной валюты показывать total, coupon subtotal и dividend subtotal выбранного периода. Она MUST объяснять, что tax operations не включены, не называя фактический результат gross, net, суммой до налогов, суммой после налогов или налоговым расчётом. Значения MUST оставаться доступными как читаемый текст, не только как графика, цвет или положение. Страница MUST NOT показывать блок «Динамика», его график, переключатель валюты графика или текстовый список временных buckets независимо от длительности выбранного периода. Удаление блока MUST NOT изменять итоги или сравнения периодов.
Страница MUST обозначать фактический результат как «Получено купонами и дивидендами» и для каждой исходной валюты показывать total, coupon subtotal и dividend subtotal выбранного периода. Она MUST объяснять, что tax operations не включены, не называя фактический результат gross, net, суммой до налогов, суммой после налогов или налоговым расчётом. Значения MUST оставаться доступными как читаемый текст, не только как графика, цвет или положение. Страница MUST NOT показывать блок «Динамика», его график, переключатель валюты графика или текстовый список временных buckets независимо от длительности выбранного периода. Страница MUST NOT показывать блоки «Предыдущий период» и «Годом ранее» или сообщения о недоступности сравнений.
#### Scenario: Итоги дохода доступны как текст
@@ -195,11 +209,11 @@
#### Scenario: Динамика отсутствует для периода любой длительности
- **WHEN** User открывает аналитику или применяет допустимый период любой длительности
- **THEN** страница показывает итоги и существующие сравнения, но не показывает блок «Динамика», график, его переключатель валюты или текстовый список временных buckets
- **THEN** страница показывает итоги и выплаты без динамики, сравнений периодов и состояний их доступности
### Requirement: Income page excludes upcoming forecasts
Страница «Пассивный доход» MUST NOT показывать блок «Ближайший прогноз», его суммы, список будущих выплат, состояния загрузки, пустоты или ошибки. Forecast values MUST NOT входить в realized total, comparisons, trend buckets или instrument breakdown. Доступность данных Future Cash Events MUST NOT влиять на отображение успешно загруженной realized analytics. Существующая самостоятельная страница «Будущие события» и её доступность из выбранного Brokerage Account MUST сохраняться.
Страница «Пассивный доход» MUST NOT показывать блок «Ближайший прогноз», его суммы, список будущих выплат, состояния загрузки, пустоты или ошибки. Forecast values MUST NOT входить в realized total, trend buckets или instrument breakdown. Доступность данных Future Cash Events MUST NOT влиять на отображение успешно загруженной realized analytics. Существующая самостоятельная страница «Будущие события» и её доступность из выбранного Brokerage Account MUST сохраняться.
#### Scenario: Известные будущие выплаты не появляются на странице дохода
@@ -214,9 +228,27 @@
#### Scenario: Данные будущих событий недоступны
- **WHEN** realized analytics успешно загружена, а данные Future Cash Events недоступны
- **THEN** итоги, сравнения и карточки выплат остаются доступными, а страница дохода не показывает состояние ошибки или загрузки прогноза
- **THEN** итоги и карточки выплат остаются доступными, а страница дохода не показывает состояние ошибки или загрузки прогноза
#### Scenario: Самостоятельная страница будущих событий сохраняется
- **WHEN** User переходит из выбранного Brokerage Account на существующую страницу «Будущие события»
- **THEN** эта страница остаётся доступной с прежним поведением независимо от отсутствия прогноза на странице дохода
### Requirement: Доход обновляется без технических сообщений
Страница дохода MUST сохранять применённый период, черновик фильтров и
доступный результат при фоновом обновлении. Временная ошибка SHALL
восстанавливаться автоматически без кнопок обновления и повтора, времени
получения данных и уведомлений об их загрузке. Изменение периода MUST NOT
подписывать прежний результат новыми датами. Ошибки пользовательского периода
MUST оставаться у полей и MUST NOT запускать автоматический повтор ошибочного
ввода. Подтверждённая потеря доступа MUST скрывать личные результаты.
#### Scenario: Доход обновляется при редактировании периода
- **WHEN** пользователь редактирует черновик дат, а фоновое обновление применённого периода завершилось
- **THEN** черновик и фокус сохраняются, результат относится к прежнему применённому периоду и техническое уведомление не появляется
#### Scenario: Новый период пока недоступен
- **WHEN** пользователь применил другой допустимый период, но его расчёт ещё не получен
- **THEN** страница показывает ожидание или недоступность нового результата, не выдаёт прежние суммы за доход нового периода и восстанавливается автоматически
+139 -11
View File
@@ -7,16 +7,30 @@
## Requirements
### Requirement: User can retrieve an account ledger
Система MUST предоставлять аутентифицированный журнал только для чтения одного Brokerage Account, принадлежащего текущему User. Журнал MUST содержать только исполненные операции T-Bank и при отсутствии периода по умолчанию охватывать интервал от начала текущего календарного года до момента запроса.
Система 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** система возвращает его исполненные операции от начала текущего календарного года до момента запроса
- **THEN** система возвращает сохранённые исполненные операции в `data` и полноту периода в `meta` за период от начала текущего календарного года в UTC до момента запроса, запрашивая отсутствующие интервалы в фоне
#### Scenario: Отклонение недоступного счёта
- **WHEN** аутентифицированный User запрашивает журнал счёта, который не является открытым поддерживаемым счётом, доступным через его T-Bank connection
- **WHEN** аутентифицированный User запрашивает журнал счёта, для которого локальные данные текущего подключения подтверждают отсутствие доступа либо неподдерживаемое или закрытое состояние
- **THEN** система возвращает существующий ответ о недоступности счёта и не раскрывает операции
#### Scenario: Общий журнал сохраняет принадлежность записей
- **WHEN** аутентифицированный User запрашивает журнал в режиме `all`
- **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 разбивать упорядоченный результат на страницы с непрозрачным cursor и возвращать признак доступности следующей страницы.
@@ -48,15 +62,25 @@
- **THEN** запись журнала передаёт её вместе с доступными идентификаторами инструмента, не меняя остальные сведения операции
### Requirement: Account overview includes recent ledger events
Обзор счёта MUST показывать пять последних исполненных записей журнала этого счёта и действие перехода к его полному журналу. Каждая показанная запись MUST иметь ту же визуальную идентичность инструмента и семантику денежного движения, что и запись полного журнала. Неуспешный или пустой запрос последних записей MUST оставаться локальным состоянием блока последних событий и MUST NOT заменять успешно отображённый обзор Portfolio.
Обзор счёта MUST показывать пять последних исполненных записей журнала этого
счёта, а в режиме «Все счета» — пять последних записей всех входящих счетов, и действие перехода к его полному журналу. Каждая запись MUST иметь ту же
визуальную идентичность инструмента и семантику денежного движения, что и запись
полного журнала; в группе режима «Все счета» визуальная идентичность, дата и тип
показываются в заголовке группы. Неуспешный или пустой запрос MUST оставаться локальным состоянием
блока и MUST NOT заменять успешно отображённый Portfolio. При временном сбое
обновления блок MUST сохранять доступные записи без технического предупреждения
или ручного повтора. При отсутствии результата он SHALL показывать первичное
ожидание либо нейтральную недоступность, автоматически восстанавливая чтение.
Неподтверждённая пустота MUST NOT представляться отсутствием операций.
#### Scenario: Отображение последних событий
- **WHEN** загружается обзор счёта и его журнал содержит исполненные операции
- **THEN** обзор показывает не более пяти последних записей и ссылку на полный журнал
#### Scenario: Последние записи недоступны
- **WHEN** обзор счёта успешно загружается, но запрос последних записей завершается ошибкой
- **THEN** обзор Portfolio остаётся доступным, а блок последних событий отображает собственное состояние восстановления
- **WHEN** обзор счёта успешно загружается, но запрос последних записей завершается временной ошибкой
- **THEN** Portfolio остаётся доступным, а блок сохраняет прежние записи либо показывает нейтральную недоступность без технического повтора и восстанавливается автоматически
#### Scenario: Последняя запись не содержит visual metadata инструмента
- **WHEN** одна из последних записей не имеет доступной presentation metadata инструмента
@@ -64,36 +88,48 @@
### Requirement: Full ledger presents compact rows
Страница полного журнала MUST показывать для каждой записи дату, тип операции, инструмент и денежное изменение в исходной валюте в компактной структурированной строке. При доступной presentation metadata инструмента строка MUST показывать его визуальную идентичность; при её отсутствии MUST использовать понятный текстовый fallback. Для каждой записи денежное изменение в исходной валюте MUST сопровождаться подписью конкретного типа события и знаком: для купонной операции — «Купон», для дивидендной — «Дивиденд», для остальных записей — подпись их категории («Сделка», «Налог», «Комиссия», «Перевод», «Прочее»). Строка MUST NOT повторять подпись типа события во вторичной метадате записи. Знак «+» / «−» и семантический цвет MUST оставаться носителями направления и MUST NOT заменяться только текстом. Страница MUST обозначать записи без движения денег нейтрально и MUST NOT показывать цену или количество.
Страница полного журнала MUST показывать для каждой записи дату, тип операции, инструмент и денежное изменение в исходной валюте в компактной структурированной строке. При доступной presentation metadata инструмента строка MUST показывать его визуальную идентичность; при её отсутствии MUST использовать понятный текстовый fallback. Для купонных и дивидендных операций на странице полного журнала и в личной истории инструмента MUST использоваться то же представление выплаты, что в доходе, прогнозе и графиках инструментов, с обозначением исполненного движения. Для каждой записи денежное изменение в исходной валюте MUST сопровождаться подписью конкретного типа события и знаком: для купонной операции — «Купон», для дивидендной — «Дивиденд», для остальных записей — подпись их категории («Сделка», «Налог», «Комиссия», «Перевод», «Прочее»). Строка MUST NOT повторять подпись типа события во вторичной метадате записи. Знак «+» / «−» и семантический цвет MUST оставаться носителями направления и MUST NOT заменяться только текстом. Страница MUST обозначать записи без движения денег нейтрально и MUST NOT показывать цену или количество. В группе режима «Все счета» дата, тип операции, инструмент и его визуальная идентичность MUST показываться один раз в заголовке группы, а строка каждой записи группы MUST показывать её счёт и денежное изменение с подписью типа и знаком.
#### Scenario: Отображение записи только с инструментом
- **WHEN** полный журнал отображает запись без денежной выплаты
- **THEN** страница определяет её как запись без движения денег
#### Scenario: Отображение поступления
- **WHEN** полный журнал отображает запись с положительной денежной выплатой
- **THEN** строка явно показывает поступление знаком и подписью конкретного типа события («Купон», «Дивиденд» или категории) и его денежное изменение в исходной валюте, не полагаясь только на цвет
#### Scenario: Отображение списания
- **WHEN** полный журнал отображает запись с отрицательной денежной выплатой
- **THEN** строка явно показывает списание знаком и подписью конкретного типа события («Купон», «Дивиденд» или категории) и его денежное изменение в исходной валюте, не полагаясь только на цвет
#### Scenario: Отображение купона
- **WHEN** полный журнал отображает запись с upstream-типом купона
- **THEN** строка показывает подпись «Купон»
- **WHEN** полный журнал или личная история инструмента отображает исполненную операцию с upstream-типом купона
- **THEN** запись показывает подпись «Купон» и исполненную сумму в том же представлении выплаты, что на других экранах
#### Scenario: Отображение дивиденда
- **WHEN** полный журнал отображает запись с upstream-типом дивиденда
- **THEN** строка показывает подпись «Дивиденд»
- **WHEN** полный журнал или личная история инструмента отображает исполненную операцию с upstream-типом дивиденда
- **THEN** запись показывает подпись «Дивиденд» и исполненную сумму в том же представлении выплаты, что на других экранах
#### Scenario: Подпись типа не дублируется
- **WHEN** полный журнал отображает запись с типом события и вторичной метадатой
- **THEN** подпись типа события присутствует в строке один раз и не повторяется во вторичной метадате записи
#### Scenario: Отсутствует visual metadata инструмента
- **WHEN** полный журнал отображает запись без доступной presentation metadata инструмента
- **THEN** строка показывает понятный текстовый fallback без подмены или сокрытия доступных данных инструмента
#### Scenario: Запись внутри группы режима «Все счета»
- **WHEN** полный журнал в режиме «Все счета» показывает запись в составе группы
- **THEN** её дата, тип и инструмент читаются из заголовка группы, а строка записи показывает счёт и денежное изменение со знаком и подписью типа
### Requirement: Кэш страниц журнала ограничивает локальные ресурсы
Система SHALL кэшировать не более 100 недавно использованных страниц Account Ledger в каждом backend process не более чем на 30 секунд. Cache key MUST включать User, Brokerage Account, нормализованные период и categories, opaque cursor и limit; порядок или дублирование categories MUST NOT создавать отдельную cache identity, а отсутствующий конец периода MUST использовать текущий 30-секундный bucket. Просроченная страница MUST NOT возвращаться, а при переполнении cache MUST удалять наименее недавно использованную страницу.
@@ -142,3 +178,95 @@
#### Scenario: Ошибка загрузки
- **WHEN** загрузка части отсутствующего периода завершается ошибкой
- **THEN** ранее подтверждённые операции сохраняются, а неполная часть не помечается полной
### Requirement: Журнал восстанавливает чтение без ручного обновления
Страница журнала SHALL сохранять записи текущей выборки при временном сбое
обновления и восстанавливать чтение автоматически по `frontend-data-resilience`.
Период, категории и выбранная страница MUST сохраняться; результат другой
выборки MUST NOT подменять текущую. Страница MUST NOT показывать время получения,
технические статусы или действия ручного повтора. Даты операций, фильтры,
действия их применения и переходы по страницам MUST оставаться доступными.
При отсутствии результата SHALL применяться первичное ожидание и нейтральная
недоступность, а не сообщение об отсутствии операций до подтверждения полноты.
#### Scenario: Обновление журнала временно не удалось
- **WHEN** для применённого периода и страницы уже есть записи, а обновление завершается временной ошибкой
- **THEN** записи и параметры просмотра сохраняются без предупреждения и кнопки повтора до автоматического восстановления
#### Scenario: Следующая страница ещё не получена
- **WHEN** пользователь перешёл к следующей странице, но её загрузка временно не удалась
- **THEN** область новой страницы показывает ожидание или нейтральную недоступность, не выдаёт прежние записи за новую страницу и восстанавливает чтение автоматически
### Requirement: All-accounts ledger groups same-day entries of one instrument
В режиме «Все счета» полный журнал и последние записи обзора MUST объединять в визуальную группу две и более показанных записи с одинаковыми днём (как на экране), identity Instrument и точным upstream-типом операции, даже если они на одном счёте. Записи без Instrument или без пары MUST показываться как раньше. В режиме одного Brokerage Account группировка MUST NOT применяться.
#### Scenario: Купоны одной облигации на двух счетах
- **WHEN** в режиме «Все счета» журнал показывает за один день купон по облигации на брокерском счёте +200 RUB и купон по той же облигации на ИИС +100 RUB
- **THEN** они показаны одной группой с облигацией, типом «Купон» и итогом +300 RUB, а под заголовком видны строки «брокерский счёт +200 RUB» и «ИИС +100 RUB»
#### Scenario: Две операции одного типа на одном счёте
- **WHEN** в режиме «Все счета» журнал показывает за один день две покупки одной бумаги на одном счёте
- **THEN** они показаны одной группой, где каждая покупка остаётся отдельной строкой со своим денежным изменением
#### Scenario: Разные типы по одной бумаге не объединяются
- **WHEN** в режиме «Все счета» за один день по одной облигации есть купон и налог с этого купона
- **THEN** купоны и налоги показаны раздельно, и ни одна группа не содержит записи разных типов
#### Scenario: Покупка и продажа не объединяются
- **WHEN** в режиме «Все счета» за один день показаны покупка и продажа одной бумаги
- **THEN** они не входят в одну группу
#### Scenario: Разные дни не объединяются
- **WHEN** в режиме «Все счета» купоны по одной облигации на двух счетах датированы на экране разными днями
- **THEN** они показаны отдельными записями без группы
#### Scenario: Запись без Instrument
- **WHEN** в режиме «Все счета» за один день показаны две записи одного типа без identity Instrument
- **THEN** они показаны отдельными записями так же, как без группировки
#### Scenario: Режим одного счёта не меняется
- **WHEN** User открывает журнал или обзор одного Brokerage Account, где есть два купона по одной облигации за один день
- **THEN** записи показаны отдельными строками без группы, как до изменения
### Requirement: All-accounts ledger group shows each entry and an exact total
Заголовок группы журнала MUST показывать дату, Instrument с визуальной идентичностью, подпись типа операции, как в строке журнала, и итог. Под ним MUST всегда быть видна, без раскрытия, отдельная строка каждой записи со счётом и денежным изменением. Итог SHALL быть точной суммой со знаком по каждой исходной валюте без конвертации и округления. Итог MUST NOT показываться, если у записи группы нет денежного изменения (включая нулевое) или полнота периода не подтверждена.
#### Scenario: Итог по нескольким валютам
- **WHEN** группа содержит записи с денежными изменениями в RUB и в USD
- **THEN** заголовок показывает отдельный точный итог в RUB и отдельный в USD, без конвертации
#### Scenario: Итог со знаком
- **WHEN** группа содержит поступление +150.10 RUB и отрицательную корректировку −50.05 RUB
- **THEN** итог группы равен +100.05 RUB, а обе записи видны отдельными строками со своими знаками
#### Scenario: Нулевой итог
- **WHEN** группа содержит +150.10 RUB и −150.10 RUB
- **THEN** заголовок показывает итог 0 RUB без знака направления, а обе записи видны со своими знаками
#### Scenario: Группа с записью без денежного изменения
- **WHEN** у одной из записей группы нет денежного изменения
- **THEN** заголовок группы не показывает итог, а записи остаются видны отдельными строками
#### Scenario: Запись с нулевой суммой скрывает итог
- **WHEN** группа содержит запись +100 RUB и запись с денежным изменением 0 RUB
- **THEN** заголовок группы не показывает итог, а запись с нулём обозначена как запись без движения денег
#### Scenario: Итог скрыт при неполном периоде
- **WHEN** журнал в режиме «Все счета» показывает группу, а период обозначен как `partial`
- **THEN** заголовок группы не показывает итог, а записи группы остаются видны со своими суммами
### Requirement: All-accounts ledger grouping preserves order and limits
Группа журнала MUST занимать позицию своей первой записи в серверном порядке; записи внутри группы и остальные строки MUST сохранять относительный серверный порядок. Группировка MUST NOT менять состав, фильтрацию, пагинацию или лимит обзора, который считается по записям. Группа собирается только из показанных записей; это осознанное исключение: её итог относится только к ним и не сопровождается пометкой о неполноте.
#### Scenario: Позиция группы в списке
- **WHEN** серверный порядок записей A, B, C, где A и C образуют группу, а B не входит ни в одну группу
- **THEN** список показывает группу (A, C) на месте A, затем B
#### Scenario: Группа на границе страницы
- **WHEN** записи одной группы попадают на текущую и на ещё не показанную страницу журнала при подтверждённой полноте периода
- **THEN** группа на текущей странице содержит только её показанные записи, итог равен их сумме, и заголовок не содержит пометки о неполноте
#### Scenario: Группировка в последних записях обзора
- **WHEN** в режиме «Все счета» среди пяти последних записей есть две записи с одинаковым ключом
- **THEN** блок обзора показывает те же пять записей, что и без группировки, две из них одной группой
@@ -84,43 +84,165 @@ Instrument в Future Cash Event MUST иметь тот же доменный с
- **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 запускать личную синхронизацию.
Страница Brokerage Account MUST предоставлять действие перехода на отдельную
страницу «Будущие события». Эта страница MUST показывать все полученные события
компактным списком без фильтров, пагинации и суммарного итога списка, с датой, типом,
инструментом и прогнозной суммой в исходной валюте, если она известна. Прогнозный итог
визуальной группы режима «Все счета» не является суммарным итогом списка. Страница
MUST объяснять, что прогноз основан на позиции счёта, указан до налогов и может
быть неполным из-за доступности данных источника. Это предметное ограничение
прогноза MUST NOT превращаться в отчёт о технической синхронизации.
При временном сбое обновления страница MUST сохранять ранее доступный результат
той же выборки с его исходными `calculatedAt` и окном, без индикации
собственного чтения и ручного повтора. Состояние источника из `meta`, включая
неуспех последней часовой попытки и неполноту, MUST представляться по
`frontend-data-resilience`. При отсутствии результата SHALL показываться
первичное ожидание либо нейтральная недоступность с автоматическим
восстановлением по `frontend-data-resilience`. Подтверждённая потеря доступа
MUST скрывать личные данные независимо от наличия сохранённого результата.
#### Scenario: Просмотр ближайших событий
- **WHEN** User открывает «Будущие события» из обзора Brokerage Account
- **THEN** он видит отсортированный read-only список событий сохранённого 30-календарного окна и `calculatedAt`, где offer обозначен без суммы
- **THEN** он видит отсортированный список событий ближайших 30 дней, где offer обозначен без суммы
#### Scenario: Пустое состояние страницы
- **WHEN** запрос будущих событий успешно возвращает пустой список
- **THEN** страница сообщает, что в сохранённом окне прогноза событий не найдено, показывает `calculatedAt` и объясняет ограничения покрытия источника
#### Scenario: Первая загрузка либо ошибка без сохранённого прогноза
- **WHEN** запрос будущих событий возвращает `data: null`, а `meta` обозначает выполнение первой загрузки либо её неуспех
- **THEN** страница показывает доступное состояние загрузки либо ошибки без частичного списка и предоставляет при ошибке действие повторить только HTTP-чтение
- **WHEN** запрос будущих событий успешно возвращает полный пустой список
- **THEN** страница сообщает, что на ближайшие 30 дней событий не найдено, и объясняет предметные ограничения покрытия источника
#### Scenario: Ошибка страницы
- **WHEN** запрос будущих событий завершается ошибкой
- **THEN** страница не показывает частичный список и предоставляет действие повторить только HTTP-чтение
- **WHEN** запрос будущих событий завершается временной ошибкой
- **THEN** страница сохраняет доступный результат той же выборки либо показывает нейтральную недоступность без ручного повтора, восстанавливая чтение автоматически и не выдавая частичные данные за полный прогноз
#### Scenario: Прогнозный дивиденд с неизвестной суммой
- **WHEN** прогноз содержит дивиденд с известной датой реестра, но без суммы
- **THEN** его карточка обозначает дату как дату реестра и сумму как неизвестную, не показывает нулевую или полученную выплату
#### Scenario: Ошибка обновления при сохранённом прогнозе на странице
- **WHEN** запрос будущих событий возвращает последний полный прогноз вместе с ошибкой его обновления
- **THEN** страница показывает этот прогноз с его исходными `calculatedAt` и окном, отдельно сообщает об ошибке обновления и предоставляет действие повторить только HTTP-чтение
- **THEN** страница показывает этот прогноз с его исходными `calculatedAt` и окном и отдельно обозначает неуспех обновления источника, не предлагая ручной повтор
#### Scenario: Первая загрузка либо ошибка без сохранённого прогноза
- **WHEN** запрос будущих событий возвращает `data: null`, а `meta` обозначает выполнение первой загрузки либо её неуспех
- **THEN** страница показывает доступное состояние первичного ожидания либо нейтральной недоступности без частичного списка и восстанавливает чтение автоматически
### 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-чтение.
Обзор Brokerage Account MUST показывать не более пяти ближайших Future Cash
Event и действие перехода на полную страницу «Будущие события». Неуспешный
или пустой запрос этого блока MUST оставаться локальным состоянием блока и
не заменять успешно отображённый Portfolio. При временном сбое обновления
блок MUST сохранять доступные события сохранённого окна без индикации
собственного чтения; при отсутствии результата SHALL показываться нейтральная
недоступность после первичного ожидания. Состояние источника из `meta` MUST
представляться по `frontend-data-resilience`. Восстановление MUST выполняться
автоматически без ручного повтора и без подмены неизвестного результата
подтверждённой пустотой.
#### Scenario: Отображение ближайших событий
- **WHEN** обзор счёта успешно загружен и сохранённый прогноз содержит будущие события
- **THEN** обзор показывает первые пять событий в порядке даты, `calculatedAt` и действие перехода к полной странице
- **WHEN** обзор счёта успешно загружен и календарь содержит будущие события
- **THEN** обзор показывает первые пять событий в порядке даты и действие перехода к полной странице
#### Scenario: Ближайшие события недоступны
- **WHEN** обзор счёта успешно загружен, но запрос будущих событий завершается временной ошибкой
- **THEN** Portfolio остаётся доступным, а блок сохраняет прежние события либо показывает нейтральную недоступность без технической ошибки и ручного повтора
#### Scenario: Ближайшие события недоступны без сохранённого прогноза
- **WHEN** обзор счёта успешно загружен, а события возвращают `data: null` и `meta` обозначает выполнение первой загрузки либо её неуспех
- **THEN** Portfolio остаётся доступным, а блок будущих событий отображает собственное доступное состояние загрузки либо ошибки без частичного списка
#### Scenario: Ближайшие события недоступны
- **WHEN** обзор счёта успешно загружен, но запрос будущих событий завершается ошибкой
- **THEN** Portfolio остаётся доступным, а блок будущих событий отображает собственную ошибку
- **THEN** Portfolio остаётся доступным, а блок будущих событий отображает собственное доступное состояние первичного ожидания либо нейтральной недоступности без частичного списка
#### Scenario: Неуспешное обновление не скрывает сохранённый прогноз
- **WHEN** обзор счёта получает сохранённый полный прогноз вместе с ошибкой его обновления
- **THEN** Portfolio и события сохранённого окна остаются доступными, а блок отдельно обозначает ошибку и предлагает повторить только HTTP-чтение
- **THEN** Portfolio и события сохранённого окна остаются доступными, а блок отдельно обозначает неуспех обновления источника, не предлагая ручной повтор
### Requirement: User can filter upcoming events by a forecast date range
The events page MUST retain its URL-backed date-range filter. When the URL has
no `from` or `to` parameters, the applied range MUST include the current date
and the following six calendar dates. The page MUST allow selecting today and
future dates in its date picker and MUST prevent selecting dates before today.
The date-picker restriction MUST NOT invalidate or rewrite a valid past date
range supplied directly in the URL. A malformed date, impossible calendar date,
or reversed range MUST be normalized to the default range, removing invalid
`from` and `to` parameters from the URL. Applying and resetting the filter MUST
continue to use the existing URL-backed period behavior.
#### Scenario: Default range starts today and spans seven calendar dates
- **WHEN** the user opens the events page without `from` or `to` parameters
- **THEN** the applied range starts on the current calendar date
- **AND** the inclusive end date is six calendar dates after the start date
#### Scenario: Future dates can be selected and past dates cannot
- **WHEN** the user opens the date picker
- **THEN** today and later dates are selectable
- **AND** dates before today are not selectable
#### Scenario: Valid past URL range is preserved
- **WHEN** the user opens the events page with valid `from` and `to` dates in
the past and `from` is not after `to`
- **THEN** the page applies that URL range without clamping or replacing it
#### Scenario: Invalid URL range falls back to the default
- **WHEN** the URL contains a malformed date, an impossible calendar date, or
a range whose `from` date is after its `to` date
- **THEN** the page applies the default seven-calendar-date range
- **AND** invalid `from` and `to` parameters are removed from the URL
### Requirement: All-accounts upcoming events group same-day events of one instrument
В режиме «Все счета» страница «Будущие события» и ближайшие события обзора MUST объединять в визуальную группу два и более показанных Future Cash Event с одинаковыми датой и видом даты (как на экране), identity Instrument и типом события, даже если они на одном счёте. События без Instrument или без пары MUST показываться как раньше. В режиме одного Brokerage Account группировка MUST NOT применяться.
#### Scenario: Прогноз купонов одной облигации на двух счетах
- **WHEN** в режиме «Все счета» на одну дату ожидается купон по облигации на брокерском счёте 200 RUB и купон по той же облигации на ИИС 100 RUB
- **THEN** они показаны одной группой с облигацией, типом «Купон» и прогнозным итогом 300 RUB, а под заголовком видны строки «брокерский счёт 200 RUB» и «ИИС 100 RUB»
#### Scenario: Разные типы или даты не объединяются
- **WHEN** в режиме «Все счета» по одной облигации ожидаются купон и погашение на одну дату либо купоны на разные даты
- **THEN** ни одна группа не содержит события разных типов или разных дат
#### Scenario: Событие без Instrument
- **WHEN** в режиме «Все счета» на одну дату показаны два события одного типа без identity Instrument
- **THEN** они показаны отдельными строками без группы
#### Scenario: Режим одного счёта не меняется
- **WHEN** User открывает «Будущие события» одного Brokerage Account
- **THEN** события показаны отдельными строками без группы, как до изменения
### Requirement: All-accounts upcoming event group shows each event and a forecast total
Заголовок группы событий MUST показывать дату (для дивиденда — как дату реестра, если она такая), Instrument, тип и прогнозный итог. Под ним MUST всегда быть видна, без раскрытия, отдельная строка каждого события со счётом и прогнозной суммой (или пометкой о неизвестной сумме). Итог SHALL быть точной суммой со знаком по каждой валюте без конвертации и округления. Итог MUST NOT показываться, если у события группы нет суммы или полнота прогноза не подтверждена.
#### Scenario: Событие с неизвестной суммой в группе
- **WHEN** в режиме «Все счета» группа содержит дивиденд на одном счёте с известной суммой и дивиденд по той же бумаге на ту же дату на другом счёте без суммы
- **THEN** заголовок группы не показывает итог, а строка второго счёта обозначает сумму как неизвестную
#### Scenario: Оферта без сумм
- **WHEN** в режиме «Все счета» на одну дату ожидается оферта по одной облигации на двух счетах
- **THEN** они показаны одной группой без итога, и каждая строка показывает свой счёт
#### Scenario: Прогнозный итог по нескольким валютам
- **WHEN** группа содержит дивиденд 10 USD на одном счёте и дивиденд 200 RUB по той же бумаге на ту же дату на другом счёте
- **THEN** заголовок показывает отдельные прогнозные итоги 10 USD и 200 RUB без конвертации
#### Scenario: Прогнозный итог скрыт при неполном прогнозе
- **WHEN** в режиме «Все счета» прогноз обозначен неполным, и на странице есть группа событий с известными суммами
- **THEN** заголовок группы не показывает итог, а строки событий остаются видны со своими суммами
#### Scenario: Группа дивидендов по дате реестра
- **WHEN** группа содержит дивиденды, датированные датой реестра
- **THEN** заголовок обозначает дату как дату реестра
### Requirement: All-accounts upcoming event grouping preserves order and limits
Группа событий MUST занимать позицию своего первого события в порядке ответа; события внутри группы и остальные строки MUST сохранять относительный порядок ответа. Группировка MUST NOT менять состав, окно, порядок по дате или лимит обзора, который считается по событиям; группы собираются только из показанных событий.
#### Scenario: Группировка в ближайших событиях обзора
- **WHEN** в режиме «Все счета» среди пяти ближайших событий есть два события с одинаковым ключом
- **THEN** блок обзора показывает те же пять событий, что и без группировки, два из них одной группой
#### Scenario: Позиция группы событий
- **WHEN** порядок ответа содержит события A, B, C, где A и C образуют группу, а B не входит ни в одну группу
- **THEN** список показывает группу (A, C) на месте A, затем B
+67 -28
View File
@@ -65,41 +65,66 @@
- **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 не выдавать отсутствие итогов за временную ошибку.
Клиент SHALL загружать список при открытии защищённого маршрута `/accounts`
без ручного обновления. Клиент MUST использовать успешный frontend-кэш списка
в течение 60 секунд. После истечения этого срока открытие или возвращение
на экран SHALL инициировать получение актуального списка; имеющийся Client
Cached Snapshot MUST оставаться видимым во время обновления и после временной
ошибки без индикации собственного чтения и действий повтора.
Автоматическое восстановление SHALL соответствовать `frontend-data-resilience`.
Открытый экран MAY периодически перечитывать backend-результат, сохраняя
видимый snapshot, и MUST NOT создавать импорт T-Bank. Состояние источника из
`meta` MUST представляться по `frontend-data-resilience`.
При успешном непустом ответе клиент 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** клиент получает успешный пустой массив
- **WHEN** клиент получает успешный полный пустой массив
- **THEN** клиент показывает штатное сообщение об отсутствии инвестиционных счетов T-Bank
#### Scenario: Последняя попытка неуспешна после полного списка
- **WHEN** клиент получает полный `data` списка и `meta` с неуспехом последней часовой попытки
- **THEN** клиент сохраняет список видимым, показывает его исходное время и текстовое либо доступно именованное предупреждение о неуспешной попытке
#### Scenario: Первая загрузка списка выполняется или неуспешна
- **WHEN** клиент получает `data: null`, а `meta` обозначает выполнение первой загрузки либо её неуспех
- **THEN** клиент показывает соответственно доступное состояние загрузки либо ошибки без карточек счетов и без представления частичного списка как полного
#### Scenario: Список берётся из свежего frontend-кэша
- **WHEN** User повторно открывает список в течение 30 минут после успешной загрузки
- **WHEN** User повторно открывает список в течение 60 секунд после успешной загрузки
- **THEN** клиент показывает сохранённые счета без нового API-запроса
#### Scenario: Устаревший список загружается повторно
- **WHEN** User открывает список после истечения 30 минут frontend-кэша и уже существует последний успешный список
- **THEN** клиент оставляет этот список видимым во время нового HTTP-чтения и ясно обозначает фоновое обновление
- **WHEN** User открывает список после истечения 60 секунд frontend-кэша и уже существует последний успешный список
- **THEN** клиент оставляет этот список видимым во время нового запроса без индикации фонового обновления
#### Scenario: Открытый экран перечитывает список
- **WHEN** экран Accounts остаётся открытым 30 минут после последнего успешного HTTP-чтения
- **THEN** клиент выполняет фоновое HTTP-перечитывание backend-результата, сохраняет видимый snapshot и не запускает импорт T-Bank
#### Scenario: Последняя попытка неуспешна после полного списка
- **WHEN** клиент получает полный `data` списка и `meta` с неуспехом последней часовой попытки
- **THEN** клиент сохраняет список видимым, показывает его исходное время и текстовое либо доступно именованное предупреждение о неуспешной попытке, не предлагая ручной повтор
#### Scenario: Первая загрузка списка выполняется или неуспешна
- **WHEN** клиент получает `data: null`, а `meta` обозначает выполнение первой загрузки либо её неуспех
- **THEN** клиент показывает соответственно доступное состояние загрузки либо локальной недоступности без карточек счетов и без представления частичного списка как полного
#### Scenario: Ошибка фонового обновления не скрывает snapshot
- **WHEN** фоновое HTTP-перечитывание списка завершается ошибкой при наличии последнего успешного Client Cached Snapshot
- **THEN** клиент сохраняет snapshot видимым, безопасно сообщает о неуспешном HTTP-чтении и предоставляет применимое действие повторной загрузки
- **WHEN** обновление устаревшего списка завершается временной ошибкой при наличии последнего успешного Client Cached Snapshot
- **THEN** клиент сохраняет snapshot видимым без предупреждения и ручного повтора и восстанавливает получение данных автоматически
#### Scenario: Карточка ограничивает валютные итоги
- **WHEN** доступная сводка счёта содержит итоги в трёх или более валютах
@@ -107,35 +132,49 @@
#### Scenario: Недоступность сводки локальна карточке
- **WHEN** клиент получает список, в котором `portfolioSummary` одного счёта равна `null`
- **THEN** он показывает временную недоступность только на этой карточке, не скрывает остальные карточки и сохраняет переход на детали недоступной сводки счёта
- **THEN** он показывает нейтральную недоступность только этой сводки, не скрывает остальные карточки и сохраняет переход на детали счёта
#### Scenario: Пустая сводка не является ошибкой
- **WHEN** клиент получает доступную `portfolioSummary` с пустым `totalsByCurrency`
- **THEN** он показывает нейтральное состояние отсутствия финансовых итогов, а не состояние временной недоступности
- **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 сохранить его видимым и показать соответствующее безопасное предупреждение и только применимые действия.
Клиент SHALL автоматически восстанавливать временно неуспешное чтение списка
по правилам `frontend-data-resilience` без кнопок обновления и повтора. При
`TBANK_NOT_CONNECTED` клиент MUST показать необходимость подключения и действие
открытия настроек T-Bank. При `INVALID_TBANK_TOKEN` клиент MUST показать
необходимость восстановить подключение и то же действие. Эти подтверждённые
состояния MUST скрывать личный список и MUST NOT запускать автоматические повторы.
При временной недоступности, включая `TBANK_UNAVAILABLE`, клиент MUST сохранять
успешный результат текущей границы доступа без предупреждений. Если результата
нет, после первичного ожидания клиент SHALL показать спокойную локальную
недоступность без технических подробностей. Неизвестная ошибка MUST получать
безопасное локальное представление; отсутствие известного кода MUST NOT
разрешать бесконечные повторы или обход доступа. Временный сбой MUST NOT
предлагать перенастройку действующего подключения как способ повторить загрузку.
#### Scenario: Подключение требуется
- **WHEN** клиент получает `TBANK_NOT_CONNECTED`
- **THEN** клиент не показывает список и предлагает открыть настройки T-Bank
#### Scenario: Известная ошибка не повторяется автоматически
- **WHEN** HTTP-чтение списка завершается известной ошибкой
- **THEN** клиент не выполняет дополнительные запросы, пока User не выберет действие повторного HTTP-чтения
- **WHEN** запрос списка завершается подтверждённой ошибкой подключения или доступа, требующей действия пользователя
- **THEN** клиент не выполняет автоматических повторов и предлагает соответствующее действие вместо технической повторной загрузки
#### Scenario: Token требует внимания
- **WHEN** клиент получает `INVALID_TBANK_TOKEN`
- **THEN** клиент не показывает список и предлагает открыть настройки T-Bank
- **THEN** клиент скрывает личный список и предлагает восстановить подключение в настройках T-Bank
#### Scenario: Временная ошибка T-Bank
- **WHEN** клиент получает `TBANK_UNAVAILABLE`
- **THEN** клиент не показывает список и предлагает повторить HTTP-чтение либо открыть настройки T-Bank
- **WHEN** клиент получает `TBANK_UNAVAILABLE` без доступного результата списка
- **THEN** клиент автоматически восстанавливает получение и после первичного ожидания показывает нейтральную недоступность без повторной загрузки и предложения перенастроить подключение
#### Scenario: Неизвестная ошибка загрузки
- **WHEN** клиент получает ошибку без известного кода списка счетов
- **THEN** клиент не показывает список и предлагает только повторить HTTP-чтение
- **WHEN** клиент получает ошибку без известного кода списка счетов и без доступного результата
- **THEN** клиент показывает безопасное нейтральное состояние без технического сообщения или кнопки повтора, применяя только допустимое ограниченное восстановление
#### Scenario: Известная ошибка при сохранённом snapshot
- **WHEN** фоновое HTTP-перечитывание списка с последним успешным Client Cached Snapshot получает `TBANK_UNAVAILABLE`
- **THEN** клиент сохраняет snapshot видимым, сообщает о временной недоступности и предлагает повторить HTTP-чтение либо открыть настройки T-Bank
- **WHEN** фоновое обновление списка с последним успешным Client Cached Snapshot получает `TBANK_UNAVAILABLE`
- **THEN** клиент сохраняет snapshot видимым без предупреждения, времени получения и ручных действий восстановления
+128 -12
View File
@@ -9,20 +9,136 @@
## 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.
Frontend MUST сохранять последний успешный результат списка Brokerage Accounts,
Portfolio и других экранов чтения при фоновом обновлении и временной ошибке
для того же пользователя, подключения и набора данных. Подтверждённый пустой
результат MUST также считаться успешным. Смена счёта, инструмента, поискового
запроса, страницы или применённого периода MUST NOT представлять данные прежней
выборки как результат новой.
Доступный результат MUST оставаться видимым без индикаторов фонового чтения
клиента, технических предупреждений об этом чтении и действий ручного повтора.
Frontend MUST NOT подменять внутренние признаки свежести и полноты, успешное
время источника или ошибку обновления только ради отсутствия их отображения.
Client Cached Snapshot MUST оставаться отличным от server-side fallback;
ошибка чтения MUST NOT изменять `Portfolio.isStale`.
Состояние источника из `meta` — время последнего успешного результата, признак
устаревания, исход последней часовой попытки и неполнота — MUST представляться
текстом либо доступно именованным элементом, а не только цветом, и MUST
различать отсутствие попытки, выполнение, полный успех, частичный успех и
неуспех. Это состояние принадлежит источнику данных, а не собственному чтению
клиента, и MUST NOT требовать от пользователя действия.
Обновление MUST сохранять выбранные фильтры, период, открытые разделы, ввод,
фокус и положение просмотра, пока соответствующее содержимое существует.
При logout, смене подключения или подтверждённом отказе доступа личные данные
прежней границы MUST перестать отображаться; поздний результат прежней загрузки
MUST NOT возвращать их на экран.
#### 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 предупреждение независимо, не представляя одно состояние другим
- **WHEN** экран Accounts уже показывает Client Cached Snapshot и начинается фоновое обновление того же списка
- **THEN** frontend оставляет список видимым без индикации собственного чтения и технических действий
#### Scenario: Resource сохраняется после неуспешной часовой попытки
- **WHEN** response содержит полный `data` и `meta`, обозначающий неуспех последней часовой попытки
- **THEN** frontend сохраняет `data` и его исходное время видимыми и отдельно обозначает неуспех попытки
- **THEN** frontend сохраняет `data` и его исходное время видимыми и отдельно обозначает неуспех попытки, не предлагая ручной повтор
#### 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 сохраняет доступные данные без технических сообщений и не подменяет один внутренний вид состояния другим
#### Scenario: Подтверждённая пустота переживает фоновую ошибку
- **WHEN** текущая выборка ранее успешно вернула полный пустой результат, а её обновление временно не удалось
- **THEN** подтверждённое пустое состояние сохраняется без предупреждения и не заменяется первичной загрузкой
#### Scenario: Изменились параметры выборки
- **WHEN** пользователь применил другой период или открыл другой счёт, а данных новой выборки ещё нет
- **THEN** экран показывает ожидание или недоступность новой выборки и не подписывает прежние суммы новым периодом или счётом
#### Scenario: Обновление не сбрасывает работу пользователя
- **WHEN** пользователь раскрыл раздел, выбрал фильтры или вводит данные, а фоновое чтение завершилось
- **THEN** обновление не сбрасывает эти действия, не переносит фокус и не возвращает пользователя в начало сохранившегося содержимого
#### Scenario: Изменился доступ к личным данным
- **WHEN** пользователь вышел, успешно сменил подключение либо получил подтверждённый отказ доступа к текущим данным
- **THEN** прежние личные данные скрываются, а позднее завершение старой загрузки не восстанавливает их
### Requirement: Временные сбои чтения восстанавливаются автоматически
Frontend SHALL автоматически повторять временно неуспешное чтение и восстанавливать
получение необходимых текущему экрану данных без ручного обновления. Повторы
одной попытки MUST иметь конечный предел и паузы, исключающие непрерывную
череду запросов. При исчерпании предела доступные данные SHALL сохраняться,
а при их отсутствии SHALL показываться локальная недоступность. Возвращение сети,
возвращение пользователя к открытому экрану и повторное открытие экрана MUST
возобновлять получение отсутствующих или требующих обновления данных без
перезагрузки страницы. Свежий полный результат MUST NOT требовать обращения
к источнику только из-за такого события.
Восстановление MUST учитывать сбой чтения результата и сбой самого получения
данных из источника, даже если чтение состояния завершилось успешно. Наблюдение
уже выполняющейся работы MUST NOT запускать её повторно. Уход с экрана MUST
прекращать новые попытки его восстановления; это не требует отменять уже начатую
работу источника. Автоматическое восстановление MUST NOT повторять запросы,
требующие исправления ввода, входа или восстановления доступа, и MUST NOT
переносить старые параметры в новую пользовательскую выборку.
#### Scenario: Кратковременный сбой чтения
- **WHEN** чтение текущей выборки временно не удалось, а следующая разрешённая автоматическая попытка успешна
- **THEN** экран самостоятельно показывает результат без ручного действия и без сообщения о сбое или восстановлении
#### Scenario: Источник завершил работу ошибкой
- **WHEN** чтение сохранённых данных успешно, но получение необходимых новых данных из источника временно не удалось
- **THEN** приложение применяет ограниченное автоматическое восстановление к неуспешной работе, а не считает успешное чтение доказательством её успеха
#### Scenario: Повторы исчерпаны
- **WHEN** временная недоступность сохраняется после конечного предела автоматических попыток
- **THEN** приложение прекращает серию повторов, сохраняет доступный результат либо локальную недоступность без бесконечной загрузки и ожидает следующего события восстановления
#### Scenario: Сеть или пользователь вернулись
- **WHEN** после временного сбоя восстановилась сеть либо пользователь вернулся к экрану с отсутствующим или требующим обновления результатом
- **THEN** получение данных возобновляется автоматически для текущей выборки и при успехе заменяет состояние недоступности содержимым
#### Scenario: Несколько событий восстановления совпали
- **WHEN** сеть восстановилась одновременно с возвращением пользователя на экран
- **THEN** приложение не запускает дублирующую работу получения одного набора данных
#### Scenario: Работа источника ещё выполняется
- **WHEN** приложение восстанавливает чтение состояния уже выполняющейся работы
- **THEN** оно продолжает получение её результата без повторного запуска той же работы
#### Scenario: Пользователь покинул экран
- **WHEN** пользователь ушёл со страницы с ожидающим восстановлением
- **THEN** новые попытки для покинутого экрана прекращаются, а уже начатая работа не изменяет содержимое другой выборки
#### Scenario: Ошибка требует действия пользователя
- **WHEN** подтверждены недействительное подключение, отсутствие доступа или некорректный ввод
- **THEN** автоматическая серия повторов прекращается и появляется соответствующее действие или локальная ошибка без технической кнопки повторной загрузки
### Requirement: Неполный результат не выдаётся за окончательный
Frontend MUST различать неизвестный результат, доступную часть данных и
подтверждённую пустоту. Неполные суммы и сравнения MUST NOT показываться как
окончательные или заменяться нулями. Доступные записи MAY оставаться видимыми,
но их список MUST NOT представляться полным при неподтверждённой полноте.
Состояние SHALL относиться к содержимому конкретной области, например
недоступному итогу, без отчёта о прогрессе синхронизации. Готовая область MUST
NOT ждать догрузки истории, которая не влияет на её результат.
#### Scenario: Итог ещё не определён
- **WHEN** часть данных, необходимых для суммы или сравнения, отсутствует
- **THEN** соответствующее значение обозначается как недоступное без вымышленного нуля или окончательного частичного итога
#### Scenario: Пустота ещё не подтверждена
- **WHEN** записи не получены и полнота выбранного периода неизвестна
- **THEN** экран не сообщает об отсутствии операций или выплат за этот период
#### Scenario: Период готов раньше остальной истории
- **WHEN** данные показанного периода полны, а не влияющая на него история ещё загружается
- **THEN** пользователь видит готовый результат без сообщения о догрузке остальной истории
@@ -59,16 +59,47 @@ MUST NOT быть основным loading-состоянием.
Frontend MUST сохранять уже доступное содержимое или элемент управления
основным контекстом, когда выполняется пользовательское действие либо фоновое
обновление Client Cached Snapshot. В этих состояниях frontend SHALL показывать
компактный progress indicator рядом с соответствующим контекстом и MUST NOT
заменять этот контекст skeleton placeholders.
обновление Client Cached Snapshot. Явное действие пользователя SHALL иметь
компактную индикацию ожидания рядом с соответствующим элементом управления.
Фоновое обновление при доступном результате MUST проходить без индикаторов,
технических сообщений и объявлений о ходе работы. В обоих случаях frontend
MUST NOT заменять уже доступный контекст skeleton placeholders.
#### Scenario: Выполняется пользовательское действие
- **WHEN** User запустил действие и его результат ещё ожидается
- **THEN** frontend сохраняет соответствующий элемент управления или окружающий контекст видимым, показывает компактную индикацию процесса и не заменяет интерфейс skeleton placeholders
#### Scenario: Обновляется уже показанный snapshot
- **WHEN** frontend обновляет данные при наличии видимого Client Cached Snapshot
- **THEN** frontend оставляет snapshot видимым, показывает компактную индикацию фонового обновления и не заменяет snapshot skeleton placeholders
- **THEN** frontend оставляет snapshot видимым без индикации фонового обновления и без замены snapshot skeleton placeholders
### Requirement: Первичное ожидание не становится бесконечной загрузкой
При отсутствии результата frontend SHALL сначала показывать структурное
ожидание соответствующей области. После неуспешного завершения получения
либо истечения ограниченного времени первичного ожидания область MUST переходить
в спокойное локализованное состояние недоступности без технических подробностей
и ручного повтора. Это состояние MUST NOT утверждать, что результат пустой,
или требовать перезагрузки страницы. Продолжающееся автоматическое восстановление
MUST NOT заставлять область постоянно переключаться между недоступностью и
skeleton. После успешного получения содержимое SHALL появляться автоматически.
Отсутствие результата одной области MUST NOT скрывать остальные доступные
области. Первичная загрузка MUST сохранять доступное обозначение занятости,
а состояние недоступности MUST NOT продолжать объявляться бесконечно занятым.
#### Scenario: Первый результат успешно получен
- **WHEN** экран ожидает первый результат и получает его в пределах первоначального ожидания
- **THEN** структурные placeholders сменяются содержимым либо подтверждённым пустым состоянием
#### Scenario: Первое получение данных не удалось
- **WHEN** у области нет результата и первоначальная загрузка завершилась ошибкой или превысила ограниченное время ожидания
- **THEN** область показывает нейтральную недоступность без кнопки повтора, вымышленных данных и бесконечного индикатора занятости
#### Scenario: Восстановление продолжается после недоступности
- **WHEN** область уже показывает недоступность и выполняется очередная разрешённая попытка восстановления
- **THEN** область сохраняет устойчивое состояние без мигания skeleton и автоматически показывает содержимое после успеха
#### Scenario: Один блок недоступен
- **WHEN** основной результат загружен, а дополнительный блок не получил данных
- **THEN** состояние недоступности ограничено этим блоком и не заменяет доступное содержимое страницы
+12 -4
View File
@@ -25,12 +25,20 @@ Frontend SHALL сбрасывать состояние route error после п
- **THEN** frontend сбрасывает предыдущую runtime-ошибку и отображает целевой route
### Requirement: Ожидаемые finance states не становятся route failures
Frontend SHALL сохранять локальное отображение и recovery actions известных состояний Brokerage Account, Portfolio и T-Bank connection. Эти состояния MUST NOT заменяться глобальным fallback.
Frontend SHALL сохранять локальное отображение известных состояний Brokerage
Account, Portfolio и T-Bank connection. Эти состояния MUST NOT заменяться
глобальным fallback. Временная ошибка фонового чтения MUST сохранять доступный
результат без предупреждения об устаревании, технической ошибки и ручного
повтора. При отсутствии результата SHALL показываться локальная недоступность
с автоматическим восстановлением. Подтверждённая необходимость входа,
восстановления подключения или выбора доступного счёта MUST сохранять понятное
объяснение и соответствующее действие, не маскируясь сохранёнными личными данными.
#### Scenario: Portfolio Snapshot устарел
- **WHEN** frontend получает устаревший Portfolio Snapshot
- **THEN** frontend отображает данные Portfolio и существующее предупреждение об устаревании вместо глобального fallback
- **WHEN** frontend получает устаревший Portfolio Snapshot в действующей границе доступа
- **THEN** frontend отображает Portfolio без предупреждения об устаревании и без глобального fallback
#### Scenario: Ошибка T-Bank connection
- **WHEN** запрос T-Bank connection завершается известной ошибкой
- **THEN** frontend показывает существующее локальное состояние ошибки и доступное для него recovery action вместо глобального fallback
- **THEN** frontend сохраняет локальное состояние: временный сбой чтения восстанавливается автоматически, а подтверждённая необходимость действия пользователя сопровождается этим действием вместо глобального fallback
@@ -0,0 +1,113 @@
# frontend-user-feedback Specification
## Purpose
Определяет спокойную и понятную обратную связь во всех разделах WealthLedger:
пользователь видит финансовые данные и результат своих действий, а не состояние
внутреннего процесса получения данных.
## Requirements
### Requirement: Чтение данных не требует внимания к техническим состояниям
На экранах счетов, портфеля, операций, доходов, событий, инструментов, поиска,
карт и настроек подключения приложение MUST NOT показывать время получения
данных, признаки их технического устаревания, ход фонового обновления и сообщения
об успехе либо временной ошибке такого обновления. Приложение MUST NOT предоставлять
кнопки или иные действия ручного обновления и повторной загрузки, включая их
иконные варианты. Эти сведения MUST NOT переноситься в уведомления, подсказки
или объявления вспомогательных технологий. Даты операций, выплат, открытия
счёта и выбранных периодов MUST сохранять свой предметный смысл.
Запрет технических действий MUST NOT удалять применение фильтров, переходы между
страницами, поиск, отправку формы, вход, подключение банка или безопасную навигацию.
После неуспешного сохранения пользователь SHALL иметь возможность вновь отправить
сохранённый ввод обычным действием формы.
#### Scenario: Фоновая загрузка завершилась ошибкой
- **WHEN** на любом из перечисленных экранов уже есть доступный результат и его фоновое обновление временно не удалось
- **THEN** результат остаётся видимым без предупреждения, времени получения, индикатора, уведомления и действия ручного повтора
#### Scenario: Фоновая загрузка завершилась успешно
- **WHEN** приложение получает обновлённые данные без явного действия сохранения со стороны пользователя
- **THEN** содержимое обновляется без сообщения об успешной загрузке и без переноса фокуса
#### Scenario: Просмотр списка карт и операций карты
- **WHEN** временная ошибка возникает при обновлении уже показанных карт, сводки или операций выбранной карты
- **THEN** соответствующий результат остаётся доступным без технического предупреждения и ручного повтора, а действия работы с картой сохраняются
#### Scenario: Поиск или новый диапазон инструмента пока недоступен
- **WHEN** пользователь изменил поисковый запрос либо диапазон графика, а соответствующий результат ещё не получен
- **THEN** экран показывает ожидание или локальную недоступность нового результата без кнопки повтора и не выдаёт результат прежнего запроса или диапазона за новый
#### Scenario: Предметные даты остаются видимыми
- **WHEN** экран показывает дату операции, выплаты или выбранный период
- **THEN** эти даты остаются доступны независимо от отсутствия времени технического получения данных
### Requirement: Краткое уведомление подтверждает неочевидный результат действия
Приложение SHALL показывать краткое локализованное уведомление об успешном
явном действии пользователя, если после завершения на экране отсутствует
однозначное подтверждение результата. Если результат уже очевиден по состоянию
формы или содержимого, приложение MUST NOT дублировать его уведомлением.
Уведомление MUST описывать выполненное действие, а не внутренний процесс.
Ожидание действия MUST NOT представляться его успешным завершением.
#### Scenario: Результат действия иначе легко пропустить
- **WHEN** действие успешно завершилось, его форма закрылась, а оставшийся экран не подтверждает результат однозначно
- **THEN** пользователь получает одно краткое уведомление о выполненном действии
#### Scenario: Результат уже очевиден
- **WHEN** успешное сохранение оставляет на экране явное подтверждение результата
- **THEN** приложение не добавляет дублирующее уведомление
#### Scenario: Приём действия ещё не означает успех
- **WHEN** приложение только начало выполнение действия и окончательный результат неизвестен
- **THEN** оно не показывает уведомление об успешном завершении
### Requirement: Уведомления не мешают работе и не дублируются
Уведомления MUST быть доступны как текст, не полагаться только на цвет и не
перехватывать клавиатурный фокус. Подтверждение успеха SHALL автоматически
закрываться и позволять доступное ручное закрытие. Повторная обработка одного
результата MUST NOT создавать повторное уведомление. Приложение MUST ограничивать
число одновременно видимых уведомлений и MUST NOT создавать каскад сообщений
из-за фонового восстановления данных.
#### Scenario: Один результат обработан повторно
- **WHEN** приложение повторно обрабатывает результат того же пользовательского действия
- **THEN** пользователь не получает второе уведомление об этом результате
#### Scenario: Работа с клавиатурой продолжается
- **WHEN** уведомление появляется во время ввода или навигации с клавиатуры
- **THEN** текущий фокус сохраняется, текст уведомления доступен вспомогательным технологиям, а действие закрытия имеет понятное доступное имя
#### Scenario: Несколько действий завершились рядом по времени
- **WHEN** несколько различных действий успешно завершаются за короткое время
- **THEN** уведомления соблюдают конечный предел одновременного показа и не блокируют доступ к содержимому и действиям страницы
### Requirement: Ошибки действий и препятствия доступа остаются понятными
Ошибка ввода MUST показываться у соответствующего поля. При неуспешном
сохранении приложение MUST сохранять ввод и показывать понятную локальную ошибку
в контексте действия, не объявляя его успешным. Если продолжение требует входа,
подключения банка или другого действия пользователя, объяснение и применимое
действие MUST оставаться доступны в соответствующей области. Исчезающее
уведомление MUST NOT быть единственным способом сообщить такое препятствие.
Приложение MUST NOT самостоятельно повторять изменение пользовательских данных
только потому, что включено автоматическое восстановление чтения.
#### Scenario: Операция не сохранилась
- **WHEN** пользователь отправил форму операции, а сохранение завершилось ошибкой
- **THEN** введённые значения остаются в форме вместе с локальной ошибкой, обычное действие сохранения снова доступно и подтверждение успеха отсутствует
#### Scenario: Поле содержит недопустимое значение
- **WHEN** пользователь пытается отправить форму с недопустимым значением
- **THEN** ошибка относится к этому полю и не заменяется общим всплывающим уведомлением
#### Scenario: Требуется восстановить доступ
- **WHEN** приложение подтвердило необходимость входа или восстановления подключения
- **THEN** соответствующая область предлагает необходимое действие и сохраняет объяснение до устранения препятствия или ухода пользователя
#### Scenario: Ответ на изменение данных не получен
- **WHEN** соединение прервалось после отправки пользовательского изменения и результат неизвестен
- **THEN** приложение не объявляет успех и не повторяет изменение автоматически как обычную загрузку данных
@@ -0,0 +1,26 @@
# instrument-income-schedules Specification
## Purpose
Позволяет отличать опубликованные календарные сведения о купонах и дивидендах
инструмента от фактически полученных личных выплат и прогноза по позиции.
## Requirements
### Requirement: Instrument payout schedules do not imply executed income
На страницах акций и облигаций известные будущие, прошедшие и недатированные события графика дивидендов или купонов MUST использовать то же представление выплаты, что и купоны/дивиденды других экранов, но MUST обозначать их как события графика, не подтверждающие фактическое зачисление. Доступные дата реестра, дата выплаты, сумма на одну бумагу и отдельная личная оценка MUST сохранять свои подписи и смысл; неизвестная сумма или дата MUST NOT подменяться нулём или вымышленной датой. Исполненная личная операция MUST оставаться отличимой от события графика, даже когда их даты совпадают.
#### Scenario: Будущий купон с известными условиями
- **WHEN** User просматривает будущий купон облигации с известными датой выплаты, суммой на облигацию и оценкой для позиции
- **THEN** единое представление показывает купон как событие графика, сохраняет обе разные суммы и дату и не называет оценку полученными деньгами
#### Scenario: Прошедший дивиденд без подтверждённой операции
- **WHEN** User просматривает в истории акции событие дивидендного графика с прошедшей датой реестра
- **THEN** представление показывает его как событие графика с суммой на акцию, а не как исполненную выплату пользователю
#### Scenario: Неизвестные условия графика
- **WHEN** в купонном или дивидендном графике отсутствует дата выплаты либо денежное значение
- **THEN** представление сохраняет известные сведения и явно различает неизвестное значение и ноль без вымышленной даты зачисления
@@ -2,6 +2,7 @@
## Purpose
Определяет единственный источник полномочий для OpenSpec и проверяемый жизненный цикл SDD-изменений независимо от текущей директории, глобальной конфигурации или agent framework.
Применяется только к работе, которую Change Owner явно начал как OpenSpec/SDD; обычная разработка идёт по задаче без planning artifacts.
## Requirements
+61 -22
View File
@@ -119,47 +119,86 @@ Portfolio Snapshot является результатом наблюдения
- **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 оставаться раздельными состояниями.
Клиент SHALL загружать Portfolio при открытии защищённого маршрута
`/accounts/:accountId` и использовать успешный ответ в течение 30 секунд.
Временные сбои и возвращение к экрану с требующими обновления данными SHALL
обрабатываться автоматическим восстановлением по `frontend-data-resilience`.
Клиент MUST NOT предоставлять ручное обновление или повторную загрузку.
При отсутствии Client Cached Snapshot и ответе `404` клиент MUST показать недоступный счёт с возвратом к списку без действия повтора. При отсутствии Snapshot и `TBANK_NOT_CONNECTED`, `INVALID_TBANK_TOKEN`, `TBANK_UNAVAILABLE` либо иной ошибке клиент MUST показать безопасное error-состояние; повтор HTTP-чтения разрешён только для временной и неизвестной ошибки.
При первичной загрузке без успешного результата того же Brokerage Account
клиент MUST показывать структурный skeleton ожидаемого содержимого, включая
сводку, без вымышленных сумм. Область MUST иметь доступное обозначение занятости;
skeleton MUST NOT быть интерактивным или получать фокус. Неуспешное или
затянувшееся первичное получение SHALL переходить в локальную недоступность
по `frontend-loading-presentation`, а не в бесконечную загрузку.
Истечение срока свежести MUST NOT скрывать доступный Client Cached Snapshot.
При обновлении и временной ошибке клиент MUST сохранять данные того же
Portfolio, включая валютные итоги, без индикации фоновой работы собственного
чтения, технических предупреждений об этом чтении и уведомлений. Исходное
`lastSuccessfulAt` и `isStale` MUST сохранять внутренний смысл: их показ или
скрытие MUST NOT делать сохранённый снимок новым успешным результатом или
подтверждением текущего доступа. Server-side stale и ошибка клиентского чтения
MUST NOT подменять друг друга.
Состояние источника из `meta` — время последнего успешного результата, признак
устаревания и исход последней часовой попытки — не относится к собственному
чтению клиента: клиент MUST представлять его текстом либо доступно именованным
элементом по `frontend-data-resilience`, не превращая в действие пользователя.
Клиент MUST показывать штатное empty-состояние только при подтверждённом полном
результате с пустыми `positions` и `cashBalances`, без вымышленных нулевых карточек.
Неполный итог MUST NOT выдаваться за окончательный; доступные независимые данные
MUST сохраняться на экране. При подтверждённом `404` клиент MUST скрыть личный
Portfolio и предложить возврат к списку счетов. При `TBANK_NOT_CONNECTED` или
`INVALID_TBANK_TOKEN` он MUST скрыть личные данные и предложить восстановление
подключения. Эти состояния MUST NOT маскироваться сохранённым снимком или
вызывать техническую повторную загрузку.
#### Scenario: Показ устаревшего Portfolio
- **WHEN** клиент получает успешный ответ с `isStale: true`
- **THEN** клиент показывает Portfolio, исходное время и нейтральное предупреждение без ручной синхронизации
#### Scenario: Первичная загрузка Portfolio
- **WHEN** клиент ожидает Portfolio без ранее полученного успешного ответа
- **THEN** он показывает структурный skeleton без вымышленных или нулевых сумм и без фокусируемых placeholders
- **WHEN** клиент получает успешный ответ с `isStale: true` в действующей границе доступа
- **THEN** клиент показывает позиции, остатки и доступные валютные итоги и обозначает устаревание источника текстом либо доступно именованным элементом, без ручного обновления
#### Scenario: Неуспешная последняя попытка сохраняет Portfolio
- **WHEN** клиент получает полный Portfolio и `meta` с неуспехом последней часовой попытки
- **THEN** он сохраняет Portfolio и исходное время видимыми и отдельно показывает текстовое либо доступно именованное предупреждение о неуспехе попытки
- **THEN** он сохраняет Portfolio и исходное время видимыми и отдельно показывает текстовое либо доступно именованное предупреждение о неуспехе попытки, не предлагая ручной повтор
#### Scenario: Недоступный счёт на клиенте
- **WHEN** для текущего Portfolio отсутствует Client Cached Snapshot и клиент получает `404`
- **THEN** клиент не показывает Portfolio и предлагает вернуться к списку Brokerage Account без действия повтора
- **WHEN** клиент получает подтверждённый `404` текущего Portfolio
- **THEN** клиент не показывает личный Portfolio, включая ранее сохранённый, и предлагает вернуться к списку Brokerage Account без действия повтора
#### Scenario: Первичная загрузка Portfolio
- **WHEN** клиент ожидает Portfolio этого Brokerage Account без ранее полученного успешного ответа
- **THEN** он показывает структурный skeleton, включая место сводки, с доступным обозначением загрузки, без вымышленных или нулевых сумм и без фокусируемых placeholders
#### Scenario: Обновление после истечения срока свежести
- **WHEN** срок свежести показанного Portfolio истёк и началось фоновое HTTP-перечитывание
- **THEN** клиент сохраняет прежние валютные итоги и время, показывает неблокирующую индикацию и не заменяет данные skeleton-состоянием
- **WHEN** срок свежести показанного Portfolio истёк и началось фоновое обновление данных того же Brokerage Account
- **THEN** клиент сохраняет прежние валютные итоги без индикации обновления и не заменяет доступный состав и сводку skeleton-состоянием
#### Scenario: Ошибка обновления после успешного ответа
- **WHEN** Client Cached Snapshot Portfolio уже видим, а повторное HTTP-чтение завершается ошибкой
- **THEN** клиент сохраняет прежние данные и время, показывает неблокирующее предупреждение и действие повтора чтения
- **WHEN** клиент уже показывает Client Cached Snapshot Portfolio, а обновление этих же данных завершается временной ошибкой
- **THEN** клиент сохраняет данные и суммы без предупреждения и действия повтора; ошибка не делает снимок свежим и не изменяет `isStale`
#### Scenario: Ошибка клиентского обновления при server-side stale
- **WHEN** клиент показывает Portfolio с `isStale: true`, а фоновое перечитывание завершается ошибкой
- **THEN** оба предупреждения остаются различимыми, а действие повтора относится только к HTTP-чтению
- **WHEN** клиент показывает Portfolio с `isStale: true`, а фоновое обновление этого же Portfolio завершается временной ошибкой
- **THEN** прежние суммы остаются видимыми без технических предупреждений и ручных действий, сохраняя различие внутренних состояний
#### Scenario: Повторное обновление успешно
- **WHEN** после ошибки HTTP-чтения User запускает повторное чтение и получает успешный Portfolio Snapshot
- **THEN** клиент показывает значения и `lastSuccessfulAt` успешного ответа, убирает предупреждение об ошибке чтения и отображает server-side предупреждение только по новому `isStale`
- **WHEN** после временной ошибки автоматическое восстановление получает успешный Portfolio Snapshot
- **THEN** до ответа прежние суммы остаются видимыми, затем клиент показывает новый результат без уведомления об обновлении и без переноса пользовательского фокуса
#### Scenario: Штатный пустой Portfolio на клиенте
- **WHEN** успешно полученный Portfolio содержит пустые `positions` и `cashBalances`
- **THEN** клиент показывает empty-состояние без валютных карточек с вымышленными нулевыми итогами
- **WHEN** успешно полученный полный Portfolio содержит пустые `positions` и `cashBalances`
- **THEN** клиент показывает существующее empty-состояние без валютных карточек с вымышленными нулевыми итогами
#### Scenario: Первичный Portfolio пока недоступен
- **WHEN** данных Portfolio нет, а первичное получение не удалось или превысило ограниченное время ожидания
- **THEN** клиент показывает нейтральную недоступность без кнопки повтора и автоматически заменяет её содержимым после успешного восстановления
#### Scenario: Требуется восстановить подключение
- **WHEN** клиент получает `TBANK_NOT_CONNECTED` или `INVALID_TBANK_TOKEN` при наличии сохранённого личного Portfolio
- **THEN** личные данные скрываются, а клиент предлагает необходимое действие подключения без технической повторной загрузки
### Requirement: Представление карточек позиций на клиенте
Клиент MUST отображать `STOCK` и `BOND` отдельными секциями. Внутри каждой секции MUST сохраняться upstream-порядок позиций. Позиции иных типов MUST оставаться доступными в отдельной общей секции.
+77
View File
@@ -0,0 +1,77 @@
# stock-screener Specification
## Purpose
Определяет поиск доступных к покупке акций по характеристикам всего доступного каталога, просмотр совпавших выпусков и навигацию по результату поиска.
## Requirements
### Requirement: Скринер ищет среди доступных к покупке акций
Система SHALL по нажатию «Найти» искать по всему доступному на момент запроса каталогу акций, не ограничивая исходный набор первыми совпадениями текстового поиска. В результаты MUST включаться только акции, для которых в полученных сведениях подтверждены и доступность покупки, и возможность торговли через API. Скринер MUST показывать время получения списка и MUST NOT представлять эти признаки как гарантию, что конкретный пользователь сможет исполнить заявку прямо сейчас.
#### Scenario: Поиск без названия или тикера
- **WHEN** пользователь не ввёл текст и нажал «Найти»
- **THEN** система ищет по доступному каталогу и показывает выпуски, удовлетворяющие условиям и обоим признакам доступности
#### Scenario: Акция недоступна для покупки
- **WHEN** у акции отсутствует подтверждение доступности покупки или торговли через API
- **THEN** система не включает её в результаты скринера независимо от остальных совпавших условий
### Requirement: Условия скринера применяются совместно
Система SHALL позволять независимо задавать необязательные условия по сектору, типу акции, валюте расчётов, площадке, стране риска и максимальному количеству бумаг в лоте, а также необязательный текстовый запрос по названию или тикеру. После нажатия «Найти» система SHALL возвращать только акции, удовлетворяющие всем заданным условиям. Отсутствующее или неизвестное значение выбранного условия MUST NOT считаться совпадением.
#### Scenario: Несколько условий без текста
- **WHEN** пользователь выбрал сектор и валюту, оставил текст пустым и нажал «Найти»
- **THEN** в результате остаются только доступные к покупке акции выбранного сектора и валюты
#### Scenario: Текст совместно с характеристиками
- **WHEN** пользователь ввёл название или тикер, выбрал тип акции и нажал «Найти»
- **THEN** система отбирает совпадения по тексту и типу среди доступного каталога до разделения результата на страницы
#### Scenario: Неизвестный сектор
- **WHEN** пользователь выбрал сектор, а у акции сектор не указан
- **THEN** акция не считается совпадением
#### Scenario: Граница максимального лота
- **WHEN** задано максимальное количество бумаг в лоте
- **THEN** акции с известным размером лота не больше указанного включаются при соблюдении остальных условий, а акции с неизвестным или большим лотом исключаются
### Requirement: Результат представляет отдельные выпуски
Система SHALL показывать не более одной карточки для каждого выпуска с одинаковым ISIN; акции с разными ISIN SHALL оставаться отдельными результатами. При отсутствии ISIN отдельным результатом SHALL быть инструмент с собственным идентификатором. Представитель выпуска MUST выбираться среди инструментов, удовлетворяющих заданным условиям, а карточка MUST вести к выбранному инструменту.
#### Scenario: Несколько подходящих режимов одного выпуска
- **WHEN** несколько инструментов с одним ISIN удовлетворяют всем условиям
- **THEN** система показывает одну карточку выпуска со ссылкой на один из этих инструментов
#### Scenario: Один режим не соответствует условиям
- **WHEN** два инструмента имеют один ISIN, но только один удовлетворяет всем условиям
- **THEN** карточка выпуска ведёт к соответствующему условиям инструменту
#### Scenario: Разные выпуски одного эмитента
- **WHEN** обыкновенная и привилегированная акции имеют разные ISIN и обе удовлетворяют условиям
- **THEN** система показывает их как отдельные результаты
### Requirement: Страницы относятся к одному результату поиска
Система SHALL упорядочивать совпадения по тикеру, затем по идентификатору инструмента, и показывать не более 30 выпусков на странице. Переход между страницами одного поиска MUST сохранять состав и порядок полученного результата без пропусков и повторов. Изменение полей без нажатия «Найти» MUST NOT заменять текущий результат; повторное нажатие «Найти», в том числе без изменения условий, SHALL запускать новый поиск и возвращать к первой странице.
#### Scenario: Переход к следующей странице
- **WHEN** пользователь открывает следующую страницу полученного результата
- **THEN** он видит следующие до 30 выпусков в том же порядке и с тем же временем получения списка, без повторения выпусков предыдущей страницы
#### Scenario: Повторный поиск
- **WHEN** пользователь изменил условия или оставил их прежними и нажал «Найти» повторно
- **THEN** система получает новый результат с новым временем получения списка и показывает его первую страницу
#### Scenario: Поля изменены, но поиск не запущен
- **WHEN** пользователь изменяет условия и ещё не нажал «Найти»
- **THEN** ранее полученные страницы не подменяются результатом для новых условий
### Requirement: Отсутствие совпадений отличается от недоступности поиска
Система MUST показывать пустой результат только после успешного поиска без совпадений. Если получить сведения для поиска не удалось, система SHALL сообщать о недоступности поиска и MUST NOT выдавать отсутствие акций за подтверждённый результат.
#### Scenario: Успешный поиск без совпадений
- **WHEN** поиск завершился успешно, но ни одна акция не удовлетворяет условиям
- **THEN** система показывает, что подходящие акции не найдены
#### Scenario: Источник каталога недоступен
- **WHEN** поиск не может получить сведения о каталоге
- **THEN** система сообщает об ошибке получения результата, не утверждая, что подходящих акций нет
+33 -4
View File
@@ -66,16 +66,45 @@
- **THEN** система сохраняет подключение и отвечает текущим статусом
### Requirement: Управление подключением в настройках
Клиент SHALL показывать в настройках статус T-Bank connection, текст о необходимости read-only T-Bank Invest token, поле token и действие подключения либо замены. Для существующего подключения клиент MUST показывать действие удаления с modal-подтверждением. Во время создания, замены или удаления все действия секции подключения и действия modal MUST быть отключены. После успешного создания или замены клиент MUST очистить поле token и обновить статус; при ошибке token MUST остаться в поле.
Клиент SHALL показывать в настройках статус T-Bank connection, текст о
необходимости read-only T-Bank Invest token, поле token и действие подключения
либо замены. Для существующего подключения клиент MUST показывать действие
удаления с modal-подтверждением. Во время создания, замены или удаления все
действия секции подключения и действия modal MUST быть отключены. После
успешного создания или замены клиент MUST очистить поле token и обновить статус;
при ошибке token MUST остаться в поле.
Временный сбой чтения статуса SHALL восстанавливаться автоматически без кнопки
повтора и технического предупреждения. Пока первый статус неизвестен, клиент
MUST NOT показывать форму как подтверждённое отсутствие подключения: после
первичного ожидания SHALL отображаться нейтральная недоступность секции. Если
статус уже известен для текущей сессии, временный сбой его фонового чтения MUST
NOT скрывать доступную секцию или очищать ввод. Подтверждённое изменение
сессии или доступа MUST NOT маскироваться старым статусом.
Ошибки создания, замены и удаления MUST оставаться локальными, сохранять ввод
и возвращать применимые действия формы. Эти действия MUST NOT повторяться
автоматически как чтение статуса. Подтверждения успеха SHALL следовать
`frontend-user-feedback`: очевидное изменение статуса не требует дублирующего
уведомления, неочевидный результат получает одно краткое подтверждение.
#### Scenario: Пользователь подтверждает удаление
- **WHEN** User подтверждает удаление в modal
- **THEN** клиент блокирует действия секции до результата и после успешного ответа показывает состояние без подключения
#### Scenario: Ошибка загрузки статуса
- **WHEN** клиент не может получить статус подключения
- **THEN** клиент показывает отдельное error-состояние с возможностью повторить запрос и не показывает форму подключения
- **WHEN** клиент временно не может получить статус подключения
- **THEN** он автоматически восстанавливает чтение, сохраняет известный статус текущей сессии либо показывает нейтральную недоступность после первичного ожидания без формы и кнопки повтора
#### Scenario: Ошибка проверки token в клиенте
- **WHEN** создание или замена получает `INVALID_TBANK_TOKEN` либо `TBANK_UNAVAILABLE`
- **THEN** клиент показывает соответственно локализованное сообщение о token и правах либо о временной недоступности T-Bank, возвращает интерактивность и сохраняет введённый token
- **THEN** клиент показывает соответственно локализованное сообщение о token и правах либо о временной недоступности T-Bank в контексте формы, возвращает интерактивность и сохраняет введённый token
#### Scenario: Фоновое чтение не сбрасывает ввод
- **WHEN** пользователь редактирует token в доступной секции, а фоновое чтение статуса временно не удалось
- **THEN** ввод и доступная секция сохраняются без технического предупреждения или уведомления
#### Scenario: Удаление не удалось
- **WHEN** пользователь подтвердил удаление подключения, а действие завершилось ошибкой
- **THEN** клиент не показывает успех, сохраняет известный статус и локальную ошибку, возвращает применимые действия и не повторяет удаление автоматически
+22 -2
View File
@@ -7,7 +7,7 @@
## Requirements
### Requirement: Регистрация пользователя
Система SHALL позволять зарегистрировать User по уникальному нормализованному email и паролю. Email MUST нормализоваться удалением внешних пробелов и приведением к нижнему регистру; он MUST содержать не более 254 символов, ровно один `@`, непустые local и domain части и не содержать пробельных или управляющих символов. Пароль MUST передаваться без изменения, содержать от 12 до 72 UTF-8 байтов и не иметь требований к составу символов. Успешная регистрация MUST создать первую Session и вернуть профиль `{ id, email }` и access token.
Система SHALL позволять зарегистрировать User по уникальному нормализованному email и паролю. Email MUST нормализоваться удалением внешних пробелов и приведением к нижнему регистру; он MUST содержать не более 254 символов, ровно один `@`, непустые local и domain части и не содержать пробельных или управляющих символов. Пароль MUST передаваться без изменения (без trim и нормализации) и состоять из 8–72 печатных ASCII-символов U+0020–U+007E (латинские буквы, цифры, спецсимволы и пробел); другие символы, включая не-ASCII и управляющие, MUST отвергаться. Требований к составу символов (обязательных классов) нет. Успешная регистрация MUST создать первую Session и вернуть профиль `{ id, email }` и access token.
#### Scenario: Успешная регистрация
- **WHEN** пользователь отправляет новый email и валидный пароль
@@ -29,8 +29,20 @@
- **WHEN** пользователь отправляет email или пароль, не соответствующий правилам регистрации
- **THEN** система отвечает `400` Problem Details с code `VALIDATION_ERROR` и указателями на невалидные поля
#### Scenario: Границы длины пароля при регистрации
- **WHEN** пользователь регистрируется с паролем из 7, 8, 72 или 73 печатных ASCII-символов
- **THEN** пароли из 8 и 72 символов принимаются, а из 7 и 73 отвергаются `400` Problem Details с code `VALIDATION_ERROR` и указателем `#/password`
#### Scenario: Недопустимые символы пароля при регистрации
- **WHEN** пользователь регистрирует пароль с кириллицей, emoji, табуляцией, переводом строки или DEL
- **THEN** система отвечает `400` Problem Details с code `VALIDATION_ERROR` и указателем `#/password`, не создавая User
#### Scenario: Пробелы пароля не изменяются
- **WHEN** пользователь регистрирует пароль с пробелами в начале или конце
- **THEN** система принимает пароль как есть, а вход требует тот же пароль с теми же пробелами
### Requirement: Вход пользователя
Система SHALL позволять User начать новую независимую Session по нормализованному email и паролю. Успешный вход MUST вернуть `{ id, email }`, access token и установить refresh-cookie. Неверный пароль и неизвестный email MUST иметь одинаковый ответ, не раскрывающий наличие User.
Система SHALL позволять User начать новую независимую Session по нормализованному email и паролю. Успешный вход MUST вернуть `{ id, email }`, access token и установить refresh-cookie. Неверный пароль и неизвестный email MUST иметь одинаковый ответ, не раскрывающий наличие User. Пароль при входе MUST быть непустой строкой не длиннее 72 UTF-8 байтов; минимальная длина и набор символов при входе MUST NOT проверяться, чтобы User, зарегистрированный по прежним правилам (в том числе с не-ASCII паролем), мог войти.
#### Scenario: Успешный вход
- **WHEN** пользователь отправляет зарегистрированный email и правильный пароль
@@ -40,6 +52,14 @@
- **WHEN** пользователь отправляет неизвестный email или неверный пароль
- **THEN** система отвечает `401` Problem Details с code `INVALID_CREDENTIALS`
#### Scenario: Вход с прежним паролем
- **WHEN** User, зарегистрированный по прежним правилам с не-ASCII паролем, отправляет правильный пароль
- **THEN** система выполняет вход, не применяя к паролю правила регистрации
#### Scenario: Пароль при входе длиннее 72 байтов
- **WHEN** пользователь отправляет пароль длиннее 72 UTF-8 байтов
- **THEN** система отвечает `400` Problem Details с code `VALIDATION_ERROR` и указателем `#/password`
### Requirement: Access и refresh сессии
Система SHALL выдавать access token сроком 15 минут и refresh token сроком 7 дней. Access token MUST передаваться клиенту только в ответе API и использоваться как Bearer token. Refresh token MUST передаваться только в `httpOnly`, `SameSite=Lax`, host-only cookie `refresh_token` с path `/api/v1/auth`; в HTTPS-среде cookie MUST иметь атрибут `Secure`. Каждый новый refresh token MUST иметь уникальный JWT ID и ротироваться при успешном обновлении Session; Session MUST оставаться активной 7 дней после последнего успешного обновления. Одновременные Session одного User MUST быть независимы и не иметь числового лимита.
+182 -615
View File
File diff suppressed because it is too large Load Diff

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