Author SHA1 Message Date
ksv741 e6c80ffa17 chore(release): 0.2.2
Conventional commits / commits (pull_request) Successful in 10s
Specs security / security (pull_request) Successful in 7s
Specs verify / verify (pull_request) Successful in 11s
Specs security / security (push) Successful in 7s
Specs verify / verify (push) Successful in 14s
2026-11-13 07:49:24 +03:00
ksv741 584a41c23b fix(deps): patch basic-ftp advisory (WealthLedger/main#56)
Conventional commits / commits (pull_request) Successful in 9s
Specs security / security (pull_request) Successful in 7s
Specs verify / verify (pull_request) Successful in 10s
Specs security / security (push) Successful in 7s
Specs verify / verify (push) Successful in 9s
2026-11-13 07:41:49 +03:00
codex-bot b6a9ff2d10 spec: require RUB-only behavior (WealthLedger/main#55) (#86)
Specs security / security (push) Successful in 8s
Specs verify / verify (push) Successful in 11s
2026-11-11 09:20:25 +03:00
codex-bot 892800301b spec: plan investment performance vs Moscow Exchange index (WealthLedger/main#54) (#85)
Specs security / security (push) Successful in 8s
Specs verify / verify (push) Successful in 11s
2026-11-08 16:30:32 +03:00
ksv741 1b20067ca7 chore(release): 0.2.1
Conventional commits / commits (pull_request) Successful in 9s
Specs security / security (pull_request) Successful in 6s
Specs verify / verify (pull_request) Successful in 9s
Specs security / security (push) Successful in 6s
Specs verify / verify (push) Successful in 9s
2026-11-06 07:41:32 +03:00
codex-bot 142f7aa225 spec: align Events and CURRENCY portfolio behavior (WealthLedger/specs#60)
Specs security / security (push) Successful in 6s
Specs verify / verify (push) Successful in 11s
2026-11-05 07:16:08 +03:00
codex-bot 8fc8e0a750 chore(deps): set Renovate release age, PR limit and update groups (WealthLedger/main#6) (#81)
Specs security / security (push) Successful in 7s
Specs verify / verify (push) Successful in 10s
2026-11-03 14:07:42 +03:00
codex-bot 0469756d3b spec: allow warning and retry after exhausted read retries (WealthLedger/specs#79) (#80)
Specs security / security (push) Successful in 7s
Specs verify / verify (push) Successful in 10s
2026-11-03 07:45:09 +03:00
codex-bot 8b2b742e52 chore: drop OpenSpec CLI version pin and strict validation (WealthLedger/specs#67) (#78)
Specs security / security (push) Successful in 6s
Specs verify / verify (push) Successful in 9s
Conventional commits / commits (pull_request) Canceled after 0s
Specs security / security (pull_request) Canceled after 0s
Specs verify / verify (pull_request) Canceled after 0s
2026-11-03 06:52:48 +03:00
codex-bot aa1a57ac20 spec: treat MOEX response without requested block as source failure (WealthLedger/specs#76) (#77)
Specs security / security (push) Successful in 7s
Specs verify / verify (push) Successful in 12s
2026-11-03 06:12:27 +03:00
codex-bot 3821d9ed58 refactor: group canonical specs by domain and archive account-top-positions (#74)
Specs security / security (push) Successful in 7s
Specs verify / verify (push) Successful in 14s
2026-11-02 21:50:49 +03:00
codex-bot 1fd7e6ad97 ci: run native OpenSpec and local link validation in verify (WealthLedger/specs#2)
Specs security / security (push) Successful in 8s
Specs verify / verify (push) Successful in 15s
npm run validate: toolchain check, openspec schema validate, openspec validate --all --strict, lychee --offline --include-fragments for root *.md and openspec/specs. CI verify installs the pinned CLI and sha256-pinned lychee and runs it.

Closes #2
2026-11-02 21:06:17 +03:00
codex-bot 7f73de76ac ci: check whitespace in committed PR/push range (WealthLedger/specs#2)
Specs security / security (push) Successful in 7s
Specs verify / verify (push) Successful in 3s
verify ran git diff --check on a clean checkout (always no-op). Now checks PR merge-base..head or push before..after; zero before falls back to the last commit. Node setup removed from the git-only job.
2026-11-02 20:36:58 +03:00
ksv741 97f358545f chore(release): 0.2.0
Conventional commits / commits (pull_request) Successful in 9s
Specs security / security (pull_request) Successful in 7s
Specs verify / verify (pull_request) Successful in 8s
Specs security / security (push) Successful in 7s
Specs verify / verify (push) Successful in 8s
2026-11-01 19:34:04 +03:00
codex-bot c66a5cd962 spec: record account-top-positions planning and implementation (WealthLedger/main#48) (#69)
Specs security / security (push) Successful in 7s
Specs verify / verify (push) Successful in 9s
2026-11-01 19:29:11 +03:00
codex-bot b9ec11f5ba refactor: make sdd-lab review-driven (WealthLedger/specs#66)
Specs security / security (push) Successful in 8s
Specs verify / verify (push) Successful in 10s
Replace executable schema checks with explicit review-driven readiness, add a requirement-writing profile, and incorporate review clarifications for source traceability and comparative validation.
2026-10-30 13:35:39 +03:00
codex-bot a2d2df3b5a spec: archive group-account-rows-by-instrument and sync canonical specs (WealthLedger/main#47) (#65)
Specs security / security (push) Successful in 6s
Specs verify / verify (push) Successful in 7s
2026-10-30 07:35:30 +03:00
codex-bot f3a6b78461 spec: plan grouping of all-account rows by instrument (WealthLedger/main#47) (#64)
Specs security / security (push) Successful in 7s
Specs verify / verify (push) Successful in 9s
2026-10-30 07:28:31 +03:00
codex-bot 322ddf6a7f feat: add experimental wealth-ledger-sdd-lab OpenSpec schema (#61)
Specs security / security (push) Successful in 6s
Specs verify / verify (push) Successful in 7s
2026-10-29 05:58:29 +03:00
ksv741 093185d44d chore(release): 0.1.6
Conventional commits / commits (pull_request) Successful in 10s
Specs security / security (pull_request) Successful in 8s
Specs verify / verify (pull_request) Successful in 10s
Specs security / security (push) Successful in 7s
Specs verify / verify (push) Successful in 9s
2026-10-24 19:15:05 +03:00
renovate-bot 08646ff505 chore(deps): update non-major dependencies (WealthLedger/specs#56)
Specs security / security (push) Successful in 16s
Specs verify / verify (push) Successful in 11s
Co-authored-by: renovate-bot <renovate-bot@ksv741.ru>
2026-10-24 19:04:54 +03:00
codex-bot c5b9cadbbd chore: bump Node to 24.22.1 (WealthLedger/backend#122)
Specs security / security (push) Successful in 10s
Specs verify / verify (push) Successful in 13s
2026-10-23 16:20:01 +03:00
ksv741 ab24ccc32b chore(release): 0.1.5
Conventional commits / commits (pull_request) Successful in 10s
Specs security / security (pull_request) Successful in 8s
Specs verify / verify (pull_request) Successful in 10s
Specs security / security (push) Successful in 7s
Specs verify / verify (push) Successful in 11s
2026-10-18 16:24:55 +03:00
codex-bot f0478c6d6b spec: archive quiet-user-feedback and sync canonical specs (#50)
Specs security / security (push) Successful in 8s
Specs verify / verify (push) Successful in 10s
Native archive применил 9 added и 12 modified требований без удалений; все 10 сценариев сохранены. Новый canonical spec frontend-user-feedback внесён в AI_INDEX.
2026-10-17 22:18:27 +03:00
codex-bot 5a7e319caf spec: reconcile quiet-user-feedback with source-state requirements (#50)
Specs security / security (push) Successful in 9s
Specs verify / verify (push) Successful in 10s
Переносит change quiet-user-feedback в develop и согласует его delta-блоки с текущими canonical specs. Нормативное поведение: тихое чтение клиента и видимое состояние источника из meta.
2026-10-17 21:46:58 +03:00
codex-bot 06d08e2972 chore: adopt linear git model in specs instructions (#48)
Specs security / security (push) Successful in 7s
Specs verify / verify (push) Successful in 10s
2026-10-17 14:14:17 +03:00
codex-bot 1bd6181585 spec: align user-authentication password requirement with ASCII 8-72 policy (WealthLedger/specs#46) (#47)
Specs security / security (push) Successful in 8s
Specs verify / verify (push) Successful in 10s
2026-10-16 10:47:41 +03:00
codex-bot 0a1db52baf Merge pull request 'chore: synchronize release 0.1.4 into develop (WealthLedger/main#30)' (#45) from chore/30-sync-release into develop
Specs security / security (push) Successful in 6s
Specs verify / verify (push) Successful in 9s
2026-10-16 07:48:20 +03:00
codex-bot 5ca44d16b1 Merge pull request 'chore: bring develop into release sync branch (WealthLedger/main#30)' (#44) from develop into chore/30-sync-release
Conventional commits / commits (pull_request) Successful in 10s
Specs security / security (pull_request) Successful in 7s
Specs verify / verify (pull_request) Successful in 9s
2026-10-16 07:41:05 +03:00
codex-bot 7da2f90e0a chore: make OpenSpec toolchain explicit and diagnosable (WealthLedger/specs#4) (#43)
Specs security / security (push) Successful in 7s
Specs verify / verify (push) Successful in 11s
Conventional commits / commits (pull_request) Successful in 10s
Specs security / security (pull_request) Successful in 7s
Specs verify / verify (pull_request) Successful in 9s
2026-10-15 06:34:41 +03:00
codex-bot 7ebfc510af spec: clarify SDD applies only on explicit request (WealthLedger/specs#1)
Specs security / security (push) Successful in 8s
Specs verify / verify (push) Successful in 10s
2026-10-14 22:43:01 +03:00
codex-bot d5d5480eed spec: add five capability entries to AI_INDEX (WealthLedger/specs#28)
Specs security / security (push) Canceled after 0s
Specs verify / verify (push) Canceled after 0s
2026-10-14 22:43:00 +03:00
codex-bot 4b2806ead5 Merge pull request 'spec: archive three completed changes (main#33, frontend#63, screener)' (#40) from specs/archive-three-changes into develop
Specs security / security (push) Successful in 17s
Specs verify / verify (push) Successful in 10s
2026-10-14 21:46:55 +03:00
ksv741 6efb989ed9 spec: archive three completed changes (main#33, frontend#63, screener)
Conventional commits / commits (pull_request) Successful in 13s
Specs security / security (pull_request) Successful in 8s
Specs verify / verify (pull_request) Successful in 12s
2026-10-14 21:40:26 +03:00
codex-bot 0ed462d157 Merge pull request 'spec: fix upcoming events date filter default and picker bounds (WealthLedger/frontend#63)' (#39) from specs/63-upcoming-events-date-filter into develop
Specs security / security (push) Successful in 9s
Specs verify / verify (push) Successful in 12s
2026-10-14 21:35:02 +03:00
ksv741 b4c0589745 spec: fix upcoming events date filter default and picker bounds (WealthLedger/frontend#63)
Conventional commits / commits (pull_request) Successful in 14s
Specs security / security (pull_request) Successful in 9s
Specs verify / verify (pull_request) Successful in 11s
2026-10-14 21:24:13 +03:00
codex-bot 6c33c770db Merge pull request 'ci: rerun commits proof on PR title edit (WealthLedger/specs#30)' (#38) from fix/30-commits-workflow-edited into develop
Specs security / security (push) Successful in 34s
Specs verify / verify (push) Successful in 11s
2026-10-13 20:33:44 +03:00
ksv741 4e3855fc35 ci: rerun commits proof on PR title edit (WealthLedger/specs#30)
Conventional commits / commits (pull_request) Successful in 12s
Specs security / security (pull_request) Successful in 8s
Specs verify / verify (pull_request) Successful in 14s
2026-10-13 20:18:57 +03:00
codex-bot 6c858a8858 spec: unify income payment cards and drop period comparisons (WealthLedger/main#33) (#37)
Specs security / security (push) Successful in 9s
Specs verify / verify (push) Successful in 12s
spec: unify income payment cards and drop period comparisons (WealthLedger/main#33)
2026-10-13 18:16:27 +03:00
ksv741 aae23d86f0 ci: validate release PR commits independently (WealthLedger/main#30)
Specs security / security (push) Successful in 7s
Specs verify / verify (push) Successful in 9s
Conventional commits / commits (pull_request) Successful in 10s
Specs security / security (pull_request) Successful in 6s
Specs verify / verify (pull_request) Successful in 10s
2026-10-12 23:39:30 +03:00
ksv741 ebc9ca872c chore(release): 0.1.4
Conventional commits / commits (pull_request) Failing after 11s
Specs security / security (pull_request) Successful in 7s
Specs verify / verify (pull_request) Successful in 10s
2026-10-12 23:19:12 +03:00
ksv741 fe295aa73a spec: archive harden-hourly-sync-lifecycle change (WealthLedger/backend#56)
Conventional commits / commits (pull_request) Successful in 10s
Specs security / security (pull_request) Successful in 8s
Specs verify / verify (pull_request) Successful in 9s
Specs security / security (push) Successful in 7s
Specs verify / verify (push) Successful in 11s
2026-10-12 22:52:44 +03:00
ksv741 e8e68a7ed6 Merge pull request 'spec: add harden-hourly-sync-lifecycle change (WealthLedger/backend#56)' (#33) from fix/56-harden-hourly-sync-lifecycle into develop
Specs security / security (push) Successful in 7s
Specs verify / verify (push) Successful in 10s
Reviewed-on: #33
2026-10-12 22:29:16 +03:00
ksv741 77b211186c spec: address hourly-sync review findings (WealthLedger/backend#56)
Conventional commits / commits (pull_request) Successful in 14s
Specs security / security (pull_request) Successful in 9s
Specs verify / verify (pull_request) Successful in 11s
2026-10-12 22:19:03 +03:00
ksv741 14174a7bb3 spec: add harden-hourly-sync-lifecycle change (WealthLedger/backend#56)
Conventional commits / commits (pull_request) Successful in 12s
Specs security / security (pull_request) Successful in 7s
Specs verify / verify (pull_request) Successful in 10s
2026-10-12 20:28:58 +03:00
codex-bot 78f01066d2 Merge pull request 'chore: align specs engineering workflow (WealthLedger/main#27)' (#32) from chore/27-engineering-workflow into develop
Specs security / security (push) Successful in 7s
Specs verify / verify (push) Successful in 10s
2026-10-12 11:00:37 +03:00
ksv741 7f12327c37 docs: pin specs workflow reference (WealthLedger/main#27)
Conventional commits / commits (pull_request) Successful in 10s
Specs security / security (pull_request) Successful in 8s
Specs verify / verify (pull_request) Successful in 10s
2026-10-12 10:22:13 +03:00
ksv741 a29cbf6aae fix: align specs commitlint workflow (WealthLedger/main#27)
Conventional commits / commits (pull_request) Canceled after 0s
Specs security / security (pull_request) Canceled after 0s
Specs verify / verify (pull_request) Canceled after 0s
2026-10-12 10:21:32 +03:00
ksv741 a441b08378 chore: align specs engineering workflow (WealthLedger/main#27)
Conventional commits / commits (pull_request) Successful in 11s
Specs security / security (pull_request) Successful in 7s
Specs verify / verify (pull_request) Successful in 9s
2026-10-12 09:21:12 +03:00
codex-bot ea7beea863 Merge pull request 'docs: clarify specs Node bootstrap' (#31) from codex/issue-24-process into develop
Specs security / security (push) Successful in 6s
Specs verify / verify (push) Successful in 9s
2026-10-11 23:23:10 +03:00
ksv741 18518e0d2d docs: clarify specs Node bootstrap
Conventional commits / commits (pull_request) Successful in 11s
Specs security / security (pull_request) Successful in 8s
Specs verify / verify (pull_request) Successful in 11s
2026-10-11 23:06:51 +03:00
codex-bot 0b23fc3013 spec: add stock screener
Specs security / security (push) Successful in 7s
Specs verify / verify (push) Successful in 11s
2026-10-11 11:45:09 +03:00
ksv741 fcdc805f6c ci: skip checks on PR metadata edits
Conventional commits / commits (pull_request) Successful in 11s
Specs security / security (pull_request) Successful in 6s
Specs verify / verify (pull_request) Successful in 8s
2026-10-11 11:36:51 +03:00
ksv741 c132770fbb ci: avoid duplicate PR checks
Specs security / security (pull_request) Successful in 7s
Specs verify / verify (pull_request) Successful in 8s
Conventional commits / commits (pull_request) Successful in 9s
2026-10-11 11:24:57 +03:00
ksv741 951f1a9a20 spec: add stock screener
Specs security / security (push) Successful in 7s
Specs verify / verify (push) Successful in 9s
Conventional commits / commits (pull_request) Successful in 12s
Specs security / security (pull_request) Successful in 7s
Specs verify / verify (pull_request) Successful in 9s
2026-10-11 11:18:21 +03:00
ksv741 329d66e767 spec: finalize wealth-ledger SDD migration
Specs security / security (push) Successful in 8s
Specs verify / verify (push) Successful in 11s
2026-10-09 21:14:22 +03:00
codex-bot 90edab6e33 Merge pull request 'spec: define hourly portfolio sync history' (#25) from release/0.1.3 into develop
Specs security / security (push) Successful in 7s
Specs verify / verify (push) Successful in 9s
Merge hourly portfolio sync history specs
2026-10-09 09:19:38 +03:00
ksv741 b24a686188 spec: define hourly portfolio sync history
Specs security / security (push) Successful in 7s
Specs verify / verify (push) Successful in 9s
Conventional commits / commits (pull_request) Successful in 10s
Specs security / security (pull_request) Successful in 7s
Specs verify / verify (pull_request) Successful in 9s
2026-10-09 09:05:04 +03:00
ksv741 1ee31e3b49 chore(release): 0.1.3
Conventional commits / commits (pull_request) Successful in 11s
Specs security / security (pull_request) Successful in 8s
Specs verify / verify (pull_request) Successful in 9s
Specs security / security (push) Successful in 8s
Specs verify / verify (push) Successful in 11s
2026-10-07 18:13:58 +03:00
ksv741 a1b0e99bdd spec: add reduce-screen-sync-polling change
Specs security / security (push) Successful in 8s
Specs verify / verify (push) Successful in 10s
OpenSpec change: screen sync status polling and observe reads run
every five seconds during active sync; terminal state observes
resources immediately.
2026-10-07 14:25:16 +03:00
ksv741 0fd148ffbb chore: run commitlint in commit-msg hook
Conventional commits / commits (pull_request) Successful in 11s
Specs security / security (pull_request) Successful in 8s
Specs verify / verify (pull_request) Successful in 11s
Specs security / security (push) Successful in 8s
Specs verify / verify (push) Successful in 11s
Local commits now get the same commitlint --strict check as CI.
2026-10-07 14:18:14 +03:00
ksv741 33cc4f3e0c fix(deps): patch release tooling advisories
Conventional commits / commits (pull_request) Successful in 11s
Specs security / security (pull_request) Successful in 10s
Specs verify / verify (pull_request) Successful in 9s
Specs security / security (push) Successful in 9s
Specs verify / verify (push) Successful in 10s
Scope patched basic-ftp and undici overrides to release-it.

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

Align portfolio freshness with persisted reads and keep income
consistency at the backend owner.
2026-10-03 11:38:08 +03:00
ksv741 4d580f145a chore(release): 0.1.2
Conventional commits / commits (pull_request) Successful in 11s
Specs security / security (pull_request) Successful in 6s
Specs verify / verify (pull_request) Successful in 10s
Specs security / security (push) Successful in 7s
Specs verify / verify (push) Successful in 9s
2026-09-26 22:10:11 +03:00
ksv741 2f5814abdd spec: complete persist-tbank-data implementation
Conventional commits / commits (pull_request) Successful in 12s
Specs security / security (pull_request) Successful in 7s
Specs verify / verify (pull_request) Successful in 11s
Specs security / security (push) Successful in 7s
Specs verify / verify (push) Successful in 9s
Record the approved proposal, delta specifications, design and 27 verified
implementation tasks. Keep the change active; archiving and main-spec synchronization
are deferred.
2026-09-26 21:29:24 +03:00
ksv741 ba76f6fccc spec: complete local frontend stack evolution
Specs security / security (pull_request) Successful in 7s
Specs verify / verify (pull_request) Successful in 11s
Conventional commits / commits (pull_request) Successful in 11s
Specs security / security (push) Successful in 7s
Specs verify / verify (push) Successful in 9s
OpenSpec change: evolve-frontend-stack
2026-09-22 22:11:05 +03:00
ksv741 7f40372c3f spec: define versioned deployment bundle
Conventional commits / commits (pull_request) Successful in 12s
Specs security / security (pull_request) Successful in 8s
Specs verify / verify (pull_request) Successful in 11s
Specs security / security (push) Successful in 7s
Specs verify / verify (push) Successful in 10s
2026-09-20 18:49:25 +03:00
ksv741 e7a3c30b5d chore(release): 0.1.1
Conventional commits / commits (pull_request) Successful in 13s
Specs security / security (pull_request) Successful in 7s
Specs verify / verify (pull_request) Successful in 9s
Specs security / security (push) Successful in 7s
Specs verify / verify (push) Successful in 10s
2026-09-16 11:35:57 +03:00
ksv741 b34f23a230 spec: define containerized application delivery #10
Specs security / security (pull_request) Successful in 6s
Specs verify / verify (pull_request) Successful in 8s
Conventional commits / commits (pull_request) Successful in 11s
Specs security / security (push) Successful in 6s
Specs verify / verify (push) Successful in 9s
2026-09-16 09:41:59 +03:00
ksv741 e4f10dedef docs: make specs instructions self-contained
Specs security / security (pull_request) Successful in 7s
Specs verify / verify (pull_request) Successful in 10s
Conventional commits / commits (pull_request) Successful in 10s
Specs security / security (push) Successful in 7s
Specs verify / verify (push) Successful in 10s
2026-09-15 20:45:41 +03:00
ksv741 9e227a563d chore(release): 0.1.0
Conventional commits / commits (pull_request) Successful in 12s
Specs security / security (pull_request) Successful in 7s
Specs verify / verify (pull_request) Successful in 10s
Specs security / security (push) Successful in 8s
Specs verify / verify (push) Successful in 11s
Include the reviewed historical OpenAPI fixture fingerprint in the release candidate. Keep full-history scanning enabled.
2026-09-14 07:09:53 +03:00
ksv741 246c754257 chore(release): 0.1.0
Specs security / security (push) Failing after 5s
Specs verify / verify (push) Successful in 9s
Conventional commits / commits (pull_request) Successful in 12s
Specs security / security (pull_request) Failing after 6s
Specs verify / verify (pull_request) Successful in 11s
2026-09-14 07:04:04 +03:00
ksv741 3c7c646f13 chore: configure releases and develop workflow 2026-09-14 07:01:15 +03:00
ksv741 50658ea8be docs: update system documentation references 2026-09-14 05:52:11 +03:00
ksv741 05e314cebb docs(specs): remove runtime OpenAPI gate 2026-09-13 19:08:13 +03:00
ksv741 9590dedc01 docs: align runtime requirements with Node 24 LTS 2026-09-13 15:05:40 +03:00
ksv741 834f2d146c docs: record completed manual bank card change
Track the approved proposal, delta specification and design with all 29 verified implementation tasks. Keep the change active without archiving or syncing main specifications.
2026-09-12 18:34:20 +03:00
ksv741 43a5577e72 docs: link to unified documentation entry 2026-09-12 14:45:48 +03:00
ksv741 bcb10da21c docs(ai): archive superseded context routing plan 2026-09-09 16:23:19 +03:00
ksv741 fbb7193e90 spec: archive income page simplification with evidence 2026-09-09 13:54:09 +03:00
ksv741 4c905c1433 spec: record approved income page simplification 2026-09-09 13:50:45 +03:00
ksv741 0608d4dc23 spec: remove income analytics period limit 2026-09-09 11:10:11 +03:00
ksv741 d5c18258c4 spec: archive compact portfolio totals 2026-09-09 10:32:27 +03:00
ksv741 b06a35bbaa spec: deliver compact portfolio totals package 2026-09-09 10:31:09 +03:00
ksv741 19a658e318 spec: archive local design token simplification 2026-09-08 20:20:51 +03:00
ksv741 f171b8bb46 spec: simplify local design token validation contract 2026-09-08 20:18:40 +03:00
306 changed files with 35490 additions and 2336 deletions
+12
View File
@@ -0,0 +1,12 @@
---
name: Bug Report
about: Неверное поведение
---
## Проблема и шаги воспроизведения
## Факт / ожидание
## Acceptance Criteria
- [ ]
## Окружение / ссылки (если применимо)
+10
View File
@@ -0,0 +1,10 @@
---
name: Engineering Task
about: CI, тесты, документация, процесс и прочее
---
## Контекст и результат
## Scope / связанные Issues (если есть)
## Acceptance Criteria
- [ ]
+12
View File
@@ -0,0 +1,12 @@
---
name: Feature Request
about: Новое наблюдаемое поведение
---
## Зачем и какой результат
## Ожидаемое поведение
## Acceptance Criteria
- [ ]
## Зависимости / связанные Issues (если есть)
+12
View File
@@ -0,0 +1,12 @@
---
name: Refactoring / Technical Debt
about: Улучшение без нового поведения
---
## Проблема и результат
## Сохраняемые инварианты / границы
## Acceptance Criteria
- [ ]
## Зависимости / ссылки (если есть)
+11
View File
@@ -0,0 +1,11 @@
## Summary
## Linked Issue
Refs
## Changes
## Verification
- Команда / результат / ограничения:
## Breaking Changes (если есть)
+34
View File
@@ -0,0 +1,34 @@
name: Conventional commits
on:
pull_request:
types: [opened, synchronize, reopened, edited]
concurrency:
group: specs-commits-${{ gitea.event.pull_request.number }}
cancel-in-progress: true
jobs:
commits:
runs-on: ubuntu-latest
steps:
- uses: https://github.com/actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0
- uses: https://github.com/actions/setup-node@v4
with:
node-version-file: .nvmrc
- run: npm ci --ignore-scripts
- name: Check PR title and commits
env:
PR_TITLE: ${{ github.event.pull_request.title }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
printf '%s\n' "$PR_TITLE" | npm run commitlint -- --strict
if [[ "$PR_TITLE" =~ ^chore\(release\):\ [0-9]+\.[0-9]+\.[0-9]+$ ]]; then
npm run commitlint -- --strict --from "$HEAD_SHA^" --to "$HEAD_SHA"
else
npm run commitlint -- --strict --from "$BASE_SHA" --to "$HEAD_SHA"
fi
+35
View File
@@ -0,0 +1,35 @@
name: Specs security
on:
push:
branches: [develop, main]
pull_request:
types: [opened, synchronize, reopened]
schedule:
- cron: '17 5 * * *'
concurrency:
group: specs-security-${{ gitea.event_name }}-${{ gitea.event.pull_request.number || gitea.ref }}
cancel-in-progress: true
jobs:
security:
runs-on: ubuntu-latest
steps:
- uses: https://github.com/actions/checkout@v4
with:
fetch-depth: 0
- name: Record checked revision
run: git rev-parse HEAD
- name: Scan history with Gitleaks 8.30.1
run: |
curl --fail --location --silent --show-error https://github.com/gitleaks/gitleaks/releases/download/v8.30.1/gitleaks_8.30.1_linux_x64.tar.gz -o /tmp/gitleaks.tar.gz
echo '551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb /tmp/gitleaks.tar.gz' | sha256sum --check
mkdir -p /tmp/gitleaks-bin
tar -xzf /tmp/gitleaks.tar.gz -C /tmp/gitleaks-bin gitleaks
/tmp/gitleaks-bin/gitleaks git --log-opts=--all --redact=100 --no-banner
- uses: https://github.com/actions/setup-node@v4
with:
node-version-file: .nvmrc
- name: Audit runtime and development dependencies
run: npm audit --include=dev --audit-level=low
+53
View File
@@ -0,0 +1,53 @@
name: Specs verify
on:
push:
branches: [develop, main]
pull_request:
types: [opened, synchronize, reopened]
concurrency:
group: specs-verify-${{ gitea.event_name }}-${{ gitea.event.pull_request.number || gitea.ref }}
cancel-in-progress: true
jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: https://github.com/actions/checkout@v4
with:
fetch-depth: 0
- name: Check whitespace in committed range
env:
PR_BASE: ${{ github.event.pull_request.base.sha }}
PR_HEAD: ${{ github.event.pull_request.head.sha }}
PUSH_BEFORE: ${{ github.event.before }}
PUSH_AFTER: ${{ github.sha }}
run: |
if [ -n "$PR_HEAD" ]; then
range="$PR_BASE...$PR_HEAD"
elif [ -n "$PUSH_BEFORE" ] && ! expr "$PUSH_BEFORE" : '0*$' >/dev/null; then
range="$PUSH_BEFORE..$PUSH_AFTER"
else
# New branch / unknown before: only the pushed commit; full tree would hit read-only archives.
range="$PUSH_AFTER^..$PUSH_AFTER"
fi
echo "Checked revision: $(git rev-parse HEAD); range: $range"
git diff --check "$range"
- uses: https://github.com/actions/setup-node@v4
with:
node-version-file: .nvmrc
- name: Install OpenSpec CLI and lychee 0.24.2
run: |
npm install -g @fission-ai/openspec
openspec store register .
curl --fail --location --silent --show-error https://github.com/lycheeverse/lychee/releases/download/lychee-v0.24.2/lychee-x86_64-unknown-linux-gnu.tar.gz -o /tmp/lychee.tar.gz
echo '1f4e0ef7f6554a6ed33dd7ac144fb2e1bbed98598e7af973042fc5cd43951c9a /tmp/lychee.tar.gz' | sha256sum --check
tar -xzf /tmp/lychee.tar.gz -C /tmp
echo /tmp/lychee-x86_64-unknown-linux-gnu >> "$GITHUB_PATH"
env:
OPENSPEC_TELEMETRY: '0'
- name: Validate schemas, specs, changes and local links
run: npm run validate
env:
OPENSPEC_TELEMETRY: '0'
+2
View File
@@ -0,0 +1,2 @@
#!/bin/sh
npx --no-install commitlint --edit "$1" --strict
+1
View File
@@ -0,0 +1 @@
node_modules/
+4
View File
@@ -0,0 +1,4 @@
# Reviewed historical false positive: the removed runtime OpenAPI check used
# base64 of 0123456789abcdef repeated twice for its disposable SQLite database.
# This fingerprint covers only that historical line; new commits remain scanned.
72444dbbb11ae267e349adbf35256bd7c5be1489:scripts/verify-runtime-openapi.sh:generic-api-key:57
+1
View File
@@ -0,0 +1 @@
24.22.1
+106
View File
@@ -0,0 +1,106 @@
{
"git": {
"requireBranch": "release/*",
"requireCleanWorkingDir": true,
"requireUpstream": false,
"requireCommits": true,
"commitMessage": "chore(release): ${version}",
"tagName": "v${version}",
"tag": false,
"push": false
},
"npm": {
"publish": false
},
"github": {
"release": false
},
"gitlab": {
"release": false
},
"hooks": {
"after:git:init": "node scripts/check-release.cjs"
},
"plugins": {
"@release-it/conventional-changelog": {
"infile": "CHANGELOG.md",
"preset": {
"name": "conventionalcommits",
"types": [
{
"type": "chore",
"scope": "release",
"effect": "hidden"
},
{
"type": "feat",
"section": "Features",
"effect": "bump"
},
{
"type": "fix",
"section": "Bug Fixes",
"effect": "bump"
},
{
"type": "perf",
"section": "Performance",
"effect": "bump"
},
{
"type": "docs",
"section": "Documentation",
"effect": "bump"
},
{
"type": "spec",
"section": "Specifications",
"effect": "bump"
},
{
"type": "refactor",
"section": "Refactoring",
"effect": "bump"
},
{
"type": "build",
"section": "Build",
"effect": "bump"
},
{
"type": "ci",
"section": "CI",
"effect": "bump"
},
{
"type": "test",
"section": "Tests",
"effect": "bump"
},
{
"type": "style",
"section": "Styles",
"effect": "bump"
},
{
"type": "chore",
"section": "Maintenance",
"effect": "bump"
},
{
"type": "revert",
"section": "Reverts",
"effect": "bump"
}
]
},
"context": {
"host": "https://git.personal.ksv741.ru",
"owner": "WealthLedger",
"repository": "specs",
"linkReferences": true,
"commit": "commit"
}
}
}
}
+32 -37
View File
@@ -1,43 +1,38 @@
# Specs
## Назначение и authority
Репозиторий содержит существующие behavioral specifications и SDD history.
Для обычной разработки не создавай и не обновляй planning artifacts ради
процесса. До edits найди/создай подходящую Gitea Issue, проверь AC и ветку;
чтение допускается без Issue. Выбор lightweight/SDD — по
[Engineering Workflow](https://git.personal.ksv741.ru/WealthLedger/main/src/commit/03b3b59e6ff2ba391f9dfc42a039a96d0d84c1c9/.ai/conventions/engineering-workflow.md)
(main#28; после merge — актуальная версия `develop`);
OpenSpec lifecycle начинается только после явного решения Change Owner.
Этот репозиторий — единственный versioned authority для change artifacts.
Workspace layout и reference policy — в root `AGENTS.md`.
Если нужна существующая behavioral spec, выбери точный target через
[AI_INDEX](AI_INDEX.md). Specs не являются вторым editable HTTP OpenAPI:
HTTP-провайдер публикует контракт, а потребитель получает отдельный artifact
и локально проверяет generated types. Specs фиксируют behavioral requirements;
другие checkout не нужны для чтения и native validation этого store.
Canonical OpenSpec store — `wealth-ledger-specs`, физический root от workspace —
`specs/openspec` (локально `openspec`). OpenSpec commands, читающие или
изменяющие change, всегда получают `--store wealth-ledger-specs`.
Архивы и чужие active changes сохраняй. Для правки индекса или specs выполни
`npm run validate` (targets/anchors, native checks) и `git diff --check`;
application gates принадлежат affected repos.
Если явно запрошена работа через OpenSpec, следуй
[локальным инструкциям OpenSpec](README.md#начало-работы)
и используй store `wealth-ledger-specs`. Перед Node-командами активируй
местную `.nvmrc`: если `nvm` не загружен в non-interactive shell, загрузи
установленный `$HOME/.nvm/nvm.sh`, затем выполни `nvm use --silent` из specs.
Если `nvm.sh` недоступен, остановись; не используй системный Node.
Proposal, Delta Specs, Design, Tasks, changes, Completion Evidence и archives
хранятся в `openspec`; `docs` владеет as-built документацией и общей policy.
## Ветки и релизы
## Workflow и verification
Новые changes используют project-local schema `wealth-ledger-sdd`.
Для planning, implementation, validation и завершения читай canonical lifecycle:
[`../docs/development/openspec-authority-and-lifecycle.md`](../docs/development/openspec-authority-and-lifecycle.md).
Сначала определить current stage и capability paths через native OpenSpec
instruction; читать main specs и supporting docs только для этих paths и
affected areas. Recovery и выбор relevant evidence определяет context discipline
из [`../docs/development/agent-token-efficiency.md`](../docs/development/agent-token-efficiency.md),
а не дублируется в schema instructions.
Для structural validation из `specs`:
```bash
openspec schema validate wealth-ledger-sdd --verbose
openspec validate <change> --type change --strict --store wealth-ledger-specs
openspec validate --specs --strict --store wealth-ledger-specs
```
Под Node 22: если локальной `.nvmrc` нет, активировать frontend `.nvmrc`
через `nvm use --silent` до перехода в specs. Native structural gate проверяет
форму; terminal safeguards определяет canonical lifecycle, доступный без skills.
## Boundaries
- Design содержит Delivery Map `frontend`, `backend`, `specs`, `docs` и условный Solution Assessment.
- Specs описывают observable behavior, не изменения кода; Design — architecture, не line-by-line implementation; Tasks — implementation checklist без будущего evidence.
- Application code и durable ADR принадлежат affected application/docs repos.
Работа — в issue-linked ветке от `develop`, затем PR в `develop`; в `main` напрямую
не пушить. 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; версии вручную не повышать.
+51
View File
@@ -0,0 +1,51 @@
# Behavioral specifications index
Эти существующие specs — источник сохранённых требований по конкретной теме.
Читай нужный requirement/scenario, если задача зависит от него; не загружай всё
дерево и не создавай OpenSpec change для обычной разработки.
Этот индекс и основные specs читаются без соседних checkout. Внешние
сведения о реализации служат справочными свидетельствами, когда предоставлены.
При конфликте с текущим поведением установи расхождение; не копируй устаревшее
правило или случайный код молча.
HTTP OpenAPI имеет единственный editable source в backend, а не здесь.
Потребитель доверяет предоставленному artifact и локально проверяет generated
types. CI каждого участника использует только его собственные входы.
Specs сгруппированы по доменам: `openspec/specs/<domain>/<capability>/spec.md`.
Новая capability кладётся в существующий домен; новый домен — только для
отдельного бизнес-контекста. Процессные правила сюда не входят.
| Домен | Тема / trigger | Existing spec |
| --- | --- | --- |
| identity | Auth/session/refresh/error contract | [User authentication](openspec/specs/identity/user-authentication/spec.md) |
| tbank | T-Bank connection/security/cache generation | [T-Bank connection](openspec/specs/tbank/tbank-connection/spec.md) |
| tbank | Владелец синхронизации T-Bank, scheduling/priority | [T-Bank data sync](openspec/specs/tbank/tbank-data-sync/spec.md) |
| tbank | Часовой обход, результат последней попытки | [Hourly investment sync](openspec/specs/tbank/hourly-investment-sync/spec.md) |
| brokerage | Счета/сводки | [Brokerage accounts](openspec/specs/brokerage/brokerage-accounts/spec.md) |
| brokerage | Операции/cursor/filters | [Account ledger](openspec/specs/brokerage/brokerage-account-ledger/spec.md) |
| brokerage | Будущие выплаты | [Upcoming events](openspec/specs/brokerage/brokerage-account-upcoming-events/spec.md) |
| brokerage | Доходы и периоды | [Income analytics](openspec/specs/brokerage/brokerage-account-income-analytics/spec.md) |
| portfolio | Portfolio positions/totals/stale | [Portfolio](openspec/specs/portfolio/portfolio-view/spec.md) |
| portfolio | Вкладка «Концентрация»: топ-5 акций/облигаций/секторов/эмитентов | [Portfolio concentration](openspec/specs/portfolio/portfolio-concentration/spec.md) |
| portfolio | История стоимости Portfolio | [Portfolio value history](openspec/specs/portfolio/portfolio-value-history/spec.md) |
| instruments | Справочные сведения/поиск инструментов (online-only) | [Online instrument information](openspec/specs/instruments/online-instrument-information/spec.md) |
| instruments | Календарь купонов/дивидендов инструмента | [Instrument income schedules](openspec/specs/instruments/instrument-income-schedules/spec.md) |
| instruments | Скринер акций | [Stock screener](openspec/specs/instruments/stock-screener/spec.md) |
| banking | Ручные дебетовые карты | [Manual bank cards](openspec/specs/banking/manual-bank-cards/spec.md) |
| frontend-platform | Каркас приложения, маршруты MVP | [App shell](openspec/specs/frontend-platform/app-shell/spec.md) |
| frontend-platform | Shared tokens/theme | [Design system](openspec/specs/frontend-platform/frontend-design-system/spec.md) |
| frontend-platform | Каталог компонентов | [UI catalog](openspec/specs/frontend-platform/frontend-ui-catalog/spec.md) |
| frontend-platform | Loading/skeleton | [Loading presentation](openspec/specs/frontend-platform/frontend-loading-presentation/spec.md) |
| frontend-platform | UI error/recovery | [UI resilience](openspec/specs/frontend-platform/frontend-ui-resilience/spec.md) |
| frontend-platform | Тихое чтение и редкие уведомления | [User feedback](openspec/specs/frontend-platform/frontend-user-feedback/spec.md) |
| frontend-platform | Query coherence/cache | [Query coherence](openspec/specs/frontend-platform/frontend-query-coherence/spec.md), [data resilience](openspec/specs/frontend-platform/frontend-data-resilience/spec.md) |
| frontend-platform | Breadcrumb/navigation | [Breadcrumbs](openspec/specs/frontend-platform/app-breadcrumb-navigation/spec.md) |
| frontend-platform | Structural boundaries/bundle size/coverage | [Quality guardrails](openspec/specs/frontend-platform/frontend-quality-guardrails/spec.md) |
| backend-platform | `/api/v1`, CORS, безопасные ошибки | [HTTP API foundation](openspec/specs/backend-platform/http-api-foundation/spec.md) |
| backend-platform | OpenAPI generation | [API contract generation](openspec/specs/backend-platform/api-contract-generation/spec.md) |
| backend-platform | Logging/request context | [Structured logging](openspec/specs/backend-platform/backend-structured-logging/spec.md) |
Active changes и archives сохраняют историю прежнего SDD workflow. Они не
являются обязательной процедурой текущей задачи. Для явно запрошенной работы
с OpenSpec используй [отдельный вход](AGENTS.md).
+236
View File
@@ -0,0 +1,236 @@
# Changelog
## [0.2.2](https://git.personal.ksv741.ru/WealthLedger/specs/compare/v0.2.1...v0.2.2) (2026-11-11)
### Specifications
* plan investment performance vs Moscow Exchange index (WealthLedger/main[#54](https://git.personal.ksv741.ru/WealthLedger/specs/issues/54)) ([#85](https://git.personal.ksv741.ru/WealthLedger/specs/issues/85)) ([8928003](https://git.personal.ksv741.ru/WealthLedger/specs/commit/892800301b1631d2e18e72518c5e12e213df21b4))
* require RUB-only behavior (WealthLedger/main[#55](https://git.personal.ksv741.ru/WealthLedger/specs/issues/55)) ([#86](https://git.personal.ksv741.ru/WealthLedger/specs/issues/86)) ([b6a9ff2](https://git.personal.ksv741.ru/WealthLedger/specs/commit/b6a9ff2d10fe1d002b0744e37ff5a3ce1959b098))
## [0.2.1](https://git.personal.ksv741.ru/WealthLedger/specs/compare/v0.2.0...v0.2.1) (2026-11-06)
### Specifications
* align Events and CURRENCY portfolio behavior (WealthLedger/specs[#60](https://git.personal.ksv741.ru/WealthLedger/specs/issues/60)) ([142f7aa](https://git.personal.ksv741.ru/WealthLedger/specs/commit/142f7aa225b4c9ca478ca9b350649df6746d6bf4))
* allow warning and retry after exhausted read retries (WealthLedger/specs[#79](https://git.personal.ksv741.ru/WealthLedger/specs/issues/79)) ([#80](https://git.personal.ksv741.ru/WealthLedger/specs/issues/80)) ([0469756](https://git.personal.ksv741.ru/WealthLedger/specs/commit/0469756d3bc983e32811470b671d743cd5009739))
* treat MOEX response without requested block as source failure (WealthLedger/specs[#76](https://git.personal.ksv741.ru/WealthLedger/specs/issues/76)) ([#77](https://git.personal.ksv741.ru/WealthLedger/specs/issues/77)) ([aa1a57a](https://git.personal.ksv741.ru/WealthLedger/specs/commit/aa1a57ac202eed8352ffed29296432afca56a3e9))
### Refactoring
* group canonical specs by domain and archive account-top-positions ([#74](https://git.personal.ksv741.ru/WealthLedger/specs/issues/74)) ([3821d9e](https://git.personal.ksv741.ru/WealthLedger/specs/commit/3821d9ed587e71e3da662e9b84f0c27bd32ce977))
### CI
* check whitespace in committed PR/push range (WealthLedger/specs[#2](https://git.personal.ksv741.ru/WealthLedger/specs/issues/2)) ([7f73de7](https://git.personal.ksv741.ru/WealthLedger/specs/commit/7f73de76ac0e54327745cad61043922047c421ec))
* run native OpenSpec and local link validation in verify (WealthLedger/specs[#2](https://git.personal.ksv741.ru/WealthLedger/specs/issues/2)) ([1fd7e6a](https://git.personal.ksv741.ru/WealthLedger/specs/commit/1fd7e6ad974193eca3390cf41749368c5c318858))
### Maintenance
* **deps:** set Renovate release age, PR limit and update groups (WealthLedger/main[#6](https://git.personal.ksv741.ru/WealthLedger/specs/issues/6)) ([#81](https://git.personal.ksv741.ru/WealthLedger/specs/issues/81)) ([8fc8e0a](https://git.personal.ksv741.ru/WealthLedger/specs/commit/8fc8e0a750a561b54251add5bac03d5ef58a94e3))
* drop OpenSpec CLI version pin and strict validation (WealthLedger/specs[#67](https://git.personal.ksv741.ru/WealthLedger/specs/issues/67)) ([#78](https://git.personal.ksv741.ru/WealthLedger/specs/issues/78)) ([8b2b742](https://git.personal.ksv741.ru/WealthLedger/specs/commit/8b2b742e52773ee3ab21fafaf6043420927640ac))
## [0.2.0](https://git.personal.ksv741.ru/WealthLedger/specs/compare/v0.1.6...v0.2.0) (2026-11-01)
### Features
* add experimental wealth-ledger-sdd-lab OpenSpec schema ([#61](https://git.personal.ksv741.ru/WealthLedger/specs/issues/61)) ([322ddf6](https://git.personal.ksv741.ru/WealthLedger/specs/commit/322ddf6a7f9b3309a875e17ee37a574c8decb1b0))
### Specifications
* archive group-account-rows-by-instrument and sync canonical specs (WealthLedger/main[#47](https://git.personal.ksv741.ru/WealthLedger/specs/issues/47)) ([#65](https://git.personal.ksv741.ru/WealthLedger/specs/issues/65)) ([a2d2df3](https://git.personal.ksv741.ru/WealthLedger/specs/commit/a2d2df3b5a7fc7f72bdd82ab42bb0ae25debfffe))
* plan grouping of all-account rows by instrument (WealthLedger/main[#47](https://git.personal.ksv741.ru/WealthLedger/specs/issues/47)) ([#64](https://git.personal.ksv741.ru/WealthLedger/specs/issues/64)) ([f3a6b78](https://git.personal.ksv741.ru/WealthLedger/specs/commit/f3a6b78461408083f0ad04bdfe64634c09f53be2))
* record account-top-positions planning and implementation (WealthLedger/main[#48](https://git.personal.ksv741.ru/WealthLedger/specs/issues/48)) ([#69](https://git.personal.ksv741.ru/WealthLedger/specs/issues/69)) ([c66a5cd](https://git.personal.ksv741.ru/WealthLedger/specs/commit/c66a5cd962fd265c3365e8c396a7958ba307bac0))
### Refactoring
* make sdd-lab review-driven (WealthLedger/specs[#66](https://git.personal.ksv741.ru/WealthLedger/specs/issues/66)) ([b9ec11f](https://git.personal.ksv741.ru/WealthLedger/specs/commit/b9ec11f5ba193dc851c79908c5ed8accc34e2e54))
## [0.1.6](https://git.personal.ksv741.ru/WealthLedger/specs/compare/v0.1.5...v0.1.6) (2026-10-24)
### Maintenance
* bump Node to 24.22.1 (WealthLedger/backend[#122](https://git.personal.ksv741.ru/WealthLedger/specs/issues/122)) ([c5b9cad](https://git.personal.ksv741.ru/WealthLedger/specs/commit/c5b9cadbbd2dfbb6f139bc71851117d81c0e7c6d))
* **deps:** update non-major dependencies (WealthLedger/specs[#56](https://git.personal.ksv741.ru/WealthLedger/specs/issues/56)) ([08646ff](https://git.personal.ksv741.ru/WealthLedger/specs/commit/08646ff505eed501e54314c07ca4d1076a4c37dd))
## [0.1.5](https://git.personal.ksv741.ru/WealthLedger/specs/compare/v0.1.4...v0.1.5) (2026-10-18)
### Specifications
* add five capability entries to AI_INDEX (WealthLedger/specs[#28](https://git.personal.ksv741.ru/WealthLedger/specs/issues/28)) ([d5d5480](https://git.personal.ksv741.ru/WealthLedger/specs/commit/d5d5480eedbc25c77c52b3b477de37e373beb8d9))
* align user-authentication password requirement with ASCII 8-72 policy (WealthLedger/specs[#46](https://git.personal.ksv741.ru/WealthLedger/specs/issues/46)) ([#47](https://git.personal.ksv741.ru/WealthLedger/specs/issues/47)) ([1bd6181](https://git.personal.ksv741.ru/WealthLedger/specs/commit/1bd618158588871f1801bfc6bead82fba31ce0ae))
* archive quiet-user-feedback and sync canonical specs ([#50](https://git.personal.ksv741.ru/WealthLedger/specs/issues/50)) ([f0478c6](https://git.personal.ksv741.ru/WealthLedger/specs/commit/f0478c6d6b5a5f259ea766276295758b34d240b3))
* archive three completed changes (main[#33](https://git.personal.ksv741.ru/WealthLedger/specs/issues/33), frontend[#63](https://git.personal.ksv741.ru/WealthLedger/specs/issues/63), screener) ([6efb989](https://git.personal.ksv741.ru/WealthLedger/specs/commit/6efb989ed9051981a314c8eb5714dffa88195a80))
* clarify SDD applies only on explicit request (WealthLedger/specs[#1](https://git.personal.ksv741.ru/WealthLedger/specs/issues/1)) ([7ebfc51](https://git.personal.ksv741.ru/WealthLedger/specs/commit/7ebfc510afb7440f85ecf95e2891d84c1b8ad843))
* fix upcoming events date filter default and picker bounds (WealthLedger/frontend[#63](https://git.personal.ksv741.ru/WealthLedger/specs/issues/63)) ([b4c0589](https://git.personal.ksv741.ru/WealthLedger/specs/commit/b4c0589745e0d1fb6b482addbea82d06c4924dad))
* reconcile quiet-user-feedback with source-state requirements ([#50](https://git.personal.ksv741.ru/WealthLedger/specs/issues/50)) ([5a7e319](https://git.personal.ksv741.ru/WealthLedger/specs/commit/5a7e319cafd8d9b6861b5a6bab7085bb57b08576))
* unify income payment cards and drop period comparisons (WealthLedger/main[#33](https://git.personal.ksv741.ru/WealthLedger/specs/issues/33)) ([#37](https://git.personal.ksv741.ru/WealthLedger/specs/issues/37)) ([6c858a8](https://git.personal.ksv741.ru/WealthLedger/specs/commit/6c858a8858dfdfc5c9607b3f925de5900ef1c903))
### CI
* rerun commits proof on PR title edit (WealthLedger/specs[#30](https://git.personal.ksv741.ru/WealthLedger/specs/issues/30)) ([4e3855f](https://git.personal.ksv741.ru/WealthLedger/specs/commit/4e3855fc3526e2b0b7ba80fd23b996d9993edcf5))
### Maintenance
* adopt linear git model in specs instructions ([#48](https://git.personal.ksv741.ru/WealthLedger/specs/issues/48)) ([06d08e2](https://git.personal.ksv741.ru/WealthLedger/specs/commit/06d08e29724e6cbe95eeca5ee1e6af7ec365a559))
* make OpenSpec toolchain explicit and diagnosable (WealthLedger/specs[#4](https://git.personal.ksv741.ru/WealthLedger/specs/issues/4)) ([#43](https://git.personal.ksv741.ru/WealthLedger/specs/issues/43)) ([7da2f90](https://git.personal.ksv741.ru/WealthLedger/specs/commit/7da2f90e0a6a7e0e5cb40b371c965a772aaa5db2))
## [0.1.4](https://git.personal.ksv741.ru/WealthLedger/specs/compare/v0.1.3...v0.1.4) (2026-10-12)
### Bug Fixes
* align specs commitlint workflow (WealthLedger/main[#27](https://git.personal.ksv741.ru/WealthLedger/specs/issues/27)) ([a29cbf6](https://git.personal.ksv741.ru/WealthLedger/specs/commit/a29cbf6aae7ebe48a51ae1979ff3f30b9fa8e305))
### Documentation
* clarify specs Node bootstrap ([18518e0](https://git.personal.ksv741.ru/WealthLedger/specs/commit/18518e0d2daecb843fea1b4d55d1c7d2cabf24f6))
* pin specs workflow reference (WealthLedger/main[#27](https://git.personal.ksv741.ru/WealthLedger/specs/issues/27)) ([7f12327](https://git.personal.ksv741.ru/WealthLedger/specs/commit/7f12327c37055ba903f962b1220138da9c7edd06))
### Specifications
* add harden-hourly-sync-lifecycle change (WealthLedger/backend[#56](https://git.personal.ksv741.ru/WealthLedger/specs/issues/56)) ([14174a7](https://git.personal.ksv741.ru/WealthLedger/specs/commit/14174a7bb335152c8cc76d9a6bbaefaaaa58b22f))
* add stock screener ([951f1a9](https://git.personal.ksv741.ru/WealthLedger/specs/commit/951f1a9a20a09695e2ff85d1ee96632e98a202d8))
* address hourly-sync review findings (WealthLedger/backend[#56](https://git.personal.ksv741.ru/WealthLedger/specs/issues/56)) ([77b2111](https://git.personal.ksv741.ru/WealthLedger/specs/commit/77b211186c297df4187e044804786ffc9096ce19))
* archive harden-hourly-sync-lifecycle change (WealthLedger/backend[#56](https://git.personal.ksv741.ru/WealthLedger/specs/issues/56)) ([fe295aa](https://git.personal.ksv741.ru/WealthLedger/specs/commit/fe295aa73ab5bc9d826067892930b7161fb4f1b9))
* define hourly portfolio sync history ([b24a686](https://git.personal.ksv741.ru/WealthLedger/specs/commit/b24a6861887e831ef2d73a97f36cc213be2be0b8))
* finalize wealth-ledger SDD migration ([329d66e](https://git.personal.ksv741.ru/WealthLedger/specs/commit/329d66e767f7e89a40b96bb8e5b499f39f67feb6))
### CI
* avoid duplicate PR checks ([c132770](https://git.personal.ksv741.ru/WealthLedger/specs/commit/c132770fbb4a4e48cae0a4ceca7407bb6a768ded))
* skip checks on PR metadata edits ([fcdc805](https://git.personal.ksv741.ru/WealthLedger/specs/commit/fcdc805f6c1b3be6a20a7e858e3e80cbd83a9121))
### Maintenance
* align specs engineering workflow (WealthLedger/main[#27](https://git.personal.ksv741.ru/WealthLedger/specs/issues/27)) ([a441b08](https://git.personal.ksv741.ru/WealthLedger/specs/commit/a441b083785d8f50c42dea543e684b23f27732a2))
## [0.1.3](https://git.personal.ksv741.ru/WealthLedger/specs/compare/v0.1.2...v0.1.3) (2026-10-07)
### Bug Fixes
* **deps:** patch release tooling advisories ([33cc4f3](https://git.personal.ksv741.ru/WealthLedger/specs/commit/33cc4f3e0c20ed3d6f5814cdc609724ffb8564ac))
### Specifications
* add reduce-screen-sync-polling change ([a1b0e99](https://git.personal.ksv741.ru/WealthLedger/specs/commit/a1b0e99bddb91f7b7ca729ddf6b7d6c688fdca41))
* simplify runtime requirements ([d861de5](https://git.personal.ksv741.ru/WealthLedger/specs/commit/d861de5ead41b4977083b4aee08fc668c35e5b34))
### Maintenance
* run commitlint in commit-msg hook ([0fd148f](https://git.personal.ksv741.ru/WealthLedger/specs/commit/0fd148ffbb39ea883a7e7799aa1b87c93b31fa17))
## [0.1.2](https://git.personal.ksv741.ru/WealthLedger/specs/compare/v0.1.1...v0.1.2) (2026-09-26)
### Specifications
* complete local frontend stack evolution ([ba76f6f](https://git.personal.ksv741.ru/WealthLedger/specs/commit/ba76f6fccc63b315e9ebf9d05ce181b283a6c094))
* complete persist-tbank-data implementation ([2f5814a](https://git.personal.ksv741.ru/WealthLedger/specs/commit/2f5814abdd52984bf1bcbd1d09b27ff4df3d7a25))
* define versioned deployment bundle ([7f40372](https://git.personal.ksv741.ru/WealthLedger/specs/commit/7f40372c3f67bb67cc77feaad17b61573c65f41e))
## [0.1.1](https://git.personal.ksv741.ru/WealthLedger/specs/compare/v0.1.0...v0.1.1) (2026-09-16)
### Documentation
* make specs instructions self-contained ([e4f10de](https://git.personal.ksv741.ru/WealthLedger/specs/commit/e4f10dedefde06d5c710ea36ab99b199cf2c1f7f))
### Specifications
* define containerized application delivery [#10](https://git.personal.ksv741.ru/WealthLedger/specs/issues/10) ([b34f23a](https://git.personal.ksv741.ru/WealthLedger/specs/commit/b34f23a23034195a6ae0ff324c20182e664bb26d))
## 0.1.0 (2026-09-14)
### Features
* add authorization ([b4515a2](https://git.personal.ksv741.ru/WealthLedger/specs/commit/b4515a201aa63a46754f11c6a9644da216efeaeb))
* add breadcrumbs ([113ad5e](https://git.personal.ksv741.ru/WealthLedger/specs/commit/113ad5e9a2ef4a0f5b7d626fe7d248f5b877bde9))
* add brocker account list ([e907542](https://git.personal.ksv741.ru/WealthLedger/specs/commit/e9075425df4f5871b7a818aeba809aa440284214))
* add future events ([d4851df](https://git.personal.ksv741.ru/WealthLedger/specs/commit/d4851dfdb7c4bab00e61efe7b778ddc377d769c3))
* add logging ([aac1b43](https://git.personal.ksv741.ru/WealthLedger/specs/commit/aac1b435af29efbe0c12428eda1d8f938bcb6ac3))
* add operations list ([e5517cc](https://git.personal.ksv741.ru/WealthLedger/specs/commit/e5517cceae2fc495a36335609b3173cada7a78f3))
* add portfolio ([e3cc3d5](https://git.personal.ksv741.ru/WealthLedger/specs/commit/e3cc3d540bdef00c700b955340dbb5ad2eeec0c3))
* harden OpenSpec agent resilience ([8462a3b](https://git.personal.ksv741.ru/WealthLedger/specs/commit/8462a3ba75d6dda491b9221699357c58d8f8e3cc))
* harden OpenSpec SDD workflow ([a8b2c4e](https://git.personal.ksv741.ru/WealthLedger/specs/commit/a8b2c4eed988f7e1a7b3d6b7188bbe6791f00e2a))
* little frontend structure refactor ([cdc5ae8](https://git.personal.ksv741.ru/WealthLedger/specs/commit/cdc5ae89951e2639ef5a635f785068bfa8d9f316))
* **portfolio-view:** sync delta spec and archive change ([678a671](https://git.personal.ksv741.ru/WealthLedger/specs/commit/678a671d23b3f3db1d2ad8989924ffb35a9687ee))
* redesign portfolio position cards ([6795ee6](https://git.personal.ksv741.ru/WealthLedger/specs/commit/6795ee6f96911fe9cd0ba12e630009ee707272e4))
### Bug Fixes
* **specs:** remove archived change duplicate ([9f68ae5](https://git.personal.ksv741.ru/WealthLedger/specs/commit/9f68ae52557409dacbae50284d9e3635dd0e031b))
### Documentation
* add baseline OpenSpec change ([2d17778](https://git.personal.ksv741.ru/WealthLedger/specs/commit/2d1777823b411846161f3c2c211e46b2e97ea1a2))
* add MVP change proposals ([e383e5f](https://git.personal.ksv741.ru/WealthLedger/specs/commit/e383e5fac6051722510f812b8787cd8c0317c216))
* add React component style change ([18a9487](https://git.personal.ksv741.ru/WealthLedger/specs/commit/18a94874325169e1e3e6fa4d4669b120c7886475))
* **agents:** link audit remediation registry ([6a38cf1](https://git.personal.ksv741.ru/WealthLedger/specs/commit/6a38cf1e25927805b5fa43b43378efff175441fd))
* **ai:** archive superseded context routing plan ([bcb10da](https://git.personal.ksv741.ru/WealthLedger/specs/commit/bcb10da21cc2e2efb5c6951dfcff89ea65ca7ab4))
* align runtime requirements with Node 24 LTS ([9590ded](https://git.personal.ksv741.ru/WealthLedger/specs/commit/9590dedc0122cc045d24ba01321bbf4715516405))
* archive baseline change ([68be476](https://git.personal.ksv741.ru/WealthLedger/specs/commit/68be4760ce1a360d61b6c8924d3ac4b597001da8))
* archive React component style change ([6898732](https://git.personal.ksv741.ru/WealthLedger/specs/commit/6898732dd8e933e9d6a587aef48ebe0eddaa0cf2))
* archive T-Bank connection change ([7edfcb9](https://git.personal.ksv741.ru/WealthLedger/specs/commit/7edfcb9dba35f75ce76f97e643c2e7492afdd82c))
* complete T-Bank connection change ([cc05b2b](https://git.personal.ksv741.ru/WealthLedger/specs/commit/cc05b2b69cf8f82ff7e76673990fb901168f88a8))
* link to unified documentation entry ([43a5577](https://git.personal.ksv741.ru/WealthLedger/specs/commit/43a5577e72ed6556f7db5d10689c2cc3cbdd60e3))
* record completed manual bank card change ([834f2d1](https://git.personal.ksv741.ru/WealthLedger/specs/commit/834f2d146cb99675be5901b40a4d32039d250210))
* **sdd:** add tbank access refactor change ([79faa29](https://git.personal.ksv741.ru/WealthLedger/specs/commit/79faa29ac07172a16de617cd09003b5e7186900e))
* **sdd:** archive tbank access refactor ([736885f](https://git.personal.ksv741.ru/WealthLedger/specs/commit/736885fe6555c09a2d68b35994692c4fafdac518))
* **specs:** add audit remediation workflow plan ([ea58797](https://git.personal.ksv741.ru/WealthLedger/specs/commit/ea587970ae6123bb5fe5289a138ef42878dbd7ab))
* **specs:** archive account-ledger-explicit-filters ([69d4e63](https://git.personal.ksv741.ru/WealthLedger/specs/commit/69d4e63a5110f744a2428dd9d40525bf025a2db8))
* **specs:** archive add-instrument-logos ([70a200e](https://git.personal.ksv741.ru/WealthLedger/specs/commit/70a200e1b41992c8909f6f2daae4264afc442b9d))
* **specs:** archive api contract resilience remediation ([800450d](https://git.personal.ksv741.ru/WealthLedger/specs/commit/800450d07549475d2c83665b58a69c143db5349c))
* **specs:** archive audit remediation workflow ([d093902](https://git.personal.ksv741.ru/WealthLedger/specs/commit/d0939026fbf5e40a55390688c58a8929f8a23927))
* **specs:** archive auth and cache remediation ([6e20cdf](https://git.personal.ksv741.ru/WealthLedger/specs/commit/6e20cdffaf2f57065f2c99c2d3dd7b66cb1d9903))
* **specs:** archive brokerage account list change ([6f47589](https://git.personal.ksv741.ru/WealthLedger/specs/commit/6f47589211afd2cfcecb77884189203038e82732))
* **specs:** archive consume-mantine-components change ([b5df309](https://git.personal.ksv741.ru/WealthLedger/specs/commit/b5df309728afd1b96f2012b0b5b4d8271ee182a3))
* **specs:** archive dependency hygiene remediation ([8897bd9](https://git.personal.ksv741.ru/WealthLedger/specs/commit/8897bd9585004b89e4347712c33bd91867c50015))
* **specs:** archive documentation entrypoints remediation ([5b7bc0f](https://git.personal.ksv741.ru/WealthLedger/specs/commit/5b7bc0fdaf8202acfc8ac1ef5510b3f3abad2bbc))
* **specs:** archive domain money contract change ([039b72e](https://git.personal.ksv741.ru/WealthLedger/specs/commit/039b72eed38b07e89e2f0234e7c12c202b6cf262))
* **specs:** archive frontend audit remediation ([ab231a6](https://git.personal.ksv741.ru/WealthLedger/specs/commit/ab231a6aac58f4e97377e4d4fb521e22d3f07fdb))
* **specs:** archive frontend auth query gating change ([6e2890f](https://git.personal.ksv741.ru/WealthLedger/specs/commit/6e2890f86f5316fdb821b043084461314eb8cf13))
* **specs:** archive frontend quality guardrails ([2029519](https://git.personal.ksv741.ru/WealthLedger/specs/commit/20295190ddb02cb279a6b404d40d944bde862c3d))
* **specs:** archive frontend query remediation ([b8a3f96](https://git.personal.ksv741.ru/WealthLedger/specs/commit/b8a3f9631696a0b50fab27922214a1d158577f31))
* **specs:** archive frontend view model boundary change ([5cc42f3](https://git.personal.ksv741.ru/WealthLedger/specs/commit/5cc42f30cab997a6d0889c124279d06d64fc8e07))
* **specs:** archive improved ledger event rows ([9a8687f](https://git.personal.ksv741.ru/WealthLedger/specs/commit/9a8687feda24edcc6ee6412313a844afce50f5eb))
* **specs:** archive income analytics change ([9ac04cd](https://git.personal.ksv741.ru/WealthLedger/specs/commit/9ac04cdb44cacdad51b60e157a0d8a2f98e3a16a))
* **specs:** archive ledger-entry-type-label change and sync main spec ([37c0133](https://git.personal.ksv741.ru/WealthLedger/specs/commit/37c0133e8baf15959a0c38e45e7b7ac320b8084f))
* **specs:** archive local verification remediation ([72444db](https://git.personal.ksv741.ru/WealthLedger/specs/commit/72444dbbb11ae267e349adbf35256bd7c5be1489))
* **specs:** archive OpenSpec resilience change ([779a1e1](https://git.personal.ksv741.ru/WealthLedger/specs/commit/779a1e1305f8c209a4f98fae7f87718e48d61b37))
* **specs:** archive session workflow changes ([216956e](https://git.personal.ksv741.ru/WealthLedger/specs/commit/216956e0c6868c75bce8390d36e90685922b2c3b))
* **specs:** archive stock card polish ([cf6bc17](https://git.personal.ksv741.ru/WealthLedger/specs/commit/cf6bc1761140f06f44ea29e2eee1a2b24d075e2a))
* **specs:** archive stock ticker change ([329f0a8](https://git.personal.ksv741.ru/WealthLedger/specs/commit/329f0a86a46b1358f83975714b0c378ebf795e7e))
* **specs:** archive T-Bank cache boundary change ([0386b6b](https://git.personal.ksv741.ru/WealthLedger/specs/commit/0386b6b26fa06014182736d1773eb2e48c0293dc))
* **specs:** archive verification simplification change ([15ce5b5](https://git.personal.ksv741.ru/WealthLedger/specs/commit/15ce5b586da20ec2cf441a59547da5257c316b78))
* **specs:** drop archived optimize change ([2c9d163](https://git.personal.ksv741.ru/WealthLedger/specs/commit/2c9d163d396e7a770bfa0be33bf7fd5e1c2860aa))
* **specs:** harden OpenSpec authority lifecycle ([51344d4](https://git.personal.ksv741.ru/WealthLedger/specs/commit/51344d498808be09bb65b5c255fc4393103d5d0d))
* **specs:** mark remediation workflow complete ([37ba133](https://git.personal.ksv741.ru/WealthLedger/specs/commit/37ba133464140925cdc72cdb745fa7e34adb041c))
* **specs:** plan bounded SDD workflow ([50a6721](https://git.personal.ksv741.ru/WealthLedger/specs/commit/50a6721a6037677d97b1650523b4ada40be9668f))
* **specs:** plan frontend query remediation ([bce16ac](https://git.personal.ksv741.ru/WealthLedger/specs/commit/bce16ac3d6638fc8a5e5ab60c8844fde27453eb7))
* **specs:** record frontend remediation progress ([ca7ceed](https://git.personal.ksv741.ru/WealthLedger/specs/commit/ca7ceedcc307319d058516c7c1b0c8c710e893cd))
* **specs:** remove runtime OpenAPI gate ([05e314c](https://git.personal.ksv741.ru/WealthLedger/specs/commit/05e314cebbcb697938d5ca1ff6e1eb226bc73963))
* **specs:** start token-efficient agent pilot ([0b48b50](https://git.personal.ksv741.ru/WealthLedger/specs/commit/0b48b50f4a0d098e42fea292807a96bcd419e212))
* **specs:** version audit snapshot in docs ([ead4834](https://git.personal.ksv741.ru/WealthLedger/specs/commit/ead48345de163870bd9f6e2cdd0f0ac9b3862dce))
* update system documentation references ([50658ea](https://git.personal.ksv741.ru/WealthLedger/specs/commit/50658ea8be85568995cc2b71568f9779452424b6))
### Specifications
* archive compact portfolio totals ([d5c1825](https://git.personal.ksv741.ru/WealthLedger/specs/commit/d5c18258c471afe80306161e5e17b59b04667190))
* archive income page simplification with evidence ([fbb7193](https://git.personal.ksv741.ru/WealthLedger/specs/commit/fbb7193e900534728a4ac1256cc1f76c0ab88eb9))
* archive loading skeleton change ([1d934e0](https://git.personal.ksv741.ru/WealthLedger/specs/commit/1d934e01d7b1ccbd2717d11e1440b6f00dcda47c))
* archive local design token simplification ([19a658e](https://git.personal.ksv741.ru/WealthLedger/specs/commit/19a658e31849083ece7a1f601baba67769dabf1a))
* archive portable design token delivery ([ca709b5](https://git.personal.ksv741.ru/WealthLedger/specs/commit/ca709b5ab9ef380f408dcd6d5bc2f5f0f265d144))
* define portable design token source ([e637a07](https://git.personal.ksv741.ru/WealthLedger/specs/commit/e637a07e6b2b1b6d5372255dc9899fe771ac08df))
* deliver compact portfolio totals package ([b06a35b](https://git.personal.ksv741.ru/WealthLedger/specs/commit/b06a35bbaab83700f3435b50a559614860e5c07c))
* record approved income page simplification ([4c905c1](https://git.personal.ksv741.ru/WealthLedger/specs/commit/4c905c143368d799c142dbbce3a97d74108d6d28))
* remove income analytics period limit ([0608d4d](https://git.personal.ksv741.ru/WealthLedger/specs/commit/0608d4dc23576c32875e3579c8fedeca92db32e7))
* simplify local design token validation contract ([f171b8b](https://git.personal.ksv741.ru/WealthLedger/specs/commit/f171b8bb4642dcaf0c9b4db8354eb6b8aaa4ea9d))
### Refactoring
* refactor future event list ([601a498](https://git.personal.ksv741.ru/WealthLedger/specs/commit/601a4989913fe5b0dbe8c8f612702012d56a7099))
### Maintenance
* configure releases and develop workflow ([3c7c646](https://git.personal.ksv741.ru/WealthLedger/specs/commit/3c7c646f1386ab0f5c1f7604da87f3933cb10afb))
* harden SDD governance workflow ([0006aea](https://git.personal.ksv741.ru/WealthLedger/specs/commit/0006aea56207ad2192c678ced562feabf1a21c45))
* remove agent projection authority ([6719160](https://git.personal.ksv741.ru/WealthLedger/specs/commit/6719160c13d1fba80b395a96b3389f86bfed87aa))
* **specs:** finish verification cleanup ([b6c141f](https://git.personal.ksv741.ru/WealthLedger/specs/commit/b6c141f4f3b5d95dbfcbd921959db910e8d1db66))
* use native OpenSpec validation ([2faf4eb](https://git.personal.ksv741.ru/WealthLedger/specs/commit/2faf4eb8f35be99c99b9711634a9dc4d9c083c07))
+91 -7
View File
@@ -1,21 +1,105 @@
# WealthLedger Specs
Этот репозиторий — единственный versioned SDD authority рабочей области. Canonical OpenSpec store `wealth-ledger-specs` расположен в `openspec/` (`specs/openspec` от workspace), новые changes используют project-local schema `wealth-ledger-sdd`.
Этот репозиторий — единственный versioned SDD authority рабочей области. Canonical OpenSpec store `wealth-ledger-specs` расположен в `openspec/` (`specs/openspec` от workspace). Новые changes используют `wealth-ledger-sdd-v2`; существующие active changes с `schema: wealth-ledger-sdd` завершаются по legacy schema без автоматической миграции.
## Начало работы
Перед созданием change прочитайте [engineering governance](../docs/development/engineering-governance.md) и [OpenSpec lifecycle](../docs/development/openspec-authority-and-lifecycle.md). Proposal, Delta Specs, Design и Tasks создаются последовательно по одному после approval Change Owner. Для любой команды, читающей или изменяющей change, явно передавайте `--store wealth-ledger-specs`.
OpenSpec применяется после явного решения Change Owner согласно [AGENTS](AGENTS.md). Для команд, читающих или изменяющих change, явно передавайте `--store wealth-ledger-specs`. Структурная готовность CLI не означает approval: перед каждым downstream artifact нужно явное решение Change Owner.
Design должен оценить `frontend`, `backend`, `specs`, `docs` в Delivery Map и при обязательном trigger включить Solution Assessment. Docs описывает as-built state и не становится параллельным store требований.
| Change | Lifecycle |
| --- | --- |
| Active `schema: wealth-ledger-sdd` | Proposal → Delta Specs → Design → Tasks → Apply; действующие approvals сохраняются. Legacy Design оценивает `frontend`, `backend`, `specs`, `docs` в Delivery Map. |
| New `schema: wealth-ledger-sdd-v2` | Proposal → Delta Specs → System Analysis → Design + QA Analysis → Docs Impact → Tasks → Apply. Design и QA Analysis готовы после System Analysis; Docs Impact ждёт обоих. |
Для changes без изменения observable behavior используется `skip_specs: true`. В v2 System Analysis/QA Analysis/Docs Impact всё равно фиксируют только применимое содержание и обоснованное N/A, а не фиктивные contracts или tests. `specs/openspec/specs/` хранит нормативное поведение, `docs/` — текущий AS-IS, backend-generated OpenAPI — текущий executable HTTP contract. Target contracts остаются в change до архива; docs обновляются только по согласованному Docs Impact через `[docs]` tasks. После завершения последнего legacy change эту переходную заметку можно удалить отдельным изменением.
## Проверка
Для native validation из этого репозитория:
```bash
openspec schema validate wealth-ledger-sdd --verbose
openspec validate <change> --type change --strict --store wealth-ledger-specs
openspec validate --specs --strict --store wealth-ledger-specs
npm run validate
```
Structural validation не заменяет semantic verification и independent read-only review перед archive. Audit-derived работа маршрутизируется через [live remediation registry](../docs/development/audit-remediation.md).
Одна команда, её же вызывает CI `verify`: `openspec schema validate` (все schemas),
`openspec validate --all` (specs и active changes) и [lychee](https://lychee.cli.rs/) 0.24.2 `--offline
--include-fragments` для относительных ссылок и anchors в корневых `*.md`
(включая `AI_INDEX`) и `openspec/specs`. Архивы и внешние URL не проверяются.
В чистой среде один раз: `openspec store register .`; lychee ставится
отдельно (например, `brew install lychee`). Для отдельного change:
`openspec status --change <change> --store wealth-ledger-specs`.
OpenSpec CLI ставится глобально (`npm install -g @fission-ai/openspec`) и не закрепляется
ни в `package.json` (его транзитивная зависимость `braces` имеет advisory без исправления
в 3.x), ни отдельным файлом версии: CI ставит актуальный выпуск. `validate` не использует
`--strict`: с 1.15 предупреждение о длинных требованиях (>500 символов) считается ошибкой
в strict-режиме. Structural validation не заменяет semantic verification и independent
read-only review перед archive.
Node задаётся собственной `.nvmrc`; после `nvm use --silent` выполните `npm ci`.
`npm run verify` локально проверяет whitespace в незакоммиченном diff; CI `verify`
выполняет тот же `git diff --check` по коммитам PR (merge-base→head) или push
(before→after, для новой ветки — последний коммит). Это структурная проверка
whitespace. Ни whitespace, ни structural validation не заменяют semantic review.
История релизов — [CHANGELOG](CHANGELOG.md).
## Релизы
Репозиторий выпускается независимо. `develop` — интеграция, `main` — принятые
релизы. Обычная работа: issue-linked ветка от актуального `develop`, PR в
`develop`; общие правила — в Engineering Workflow по ссылке из AGENTS.
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`. Проверь
инвариант. Создай `release/X.Y.Z` от проверенного `develop`;
`nvm use --silent`, `npm ci`.
2. На чистой ветке выполни `npm run release:preview`, просмотри версию/changelog,
затем `npm run release:prepare`. Команда создаёт `chore(release): X.Y.Z`,
обновляет manifest/lockfile/CHANGELOG; тег и push не выполняет. Не обходи
guards ветки, чистого дерева, отсутствующих тегов или уже подготовленной версии.
3. Проверь diff и штатные gates этого репозитория. Отправь только release-ветку,
открой PR в **`develop`** с заголовком `chore(release): X.Y.Z`. После зелёных
`verify`, `security`, `commits` выполни fast-forward-only merge. Если
`develop` изменился, обнови release-ветку и повтори проверки кандидата.
4. Открой PR `develop → main` с тем же заголовком и слей fast-forward-only.
5. Получи удалённый `main`, сравни его HEAD с проверенным release SHA и версией
manifest/lockfile. Создай аннотированный `vX.Y.Z` именно на этом SHA:
`git tag -a vX.Y.Z <release-sha> -m "Release X.Y.Z"`, затем отправь только
`git push origin refs/tags/vX.Y.Z`. Не запускай release-it повторно на `main`.
Обратная синхронизация не нужна: release-коммит попал в `develop` на шаге 3.
Опубликованные теги не перемещай. При сбое после merge создай отсутствующий тег
без нового bump; до merge исправь и проверь существующий кандидат без
повторного повышения версии.
Freeze релиза не поддерживается: пока открыт релизный PR, `develop` не двигают.
Срочный fix — обычная ветка `fix/*` от `develop` с последующим релизом;
пути от `main` нет, он сделал бы `main` не-предком `develop`. Исключение
требует явного решения: `release/hotfix-<name>` от `main`, PR в `main`, тег,
затем merge `main → develop` для восстановления инварианта.
`feat:` повышает minor, `fix:`/`perf:` и прочие разрешённые типы — patch;
`!`/`BREAKING CHANGE:` — major, включая версии до 1.0. `chore(release):`
исключён из рекомендации и changelog. Конфигурация — `.release-it.json`.
В Gitea для `main`/`develop` должны быть запрещены прямой/force push и обход
администратором, включена блокировка устаревшей ветки, обязательны `verify`,
`security`, `commits`; теги `v*` защищены. Линейность обеспечивают merge styles
репозитория: merge-коммиты и rebase `--no-ff` выключены, доступны squash,
rebase-merge и fast-forward-only; стиль по умолчанию — squash.
Workflow сам не включает эти настройки. Отдельное review approval для одного
владельца не требуется. Npm publishing, deploy и Gitea Releases не включены.
+12
View File
@@ -0,0 +1,12 @@
module.exports = {
extends: ["@commitlint/config-conventional"],
defaultIgnores: false,
ignores: [
message => /^chore\(release\): \d+\.\d+\.\d+$/.test(message.trim()),
message => /^Merge pull request ['"].+['"] \(#\d+\) from .+ into .+$/.test(message.trim()),
],
rules: {
"type-enum": [2, "always", ["feat", "fix", "perf", "docs", "spec", "refactor", "build", "ci", "test", "style", "chore", "revert"]],
"references-empty": [2, "never"],
},
};
-1
View File
@@ -1 +0,0 @@
1.10.0
@@ -0,0 +1,2 @@
schema: wealth-ledger-sdd-v2
created: 2026-11-07
@@ -0,0 +1,261 @@
# Design
Issue: WealthLedger/main#54
## Context and Constraints
- Backend: NestJS, Prisma + SQLite. Деньги — точная BigInt-арифметика
(`src/common/decimal.ts`: `add`, `subtract`, `multiply`, `divide`,
`compare`). Возведения в дробную степень нет.
- Операции T-Bank уже сохраняются синхронизацией. Покрытие и граница истории
вычисляются в `operations/domain/operations-coverage.ts`. Доступ и режим
`all` проверяет `TbankAccessService.local`. Последний Portfolio отдаёт
`PortfolioService.saved` (`portfolio.api.ts`).
- Признак полной оценки Portfolio сейчас записан прямо в
`portfolio.repository.ts:141-146` и нигде больше не используется.
- MOEX-клиент (`integrations/moex/moex.client.ts`) использует fetch с
таймаутом 10 с и умеет читать ISS-блоки и cursor-пагинацию. Он отдаёт
только данные облигаций.
- Frontend: вкладки счёта — Mantine `Tabs` с `keepMounted={false}`, состояние
во VM. Данные берутся из entity-хуков на `openapi-react-query`. Графики —
`@mantine/charts` (`AreaChart` в `InstrumentChart`). Компоненты разбиты на
container / `vm` / `view`. Строки интерфейса лежат в `ru.ts`.
## Technical Goals / Non-Goals
**Goals:** реализовать утверждённый контракт и спецификации. Расчёт
выполняется на backend и сверяется с независимым эталоном XIRR. Вкладка
«Результат» соответствует варианту A прототипа.
**Non-Goals:**
- общий финансовый движок «на будущее» (TWR, периоды);
- фоновая синхронизация индексов;
- изменения синхронизации T-Bank;
- новая зависимость ради XIRR.
## Implementation Impact Map
| Area | Статус | Outcome |
| --- | --- | --- |
| `main` | N/A | Нет изменений compose, env или релиза. MOEX ISS уже используется без конфигурации |
| `backend` | affected | Модуль `performance`, хранилище и клиент истории индексов, миграция, маршруты, обновлённый `openapi.json` |
| `frontend` | affected | Обновлённый OpenAPI snapshot, entity `performance`, вкладка «Результат» |
| `qa` | N/A | Отдельного QA-репозитория нет |
## Decisions
### backend
1. **Модуль `src/modules/investments/performance/`** устроен по образцу
`income`:
- контроллер с маршрутом `all/performance` перед `:accountId/performance`;
- application-сервис;
- чистый `domain/`: потоки, XIRR, теневой портфель, причины.
Сервис зависит от `TbankAccessService`, `BrokerageOperationsService` (чтение
сохранённых операций и покрытия), `PortfolioService.saved` и
`MarketIndexHistoryService`. *Отклонено:* расширять `income` — это другой
предметный смысл, а там запрещён термин «доходность».
2. **Признак полной оценки** выносится в чистую функцию
`isCompleteValuation(snapshot)` в portfolio domain и экспортируется через
`portfolio.api.ts`. Его используют и `portfolio.repository.ts`, и расчёт
результата. Так у «полной оценки» одно правило.
3. **XIRR на `number`, деньги — на `Decimal`.** Агрегаты, потоки, units и
стоимость теневого портфеля считаются точно. Ставка — итерационное
приближение, точная десятичная степень для неё бессмысленна. Поэтому
решатель работает на float64 и выдаёт результат до 6 знаков (точности
хватает с запасом).
- При обычном наборе потоков с одной сменой знака по датам Descartes'
rule доказывает единственность корня: уточнить его бисекцией в
`[−0.9999, 100]` до |NPV| ≤ 0,01 ₽. Нет корня → `NO_SOLUTION`.
При нескольких сменах знака нет доказательства единственности — пока
консервативно вернуть `AMBIGUOUS_RETURN`, даже если фактически корень
единственный. Полное выделение всех корней добавлять только при
подтверждённой потребности.
- *Отклонено:* Ньютон от одного старта и проверка только знаков на концах:
они не выявляют несколько решений. Новая зависимость не нужна.
4. **Переводы в `all`.** Деньги и бумаги сопоставляются в domain только
для выявления кандидатов (сумма/время либо инструмент/количество).
Кандидат без независимого признака связи не исключается из потоков:
затронутые счета исключаются из результата `all` с причиной
`UNCONFIRMED_TRANSFER`. Существующий T-Bank contract не даёт такого
признака, поэтому автоматически подтверждённых пар сейчас нет.
5. **История индексов: `MarketIndexHistoryService`** в
`src/modules/investments/market-index/`. В `MoexClient` добавляется метод
`indexHistory(secid, from, till)` с пагинацией по `history.cursor`. Если
страница не проходит валидацию, вся загрузка считается сбоем, а частичные
строки не сохраняются.
- Single-flight: `Map<secid, Promise>` с удалением в `finally`, по
образцу существующего `pending` map.
- Время последней попытки хранится в БД, а не в памяти: правило «не чаще
раза в час» переживает рестарт.
6. **Расчёт без кэша.** Каждый GET пересчитывает результат из сохранённых
данных, это O(операций + дней). Кэш результата не нужен, пока ответ
укладывается в 1 с (см. NFR).
7. **Маппинг типов операций** — единая таблица в domain (enum T-Bank → роль
`deposit | withdrawal | securitiesTransfer | internal`) рядом с
существующим `brokerage-operation.mapper.ts`. Существующие категории
журнала не меняются.
### frontend
1. **Entity `src/entities/performance/`**: хук `usePerformance(accountId)`
на `apiQueryClient.queryOptions("get", "/api/v1/brokerage-accounts/{accountId}/performance")`
с `meta.accessGroup: "brokerage"` и `staleTime: 30_000`. Для `all`
передаётся литерал пути, как в income.
2. **Вкладка `performance`** («Результат») — последняя в
`AccountPageLoaded.view.tsx`. Панель — самостоятельный container
`PerformanceTab` в `src/pages/account/ui/AccountPage/components/Performance/`.
Хук вызывается внутри панели, поэтому запрос уходит только при открытой
вкладке (`keepMounted={false}`). Ошибка вкладки не задевает остальные
вкладки.
3. **VM (`Performance.vm.ts`)** переводит ответ в состояние вкладки:
- `loading`, `error`;
- `unavailable`: причины из `meta.unavailable`;
- `ready`: KPI, график, предупреждения.
Вне VM доли не превращаются в проценты. Подпись берётся из
`return.basis` и `period.days`. Тон: `> 0` — positive, `< 0` — negative,
`0` — neutral. Неизвестный `UnavailableReason` показывается общей
формулировкой.
4. **Форматирование.** Добавить в `shared/lib/display.ts`:
- `formatSignedPercent(rate)`: доля → «+15,1 %», 1 знак;
- `formatPercentagePoints(rate)`: «+4,3 п.п.».
Используется минус «−» (U+2212), как в прототипе. Суммы со знаком
выводятся через `formatMoney`.
5. **График** — `@mantine/charts` `LineChart`:
- серии: `netInvested` (`curveType="stepAfter"`), `benchmarkValue`,
`referenceValue` (`strokeDasharray`);
- текущая стоимость — `ReferenceDot` из recharts в дату `period.to`;
- пользовательский tooltip — по образцу `PortfolioStructure`.
Текстовая альтернатива: `aria-label` с итогами плюс скрытая визуально
таблица итоговых значений. Полной таблицы точек нет: она вне scope, как
и таблица потоков. Если `series` пуст, графика нет, вместо него текст о
недоступности индекса.
6. **Строки интерфейса** — в `ru.ts`, namespace `portfolio.performance.*`,
включая тексты причин и блок «Как считали».
7. **Hint «?»** — Mantine `Tooltip` с `events={{ hover: true, focus: true, touch: true }}`
на `ActionIcon` с `aria-label`.
## Cross-cutting Implementation
- **Безопасность.** До чтения данных — `TbankAccessService.local`. Счета без
доступа не попадают ни в расчёт, ни в `meta.unavailable`.
- **Логирование.** `integration.moex.index_history.succeeded/failed` (secid,
from, till, pages, durationMs). Расчёт пишет debug
`performance.calculated` (accountsIncluded, accountsExcluded, durationMs)
без сумм.
- **Конкурентность.** Single-flight догрузки на каждый индекс. Upsert по
`(secid, tradeDate)` идемпотентен.
- **Ошибки.** Сбой MOEX ловится в `MarketIndexHistoryService` и
превращается в результат «недостаточно покрытия». Наружу как 503 он не
выходит.
## Data / Storage Changes
Аддитивная миграция `prisma/migrations/<ts>_add_market_index_history`:
```prisma
model MarketIndexDailyClose {
secid String // MCFTRR | IMOEX
tradeDate String // YYYY-MM-DD, Europe/Moscow
close String // точная десятичная строка
createdAt DateTime @default(now())
@@id([secid, tradeDate])
}
model MarketIndexSyncState {
secid String @id
lastAttemptAt DateTime?
lastSuccessAt DateTime?
}
```
- Данные публичные, к пользователю не привязаны. Удаление подключения их не
трогает.
- Backfill не нужен. Откат — drop двух таблиц; пользовательские данные не
затрагиваются.
- Миграция проверяется на одноразовой копии БД по `.ai/conventions/data.md`.
## Contract Implementation
- DTO повторяют
`system-analysis/contracts/investment-performance.openapi.yaml`;
`UnavailableReason` — экспортируемый enum.
- После `npm run openapi:generate` полученный `openapi.json` сверяется с
целевым контрактом по ключевым свойствам: маршруты, required-поля,
nullable, enum, форматы `Rate`/`Decimal`. Расхождение возвращает change
в System Analysis.
- `npm run openapi:check` входит в gates.
- Frontend копирует новый `openapi.json` в `src/shared/api/spec/` и
перегенерирует типы (`openapi:generate`). `openapi:check` входит в его
`verify`.
## Developer Verification Strategy
**backend:**
- unit-тесты domain:
- XIRR на эталонных наборах: значения считаются заранее независимо, в
LibreOffice `XIRR` или Python `scipy.optimize`, и записываются как
константы с допуском 1e-6;
- примеры из спецификации: 10 % на 730 дней, 110 000 ₽ по индексу;
- `basis = period`, `NO_SOLUTION`, `AMBIGUOUS_RETURN` (несколько корней), поток в выходной;
- кандидаты на переводы без подтверждённой связи, порядок причин;
- тест клиента MOEX: пагинация, невалидная страница, таймаут — на
подменённом fetch, как в `moex.client.spec.ts`;
- e2e на SQLite: маршрут, доступ, `all`, сбой MOEX с сохранённой и без
сохранённой истории;
- `npm run verify`, `npm run openapi:check`.
**frontend:**
- тесты VM: состояния, подписи basis, тоны, неизвестная причина;
- тесты view через RTL + MSW: обгоняю, отстаю, меньше года, неполный,
недоступно, загрузка, ошибка;
- browser-тест вёрстки 390px;
- `npm run verify`.
## UI/UX Package
**new.** Источник —
`prototype/index.html` (sha256
`b7d69ecb4e905350f30d41f89c9f7f5431101edba59478159cd88b761517f0b0`), вариант
A. Решения Change Owner от 2026-11-07: таблицы потоков нет, коды индексов
только в «Как считали».
- **Экран:** вкладка «Результат» на `/accounts/:accountId` (и `all`).
- **Состояния:** загрузка (skeleton из 4 карточек и графика), обгоняю,
отстаю, меньше года (info-alert и «за N дн.»), неполный (warning-alert со
списком исключённых счетов), недоступно (центрированный блок с причиной),
ошибка (существующий паттерн ошибки секции с повтором), индекс недоступен
(KPI без сравнения, текст вместо графика).
- **Компоненты:** сетка из 4 KPI-карточек (`SimpleGrid` cols 1/2/4, как
`CurrencyCards`), `Paper` с графиком, `Accordion` «Как считали». Цвета —
токены `--wl-color-positive/negative`, серии — `blue.6` (индекс),
`dark.4` (вложено), `gray.5` пунктир (без дивидендов).
- **Responsive.** На 390px одна колонка карточек, нет горизонтальной
прокрутки, у графика `h={220}`.
- **A11y:**
- смысл передаётся знаком и текстом, а не только цветом;
- hint доступен с клавиатуры;
- у графика есть текстовая альтернатива;
- alert'ы имеют `role="status"`;
- анимация skeleton уважает `prefers-reduced-motion` (Mantine по
умолчанию).
## Rollout / Rollback
Один релиз backend и frontend; backend выкатывается первым (новые маршруты
аддитивны). Feature flag не нужен. Откат: frontend без вкладки, затем
backend — маршруты и drop таблиц индексов.
## Technical Risks
- **Неподтверждённые переводы** делают результат `all` неполным. Меры:
показать исключённые счета и причину; проверить на реальных данных,
появится ли надёжный признак связи.
- **Неоднозначность XIRR** при крупных выводах (несколько корней). Мера:
поиск возможных корней и `AMBIGUOUS_RETURN`, а не произвольный корень.
- **Формат ISS** может измениться. Мера: строгий разбор, сбой вместо
частичных данных, сохранённая история остаётся.
@@ -0,0 +1,64 @@
# Docs Impact
Issue: WealthLedger/main#54
## Summary
Обновление долговременной документации нужно. Появляется новое понятие
«Результат инвестирования» с методикой. Из-за него снимается общий запрет
на термин «доходность», и это архитектурно значимое решение: оно требует
ADR. Ещё добавляются новый источник данных (индексы MOEX), сохраняемая
публичная история и новые маршруты HTTP API. Существующие формулировки
«не доходность» для других понятий сохраняются, но получают ссылку на новое
понятие.
## Documentation Changes
| Action | Path | Content | Source | Reason |
| --- | --- | --- | --- | --- |
| create | `docs/adr/023-investment-performance-vs-index.md` | Решение: XIRR как «доходность»; сравнение с теми же потоками в MCFTRR (PME по Long–Nickels), IMOEX только для справки; только RUB; «за N дн.» при истории меньше года; отклонены простой ROI, TWR без истории стоимости, ценовой IMOEX как ориентир и конвертация валют; сохранение публичных закрытий индексов как исключение из ADR 019; оговорка о налоговых ставках MCFTRR и условиях ISS | proposal, system-analysis «Расчёт», «Индексы MOEX», design Decisions backend 3, 5 | Архитектурно значимое решение о методике и хранимых данных |
| create | `docs/domains/portfolio/investment-performance.md` | Что показывает вкладка «Результат», внешние и внутренние потоки, смысл прибыли, доходности и «тех же денег в индексе», опережение и отставание, правило меньше года, «Все счета» и переводы между своими счетами, причины недоступности и неполноты, отсутствие линии прошлой стоимости, ссылки на спецификацию и HTTP API | specs `investment-performance`, system-analysis «Маппинг операций», «Переводы», «Причины недоступности» | Новая долговременная предметная область |
| update | `docs/glossary.md` | Новые термины: «Результат инвестирования», «Доходность (XIRR)», «Те же деньги в индексе Мосбиржи», «Опережение рынка», «Внешний денежный поток», каждый с _Не использовать_. У «Точки истории стоимости портфеля» и «Realized Income Analytics» вместо запрета «доходность» — уточнение «не является результатом инвестирования» со ссылкой | specs, ADR 023 | Терминология и снятие конфликтующего запрета |
| update | `docs/overview.md` | Вкладка «Результат» в списке возможностей; абзац «что есть сейчас / что исполнено…» дополнить вопросом «как сработали вложенные деньги». Оговорка, что выплаты сами по себе не равны доходности, сохраняется | proposal | Обзор продукта |
| update | `docs/domains/portfolio/overview.md` | Строки 3–6 и 31: снимок по-прежнему не является расчётом полной доходности; полная доходность считается в «Результате инвестирования» (ссылка) | specs | Устранить противоречие |
| update | `docs/domains/portfolio/all-accounts.md` | «Результат» в режиме «Все счета»: исключение счетов с пометкой о неполноте, переводы между включёнными счетами | specs | Поведение режима `all` |
| update | `docs/domains/portfolio/data-reference.md` | Поля результата: доли (`Rate`), `basis`, `UnavailableReason` и их отображение; деньги в RUB | system-analysis «Contracts» | Справочник смысла данных |
| update | `docs/domains/income/overview.md` | Строка 97: вместо «не рассчитывает доходность» — «не рассчитывает доходность; её показывает Результат инвестирования» (ссылка) | specs | Навигация и отсутствие противоречия |
| update | `docs/integrations/moex.md` | Раздел «Индексы»: MCFTRR и IMOEX, маршрут ISS history, пагинация, догрузка при чтении не чаще раза в час, локальное хранение закрытий, поведение при недоступности, события лога, условия использования данных ISS | system-analysis «Индексы MOEX», design 5 | Новый поток интеграции |
| update | `docs/architecture/context.md` | Строка 34: MOEX поставляет также дневные закрытия индексов Мосбиржи | system-analysis | Контекст системы |
| update | `docs/architecture/http-api.md` | Маршруты `GET /brokerage-accounts/{accountId\|all}/performance`: смысл `data: null`, `meta.unavailable`, `completeness`; ссылка на backend-generated OpenAPI, без копии схемы | contract | Описание HTTP API |
| update | `docs/adr/README.md` | Строка индекса ADR 023 | ADR 023 | Навигация |
## Diagrams
Отдельная диаграмма не переносится. Последовательности чтения и догрузки
остаются в архивированном change. В `docs/integrations/moex.md` достаточно
текстового описания догрузки.
## ADR
- **Создать** ADR 023 (см. таблицу), статус Accepted. Ссылки: Issue
WealthLedger/main#54 и этот change.
- ADR 019 не переписывается: ADR 023 явно дополняет его исключением для
публичной истории индексов, по образцу ADR 022.
## Non-Changes
- `docs/domains/instruments/bonds.md`, `stocks.md`: «открытый результат не
является доходностью к погашению» — другое понятие, текст не меняется.
- `docs/architecture/containers.md`: состав контейнеров не меняется (MOEX
уже есть).
- `docs/architecture/deployment.md`: конфигурация и сеть не меняются.
- `docs/design/`: новых долговременных визуальных принципов нет; вкладка
использует существующие паттерны.
- `docs/domains/operations/`: журнал операций не меняется; маппинг типов на
потоки описывается в новой странице результата.
## Verification
- После `[docs]` tasks выполнить `npm run verify` в `docs/` (markdownlint
с проверкой относительных ссылок и `git diff --check`).
- Термин «доходность» в `docs/` встречается только в значении «Результат
инвестирования» или в явных оговорках «не является»:
`grep -rn "доходност" docs/`.
- Ссылки на specs и OpenAPI ведут на commit после merge реализации.
@@ -0,0 +1,121 @@
# Proposal
Issue: WealthLedger/main#54
## Why
Пользователь хочет знать, насколько хорошо у него получается инвестировать.
Конкретно: какой процент приносят вложенные деньги и обгоняет ли он индекс
Московской биржи. Сейчас приложение показывает только нереализованный результат
открытых позиций относительно средней цены покупки. Этот показатель не учитывает:
- когда и сколько денег вносилось и выводилось;
- результат закрытых позиций;
- купоны, дивиденды, налоги и комиссии.
Поэтому из него нельзя получить ни честный процент годовых, ни сравнение с
рынком. Текущие принципы продукта прямо запрещают называть показатели
«доходностью», потому что честного расчёта не было. Это изменение вводит
такой расчёт с явной методикой.
## What Changes
- Для брокерского счёта и режима «Все счета» появляется результат
инвестирования за всё время. В него входят:
- **прибыль** в рублях: текущая стоимость минус чистые внешние пополнения;
- **доходность** с учётом сумм и дат пополнений и выводов (money-weighted, XIRR).
- Появляется сравнение с альтернативой «те же деньги в индексе Мосбиржи». Те же
пополнения и выводы в те же даты применяются к индексу Мосбиржи с учётом
дивидендов после налогов. Показываются его стоимость, его доходность и
**опережение или отставание** пользователя в процентных пунктах и рублях.
- Привычный ценовой индекс Мосбиржи (без дивидендов) показывается только для
справки и в сравнении не участвует.
- Пока история короче года, проценты показываются за фактический период, без
пересчёта в годовые.
- Когда честный расчёт невозможен, результат помечается как «недоступно» или
«неполный» с конкретной причиной, а не показывается нулём или
приблизительным числом.
- На странице счёта появляется вкладка «Результат»: ключевые показатели,
график «вложено / те же деньги в индексе / справочный индекс / текущая
стоимость» и пояснение «Как считали». Утверждённый вид — вариант A прототипа
`prototype/index.html`.
- Термин «доходность» разрешается для этой метрики с точным определением.
Остальные запреты на этот термин сохраняются.
## Scope
### In Scope
- Результат за всё время для одного брокерского счёта и для «Все счета».
- Только рублёвые счета и потоки. В «Все счета» счёт, который нельзя посчитать,
исключается с явной пометкой о неполноте результата. Если не осталось ни
одного счёта, который можно посчитать, результат «недоступен».
- Переводы денег и бумаг между счетами, включёнными в «Все счета», не считаются
внешними пополнениями и выводами.
- Получение и локальное хранение дневной истории двух индексов Мосбиржи: с
дивидендами после налогов и ценового. Недоступность источника не мешает
остальным разделам счёта.
- Нормативное поведение, публичный контракт для интерфейса, вкладка
«Результат», методика и глоссарий в документации.
### Out of Scope / Non-Goals
- Доходность, взвешенная по времени (TWR), периоды «с начала года», «за год» и
произвольные, а также восстановление прошлой стоимости портфеля. Линия
стоимости портфеля в прошлом на графике не появляется.
- Валютная конвертация и счета с потоками или позициями не в рублях.
- Выбор другого ориентира (облигации, инфляция, депозит, составной индекс).
- Доходность отдельных позиций, налоговый расчёт, прогнозы, риск-метрики,
доказательство «альфы».
- Таблица пополнений и выводов с расчётом паёв индекса.
- Изменение существующих показателей «нереализованный результат» и «Доход».
## Capabilities
### New Capabilities
- `specs/portfolio/investment-performance/spec.md`: результат инвестирования
счёта и «Все счета», сравнение с индексом Мосбиржи, состояния неполноты и
недоступности, вкладка «Результат».
- `specs/instruments/market-index-history/spec.md`: получение и хранение
дневной истории индексов Мосбиржи и поведение при недоступности источника.
### Modified Capabilities
Нет. Существующие требования к портфелю, доходу и истории оценок не меняются.
## Impact
- **Пользователь:** получает ответ на вопросы «сколько % годовых» и «обгоняю ли
я рынок» на странице счёта.
- **Домены:** портфель (новая метрика), операции брокерского счёта (источник
внешних денежных потоков), инструменты и рыночные данные (история индексов).
- **Внешние системы:** новая зависимость от публичных исторических данных
Московской биржи (индексы) и от полноты истории операций T-Bank.
- **Затрагиваемые области:** behavioral specs, backend (расчёт, данные индексов,
публичный контракт), frontend (вкладка), docs (ADR о методике и термине,
глоссарий, домены, интеграция с биржей).
## Success Criteria
- Для рублёвого счёта с известным началом истории пользователь видит прибыль,
доходность, результат «тех же денег в индексе» и опережение. Значения
совпадают с независимым расчётом XIRR на тех же потоках с точностью до 0,1 п.п.
- При истории короче года ни один процент не подписан как «годовых».
- Ни в одном из случаев «начало истории неизвестно», «операции не в рублях»,
«ввод бумаг без денежной оценки», «расчёт не сходится», «история индекса
недоступна» не показывается число, выдаваемое за результат. Причина видна
пользователю.
- Отказ источника индексов не ломает другие вкладки счёта.
## Assumptions / Dependencies
- История операций T-Bank с даты открытия счёта содержит все пополнения и
выводы рублей. Это проверяется на реальных данных первым запуском.
- Индекс с дивидендами после налогов рассчитан по налоговым ставкам для
организаций, а не по НДФЛ физлица. Это ориентир альтернативы, а не точная
модель налогов пользователя. Это фиксируется в методике.
- Публичные данные Мосбиржи используются в личном локальном приложении
с указанием источника. Публичное распространение не планируется.
- Текущая стоимость портфеля берётся из последней полной оценки. Если
оценка неполная, результат недоступен.
@@ -0,0 +1,265 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Прототип: вкладка «Результат» (WealthLedger/main#54)</title>
<style>
/* Значения взяты из frontend/packages/tokens/tokens.json */
:root {
--canvas: #e9ecef; --surface: #fff; --subtle: #f8f9fa; --text: #242424; --muted: #495057;
--border: #dee2e6; --primary: #2563eb; --primary-0: #eff6ff; --positive: #237032; --positive-0: #ebfbee;
--negative: #c92a2a; --negative-0: #fff5f5; --warning: #fab005; --warning-0: #fff9db;
--c-portfolio: #2563eb; --c-index: #f76707; --c-imoex: #adb5bd; --c-invested: #495057;
--r: 8px; --sp-xs: 10px; --sp-sm: 12px; --sp-md: 16px; --sp-lg: 20px; --sp-xl: 32px;
}
* { box-sizing: border-box; }
body { margin: 0; background: var(--canvas); color: var(--text);
font: 16px/1.55 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
.toolbar { position: sticky; top: 0; z-index: 5; display: flex; flex-wrap: wrap; gap: var(--sp-md);
align-items: center; padding: var(--sp-sm) var(--sp-lg); background: #212529; color: #fff; font-size: 14px; }
.toolbar fieldset { border: 0; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 4px; align-items: center; min-width: 0; }
.toolbar legend { float: left; margin-right: 8px; color: #adb5bd; }
.toolbar label { padding: 4px 10px; border-radius: 4px; cursor: pointer; background: #343a40; }
.toolbar input { position: absolute; opacity: 0; }
.toolbar input:checked + span { color: #fff; }
.toolbar label:has(input:checked) { background: var(--primary); }
.toolbar label:has(input:focus-visible) { outline: 2px solid #93c5fd; }
main { max-width: 1040px; margin: 0 auto; padding: var(--sp-lg); }
h1 { font-size: 26px; line-height: 1.35; margin: 0 0 4px; }
.sub { color: var(--muted); font-size: 14px; margin: 0 0 var(--sp-md); }
.tabs { display: flex; gap: 2px; border-bottom: 2px solid var(--border); margin-bottom: var(--sp-lg); overflow-x: auto; }
.tabs span { padding: 8px 14px; font-size: 14px; color: var(--muted); white-space: nowrap; }
.tabs .active { color: var(--primary); font-weight: 600; border-bottom: 2px solid var(--primary); margin-bottom: -2px; }
.paper { background: var(--surface); border: 1px solid var(--border); border-radius: var(--r); padding: var(--sp-md); }
.stack { display: grid; gap: var(--sp-md); grid-template-columns: minmax(0, 1fr); }
.grid { display: grid; gap: var(--sp-sm); grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); }
.kpi { margin: 0; }
.kpi dt { font-size: 14px; color: var(--muted); display: flex; gap: 6px; align-items: center; }
.kpi dd { margin: 2px 0 0; font-size: 26px; font-weight: 700; line-height: 1.3; font-variant-numeric: tabular-nums; }
.kpi .note { font-size: 13px; color: var(--muted); font-weight: 400; }
.pos { color: var(--positive); } .neg { color: var(--negative); }
.badge { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 13px; font-weight: 600; }
.badge.pos { background: var(--positive-0); } .badge.neg { background: var(--negative-0); }
.badge.muted { background: var(--subtle); color: var(--muted); border: 1px solid var(--border); }
.hint { display: inline-grid; place-items: center; width: 16px; height: 16px; border-radius: 50%;
border: 1px solid var(--muted); font-size: 11px; cursor: help; color: var(--muted); }
.section-title { font-size: 18px; font-weight: 600; margin: 0 0 var(--sp-xs); }
.legend { display: flex; flex-wrap: wrap; gap: var(--sp-md); font-size: 13px; color: var(--muted); margin-bottom: 6px; }
.legend i { display: inline-block; width: 14px; height: 3px; vertical-align: middle; margin-right: 6px; border-radius: 2px; }
.legend i.dash { background: repeating-linear-gradient(90deg, var(--c-imoex) 0 4px, transparent 4px 7px); }
svg { width: 100%; height: auto; display: block; }
.chart-wrap { position: relative; }
.tooltip { position: absolute; pointer-events: none; background: #212529; color: #fff; font-size: 12px; padding: 6px 8px;
border-radius: 4px; white-space: nowrap; transform: translate(-50%, -110%); display: none; font-variant-numeric: tabular-nums; }
details { font-size: 14px; } summary { cursor: pointer; font-weight: 600; }
details ul { margin: 8px 0 0; padding-left: 20px; color: var(--muted); }
.alert { border-radius: var(--r); padding: var(--sp-sm) var(--sp-md); font-size: 14px; border: 1px solid; }
.alert.warn { background: var(--warning-0); border-color: #ffe066; }
.alert.info { background: var(--primary-0); border-color: #bfdbfe; }
.alert b { display: block; margin-bottom: 2px; }
.skeleton { background: linear-gradient(90deg, #f1f3f5, #e9ecef, #f1f3f5); background-size: 200% 100%;
animation: sh 1.2s infinite; border-radius: 4px; }
@keyframes sh { to { background-position: -200% 0; } }
@media (max-width: 48em) {
.kpi dd { font-size: 22px; } main { padding: var(--sp-sm); }
}
@media (prefers-reduced-motion: reduce) { .skeleton { animation: none; } }
</style>
</head>
<body>
<form class="toolbar" id="controls" aria-label="Настройки прототипа">
<fieldset><legend>Состояние</legend>
<label><input type="radio" name="state" value="ahead" checked><span>Обгоняю</span></label>
<label><input type="radio" name="state" value="behind"><span>Отстаю</span></label>
<label><input type="radio" name="state" value="short"><span>&lt; 1 года</span></label>
<label><input type="radio" name="state" value="partial"><span>Неполные данные</span></label>
<label><input type="radio" name="state" value="unavailable"><span>Недоступно</span></label>
<label><input type="radio" name="state" value="loading"><span>Загрузка</span></label>
</fieldset>
</form>
<main>
<h1>Все счета</h1>
<p class="sub">Брокерский счёт · ИИС · T-Bank</p>
<nav class="tabs" aria-label="Разделы счёта">
<span>Позиции</span><span>Концентрация</span><span>Деньги</span><span>Операции</span><span>События</span><span>Доход</span>
<span class="active" aria-current="page">Результат</span>
</nav>
<div id="root" aria-live="polite"></div>
</main>
<script>
// ---------- Моковые данные ----------
const DAY = 864e5;
const d = (s) => new Date(s + 'T00:00:00Z');
const NOW = d('2026-11-06');
// Детерминированный ПСЧ (генератор псевдослучайных чисел), чтобы ряд индекса был одинаковым при каждой загрузке
function rng(seed) { return () => (seed = (seed * 16807) % 2147483647) / 2147483647; }
// Ряд индекса MCFTRR, IMOEX ≈ MCFTRR без дивидендов (~6%/год)
function makeIndex(from, to, drift, seed) {
const r = rng(seed), raw = []; let noise = 0;
for (let t = from.getTime(); t <= to.getTime(); t += DAY) {
const wd = new Date(t).getUTCDay(); if (wd === 0 || wd === 6) continue;
noise += (r() - 0.5) * 0.026 + (t > d('2025-06-01').getTime() && t < d('2025-09-01').getTime() ? -0.0018 : 0);
raw.push({ t, noise });
}
// Шум нормируется так, чтобы итоговый рост ряда точно совпал с drift: годовых для MCFTRR, drift − 6 % для IMOEX
const end = raw[raw.length - 1], years = (end.t - raw[0].t) / (365 * DAY);
return raw.map(({ t, noise: n }) => {
const f = (t - raw[0].t) / (end.t - raw[0].t), wiggle = n - f * end.noise;
return { t, mcftrr: 7000 * Math.pow(1 + drift, f * years) * Math.exp(wiggle), imoex: 3000 * Math.pow(1 + drift - 0.06, f * years) * Math.exp(wiggle) };
});
}
const SCENARIOS = {
ahead: { start: '2023-03-14', flows: [['2023-03-14', 300000], ['2023-09-01', 150000], ['2024-02-10', 200000], ['2024-08-20', 100000], ['2025-03-05', 250000], ['2025-10-01', -120000], ['2026-04-15', 180000]], value: 1536400, drift: 0.13 },
behind: { start: '2023-03-14', flows: [['2023-03-14', 300000], ['2023-09-01', 150000], ['2024-02-10', 200000], ['2024-08-20', 100000], ['2025-03-05', 250000], ['2025-10-01', -120000], ['2026-04-15', 180000]], value: 1172400, drift: 0.13 },
short: { start: '2026-02-02', flows: [['2026-02-02', 400000], ['2026-05-12', 150000], ['2026-08-03', 100000]], value: 688900, drift: 0.11 },
partial: { start: '2023-03-14', flows: [['2023-03-14', 300000], ['2023-09-01', 150000], ['2024-02-10', 200000], ['2024-08-20', 100000], ['2025-03-05', 250000], ['2025-10-01', -120000], ['2026-04-15', 180000]], value: 1536400, drift: 0.13,
warnings: ['Счёт «Валютный» исключён: есть операции в USD. Результат посчитан по 2 из 3 счетов.'] },
};
// ---------- Расчёт: та же методика, что предлагается для backend ----------
// XIRR: найти r, при котором Σ CF_i·(1+r)^(-(t_i−t0)/365) = 0. Пополнение со знаком «−» (деньги ушли от инвестора), текущая стоимость со знаком «+».
function npv(rate, cfs) { const t0 = cfs[0].t; return cfs.reduce((s, c) => s + c.amount / Math.pow(1 + rate, (c.t - t0) / (365 * DAY)), 0); }
function xirr(cfs) {
let lo = -0.9999, hi = 10; let flo = npv(lo, cfs), fhi = npv(hi, cfs);
if (flo * fhi > 0) return null; // ponytail: бисекция; Ньютон + бисекция как запасной вариант в backend, если важна скорость
for (let i = 0; i < 200; i++) { const m = (lo + hi) / 2, fm = npv(m, cfs); if (Math.abs(fm) < 1e-6) return m; (flo * fm < 0) ? (hi = m) : (lo = m, flo = fm); }
return (lo + hi) / 2;
}
const periodReturn = (annual, days) => Math.pow(1 + annual, days / 365) - 1;
function compute(sc) {
const idx = makeIndex(d(sc.start), NOW, sc.drift, 42);
const flows = sc.flows.map(([s, a]) => ({ t: d(s).getTime(), amount: a }));
// Теневой портфель: каждое пополнение покупает «паи» индекса по закрытию дня, вывод их продаёт
let units = 0, unitsImoex = 0, invested = 0, fi = 0; const series = [];
for (const p of idx) {
while (fi < flows.length && flows[fi].t <= p.t) { invested += flows[fi].amount; units += flows[fi].amount / p.mcftrr; unitsImoex += flows[fi].amount / p.imoex; fi++; }
series.push({ t: p.t, invested, shadow: units * p.mcftrr, imoex: unitsImoex * p.imoex });
}
const last = series[series.length - 1];
const netInvested = flows.reduce((s, f) => s + f.amount, 0);
const cfs = (end) => [...flows.map((f) => ({ t: f.t, amount: -f.amount })), { t: NOW.getTime(), amount: end }];
const days = Math.round((NOW - d(sc.start)) / DAY);
const you = xirr(cfs(sc.value)), shadow = xirr(cfs(last.shadow)), imoex = xirr(cfs(last.imoex));
return { series, days, netInvested, value: sc.value, profit: sc.value - netInvested,
shadowValue: last.shadow, imoexValue: last.imoex, you, shadow, imoex, short: days < 365, warnings: sc.warnings || [], start: sc.start };
}
// ---------- Форматирование (ru-RU, как в shared/lib) ----------
const rub = (v, sign) => (sign && v > 0 ? '+' : v < 0 ? '−' : '') + Math.abs(Math.round(v)).toLocaleString('ru-RU') + ' ₽';
const pct = (v, sign) => (sign && v > 0 ? '+' : v < 0 ? '−' : '') + Math.abs(v * 100).toLocaleString('ru-RU', { minimumFractionDigits: 1, maximumFractionDigits: 1 }) + ' %';
const pp = (v) => (v > 0 ? '+' : v < 0 ? '−' : '') + Math.abs(v * 100).toLocaleString('ru-RU', { minimumFractionDigits: 1, maximumFractionDigits: 1 }) + ' п.п.';
const date = (t) => new Date(t).toLocaleDateString('ru-RU', { day: 'numeric', month: 'short', year: 'numeric', timeZone: 'UTC' });
const tone = (v) => v > 0 ? 'pos' : v < 0 ? 'neg' : '';
const esc = (s) => String(s).replace(/[&<>]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;' }[c]));
// Пока история короче года, показываем доходность за период, а не годовую
function rate(m, key) {
const a = m[key]; if (a == null) return { text: '—', label: '' };
return m.short ? { v: periodReturn(a, m.days), text: pct(periodReturn(a, m.days), true), label: `за ${m.days} дн.` }
: { v: a, text: pct(a, true), label: 'годовых' };
}
function delta(m) { const y = rate(m, 'you'), s = rate(m, 'shadow'); return { pp: y.v - s.v, rub: m.value - m.shadowValue }; }
// ---------- График (inline SVG, в production — Mantine AreaChart/LineChart) ----------
function chart(m, { height = 260 } = {}) {
const W = 960, H = height, P = { l: 70, r: 16, t: 12, b: 28 };
const s = m.series, xs = (t) => P.l + (t - s[0].t) / (s[s.length - 1].t - s[0].t) * (W - P.l - P.r);
const max = Math.max(...s.map((p) => Math.max(p.shadow, p.invested, p.imoex)), m.value) * 1.05;
const ys = (v) => P.t + (1 - v / max) * (H - P.t - P.b);
const line = (k) => s.map((p, i) => (i ? 'L' : 'M') + xs(p.t).toFixed(1) + ' ' + ys(p[k]).toFixed(1)).join('');
const step = s.map((p, i) => (i ? `H${xs(p.t).toFixed(1)}V${ys(p.invested).toFixed(1)}` : `M${xs(p.t).toFixed(1)} ${ys(p.invested).toFixed(1)}`)).join('');
const ticks = [0, .25, .5, .75, 1].map((f) => max * f);
const years = [...new Set(s.map((p) => new Date(p.t).getUTCFullYear()))];
const ex = xs(s[s.length - 1].t), ey = ys(m.value);
return `<div class="chart-wrap">
<div class="legend" aria-hidden="true">
<span><i style="background:var(--c-invested)"></i>Вложено (нетто)</span>
<span><i style="background:var(--c-index)"></i>Те же деньги в индексе Мосбиржи</span>
<span><i class="dash"></i>Индекс Мосбиржи без дивидендов</span>
<span><i style="background:var(--c-portfolio);height:10px;width:10px;border-radius:50%"></i>Ваш портфель сейчас</span>
</div>
<svg viewBox="0 0 ${W} ${H}" role="img" aria-label="Вложено ${rub(m.netInvested)}, те же деньги в индексе Мосбиржи стоили бы ${rub(m.shadowValue)}, ваш портфель ${rub(m.value)}">
${ticks.map((v) => `<line x1="${P.l}" x2="${W - P.r}" y1="${ys(v)}" y2="${ys(v)}" stroke="#f1f3f5"/><text x="${P.l - 8}" y="${ys(v) + 4}" font-size="12" text-anchor="end" fill="#868e96">${(v / 1e6).toLocaleString('ru-RU', { maximumFractionDigits: 2 })} млн</text>`).join('')}
${years.map((y) => { const t = Math.max(d(y + '-01-01').getTime(), s[0].t); return `<text x="${xs(t)}" y="${H - 8}" font-size="12" fill="#868e96">${y}</text>`; }).join('')}
<path d="${line('shadow')} V${ys(0)} H${P.l} Z" fill="var(--c-index)" opacity=".08"/>
<path d="${line('imoex')}" fill="none" stroke="var(--c-imoex)" stroke-width="1.5" stroke-dasharray="4 3"/>
<path d="${line('shadow')}" fill="none" stroke="var(--c-index)" stroke-width="2"/>
<path d="${step}" fill="none" stroke="var(--c-invested)" stroke-width="2"/>
<line x1="${ex}" x2="${ex}" y1="${ys(m.shadowValue)}" y2="${ey}" stroke="var(--c-portfolio)" stroke-dasharray="2 2"/>
<circle cx="${ex}" cy="${ey}" r="6" fill="var(--c-portfolio)" stroke="#fff" stroke-width="2"/>
<rect class="hit" x="${P.l}" y="${P.t}" width="${W - P.l - P.r}" height="${H - P.t - P.b}" fill="transparent"/>
</svg><div class="tooltip"></div></div>
<p class="sub" style="margin:6px 0 0">Линии стоимости вашего портфеля в прошлом нет: история стоимости ещё не накоплена. Точка справа — текущая оценка.</p>`;
}
function bindTooltip(m) {
document.querySelectorAll('.chart-wrap').forEach((w) => {
const svg = w.querySelector('svg'), tip = w.querySelector('.tooltip'), s = m.series;
svg.addEventListener('mousemove', (e) => {
const r = svg.getBoundingClientRect(), f = Math.min(1, Math.max(0, ((e.clientX - r.left) / r.width * 960 - 70) / (960 - 86)));
const p = s[Math.round(f * (s.length - 1))];
tip.style.display = 'block'; tip.style.left = (e.clientX - r.left) + 'px'; tip.style.top = (e.clientY - r.top) + 'px';
tip.innerHTML = `${date(p.t)}<br>Вложено: ${rub(p.invested)}<br>В индексе: ${rub(p.shadow)}<br>Без дивидендов: ${rub(p.imoex)}`;
});
svg.addEventListener('mouseleave', () => (tip.style.display = 'none'));
});
}
// ---------- Общие блоки ----------
const methodology = (m) => `<details class="paper"><summary>Как считали</summary><ul>
<li>Период: с ${date(d(m.start))} (начало истории счёта) по ${date(NOW)}, ${m.days} дн.</li>
<li>Учтены пополнения и выводы рублей. Купоны, дивиденды, комиссии и налоги уже входят в текущую стоимость портфеля.</li>
<li><b>Доходность (XIRR)</b> учитывает, когда и сколько вы вносили. ${m.short ? 'История меньше года, поэтому процент показан за период, а не в годовых.' : 'Процент годовых.'}</li>
<li><b>Те же деньги в индексе Мосбиржи</b>: каждое пополнение в тот же день «покупает» индекс Мосбиржи с учётом дивидендов после налогов (MCFTRR), каждый вывод его «продаёт».</li>
<li>Привычный индекс Мосбиржи (IMOEX) не учитывает дивиденды, поэтому показан на графике только для справки и в сравнении не участвует.</li>
<li>Цены индексов — MOEX ISS, закрытие дня. Это не налоговый расчёт.</li></ul></details>`;
const warnings = (m) => m.warnings.map((w) => `<div class="alert warn" role="status"><b>Результат неполный</b>${esc(w)}</div>`).join('');
const shortNote = (m) => m.short ? `<div class="alert info"><b>История меньше года</b>Проценты показаны за ${m.days} дн. без пересчёта в годовые: короткий срок даёт неустойчивые годовые значения.</div>` : '';
const hint = (t) => `<span class="hint" tabindex="0" title="${t}" aria-label="${t}">?</span>`;
// ---------- Вариант A (выбран Change Owner 2026-11-07) ----------
function variantA(m) {
const y = rate(m, 'you'), s = rate(m, 'shadow'), dl = delta(m);
return `<div class="stack">${warnings(m)}${shortNote(m)}
<div class="grid">
<dl class="paper kpi"><dt>Доходность ${hint('XIRR: ставка, при которой ваши пополнения и выводы дают текущую стоимость')}</dt><dd class="${tone(y.v)}">${y.text}</dd><dd class="note">${y.label}</dd></dl>
<dl class="paper kpi"><dt>Прибыль</dt><dd class="${tone(m.profit)}">${rub(m.profit, true)}</dd><dd class="note">${rub(m.value)} сейчас · вложено ${rub(m.netInvested)}</dd></dl>
<dl class="paper kpi"><dt>Те же деньги в индексе Мосбиржи ${hint('Если бы каждое пополнение в тот же день вкладывалось в индекс Мосбиржи с учётом дивидендов после налогов')}</dt><dd>${s.text}</dd><dd class="note">${s.label} · стоили бы ${rub(m.shadowValue)}</dd></dl>
<dl class="paper kpi"><dt>${dl.pp >= 0 ? 'Опережение рынка' : 'Отставание от рынка'}</dt><dd class="${tone(dl.pp)}">${pp(dl.pp)}</dd><dd class="note"><span class="badge ${tone(dl.rub)}">${rub(dl.rub, true)}</span> к индексу</dd></dl>
</div>
<section class="paper"><h2 class="section-title">Ваш портфель против индекса</h2>${chart(m)}</section>
${methodology(m)}</div>`;
}
// ---------- Состояния без данных ----------
const unavailable = () => `<div class="stack"><div class="paper" style="text-align:center;padding:var(--sp-xl)">
<div class="section-title">Доходность недоступна</div>
<p class="sub" style="max-width:520px;margin:8px auto">Для счёта «ИИС» неизвестна дата открытия. Без полной истории пополнений нельзя честно посчитать доходность: показали бы неверное число вместо нуля.</p>
<span class="badge muted">Причина: начало истории неизвестно</span>
<p class="sub" style="margin-top:var(--sp-md)">Другие возможные причины: операции не в рублях · ввод или вывод ценных бумаг без денежной оценки · расчёт не сходится · история индекса MOEX временно недоступна.</p>
</div></div>`;
const loading = () => `<div class="stack"><div class="grid">${'<div class="paper"><div class="skeleton" style="height:14px;width:60%"></div><div class="skeleton" style="height:30px;width:80%;margin-top:8px"></div></div>'.repeat(4)}</div>
<div class="paper"><div class="skeleton" style="height:240px"></div></div></div>`;
// ---------- Отрисовка ----------
const form = document.getElementById('controls'), root = document.getElementById('root');
const cache = {};
function render() {
const state = form.state.value;
if (state === 'unavailable') return (root.innerHTML = unavailable());
if (state === 'loading') return (root.innerHTML = loading());
const m = cache[state] ||= compute(SCENARIOS[state]);
root.innerHTML = variantA(m);
bindTooltip(m);
}
form.addEventListener('change', render);
render();
</script>
</body>
</html>
@@ -0,0 +1,139 @@
# QA Analysis
Issue: WealthLedger/main#54
## Verification Scope
**In:**
- все Requirements и Scenarios `portfolio/investment-performance` и
`instruments/market-index-history`;
- контракт `investment-performance.openapi.yaml`;
- семантика ошибок System Analysis.
**Out:**
- TWR, периоды, валютная конвертация;
- корректность самой синхронизации T-Bank (покрыта её спецификациями);
- точность значений MCFTRR у источника.
## Coverage Matrix
| Requirement / Scenario | Проверка на уровне системы |
| --- | --- |
| Результат счёта: рублёвый счёт с известным началом | API: ответ 200 со всеми полями; значения совпадают с эталоном (ниже) |
| Результат счёта: счёт недоступен через подключение | API: чужой `accountId` → 404, в теле нет финансовых полей |
| Внешние потоки: пополнения, вывод, дивиденды | Эталонный счёт A: `netInvested = 130000`, `profit = 10000` |
| Внешние потоки: кандидат на перевод между счетами в `all` | Счета B и C с равными суммами в пределах 5 минут исключаются с `UNCONFIRMED_TRANSFER`, а не объявляются внутренней парой |
| Внешние потоки: перевод на счёт вне результата | Счёт B исключён (USD), его пополнение от C → вывод у C в `all` |
| Доходность: период больше года | Счёт D: 100 000 → 121 000 за 730 дн. → `return.value = 0.1`, `basis = annual` |
| Доходность: меньше года | Счёт E, 200 дн.: `basis = period`; в UI нет слова «годовых» ни в одном значении |
| Доходность: нет решения | Счёт F (текущая стоимость 0 после пополнений, без выводов): `NO_SOLUTION`, `profit` доступен, `outperformance = null` |
| Доходность: несколько решений | Набор потоков с двумя корнями XIRR: `AMBIGUOUS_RETURN`, прибыль доступна, `outperformance = null` |
| Индекс: пополнение и рост индекса | Подменённая история (1 000 → 1 100) или ручной расчёт по реальным закрытиям: `benchmark.value = 110000`, `outperformance.money = 5000` |
| Индекс: поток в выходной | Пополнение в субботу → используется закрытие пятницы (проверяется по `series` в день потока) |
| Индекс: отставание | Счёт со стоимостью ниже теневой: отрицательные `percentagePoints` и `money`, в UI «Отставание от рынка» |
| Справочный индекс недоступен | IMOEX нет, MCFTRR есть: `reference.unavailableReason` задан, `benchmark` и `outperformance` есть, в UI нет пунктирной линии |
| Недоступно: неизвестная дата открытия | Счёт с `historyBoundary = unknown`: `data: null`, `HISTORY_START_UNKNOWN`; в UI «Доходность недоступна», нет нулей |
| USD-поток игнорируется | Счёт с пополнением в USD: результат считается по рублёвым потокам, `excludedAccounts` пуст |
| Недоступно: даты оценок в `all` различаются | Последние оценки A и B в разные московские дни → `data: null`, `ACCOUNT_VALUATION_DATES_DIFFER`; стоимость не складывается |
| Недоступно: индекс недоступен | MOEX заблокирован и истории нет: `profit` есть, `benchmark.unavailableReason = INDEX_HISTORY_UNAVAILABLE`, `series = []` |
| Недоступно: остальные причины | Перевод бумаг без пары → `UNVALUED_SECURITIES_TRANSFER`; позиция без цены → `PORTFOLIO_VALUATION_INCOMPLETE`; незавершённая синхронизация → `OPERATIONS_HISTORY_INCOMPLETE` |
| Вкладка: обгоняю, меньше года, неполный, недоступный, методика | UI по соответствующим данным; формулировки и тоны как в UI/UX Package; «Как считали» содержит все пункты спецификации |
| Вкладка: загрузка, ошибка не ломает другие вкладки | Задержка ответа → skeleton без чисел; 500 на `/performance` → ошибка только во вкладке, остальные вкладки работают |
| График: наведение, отсутствие прошлой стоимости | Наведение и фокус показывают дату и 3 значения; есть пояснение под графиком; `aria-label` с итогами |
| История индексов: первое получение, выходной | После первого чтения есть закрытия торговых дней без дубликатов; повторное чтение не меняет число записей; субботних дат нет |
| История индексов: источник недоступен, история есть / нет | Блокировка `iss.moex.com` после первичной загрузки → результат полный; на чистой БД → `INDEX_HISTORY_UNAVAILABLE`, остальные вкладки работают |
**Эталон.** Для счетов A–F и реальных данных пользователя XIRR
пересчитывается независимо (LibreOffice `XIRR` по тем же потокам и
`currentValue` в дату `to`). Допуск — 0,1 п.п., по Success Criteria.
## Cross-system Verification
- **Контракт.** Ответы обоих маршрутов валидируются по
`investment-performance.openapi.yaml`: required, nullable, enum, шаблон
`Decimal`. Неизвестное значение `UnavailableReason`, подставленное в ответ
через прокси или мок, UI показывает общей недоступностью без падения.
- **Ошибки доступа.** 401 без сессии; 503 `TBANK_NOT_CONNECTED` без
подключения; 404 для счёта не из подключения. `all` без подключения тоже
503.
- **Таймаут MOEX.** Ответ ISS задерживается больше 10 с: результат приходит
с сохранённой историей или с `INDEX_HISTORY_UNAVAILABLE`. Время ответа —
не больше ~10 с плюс время расчёта, без бесконечного ожидания.
- **Параллельные чтения.** Два одновременных GET на чистой БД дают одну
серию запросов к ISS и одинаковые ответы.
- **Частота догрузки.** Повторное чтение в течение часа после сбоя не
обращается к ISS (проверяется по логам).
- **Порядок данных.** Пополнение, исполненное после `fetchedAt` Portfolio, в
потоки не входит.
## Risk-based Priorities
1. **Критично — деньги.** Значения прибыли, XIRR и опережения совпадают с
эталоном. Ни один недоступный случай не показывает число.
2. **Критично — доступ.** Нет утечки чужих счетов в `all` и `meta.unavailable`.
3. **Высокий риск — переводы в `all`** (эвристика парности) и счета с
реальной историей пользователя.
4. **Высокий риск — меньше года.** Ни одного «годовых».
5. **Средний риск — устойчивость к MOEX**, график и a11y.
## Test Data and Environment Needs
- Пользователь с подключением T-Bank и наборами счетов:
- A — пополнения, вывод, дивиденды;
- B — USD;
- C — перевод с B и C;
- D — 730 дн.;
- E — 200 дн.;
- F — нет решения;
- G — `historyBoundary = unknown`;
- H — перевод бумаг без пары;
- I — позиция без цены.
Если sandbox T-Bank такие данные не даёт, их записывают напрямую в
одноразовую SQLite-копию.
- Возможность блокировать `iss.moex.com` (сеть или прокси) и задерживать
его ответы.
- Чистая БД и БД с предзагруженной историей индексов.
- Реальные данные Change Owner для финальной приёмки.
## Manual / Exploratory Scope
- Реальные счета пользователя:
- правдоподобие результата;
- парность переводов между своими счетами;
- какие счета исключаются и почему.
- Вёрстка 390px и desktop; клавиатурная навигация (вкладка, hint, «Как
считали», график); экранный диктор читает итоги графика.
- Переключение счёт ↔ `all` и обратно: нет устаревших данных другого счёта.
## Automation Candidates
- Black-box API-проверки контракта и сценариев A–I: deferred until QA repo
exists.
- E2E вкладки «Результат» (состояния ready, unavailable, incomplete,
error): deferred until QA repo exists.
## Observability Verification
- `integration.moex.index_history.succeeded` / `.failed` появляются при
догрузке и сбое; в них есть secid, диапазон, pages, durationMs и нет
данных пользователя.
- В логах расчёта нет сумм и идентификаторов операций.
## Regression Impact
- Вкладки счёта: позиции, концентрация, деньги, операции, события, доход —
переключение и загрузка не меняются.
- Запись истории оценки Portfolio после выноса признака полной оценки
(`portfolio-value-history`): полные оценки по-прежнему создают точки,
неполные — нет.
- Существующие запросы к MOEX по облигациям.
- `openapi:check` обоих репозиториев.
## Gaps / Blockers
- **Подтверждение переводов** отсутствует в текущем контракте T-Bank.
Routing: System Analysis при появлении нового признака связи; до этого
кандидаты исключаются с явной причиной, не выдавая сомнительный процент.
- **Sandbox T-Bank** может не воспроизвести все наборы. Routing: Test Data —
наполнение одноразовой SQLite-копии; не блокирует.
@@ -0,0 +1,26 @@
## Purpose
Определяет получение и локальное хранение дневной истории индексов Московской биржи, нужных для сравнения результата инвестирования, и поведение при недоступности источника.
## ADDED Requirements
### Requirement: Дневная история индексов Мосбиржи
Система SHALL получать из публичных исторических данных Московской биржи и хранить локально значения закрытия торговых дней двух индексов: индекса с дивидендами после налогов (MCFTRR) и ценового индекса (IMOEX). Значение MUST храниться точно, с датой торгового дня по Europe/Moscow. Повторное получение того же дня MUST NOT создавать дубликат. Отсутствие торгов в день MUST NOT заполняться выдуманным значением.
#### Scenario: Первое получение истории
- **WHEN** результату нужен период, не покрытый сохранённой историей индекса
- **THEN** система получает недостающие торговые дни и сохраняет их без дубликатов
#### Scenario: Выходной день
- **WHEN** в календарный день не было торгов
- **THEN** для этого дня не сохраняется никакое значение индекса
### Requirement: Свежесть и недоступность истории индексов
Сохранённая история MUST переиспользоваться без повторного получения уже сохранённых дней. История MUST дополняться до последнего доступного торгового дня не реже раза в сутки при запросе результата. Недоступность или ошибка источника MUST NOT удалять сохранённую историю и MUST NOT задерживать или ломать другие ответы системы. Если сохранённая история не покрывает нужный период, потребитель MUST получить явную недоступность индекса, а не неполные значения, выдаваемые за полные.
#### Scenario: Источник недоступен, история сохранена
- **WHEN** источник Московской биржи недоступен, а сохранённая история покрывает период результата до предыдущего торгового дня
- **THEN** результат рассчитывается по сохранённой истории
#### Scenario: Источник недоступен, истории нет
- **WHEN** источник Московской биржи недоступен и история индекса за период не сохранена
- **THEN** сравнение с индексом недоступно с причиной «история индекса недоступна», а остальные разделы счёта работают без изменений
@@ -0,0 +1,152 @@
## Purpose
Определяет результат инвестирования брокерского счёта и режима «Все счета» за всё время: прибыль, доходность с учётом сумм и дат пополнений и сравнение с теми же деньгами в индексе Мосбиржи.
## ADDED Requirements
### Requirement: Результат инвестирования выбранного счёта
Система SHALL предоставлять аутентифицированному User результат инвестирования за всё время для открытого поддерживаемого Brokerage Account, доступного через текущий T-Bank connection User, и для режима «Все счета» (`all`). История операций MUST быть полной с известной даты начала истории счёта; период результата MUST начинаться с даты первого внешнего потока и заканчиваться датой последней полной оценки Portfolio. Даты MUST определяться календарными днями по Europe/Moscow. Результат MUST рассчитываться только в RUB без валютной конвертации.
#### Scenario: Рублёвый счёт с известным началом истории
- **WHEN** User открывает результат рублёвого счёта с известным началом истории, полной историей операций и полной текущей оценкой Portfolio
- **THEN** система возвращает период результата, чистые внешние вложения, текущую стоимость, прибыль, доходность, результат тех же денег в индексе и опережение
#### Scenario: Счёт недоступен через текущее подключение
- **WHEN** User запрашивает результат счёта, недоступного через текущий T-Bank connection
- **THEN** система не раскрывает никаких значений результата этого счёта
### Requirement: Внешние денежные потоки
Внешним денежным потоком MUST считаться только пополнение счёта рублями (положительный поток) и вывод рублей со счёта (отрицательный поток) на дату исполнения. Пополнения и выводы не в RUB MUST NOT учитываться и MUST NOT делать результат недоступным: система поддерживает только RUB и не конвертирует валюты. Сделки, купоны, дивиденды, погашения, амортизация, налоги и комиссии MUST NOT считаться внешними потоками: их влияние входит в текущую стоимость. В режиме `all` переводы денег и бумаг между счетами, входящими в результат, MUST NOT считаться внешними потоками. Чистые внешние вложения MUST равняться точной сумме внешних потоков периода. Прибыль MUST равняться точной разности текущей стоимости и чистых внешних вложений.
#### Scenario: Пополнения, вывод и дивиденды
- **WHEN** на счёт внесено 100 000 RUB и 50 000 RUB, выведено 20 000 RUB, получены дивиденды 3 000 RUB, а текущая стоимость Portfolio 140 000 RUB
- **THEN** чистые внешние вложения равны 130 000 RUB, прибыль равна 10 000 RUB, а дивиденды не учитываются как отдельный поток
#### Scenario: Подтверждённый перевод между счетами в режиме «Все счета»
- **WHEN** в режиме `all` источник однозначно подтверждает перевод 50 000 RUB со счёта A на счёт B, и оба счёта входят в результат
- **THEN** перевод не меняет чистые внешние вложения и доходность режима `all`
#### Scenario: Перевод на счёт вне результата
- **WHEN** в режиме `all` деньги переведены на счёт, не входящий в результат
- **THEN** перевод учитывается как вывод в дату исполнения
#### Scenario: Совпадение операций не доказывает перевод
- **WHEN** в режиме `all` вывод на одном счёте и пополнение другого совпадают по сумме и времени, но источник не подтверждает связь между ними
- **THEN** система не объявляет их внутренним переводом, исключает затронутые счета из рассчитанной части результата и показывает причину неполноты; если не осталось счетов для расчёта, результат недоступен
### Requirement: Доходность с учётом сумм и дат потоков
Доходность MUST быть годовой ставкой r, при которой сумма всех внешних потоков, дисконтированных по фактическому числу дней от начала периода по формуле `(1 + r)^(дни / 365)`, и текущей стоимости равна нулю (XIRR). Пополнения MUST входить в расчёт как вложение User, выводы и текущая стоимость — как возврат. Если продолжительность периода меньше 365 дней, система MUST возвращать и показывать доходность за период `(1 + r)^(дни / 365) − 1` с явным числом дней и MUST NOT показывать годовое значение. Если единственное решение не найдено, доходность MUST быть недоступной с причиной «расчёт не сходится», а не нулём.
#### Scenario: Период больше года
- **WHEN** 100 000 RUB внесены ровно 730 дней назад, других потоков не было, а текущая стоимость 121 000 RUB
- **THEN** доходность равна 10,0 % годовых
#### Scenario: Период меньше года
- **WHEN** период результата составляет 200 дней
- **THEN** доходность показывается как процент за 200 дней, и ни одно значение результата не подписано как «годовых»
#### Scenario: Первое пополнение позже открытия счёта
- **WHEN** счёт открыт 300 дней назад, 100 000 RUB внесены 100 дней назад, а текущая стоимость 110 000 RUB
- **THEN** период равен 100 дням, а доходность показывается как +10,0 % за 100 дней
#### Scenario: Нет решения
- **WHEN** набор потоков и текущая стоимость не дают ни одной ставки, обнуляющей сумму
- **THEN** доходность и опережение недоступны с причиной «расчёт не сходится», а прибыль остаётся доступной
#### Scenario: Несколько ставок
- **WHEN** набор потоков допускает более одной ставки доходности
- **THEN** доходность и опережение недоступны с причиной «расчёт неоднозначен», а прибыль остаётся доступной
### Requirement: Те же деньги в индексе Мосбиржи
Система SHALL рассчитывать альтернативный результат: каждый внешний поток в ту же дату применяется к индексу Мосбиржи с учётом дивидендов после налогов (MCFTRR) по значению закрытия этого дня. Если в дату потока не было торгов, MUST использоваться последнее предшествующее закрытие. Стоимость альтернативы MUST равняться остатку условных паёв индекса, умноженному на последнее закрытие не позже конца периода. Доходность альтернативы MUST считаться той же методикой и за тот же период, что и доходность User. Опережение MUST равняться разнице доходностей User и альтернативы в процентных пунктах и разнице текущей стоимости и стоимости альтернативы в RUB. Отрицательное опережение MUST называться отставанием.
#### Scenario: Пополнение и рост индекса
- **WHEN** 100 000 RUB внесены в день, когда закрытие индекса было 1 000, к концу периода закрытие индекса 1 100, а текущая стоимость Portfolio 115 000 RUB
- **THEN** стоимость тех же денег в индексе равна 110 000 RUB, а опережение составляет +5 000 RUB
#### Scenario: Поток в неторговый день
- **WHEN** пополнение исполнено в субботу
- **THEN** для него используется закрытие индекса последнего торгового дня до этой субботы
#### Scenario: Отставание от индекса
- **WHEN** доходность User ниже доходности тех же денег в индексе
- **THEN** результат показывается как отставание с отрицательными значениями в процентных пунктах и RUB
### Requirement: Справочный индекс без дивидендов
Система SHALL дополнительно рассчитывать тот же альтернативный результат по ценовому индексу Мосбиржи (IMOEX) только для справки. Справочный индекс MUST NOT участвовать в опережении и отставании. Его недоступность MUST NOT делать недоступным основной результат.
#### Scenario: Справочный индекс недоступен
- **WHEN** история ценового индекса недоступна, а история индекса с дивидендами доступна
- **THEN** основной результат и опережение показываются, а справочная линия отсутствует с пометкой о недоступности
### Requirement: Недоступный и неполный результат
Система MUST NOT возвращать числовой результат, выдаваемый за доходность, если его нельзя рассчитать честно. Для одного счёта результат MUST быть недоступным с машиночитаемой причиной, если:
- начало истории операций неизвестно;
- есть ввод или вывод ценных бумаг без денежной оценки, а в режиме `all` — неподтверждённый кандидат на перевод между своими счетами;
- текущая оценка Portfolio неполная;
- история операций не получена полностью.
Если история индекса с дивидендами не покрывает период, доступными MUST оставаться только чистые вложения, текущая стоимость и прибыль, а доходность альтернативы и опережение MUST быть недоступны с причиной. В режиме `all` счёт с причиной недоступности MUST исключаться из результата, а результат MUST помечаться как неполный с перечнем исключённых счетов и причин. Если не осталось ни одного счёта, результат режима `all` MUST быть недоступным.
#### Scenario: Неизвестная дата открытия
- **WHEN** у счёта неизвестно начало истории операций
- **THEN** результат счёта недоступен с причиной «начало истории неизвестно», и ни одно значение не показывается как ноль
#### Scenario: Пополнение в USD не учитывается
- **WHEN** у рублёвого счёта есть пополнение в USD
- **THEN** результат рассчитывается только по рублёвым пополнениям и выводам, без причины недоступности
#### Scenario: Счета оценены в разные московские дни
- **WHEN** в режиме `all` последние полные оценки включённых счетов относятся к разным московским календарным дням
- **THEN** сводный результат недоступен с причиной «оценки счетов на разные даты»; последние стоимости не складываются как одновременные
#### Scenario: Индекс недоступен
- **WHEN** история индекса с дивидендами не получена за период результата
- **THEN** прибыль показывается, а результат тех же денег в индексе и опережение недоступны с причиной «история индекса недоступна»
### Requirement: Вкладка «Результат» на странице счёта
Страница брокерского счёта и режима «Все счета» SHALL содержать вкладку «Результат», которая показывает:
- доходность с подписью «годовых» либо «за N дн.»;
- прибыль в RUB с текущей стоимостью и чистыми вложениями;
- «Те же деньги в индексе Мосбиржи» с доходностью и стоимостью;
- «Опережение рынка» либо «Отставание от рынка» в процентных пунктах и RUB;
- график;
- раскрываемый блок «Как считали».
Положительные значения MUST выделяться положительным тоном, отрицательные — отрицательным, и смысл MUST NOT передаваться только цветом (знак «+» или «−» и текст обязательны). Технические коды индексов MUST NOT использоваться как основные подписи и MAY приводиться только в блоке «Как считали». Пока результат загружается, вкладка MUST показывать skeleton без чисел. Ошибка загрузки результата MUST NOT влиять на другие вкладки счёта.
#### Scenario: Пользователь обгоняет рынок
- **WHEN** доходность User выше доходности тех же денег в индексе
- **THEN** вкладка показывает «Опережение рынка» со значениями «+N п.п.» и «+N ₽» в положительном тоне
#### Scenario: История меньше года
- **WHEN** период результата короче 365 дней
- **THEN** вкладка показывает пояснение, что проценты даны за период без пересчёта в годовые, и подписывает каждый процент «за N дн.»
#### Scenario: Неполный результат
- **WHEN** результат режима `all` неполный
- **THEN** над показателями отображается предупреждение «Результат неполный» с исключёнными счетами и причинами
#### Scenario: Недоступный результат
- **WHEN** результат недоступен
- **THEN** вкладка вместо показателей показывает «Доходность недоступна» и понятное описание причины, без нулевых значений
#### Scenario: Пояснение методики
- **WHEN** User раскрывает «Как считали»
- **THEN** видны период, учтённые потоки, смысл доходности, правило «тех же денег в индексе», пометка о справочном характере индекса без дивидендов, источник данных индексов и оговорка, что это не налоговый расчёт
### Requirement: График результата
График вкладки «Результат» SHALL показывать за период результата:
- ступенчатую линию чистых внешних вложений;
- линию стоимости тех же денег в индексе Мосбиржи по закрытиям торговых дней;
- справочную линию тех же денег в индексе без дивидендов;
- отдельную отметку текущей стоимости Portfolio на конец периода.
График MUST NOT показывать прошлую стоимость Portfolio User и MUST сообщать, что её линия отсутствует. Значения при наведении или фокусе MUST показывать дату и все значения линий на эту дату. У графика MUST быть текстовая альтернатива с итоговыми значениями.
#### Scenario: Наведение на дату графика
- **WHEN** User наводит курсор на дату графика
- **THEN** показываются дата, чистые вложения, стоимость в индексе и стоимость в индексе без дивидендов на эту дату
#### Scenario: Отсутствие прошлой стоимости Portfolio
- **WHEN** график отображается
- **THEN** стоимость Portfolio показана только отметкой на конце периода, а под графиком есть пояснение об отсутствии линии прошлой стоимости
@@ -0,0 +1,244 @@
# System Analysis
Issue: WealthLedger/main#54
## Requirement Coverage
| Нормативная область | Поток и контракт |
| --- | --- |
| `investment-performance`: результат счёта и `all`, доступ | [performance-read.mmd](sequences/performance-read.mmd), [contract](contracts/investment-performance.openapi.yaml) `Performance`, `PerformanceResource` |
| `investment-performance`: внешние потоки, переводы в `all` | [Маппинг операций](#маппинг-операций-t-bank), [Переводы в all](#переводы-между-счетами-в-all) |
| `investment-performance`: доходность, basis annual/period, нет решения | [Расчёт](#расчёт), `ReturnValue` |
| `investment-performance`: те же деньги в индексе, неторговый день, отставание | [Расчёт](#расчёт), `IndexResult`, `Outperformance` |
| `investment-performance`: справочный индекс | `reference`, `SeriesPoint.referenceValue` |
| `investment-performance`: недоступный и неполный результат | [Причины недоступности](#причины-недоступности), `UnavailableReason`, `excludedAccounts`, `meta.unavailable` |
| `investment-performance`: вкладка и график | `series`, `period.days`, `return.basis`; поведение UI — в Design |
| `market-index-history`: хранение, свежесть, недоступность | [index-refresh.mmd](sequences/index-refresh.mmd), [Индексы MOEX](#индексы-moex) |
## Systems and Responsibilities
- **Frontend**: вкладка «Результат». Делает один GET на страницу счёта или
`all`, сам ничего финансово не рассчитывает. Отличает загрузку,
HTTP-ошибку, `data: null` (недоступно) и `completeness: incomplete`.
Форматирует доли в проценты и п.п., подпись берёт из `return.basis` и
`period.days`.
- **Backend**: единственный владелец расчёта. Проверяет доступ, отбирает
счета для расчёта, выделяет внешние потоки из сохранённых операций, берёт
последнюю полную оценку Portfolio, считает XIRR и теневой портфель, хранит и
догружает историю индексов.
- **Сохранённые данные T-Bank** (существующая синхронизация): операции,
граница истории и покрытие счёта, последний Portfolio. Новых вызовов T-Bank
нет.
- **MOEX ISS**: публичный источник дневных закрытий MCFTRR и IMOEX.
Вызывается только backend.
## Interaction Flows
- [performance-read.mmd](sequences/performance-read.mmd): чтение результата
со всеми ветками доступа, недоступности и сбоя индекса.
- [index-refresh.mmd](sequences/index-refresh.mmd): догрузка истории индекса
при чтении, пагинация, сбой источника.
Фоновой задачи нет: история индекса догружается при чтении результата.
Однодневная свежесть (требование `market-index-history`) обеспечивается тем,
что каждое чтение проверяет покрытие до `period.to`.
## Contracts
| Producer → Consumer | Операция | Схема | Совместимость |
| --- | --- | --- | --- |
| Backend → Frontend | `GET /api/v1/brokerage-accounts/{accountId}/performance`, `GET /api/v1/brokerage-accounts/all/performance` (HTTP/JSON, сессия как у остальных `/api/v1`) | [investment-performance.openapi.yaml](contracts/investment-performance.openapi.yaml) | Новые маршруты, аддитивно. Enum `UnavailableReason` расширяемый: неизвестное значение клиент показывает как общую недоступность |
| MOEX ISS → Backend | `GET https://iss.moex.com/iss/history/engines/stock/markets/index/securities/{MCFTRR\|IMOEX}.json?from&till&start&iss.meta=off&history.columns=TRADEDATE,CLOSE` | Блок `history` (TRADEDATE `YYYY-MM-DD`, CLOSE число), `history.cursor` (INDEX, TOTAL, PAGESIZE = 100) | Внешний, без версии. Неожиданная форма ответа считается сбоем источника, частичный разбор запрещён |
- HTTP-ошибки — существующий `ProblemDetailsDto` и коды `UNAUTHORIZED`,
`NOT_FOUND`, `TBANK_NOT_CONNECTED`, `TBANK_UNAVAILABLE`,
`INTERNAL_SERVER_ERROR`. Новых problem codes нет.
- Недоступность расчёта не является HTTP-ошибкой: ответ 200, а причина в
`UnavailableReason`. `MOEX_UNAVAILABLE` в HTTP-ответе не возвращается:
сбой индекса превращается в `INDEX_HISTORY_UNAVAILABLE` внутри
`benchmark`/`reference`.
- Числа: деньги — `MoneyDto` (RUB, десятичная строка, округление до копеек
half away from zero, как общий decimal helper); доли (`Rate`) — десятичная строка до 6 знаков, `0.151` = 15,1 %;
точки `series` — десятичные RUB до копеек.
- Идемпотентность: GET без побочных эффектов, кроме догрузки истории индекса.
Её повтор безопасен (upsert по индексу и торговому дню).
## Data and State Semantics
### Источники правды
| Данные | Владелец | Используемое значение |
| --- | --- | --- |
| Внешние потоки | Сохранённые операции T-Bank (EXECUTED) | `type`, `occurredAt`, `payment` (знак и валюта) |
| Начало истории | Граница истории счёта | `historyBoundary = known` → `historyFrom` |
| Полнота операций | Покрытие операций счёта | `historyState = complete` от `historyFrom` до времени оценки |
| Текущая стоимость | Последний сохранённый Portfolio | `totalsByCurrency[RUB]`, `fetchedAt` |
| Закрытия индексов | Новое локальное хранилище истории индексов | (индекс, торговый день) → точное CLOSE |
### Маппинг операций T-Bank
| Тип `OPERATION_TYPE_*` | Роль |
| --- | --- |
| `INPUT`, `INPUT_ACQUIRING`, `INPUT_SWIFT`, `INP_MULTI` | Внешнее пополнение: `+|payment|` |
| `OUTPUT`, `OUTPUT_ACQUIRING`, `OUTPUT_SWIFT`, `OUT_MULTI` | Внешний вывод: `−|payment|` |
| `INPUT_SECURITIES`, `OUTPUT_SECURITIES`, `TRANS_IIS_BS`, `TRANS_BS_BS` | Перевод бумаг: у отдельного счёта всегда `UNVALUED_SECURITIES_TRANSFER`; в `all` — см. ниже |
| Сделки, купоны, дивиденды, погашения, амортизация, налоги, комиссии (включая `OUT_FEE`, `OUTPUT_PENALTY`), прочие | Внутренние, в потоки не входят |
- Поток в валюте, отличной от RUB, не учитывается (RUB-only, WealthLedger/main#55).
- Учитываются только операции с `occurredAt ≤ fetchedAt` Portfolio этого
счёта. Иначе пополнение после оценки исказит результат.
- Дата потока — календарный день `occurredAt` в Europe/Moscow.
### Переводы между счетами в `all`
T-Bank не передаёт в операции счёт-контрагента. Совпадение операций —
лишь кандидат на перевод, а не подтверждение. Не объявлять кандидата
внутренним потоком без независимого признака связи.
- **Деньги.** Вывод на счёте A и пополнение на счёте B равной суммы RUB
в пределах 5 минут — только кандидат. Пока нет подтверждения связи,
оба счёта исключаются из расчётной части `all` с причиной
`UNCONFIRMED_TRANSFER`. Без кандидата операция остаётся внешним потоком.
- **Бумаги.** `OUTPUT_SECURITIES` / `TRANS_*` на счёте A и
`INPUT_SECURITIES` / `TRANS_*` на счёте B с тем же инструментом и
количеством в тот же день — только кандидат; оба счёта исключаются.
Для одиночного ввода или вывода применяется `UNVALUED_SECURITIES_TRANSFER`.
В текущем контракте T-Bank нет признака, подтверждающего пару. Полностью
корректное включение счетов с переводами в `all` отложено до появления
подтверждённой связи; не вычислять процент на предположении.
### Причины недоступности
Причины проверяются по каждому счёту в таком порядке; используется первая
сработавшая:
1. `HISTORY_START_UNKNOWN`: `historyBoundary = unknown`.
2. `OPERATIONS_HISTORY_INCOMPLETE`: покрытие не `complete`.
3. `PORTFOLIO_VALUATION_INCOMPLETE`: нет Portfolio или у ненулевой позиции
нет вычислимой стоимости (то же правило, что у `portfolio-value-history`).
4. `UNVALUED_SECURITIES_TRANSFER`.
5. `UNCONFIRMED_TRANSFER` (только `all`, когда есть кандидат на внутренний
перевод без доказанной связи).
Как причины попадают в ответ:
- Для отдельного счёта любая причина даёт `data: null` и одну запись в
`meta.unavailable`.
- В `all` исключённые счета перечисляются в `excludedAccounts`, а
`completeness = incomplete`. Если не осталось ни одного счёта, ответ
`data: null`, а `meta.unavailable` содержит все счета.
- `NO_SOLUTION` и `AMBIGUOUS_RETURN` бывают только в `return`
(пользователя или индекса) и не
исключает счёт.
- Если даты последних полных оценок оставшихся счетов `all` в Москве
различаются, сводка целиком `data: null` с причиной
`ACCOUNT_VALUATION_DATES_DIFFER` в `meta.unavailable` для каждого
затронутого счёта: суммы разных дат не складываются.
- `INDEX_HISTORY_UNAVAILABLE` бывает только в `benchmark` / `reference`.
### Расчёт
- **Период.** `from` — дата первого внешнего потока по Москве (решение
Change Owner: не дата открытия счёта). Полнота истории по-прежнему
проверяется от `historyFrom`. `to` — дата по Москве `fetchedAt`; для `all` она общая
только когда все включённые счета оценены в один московский день.
`days = to − from` в календарных днях.
`basis = annual` при `days ≥ 365`, иначе `period`.
- **Агрегаты.** `netInvested = Σ потоков`,
`currentValue = Σ totalsByCurrency[RUB]`,
`profit = currentValue − netInvested`. Точная десятичная арифметика,
финальное округление до копеек.
- **XIRR.** Найти r из `Σ CFᵢ / (1 + r)^((dᵢ − d₀) / 365) = 0`, где
пополнение входит как `−`, вывод — как `+`, а `currentValue` — как `+` в
дату `to`. d₀ — дата первого потока. Корень ищется на `(−0.9999, 100]`.
- Нет корня или корень не найден с точностью |NPV| ≤ 0,01 ₽ →
`NO_SOLUTION`. Если решений более одного — `AMBIGUOUS_RETURN`,
произвольный корень не выбирается. Смена знака на концах диапазона
не доказывает единственность решения.
- Нет потоков → `NO_SOLUTION`.
- Для `basis = period` значение равно `(1 + r)^(days / 365) − 1`.
- **Теневой портфель.** Для каждого потока берётся закрытие индекса на дату
потока или на последнюю торговую дату до неё:
`units += CF / close` (масштаб 12 знаков).
`benchmark.value = units × close(asOf)`, где `asOf` — последнее закрытие
`≤ to`. XIRR индекса считается на тех же потоках с `benchmark.value` в
дату `to`.
- **Опережение.**
`outperformance.percentagePoints = return.value − benchmark.return.value`
(одинаковый `basis`), `outperformance.money = currentValue − benchmark.value`.
Если любая из сторон `null`, `outperformance = null`.
- **Series.** Одна точка на торговый день MCFTRR в `[from, to]`:
- `netInvested` — накопленная сумма потоков к этому дню;
- `benchmarkValue` — `units × close` на этот день;
- `referenceValue` — то же для IMOEX, `null` при его недоступности.
Около 250 точек в год, без прореживания.
### Индексы MOEX
- Хранится (индекс, торговый день по Москве) → точное CLOSE, upsert.
Нерабочие дни не хранятся.
- **Свежесть.** Расширение диапазона назад загружается сразу; хвост до
`to`, где закрытия ещё нет, перепроверяется не чаще раза в час после
успешной проверки; повтор после неудачной попытки — не чаще раза в час.
На каждый индекс
одновременно идёт не больше одной догрузки, повторные чтения ждут её
результат. Первая загрузка начинается с `from − 10 дней`, чтобы было
закрытие для потока в выходной.
- **Достаточное покрытие.** Есть закрытие `≤` даты первого потока,
подтверждённый диапазон доходит до даты последнего потока и
последнее закрытие `≥ to − 10` календарных дней. 10 дней покрывают
новогодние каникулы биржи. Иначе `INDEX_HISTORY_UNAVAILABLE`.
- **Лицензия.** ISS бесплатно отдаёт данные для ознакомления. Приложение
личное, локальное и не распространяет данные. Источник указывается в
«Как считали».
## Error and Recovery Semantics
| Ситуация | Тип | Ответ | Восстановление |
| --- | --- | --- | --- |
| Нет сессии / нет подключения / счёт не найден или недоступен | Доступ | 401 / 503 `TBANK_NOT_CONNECTED` / 404 | Существующие сценарии счёта |
| Причина недоступности счёта | Бизнес | 200, `data: null` или `excludedAccounts` | Исчезает после синхронизации или изменения данных |
| XIRR не сходится или неоднозначен | Бизнес | 200, `return.unavailableReason = NO_SOLUTION` либо `AMBIGUOUS_RETURN` | Нет |
| MOEX недоступен, сохранённой истории хватает | Технический | 200, полный результат | Догрузка при следующем чтении (не чаще раза в час) |
| MOEX недоступен, истории не хватает | Технический | 200, `benchmark.unavailableReason = INDEX_HISTORY_UNAVAILABLE`, `series: []` | То же |
| Сбой хранилища или непредвиденная ошибка | Технический | 500 Problem Details | Повтор из UI; другие вкладки не затронуты |
Таймаут MOEX — 10 с на запрос, как у существующего клиента. Повторных
попыток внутри одного чтения нет. Сбой догрузки не удаляет сохранённые
закрытия.
## Non-Functional and Operational Constraints
- **Ответ.** Без догрузки индекса — меньше 1 с для 10 лет истории (~2 500
точек на индекс, до нескольких тысяч операций). С холодной догрузкой —
до `ceil(дней_торгов / 100)` запросов к ISS (~25 на 10 лет на индекс).
- **Наблюдаемость.** События `integration.moex.index_history.succeeded` и
`.failed` с индексом, диапазоном, числом страниц и длительностью. Счёт и
суммы в логи не пишутся.
- **Безопасность.** Значения отдаются только после подтверждения доступа,
как у остальных инвестиционных ресурсов. В MOEX не уходят данные
пользователя.
## Compatibility and Migration
- Новые маршруты и новое хранилище; существующие контракты не меняются.
- Миграция данных не нужна: история индекса заполняется при первом чтении.
- Откат: удалить маршруты и хранилище. Пользовательские данные не
затрагиваются.
## Open Questions / Assumptions
- **Assumption.** Окно 5 минут и правила парности переводов соответствуют
реальным операциям T-Bank. Проверяется на данных пользователя до закрытия
change; при расхождении правило уточняется здесь.
- **Assumption.** Остаток в валюте до 1 единицы (например, копейки USD
после конвертации) всё равно даёт `NON_RUB_ACTIVITY`. Порог не вводится,
пока такой случай не встретится на реальных данных.
- **Elaboration.** Для `market-index-history` уточнены параметры: окно
покрытия 10 дней и интервал повторных попыток 1 час. Новое поведение они
не вводят.
- **Spec alignment.** Формулировка причины уточнена: «ввод или вывод ценных
бумаг без денежной оценки». В delta spec и прототипе текст обновлён: вывод
бумаг без оценки так же искажает расчёт.
@@ -0,0 +1,191 @@
openapi: 3.1.0
info:
title: WealthLedger investment performance (target contract, WealthLedger/main#54)
version: 1.0.0
description: |
Целевой контракт новых маршрутов. После реализации исполняемым источником
становится backend-generated OpenAPI, который MUST быть семантически
совместим с этим файлом. Общие схемы `MoneyDto`, `ProblemDetailsDto` и
`InvestmentDependencyDto` остаются в опубликованном OpenAPI и здесь только
упоминаются.
paths:
/api/v1/brokerage-accounts/{accountId}/performance:
get:
operationId: getBrokerageAccountPerformance
summary: Результат инвестирования счёта за всё время
parameters:
- name: accountId
in: path
required: true
schema: { type: string, minLength: 1 }
responses:
'200': { $ref: '#/components/responses/Performance' }
'401': { $ref: '#/components/responses/Problem' }
'404': { $ref: '#/components/responses/Problem' }
'503': { $ref: '#/components/responses/Problem' }
/api/v1/brokerage-accounts/all/performance:
get:
operationId: getAllBrokerageAccountsPerformance
summary: Результат инвестирования режима «Все счета»
responses:
'200': { $ref: '#/components/responses/Performance' }
'401': { $ref: '#/components/responses/Problem' }
'503': { $ref: '#/components/responses/Problem' }
components:
responses:
Problem:
description: Ошибка доступа, подключения или технический сбой (существующий Problem Details; коды из PROBLEM_CODES)
content:
application/problem+json:
schema: { $ref: 'openapi.json#/components/schemas/ProblemDetailsDto' }
Performance:
description: Результат либо явная недоступность. Недоступность расчёта — не ошибка HTTP.
content:
application/json:
schema: { $ref: '#/components/schemas/PerformanceResource' }
schemas:
Decimal:
type: string
pattern: '^-?(0|[1-9][0-9]*)(\.[0-9]*[1-9])?$'
description: Точное десятичное значение без экспоненты и незначащих нулей
Rate:
allOf: [{ $ref: '#/components/schemas/Decimal' }]
description: Доля, не проценты. 0.151 = 15,1 %. Округление до 6 знаков, half away from zero.
Money:
$ref: 'openapi.json#/components/schemas/MoneyDto'
UnavailableReason:
type: string
enum:
- HISTORY_START_UNKNOWN
- OPERATIONS_HISTORY_INCOMPLETE
- UNVALUED_SECURITIES_TRANSFER
- UNCONFIRMED_TRANSFER
- ACCOUNT_VALUATION_DATES_DIFFER
- PORTFOLIO_VALUATION_INCOMPLETE
- NO_SOLUTION
- AMBIGUOUS_RETURN
- INDEX_HISTORY_UNAVAILABLE
description: |
Новые значения MAY добавляться; клиент MUST показывать неизвестное
значение как общую недоступность, а не как ошибку.
ReturnValue:
type: object
required: [value, basis, unavailableReason]
properties:
value:
oneOf: [{ $ref: '#/components/schemas/Rate' }, { type: 'null' }]
basis:
type: string
enum: [annual, period]
description: annual при period.days >= 365, иначе period (доля за весь период)
unavailableReason:
oneOf: [{ $ref: '#/components/schemas/UnavailableReason' }, { type: 'null' }]
IndexResult:
type: object
required: [code, value, return, asOf, unavailableReason]
properties:
code:
type: string
enum: [MCFTRR, IMOEX]
value:
oneOf: [{ $ref: '#/components/schemas/Money' }, { type: 'null' }]
description: Стоимость тех же потоков в индексе на конец периода (RUB)
return: { $ref: '#/components/schemas/ReturnValue' }
asOf:
oneOf: [{ type: string, format: date }, { type: 'null' }]
description: Дата последнего использованного закрытия индекса (Europe/Moscow)
unavailableReason:
oneOf: [{ $ref: '#/components/schemas/UnavailableReason' }, { type: 'null' }]
Outperformance:
type: object
required: [percentagePoints, money]
properties:
percentagePoints:
$ref: '#/components/schemas/Rate'
description: Разница долей return пользователя и benchmark (того же basis)
money: { $ref: '#/components/schemas/Money' }
SeriesPoint:
type: object
required: [date, netInvested, benchmarkValue, referenceValue]
properties:
date: { type: string, format: date }
netInvested: { $ref: '#/components/schemas/Decimal' }
benchmarkValue: { $ref: '#/components/schemas/Decimal' }
referenceValue:
oneOf: [{ $ref: '#/components/schemas/Decimal' }, { type: 'null' }]
description: Значения RUB на торговый день MCFTRR; валюта точки всегда RUB
ExcludedAccount:
type: object
required: [accountId, name, reason]
properties:
accountId: { type: string }
name: { type: string }
reason: { $ref: '#/components/schemas/UnavailableReason' }
Performance:
type: object
required:
- accountId
- period
- netInvested
- currentValue
- profit
- return
- benchmark
- reference
- outperformance
- series
- completeness
- excludedAccounts
- calculatedAt
properties:
accountId: { type: string, description: 'id счёта или all' }
period:
type: object
required: [from, to, days]
properties:
from: { type: string, format: date }
to: { type: string, format: date, description: 'дата последней полной оценки Portfolio' }
days: { type: integer, minimum: 0 }
netInvested: { $ref: '#/components/schemas/Money' }
currentValue: { $ref: '#/components/schemas/Money' }
profit: { $ref: '#/components/schemas/Money' }
return: { $ref: '#/components/schemas/ReturnValue' }
benchmark:
allOf: [{ $ref: '#/components/schemas/IndexResult' }]
description: code = MCFTRR
reference:
allOf: [{ $ref: '#/components/schemas/IndexResult' }]
description: code = IMOEX; только справка
outperformance:
oneOf: [{ $ref: '#/components/schemas/Outperformance' }, { type: 'null' }]
description: null, если недоступен return пользователя или benchmark
series:
type: array
items: { $ref: '#/components/schemas/SeriesPoint' }
description: Пусто, если benchmark недоступен
completeness:
type: string
enum: [complete, incomplete]
excludedAccounts:
type: array
items: { $ref: '#/components/schemas/ExcludedAccount' }
description: Только для all; incomplete <=> непустой массив
calculatedAt: { type: string, format: date-time }
PerformanceResource:
type: object
required: [data, meta]
properties:
data:
oneOf: [{ $ref: '#/components/schemas/Performance' }, { type: 'null' }]
description: null, если результат недоступен целиком
meta:
type: object
required: [dependencies, unavailable]
properties:
dependencies:
type: array
items: { $ref: 'openapi.json#/components/schemas/InvestmentDependencyDto' }
unavailable:
type: array
description: Непусто <=> data = null; причины по каждому счёту
items: { $ref: '#/components/schemas/ExcludedAccount' }
@@ -0,0 +1,25 @@
sequenceDiagram
participant API as WealthLedger backend
participant Idx as Saved index history
participant MOEX as MOEX ISS
API->>Idx: Последняя сохранённая дата, время последней попытки (по индексу)
alt Покрыто до дня `to` или попытка была < 1 ч назад
Idx-->>API: Сохранённые закрытия
else Нужна догрузка (одна одновременная попытка на индекс)
loop Страницы по 100 строк (start = 0, 100, …) до TOTAL
API->>MOEX: GET /iss/history/engines/stock/markets/index/securities/{SECID}.json<br/>?from&till&start&iss.meta=off&history.columns=TRADEDATE,CLOSE
alt 200 с валидными строками
MOEX-->>API: TRADEDATE, CLOSE + history.cursor(INDEX,TOTAL,PAGESIZE)
API->>Idx: Upsert (индекс, торговый день) → точное закрытие
else Таймаут 10 с / не-2xx / невалидный ответ
MOEX-->>API: сбой
Note over API: Прервать догрузку; сохранённое не удалять;<br/>лог integration.moex.index_history.failed
end
end
Idx-->>API: Сохранённые закрытия
end
alt Последнее закрытие ≥ to − 10 календарных дней
Note over API: Покрытие достаточно; asOf = последняя дата закрытия
else Иначе
Note over API: INDEX_HISTORY_UNAVAILABLE для этого индекса
end
@@ -0,0 +1,38 @@
sequenceDiagram
actor User
participant UI as WealthLedger frontend
participant API as WealthLedger backend
participant Store as Saved investment data
participant Idx as Saved index history
participant MOEX as MOEX ISS
User->>UI: Открывает вкладку «Результат» (accountId | all)
UI->>API: GET /brokerage-accounts/{accountId|all}/performance
API->>Store: Сессия, connection, доступ к счёту(ам)
alt Нет сессии / нет подключения / счёт недоступен
API-->>UI: Problem Details 401 / 503 TBANK_NOT_CONNECTED / 404
UI-->>User: Существующие состояния ошибки счёта
else Доступ подтверждён
API->>Store: Граница истории, покрытие операций, последняя полная оценка Portfolio
Note over API: По каждому счёту: причины недоступности<br/>(HISTORY_START_UNKNOWN, OPERATIONS_HISTORY_INCOMPLETE,<br/>NON_RUB_ACTIVITY, UNVALUED_SECURITIES_TRANSFER,<br/>PORTFOLIO_VALUATION_INCOMPLETE, UNCONFIRMED_TRANSFER)<br/>all: ACCOUNT_VALUATION_DATES_DIFFER<br/>XIRR: NO_SOLUTION / AMBIGUOUS_RETURN
alt Не осталось счетов для расчёта
API-->>UI: 200 {data: null, meta.unavailable: [...]}
UI-->>User: «Доходность недоступна» + причина
else Есть счета для расчёта
API->>API: Внешние потоки по дате, чистые вложения, прибыль, XIRR
API->>Idx: Закрытия MCFTRR, IMOEX за [from, to]
opt История не покрывает период (см. index-refresh.mmd)
Idx->>MOEX: Догрузка недостающих торговых дней
end
alt MCFTRR покрывает период
API->>API: Теневой портфель, XIRR индекса, опережение, series
else MCFTRR недоступен
API->>API: benchmark/outperformance недоступны (INDEX_HISTORY_UNAVAILABLE), series = []
end
API-->>UI: 200 {data: Performance, meta.dependencies}
UI-->>User: Показатели, график, «Как считали», предупреждение о неполноте
end
end
alt Технический сбой backend
API-->>UI: Problem Details 5xx
UI-->>User: Ошибка только во вкладке «Результат», с повтором
end
@@ -0,0 +1,108 @@
# Tasks
Issue: WealthLedger/main#54. Ветки в каждом repo:
`feature/54-investment-performance-vs-index` от актуального `develop`.
## 1. Backend: история индексов
- [x] 1.1 [backend] Хранилище дневных закрытий индексов и состояния догрузки (аддитивная миграция)
- **Covers:** `instruments/market-index-history` — `Дневная история индексов Мосбиржи`
- **Source:** design «Data / Storage Changes»
- **Verification:** `npx prisma migrate dev` на одноразовой копии БД; `npm run verify`
- **Depends on:** none
- [x] 1.2 [backend] `MoexClient.indexHistory(secid, from, till)`: пагинация `history.cursor`, строгий разбор, сбой без частичных строк
- **Covers:** `instruments/market-index-history` — `Дневная история индексов Мосбиржи`
- **Source:** system-analysis «Contracts» (MOEX ISS), [index-refresh.mmd](system-analysis/sequences/index-refresh.mmd); design backend 5
- **Verification:** `moex.client.spec.ts`: несколько страниц, невалидная страница, таймаут, не-2xx
- **Depends on:** none
- [x] 1.3 [backend] `MarketIndexHistoryService`: покрытие периода, догрузка не чаще раза в час, single-flight, upsert, лог `integration.moex.index_history.*`, сбой → недостаточно покрытия
- **Covers:** `instruments/market-index-history` — `Дневная история индексов Мосбиржи`, `Свежесть и недоступность истории индексов`
- **Source:** system-analysis «Индексы MOEX», «Error and Recovery Semantics»; design backend 5, «Cross-cutting Implementation»
- **Verification:** unit-тесты сервиса (покрытие ±10 дн., час, параллельные вызовы, сбой с историей и без); `npm run verify`
- **Depends on:** 1.1, 1.2
## 2. Backend: расчёт результата
- [x] 2.1 [backend] Вынести `isCompleteValuation(snapshot)` в portfolio domain и использовать его при записи истории оценки
- **Covers:** `portfolio/investment-performance` — `Недоступный и неполный результат`
- **Source:** design backend 2; qa-analysis «Regression Impact»
- **Verification:** существующие тесты `portfolio-value-history` и новые unit-тесты функции проходят; `npm run verify`
- **Depends on:** none
- [x] 2.2 [backend] Domain: маппинг типов операций → роли, внешние потоки по дате Europe/Moscow, отсечка по `fetchedAt`, кандидаты на переводы в `all` без автоматического подтверждения, причины недоступности в порядке приоритета
- **Covers:** `portfolio/investment-performance` — `Внешние денежные потоки`, `Недоступный и неполный результат`
- **Source:** system-analysis «Маппинг операций T-Bank», «Переводы между счетами в all», «Причины недоступности»; design backend 4, 7
- **Verification:** unit-тесты: счёт A (130 000 / 10 000), `UNCONFIRMED_TRANSFER` при совпадении операций, перевод на исключённый счёт, бумаги без пары, USD, порядок причин
- **Depends on:** none
- [x] 2.3 [backend] Domain: XIRR (Ньютон + бисекция, проверка знака), basis annual/period, теневой портфель MCFTRR/IMOEX, опережение, series
- **Covers:** `portfolio/investment-performance` — `Доходность с учётом сумм и дат потоков`, `Те же деньги в индексе Мосбиржи`, `Справочный индекс без дивидендов`, `График результата`
- **Source:** system-analysis «Расчёт»; design backend 3
- **Verification:** unit-тесты с эталонными константами (LibreOffice/scipy, допуск 1e-6): 10 % за 730 дн., 110 000 ₽ по индексу, поток в выходной, `NO_SOLUTION`, `AMBIGUOUS_RETURN`, period basis, отставание
- **Depends on:** none
- [x] 2.4 [backend] Модуль `performance`: маршруты `GET /brokerage-accounts/{accountId|all}/performance`, доступ, сборка ответа, DTO, `UnavailableReason`, лог `performance.calculated`
- **Covers:** `portfolio/investment-performance` — `Результат инвестирования выбранного счёта`, `Недоступный и неполный результат`; `instruments/market-index-history` — `Свежесть и недоступность истории индексов`
- **Source:** contract [investment-performance.openapi.yaml](system-analysis/contracts/investment-performance.openapi.yaml), [performance-read.mmd](system-analysis/sequences/performance-read.mmd); design backend 1, 6
- **Verification:** e2e на SQLite (`npm run test:sqlite`): 200 ready, `data: null`, `all` incomplete, 401/404/503, MOEX недоступен с историей и без; `npm run verify`
- **Depends on:** 1.3, 2.1, 2.2, 2.3
- [x] 2.5 [backend] Обновить generated OpenAPI и сверить его с целевым контрактом
- **Covers:** `portfolio/investment-performance` — `Результат инвестирования выбранного счёта`
- **Source:** design «Contract Implementation»
- **Verification:** `npm run openapi:generate`; `npm run openapi:check`; построчная сверка маршрутов, required, nullable, enum и форматов с `investment-performance.openapi.yaml`, результат — в описании PR
- **Depends on:** 2.4
## 3. Frontend
- [x] 3.1 [frontend] Обновить OpenAPI snapshot и типы, entity `performance` с `usePerformance(accountId)`
- **Covers:** `portfolio/investment-performance` — `Результат инвестирования выбранного счёта`
- **Source:** design frontend 1, «Contract Implementation»
- **Verification:** `npm run openapi:generate`; `npm run openapi:check`; `npm run typecheck`
- **Depends on:** 2.5
- [x] 3.2 [frontend] Форматтеры `formatSignedPercent`, `formatPercentagePoints` (знак U+2212, 1 знак)
- **Covers:** `portfolio/investment-performance` — `Вкладка «Результат» на странице счёта`
- **Source:** design frontend 4
- **Verification:** unit-тесты: положительное, отрицательное, ноль, округление
- **Depends on:** none
- [x] 3.3 [frontend] Вкладка «Результат»: container, VM, view, KPI, alerts, недоступно, skeleton, ошибка секции, «Как считали», hint, строки `portfolio.performance.*`
- **Covers:** `portfolio/investment-performance` — `Вкладка «Результат» на странице счёта`, `Недоступный и неполный результат`
- **Source:** design frontend 2, 3, 6, 7, «UI/UX Package»; prototype вариант A
- **Verification:** тесты VM и view (RTL + MSW): обгоняю, отстаю, меньше года, неполный, недоступно, неизвестная причина, загрузка, ошибка только во вкладке; `npm run verify`
- **Depends on:** 3.1, 3.2
- [x] 3.4 [frontend] График результата: LineChart с тремя сериями и точкой текущей стоимости, tooltip, текстовая альтернатива, пояснение об отсутствии прошлой стоимости, состояние без series
- **Covers:** `portfolio/investment-performance` — `График результата`, `Справочный индекс без дивидендов`
- **Source:** design frontend 5, «UI/UX Package»
- **Verification:** тесты view: серии, `aria-label` с итогами, отсутствие референсной линии, пустой series; browser-тест на 390px без горизонтальной прокрутки; `npm run verify`
- **Depends on:** 3.3
## 4. Docs
- [x] 4.1 [docs] ADR 023 и строка в `adr/README.md`
- **Covers:** `N/A` — документация решения
- **Source:** docs-impact: `docs/adr/023-investment-performance-vs-index.md`, `docs/adr/README.md`
- **Verification:** `npm run verify`
- **Depends on:** none
- [x] 4.2 [docs] Страница `domains/portfolio/investment-performance.md` и обновления `all-accounts.md`, `data-reference.md`, `portfolio/overview.md`
- **Covers:** `N/A` — документация текущего поведения
- **Source:** docs-impact: соответствующие строки таблицы
- **Verification:** `npm run verify`; сверка с реализованным поведением 2.4 и 3.3
- **Depends on:** 2.4, 3.3, 4.1
- [x] 4.3 [docs] Глоссарий, `overview.md`, `income/overview.md`: новые термины и снятие конфликтующих запретов
- **Covers:** `N/A` — терминология
- **Source:** docs-impact: `docs/glossary.md`, `docs/overview.md`, `docs/domains/income/overview.md`
- **Verification:** `npm run verify`; `grep -rn "доходност" .` — только новое значение или оговорки «не является»
- **Depends on:** 4.1
- [x] 4.4 [docs] `integrations/moex.md` (раздел «Индексы»), `architecture/context.md`, `architecture/http-api.md`
- **Covers:** `N/A` — документация интеграции и API
- **Source:** docs-impact: соответствующие строки таблицы
- **Verification:** `npm run verify`; сверка с generated OpenAPI из 2.5
- **Depends on:** 2.5
## 5. Завершение
- [x] 5.1 [specs] Commit planning artifacts и прототипа; PR в `develop`
- **Covers:** `N/A` — поставка planning artifacts
- **Source:** engineering workflow
- **Verification:** `npm run validate`; `git diff --check`
- **Depends on:** none
Ручная приёмка на реальных данных Change Owner (парность переводов,
правдоподобие результата) и semantic verify/archive выполняются по
qa-analysis до закрытия Issue. Это не implementation task.
@@ -0,0 +1,2 @@
schema: wealth-ledger-sdd
created: 2026-09-08
@@ -0,0 +1,434 @@
# Completion Evidence
## Delivery и authorization
Change `simplify-local-design-tokens`, store `wealth-ledger-specs`, 2026-09-08.
Владелец явно согласовал удаление избыточной runtime-схемы и её тестов с
сохранением компактной проверки адаптера: «да, давай избавимся от избыточной
сложности». Сохраняется разрешение беседы на coordinated planning, implementation,
docs и completion без sdd-session, session records и промежуточных approval stops.
Отдельные artifact approvals не выдумываются. Tasks выполнены 3/3.
| Area | Status / delivered outcome | Delivery commit |
| --- | --- | --- |
| frontend | affected: direct JSON adapter, два tests в theme.test.ts, ownership docs; net −301 строка | 8b29cdfd98790cb3316f1db0f8933c8b6bc4b335 |
| backend | N/A: API/auth/storage и финансовые вычисления не меняются | N/A |
| specs | affected: trusted local source contract и planning | f171b8bb4642dcaf0c9b4db8354eb6b8aaa4ea9d |
| docs | affected: as-built adaptation и пределы проверки | b0e747fdee2f726f18a4222f4bfc249c8f6d845b |
`runtime-openapi`: N/A; ни одна сторона API contract не меняется.
JSON bytes, public exports, provider wiring, CSS и dependencies неизменны.
После commits и frontend hooks все 303 reviewed entries сохранили binding.
Completion commit сообщается отдельно от evidence во избежание self-reference.
## Planning binding
Design UI/UX applicability N/A: внешний вид и поведение не меняются,
новый visual package, attachments и immutable reuse inputs отсутствуют.
Canonical fingerprints `wealth-ledger-input-v1`; только approval Tasks
нормализуют checked prefixes. Review/PASS используют raw Tasks bytes.
| Artifact | Fingerprint |
| --- | --- |
| Proposal | `sha256:7e5df002c15fb57ca7c771cdfdca7c71f36b341008659fbfc2e792e44e74e036` |
| Delta Specs | `sha256:e0b18a6a04b49361691f4a67df557bc049b94cf38a78a37a3d45b9a2c1e300a7` |
| Design | `sha256:c0d00f75623f3fa569478e5f1fc43a5bee527d97ba12e2dc4ca09eab89d378ac` |
| Tasks | `sha256:3d8a5f3df2ada9379a79b86c20bc11b80cc65e1dec7704d455aa50214aaf51f5` |
## Observed verification
Перед Node-командами использованы `source ~/.nvm/nvm.sh` и `nvm use --silent`
в affected repository; specs использует frontend .nvmrc до перехода в specs.
Node 22 и существующие lockfiles. Неизменившиеся gates повторно не запускались.
| Cwd | Exact command | Observed outcome |
| --- | --- | --- |
| frontend | `npm ci` | exit 0, 499 packages |
| frontend | `npm run test:run -- src/shared/theme/tokens.test.ts` | baseline exit 0, 16 tests |
| frontend | `npm run verify` | final exit 0: OpenAPI snapshot, strict TS, Biome, tests/coverage, Knip, deps, deps:storybook, app и catalog builds |
| frontend | `npm run test:coverage` (в verify) | 25 files / 112 tests PASS; statements 89.85%, branches 84.5%, functions 88.08%, lines 91.58% |
| frontend | `npm run deps` (в verify и commit hook) | exit 0, 188 modules / 705 dependencies, no violations |
| frontend | `npm run deps:storybook` (в verify) | exit 0, 189 modules / 670 dependencies, no violations |
| docs | `npm ci` | exit 0, 86 packages |
| docs | `npm run verify` | exit 0, 44 Markdown files / 0 issues, diff check |
| specs | `openspec schema validate wealth-ledger-sdd --verbose` | exit 0 |
| specs | `openspec validate simplify-local-design-tokens --type change --strict --store wealth-ledger-specs` | exit 0, в том числе после проверки всех Tasks |
| specs | `openspec validate --specs --strict --store wealth-ledger-specs` | exit 0, 25 main specs до sync |
| workspace | affected local Markdown link resolution | все ссылки существуют, включая новый theme.test.ts |
| workspace | `python3 /tmp/wl-simplify-fingerprint.py` | после delivery commits reviewed binding неизменен |
Первый implementation verify обнаружил типовой индекс в Foundations, который
раньше скрывался за Record из Zod: обход переключён на JSON entries без изменения
содержимого. Полный gate повторён после исправления. Build сохраняет обычное
неблокирующее chunk-size warning. Gate exclusions и coverage floors не менялись.
## Browser proof
Playwright Chromium. На `http://127.0.0.1:5173/login` до/после видны «Вход»,
Email/Пароль, submit и ссылка регистрации. Сравнены theme, colorRoles и computed
CSS maps девяти элементов — полностью равны. Это smoke/preservation без submit
и authentication. Raw captures `/tmp/wl-simplify-before.json` и
`/tmp/wl-simplify-after.json` использованы reviewer как временное evidence.
Storybook из свежего build: `npm run preview:catalog -- --host 127.0.0.1 --port 6007`.
Sandbox EPERM при bind разрешён повторным запуском с escalation. Manager содержит
Design System и Product. На ширине 375px отрендерились Foundations Colors,
Typography, Layout, Button Primary, Product PositionCard Bond Loss и
PortfolioRefreshState Refresh Error: все 6 без горизонтального overflow и
pageerrors. Fetch/XHR только к catalog index.json; application API/auth нет.
Enter на retry сохраняет focus. Наличие action callback отдельно не заявляется.
Старый dev server потерял manager bundles после npm ci. Перезапуск собственной
сессии командой `npm run dev:catalog -- --host 127.0.0.1` восстановил 6006;
в браузере повторно подтверждены оба блока и заголовок Colors. Временный preview
6007 остановлен. Индекс и story IDs не менялись; новые stories не добавлялись.
Полный visual regression всех маршрутов или новый продуктовый пилот не заявлены.
## Independent read-only review
Reviewer `/root/simplify_tokens_review`: **PASS**, Critical/Important findings нет.
Прочитаны artifacts, diff, docs и актуальные gate/browser results. Независимо
сверены baseline/after JSON, сохранение Purpose и unrelated requirement/scenarios,
локальные ссылки. Reviewer не редактировал source/planning/Tasks.
Review выявил пропуск ignored frontend/.env в inventory. Включены все существующие
.env* inputs (значения не раскрыты); mtime .env предшествует gates. После уточнения
scope reviewer независимо re-expanded inventory и пересчитал каждый entry своей
реализацией; bytes/modes/missing entries, subsets и отсутствие conflicts совпали.
Новой реализации не требовалось, источник и gates не изменились.
PASS: `sha256:21239824e79f24738c39448a304c4b47e34bad59c443c1d29d80b373b1041378`.
| Gate scope | Entries | Fingerprint |
| --- | --- | --- |
| frontend | 202 | `sha256:ec148234ae35ab988012b988b76b50ef9202d879b480e2a621e1ec4a5196aa39` |
| docs | 62 | `sha256:d06e53ee6f5f0c82d47423b2dd07d06e33ae10e8ba1b8e30be6da0d5e9f0d63d` |
| specs | 38 | `sha256:6c45988ff6302545b81eda7402caf2e702b98b6752ee8fbd10aeb00bf8546d4c` |
## Semantic sync
Current native status определил только `specs/frontend-design-system/spec.md`.
Native specs instructions прочитаны. MODIFIED сохраняет все существующие scenario
identities; у «Исходные токены некорректны» меняется гарантия согласно запросу
владельца. Первый структурный проход отклонил удаление identity, и delta исправлена
с сохранением heading и явным новым смыслом, без обхода validator. Purpose,
остальные scenarios и requirement об общих компонентах сохранены. Ожидаемый diff
показан до archive; expected main сохранён для exact post-sync comparison.
Historical archives, другие changes/state и references вне mutations.
## Reviewed input inventory
Полные frontend/docs tracked и nonignored untracked inputs; frontend .env*;
main specs, local schemas, current change, config и применимые AGENTS.
Исключены собственное Completion Evidence, другие changes и historical archives,
unrelated specs tooling, node_modules/build outputs. Runtime задан lockfiles,
npm ci и digest environment inputs; содержимое .env не скопировано.
Deleted files сохранены как missing. Entry fields отсортированы по UTF-8 path,
kind/mode/raw SHA-256 образуют NUL-delimited `wealth-ledger-input-v1` digest.
Expected archive/sync и собственное evidence — отдельные completion outputs.
```text
file 100644 3e5ed52ca50a0424fb967a92afeaca6e162f3c16e01d0fcb352e48362235cd7f AGENTS.md
file 100644 4d56952b0fb13bf8f9b6c13a6d4c34a075bac3af447636a1df4335d7576e2f97 docs/.gitignore
file 100644 be63447d1d31bcedba10e009b12bd9bab07d5918bc8de8093865c74a54e7fe78 docs/.markdownlint-cli2.jsonc
file 100644 f14b4987904bcb5814e4459a057ed4d20f58a633152288a761214dcd28780b56 docs/.nvmrc
file 100644 a934307476c518f447a91c83e139b17860da32608cee69e210cb4ec985f5d9d1 docs/AGENTS.md
file 100644 e43d6d771bebd53bc6864646cb13b5f7501d610edac17fe13b2e7ac945613757 docs/adr/001-prisma-sqlite-local-mvp.md
file 100644 13b55e00d9e362d4764ef55976bb250ea78c0931a1da8f0453baaa091b5a8bd2 docs/adr/002-nest-generated-openapi-contract.md
file 100644 913388ffa0dec5dabed83e72805aa1074390ea09e600c35db744d7dd10385ed6 docs/adr/003-tbank-server-side-grpc-boundary.md
file 100644 7345db35f23bf7436a05749a7223191cdd43a0490b80252d57f8aed52cf5eb81 docs/adr/004-encrypted-per-user-tbank-token.md
file 100644 d176fe9d114c3f3cfef5437ad109fc4c0763996df13811f82d94db375ed81416 docs/adr/005-tbank-upstream-resilience.md
file 100644 ad17b1ca13be43b476f47b4395eff0639162928a761fa0c540ba8d9f2de23661 docs/adr/006-auth-session-policy.md
file 100644 deb1c7cff757e6f500d8ba1a6982baaeb79d8fdf4aa0127cbc32d8530df86665 docs/adr/007-react-query-fetch-policy.md
file 100644 3420b3fbe6893b08390366a418fed0e7024a5e8566c037b6d3e9158bf8fc3848 docs/adr/008-pino-backend-logging.md
file 100644 0eb9ec3f94fe5cd7eddf601d8dd98b1ea1c482c58f4af039ed0d36d9219d265f docs/adr/009-concurrent-refresh-rotation-policy.md
file 100644 b53109e4c6ccf104a8ef87d8f8f007cf8291de6cf99351201741df390e89dee1 docs/adr/010-native-openspec-sdd-and-local-orchestration.md
file 100644 87397b932d073691809e8c7d16c0979e6251fe8babdd8c3545a951feef0dc13a docs/adr/README.md
file 100644 e84fba694c45cb6d5c747e7b44a4678280794b828c33a7ce0c3b6dc75f38b614 docs/architecture/README.md
file 100644 246278c09dfe2a53cbece93b0db1ae1ab44f08cfa87167da2e5d98f37b39f8b1 docs/architecture/domain-glossary.md
file 100644 6b9b0b24f3b22a0e78f6f3315045af33d101a31309eab9063f5f099ea002a921 docs/architecture/system-overview.md
file 100644 b38ec6ac31d18cf6a4ceb8bf5c176640f8909c8d708884eb1d2106e503423321 docs/audits/2026-08-26-wealth-ledger-sdd-v2-review/README.md
file 100644 a2cc3b5ebf4c4ced326d4ca8cad1da59c725ae0538c5f5df0b885203d03177f4 docs/audits/2026-08-26-wealth-ledger-sdd-v2-review/deepseek-audit.md
file 100644 528952869ceeef2502a90baadb9d4802aada1fbf342730acc35535f3adbc77e5 docs/audits/2026-08-26-wealth-ledger-sdd-v2-review/gemini-audit.md
file 100644 a57789cb6f6c7f9415ba45a279d347ee8e5749063b62d2d9d8a4c214938d8136 docs/audits/2026-08-26-wealth-ledger-sdd-v2-review/grok-audit.md
file 100644 67f5d725f548c221c824a90ee0254f2d4805479cd3069563c3732e844fd625b7 docs/audits/2026-08-26-wealth-ledger-sdd-v2-review/kimi-audit.md
file 100644 74b41a6c1f8302da649e00c5bf83b34804a604f0b0a563261fb3e5ac6f30abe5 docs/audits/2026-08-26-wealth-ledger-sdd-v2-review/qwen-audit.md
file 100644 af06d4060e43d91ba2aac2ca453f0cea6a226fc0ccff66d68412c3d646018979 docs/audits/2026-08-26-wealth-ledger-sdd-v2-review/zai-audit.md
file 100644 7d9949ff660b77df66c6c1a863681dcd0ee6af00b131394ffa9c3154034c52a6 docs/audits/2026-08-26-wealth-ledger-sdd/README.md
file 100644 2d50f4c195507ceb079d69489c03f1ea8a81185e2594dd51ff6a293ab8198db8 docs/audits/2026-08-26-wealth-ledger-sdd/deepseek-result.md
file 100644 af95a4c208c993621a6c15102a1e26684eee6c2966b166f6ba23b600d0e33696 docs/audits/2026-08-26-wealth-ledger-sdd/gemini-result.md
file 100644 31d423903cde2d7c1feb711cc625c77864964bb66cfd0cacb734c5c6bbdddd17 docs/audits/2026-08-26-wealth-ledger-sdd/kimi-result.md
file 100644 1b6582efe55b7044d2bebc9c1a146df1963e4103c5517c259ccf617d6d2b4f14 docs/audits/2026-08-26-wealth-ledger-sdd/qwen-result.md
file 100644 0bdb7785087c8d1b6fe532d578804aa498bfc90935623d79c1003cd1899d5bad docs/audits/2026-08-26-wealth-ledger-sdd/zai-result.md
file 100644 49054cc1df4206a105a2d192457012097fc1fe856d68b95dec7c497b8138ce31 docs/audits/2026-08-frontend-repository-audits/README.md
file 100644 6b7968ebeb7eac1017e2c75e7e7888486d8a232bed7730f760960d77c5343e24 docs/audits/2026-08-frontend-repository-audits/frontend-repository-audit-2026-08-28.md
file 100644 1071171cfc987113ea209383c687b0b54932e5a390726ca62650b0de90a722b6 docs/audits/2026-08-frontend-repository-audits/frontend-repository-audit.md
file 100644 7b5de5c82951f70b8ec7431468ed5cffee3bcf98ba09b029d327a8bb72c1fc36 docs/audits/2026-08-frontend-repository-audits/frontend-structural-maintainability-audit.md
file 100644 84d665f0cbe510f379e4473750c962ea1a0e2aa24161c86ef6f742ba66227f4b docs/audits/README.md
file 100644 3e5829193d0854156bcfac1214e7bfca60c6e8d8ce8db9154fa807a431c636c8 docs/audits/audit-13-08-2026.md
file 100644 27e2cc3fc3ad3d3f2cdc20728d6b812254d923a2f0e1e45be2f14490ece0dabf docs/audits/audit-20-08-2026.md
file 100644 163085a69de87ba1dd28e3f82118786167f9c0489ea9dd2695cf9ab2e8a68bac docs/audits/audit-24-08-2026.md
file 100644 e2d805d48dc793468bcc2f5126ab72cdfcfec6d1ec8d5916897dfe432192a73d docs/audits/codex-sessions-token-audit-2026-08-26.md
file 100644 08b06c2ad2c7e6ad32320a4cbc42e251a612ac82900ef74bbaecf376b4b1a2f9 docs/development/README.md
file 100644 dae11d6a0dccc381d671173162740e74908c57414b4db07e12f79f316a13562a docs/development/agent-token-efficiency.md
file 100644 be6a892e8f97dca5549b6508281f52d529946988408035ac8ce1801994423ce4 docs/development/audit-remediation.md
file 100644 5c87fa0c931f56efddc0fa887627e55ec6728467c61fbf1394447f78cef9896d docs/development/backend-workflow.md
file 100644 5c1f3fb3c72613c25c8591355a56cdd10749295d22425df6dd7f36b27fbc2434 docs/development/completion-evidence.md
file 100644 2e82bb27e2c7dde5f31315f731c8cd97258570608dcc8518c9679688c8145eec docs/development/design-system.md
file 100644 643822346cfa50a30e79d3bcfdd649a7902a43da7a8a31ce773e984fb179fa5c docs/development/engineering-governance.md
file 100644 5eed875b21f69812ff92660ac3199f8b2a9e55427092f1e9713d8c81501feef7 docs/development/frontend-workflow.md
file 100644 ddf26cabeecc70b512fadc6c24958bc33c408cf91bbb45d49f9483a1851e4c31 docs/development/openspec-authority-and-lifecycle.md
file 100644 499989c73752b75e7ba92c268c8eb5fdf26ea1e709467c63173a157a63b22014 docs/development/runtime-openapi-verification.md
file 100644 4157bc32721a7aa76abbf45f69a250b9512c74ef4de2ac6186e13e72398cb8be docs/development/ui-catalog.md
file 100644 2a38cae1914bcf930596cb893890e78c1a65faa91543564422fb6b38fa405bea docs/features/README.md
file 100644 68d987a914c0a37e7e39fae167be0aad25c8f33552999a0f9d0eb35776bccdfe docs/features/authentication.md
file 100644 bf7b7a0f8ce554efa762574c45608938aa8fb51db4d01dc9a6c3dbc13f63c5f2 docs/features/brokerage-account-income-analytics.md
file 100644 b2d10d9da77e6e96c511b2f4bb55dfa87da4f530c4c9666a49b93b835b4647d1 docs/features/brokerage-account-ledger.md
file 100644 f041f612a4cdf784ce75e11fcea673f673bcf2cfe5b63602277a0c976935b7e2 docs/features/brokerage-account-upcoming-events.md
file 100644 7e32238f1addf3e229b210ecf35f112d742e40dcf9bda42e545f2961250309ea docs/features/brokerage-accounts.md
file 100644 fffcfa56a1d9d073db603d4e42e739b47ba2066acca51e994c1b7b7eaac43f27 docs/features/frontend-ui-resilience.md
file 100644 8b590274bdc9ccb06cc8d1367d42b64d6758fd43fabf4c79fb2aef321d1f48ec docs/features/portfolio-view.md
file 100644 0846ccfa3c2a821fea374c3bebba0038d449a7e588fcd6e4871a9d7d04668702 docs/features/tbank-connection.md
file 100644 e71ed9ea9040268cbb5f4002bf7a60d85f1808fc3eb5288305eae35559088d55 docs/package-lock.json
file 100644 f183b2f571b7ff73af7a071d49be568f3616c8e84b91127358ed679778227973 docs/package.json
file 100644 71153d43d010a50437ea3c54c34fd26c55470399219021918428a0a7178e9213 frontend/.env
file 100644 71153d43d010a50437ea3c54c34fd26c55470399219021918428a0a7178e9213 frontend/.env.example
file 100644 1820d069d49f5ea71159a8d50e55db1d02b5010825e1da111386a0a5be01eaed frontend/.gitignore
file 100755 ba793c27e4dd0c45841bb290fb8771ef92611eebfd7b8fde2f15394b2baa7dee frontend/.husky/commit-msg
file 100755 3166f419c8d677f3556b87d2c1a502664cfebe9c7d2149ac3699359c30e2409d frontend/.husky/pre-commit
file 100644 f14b4987904bcb5814e4459a057ed4d20f58a633152288a761214dcd28780b56 frontend/.nvmrc
file 100644 0d30b6034cab756bb1beb1cbd720c8fe73026278aeb0100f17e0133582547694 frontend/.storybook/dependency-cruiser.cjs
file 100644 0ee75a2839e02389cdfe113cafaf61b4ceae864656a5bf0e603c5b9de096d61c frontend/.storybook/main.ts
file 100644 05434fa7b625bf0ecb58c0d81684b30055c6c6ee6cb2b42b38591e7a33781ba3 frontend/.storybook/preview.tsx
file 100644 cacf1dc8f6dad83c1c35212ed806204eb28faf9bc8d7217cb2da372411008cc7 frontend/AGENTS.md
file 100644 ffdaeb7f2a05d3f842f102766f7471fd66c470728884410bc6d90a637b1721a5 frontend/README.md
file 100644 d42ba2c972d01e98cb03a474c50afed340e426cb6ff98bcf7495eacaf8372f0f frontend/biome.json
file 100644 eda2b3989c752320cce88af66b8d488ca4c6f8c417fcd3b9f4e8fd54fbda9284 frontend/commitlint.config.cjs
file 100644 c931344af98938f3e4df01abb0db759b3e47d16084fc7c198edff2a846f5021d frontend/dependency-cruiser.cjs
file 100644 9bc5e3fa848a49f7990c2d2727381d47c21ec9d4ea73a7223d5ae3c93be14f88 frontend/docs/API-LAYER.md
file 100644 75fb467b458a40e49d6a837cbc22809838f15e1010e8f6c7d671414b5e833b80 frontend/docs/CONVENTIONS.md
file 100644 4a4f69aab159b4f6e68b0d7d1e5b997691db3c0dc77b8ea367eeb4354c3b3986 frontend/docs/README.md
file 100644 f6fdec18b0b86baad1009b40d431d9d5c66b5aee3cd2e676e9836d52a367c50a frontend/docs/STRUCTURE.md
file 100644 85dd9e70440f2d395eee30d9790ba2e0eabba69c5571578dad9c475144d478ea frontend/docs/TESTING.md
file 100644 13e90ca029e66456a066d67ce8338294ca540bf2fd368309c59571ef23d2588c frontend/docs/UI-PATTERN.md
file 100644 71a8cff454e6f9f761a0a934d3cd58f8fa72d1c6281e53ae22b38711b8730288 frontend/index.html
file 100644 a195a4ae649c0d6998ae34328481f2b7426c658c3dcfb40ff69c11bb7cceea08 frontend/knip.json
file 100644 341a036a0fce7efd6f507e09893b6613ee3513d70b6cb4cb3733e14ecb6ce1d5 frontend/package-lock.json
file 100644 a375786c0da27d74347539ed395135f540bb94d8a0ff40a5bc425ba66f87996e frontend/package.json
file 100644 a63748301b50343c1bf3729564d40e36633ce8847987c280ac2f5a3646841b0d frontend/public/favicon.svg
file 100644 8a06bf817b3cb62c3c9e7585dd5975d66c6be15e63d565daea59d4cac40a4a39 frontend/public/icons.svg
file 100644 e03760ce70a5e7fee38f2862ddc559c31ab18423b475c03619b7f9854906c937 frontend/scripts/check-openapi.mjs
file 100644 916c2f50916e33565bd7d55e4b751ba227bc516de53704d59df58b5efe0727c9 frontend/src/app/index.ts
file 100644 270e09cb6081ed03f60e64df61aae4614370ae1a85577b9ea73b40473854430f frontend/src/app/routes/AppRoutes.test.tsx
file 100644 2fa3448504fd351d53856f80e82ab4f5e6f7616c2ee51fca765602e08302efd9 frontend/src/app/routes/AppRoutes.tsx
file 100644 e9f05e96f393e161b039d342440b64d14f0656431a1712e043735b6b855294a1 frontend/src/app/routes/ProtectedLayout.tsx
file 100644 cb21d17cc519ecd294f49bf7e81758b95012a58eb3874497bf8deaefa8ca6d1a frontend/src/app/routes/RouteErrorBoundary.test.tsx
file 100644 45d27f3afda5c382751f6e3cc8547a1c4562361eb8780ddb7e577256ce865d0b frontend/src/app/routes/RouteErrorBoundary.tsx
file 100644 187b0c0c3a828d8828d8b234d5ba722fbbdc246ed8c852548f323ce5e52e455d frontend/src/app/routes/RouteFallback.tsx
file 100644 0ece306b18df665427bf00794ffc52112f3893321edd4c01cb2cf709911b6a34 frontend/src/app/routes/RouteGuards.tsx
file 100644 5349945f286313fd06df177d43d226b727646056fbfea4ab737ac5301790eac7 frontend/src/app/routes/index.ts
file 100644 c38291e83facd7105c710e8e1827d9d8adc01c98943cfb3449bea6e90e5f5826 frontend/src/app/ui/AccountBreadcrumbs.tsx
file 100644 2cffedebe0b98d60bfe8d1d26752dcd3ee1b0da3a4b575d1bdb03471dff0030e frontend/src/app/ui/App.tsx
file 100644 5ee3c223a10327120bed54a71b9d22023d8597e9db347e9e91f5c9276a33c7da frontend/src/app/ui/AppBreadcrumbs.module.css
file 100644 a881a844060e88f2d0f062ff232160eb35980587a7e22794186183acddfcde2c frontend/src/app/ui/AppBreadcrumbs.test.tsx
file 100644 94335087a5c175c313b072801536480e3b48057bf19e00213d812d06198010b7 frontend/src/app/ui/AppBreadcrumbs.tsx
file 100644 ab0ce496eb8d73c015719f36f0c66e98a9d3da4c56cdfdd5fd69848dc40c2269 frontend/src/app/ui/BreadcrumbTrail.tsx
file 100644 77fd6a747b349594d3c4145bc0e2bbb3e90d70a5b7805e94557e3a7d5136200f frontend/src/app/ui/index.ts
file 100644 881ffbcaafc212e49addad08846a5b82761355fa20624253af3477ba33262c5c frontend/src/assets/hero.png
file 100644 35ef61ed53b323ae94a16a8ec659b3d0af3880698791133f23b084085ab1c2e5 frontend/src/assets/react.svg
file 100644 5be21acd42eb7b896e517f4e0f0f11eb5c5d9e54fbbcebe9453f033008fcca6f frontend/src/assets/vite.svg
file 100644 8e926e8984f57c554a129b43a71ab32ad6964f7d42544084de52503808df807c frontend/src/entities/broker-operation/api/useBrokerageOperations.test.tsx
file 100644 940c1e6828af0a0111f44ea7b3e459e53e0fbae51147c4408b64503150655ed5 frontend/src/entities/broker-operation/api/useBrokerageOperations.ts
file 100644 4e09b8b326c418535e843d5ce6a40e781982e0309771f2c39662a81aaf35fa2e frontend/src/entities/broker-operation/index.ts
file 100644 c2f624d4159f6abef51a02ae891b4756f8d891219a2fa40f19371bcc769bbfbb frontend/src/entities/broker-operation/model/display.ts
file 100644 c20f2c6fc42362f1f5786a58b7ab81fb70d4a620438a8b2d1f85de1edd4f49cb frontend/src/entities/broker-operation/ui/LedgerEntryRow.module.css
file 100644 1b863f3e11e53b820135d8c8f74c1f51868528abcec866a3a0bb90dd0dd9bb7e frontend/src/entities/broker-operation/ui/LedgerEntryRow.tsx
file 100644 5547bba2b73d4362bf8a28c9542c552d27895176af85dc0e2815514dbc42f3f6 frontend/src/entities/brokerage-account/api/useBrokerageAccounts.test.tsx
file 100644 777dc47b9e5373d00d1e9aafeda16d903961332044a8a53bce75b27cb4e2e970 frontend/src/entities/brokerage-account/api/useBrokerageAccounts.ts
file 100644 c39922c507a2fd0b00555e6403258d453a44c3ff6d15bc5549664b49ff7d465f frontend/src/entities/brokerage-account/index.ts
file 100644 cf8ac5c71ed4880c4ecdda54d9654490d541497dad837398d2e768aeda09c5b7 frontend/src/entities/brokerage-account/model/display.ts
file 100644 1214d8657833a336190a037745c88e205d03162a2a5c6fea7dc4a7e89b8194b3 frontend/src/entities/brokerage-account/ui/BrokerageAccountList.module.css
file 100644 f901105c428c0c1149ff6ae6fc180d853036d6e5b5fa7d114f58f019a6f4a804 frontend/src/entities/brokerage-account/ui/BrokerageAccountList.stories.tsx
file 100644 8b8c836509486010ea3d1e6103edcf16cb5845ed5b8b174fe76a6c7ab4487855 frontend/src/entities/brokerage-account/ui/BrokerageAccountList.test.tsx
file 100644 c77c41ca8984c36402b2ae23a178e7e3d2a6955e76fa0ddfd82e61054702a815 frontend/src/entities/brokerage-account/ui/BrokerageAccountList.tsx
file 100644 63211bf87150b004c58404d6a41806148eaddd945272ce7934ca88266d137896 frontend/src/entities/brokerage-event/api/useBrokerageEvents.test.tsx
file 100644 8b0d8a3d3202aedd69efca839e777711b44d8c79b677cedf75b2a0941673c1d7 frontend/src/entities/brokerage-event/api/useBrokerageEvents.ts
file 100644 6e8a418ef85d3fa95f6a727297a70609f01160ef254ff8b4d1dffb0c16b4b9e9 frontend/src/entities/brokerage-event/index.ts
file 100644 e4f29cab3639aca0750913c364b01f5d876784f2abd19c98f94994f3a1b761d6 frontend/src/entities/income/api/useIncome.ts
file 100644 31be30a5f707316af8a3307bd86d38a4152a5601170f7defca637aca1b6b667d frontend/src/entities/income/index.ts
file 100644 3ca6f73af2e1bf44c88eec3590e7ffe072e617d4401cf7a23643ff8ef44782a2 frontend/src/entities/portfolio/api/usePortfolio.test.tsx
file 100644 db5edf155da5992f4f60e2909c2d9872b69add47a778a4a15808b1381bbbc21e frontend/src/entities/portfolio/api/usePortfolio.ts
file 100644 741bed6b52affbde608fa3809e153db54ce0e5368f61c86ecfd97b41f3173db7 frontend/src/entities/portfolio/index.ts
file 100644 bd7a0c75ca7aac94c99addc4a80150432958b966d20689f6858798460aa32bb6 frontend/src/entities/portfolio/model/display.test.ts
file 100644 05c82e80532566ad042d0c4a8e491cf853a6fe3a5a8a5ee1e64afa2cd21d9abc frontend/src/entities/portfolio/model/display.ts
file 100644 e753ea28bc12ee02b3f5c5c88e0135b0647cfc923f31c067585752b359d7dccb frontend/src/features/auth/index.ts
file 100644 124b93ecd38607425930067a6021f0a73c0b49d94c86693169df200c7f924d7d frontend/src/features/auth/model/auth.test.tsx
file 100644 3cabda4d2c863cd7ec4edc26e783086d3cded61dbcf600c396b8e248e6727111 frontend/src/features/auth/model/auth.tsx
file 100644 3a75a1cfcf0adff1ad15fcd8e030d63731dd6a8c3a8195ea3758b81d21434ca1 frontend/src/features/auth/model/validation.ts
file 100644 d45861d840614c9f3226ffcb82397feae7b63415aef47967ba609d4c830880ad frontend/src/features/auth/ui/AuthForm.module.css
file 100644 2ecce514e944a596158b4c7a9a62dc71e27035a2dad30d244c470f1acf27a43f frontend/src/features/auth/ui/AuthForm.test.tsx
file 100644 8324ca8b2435d72f9808869c7884b7e8ab3fe511141dd076472970815b1b9635 frontend/src/features/auth/ui/AuthForm.tsx
file 100644 0f0d56b173312701a8b0412bb8b6da3f098a867b131c4b23d442ecbc79061cf7 frontend/src/features/auth/ui/AuthPageLayout.module.css
file 100644 83111c2fb3517b80756f890ca3f7d5d9f6da5e5d9758b7ff5d840a979a8c083e frontend/src/features/auth/ui/AuthPageLayout.tsx
file 100644 8e4561241c21d14c87e4b9f5d413d8bbe0adeeb4204fb8b91b5b6b735aa443d3 frontend/src/features/auth/ui/LoginForm.tsx
file 100644 946ac85c6efb952092fac8a8f6a58c26ed27d9eebf57c609935f0d0517824e92 frontend/src/features/auth/ui/RegisterForm.tsx
file 100644 fb48585faea80f8e1f252e222a7256486518d767b66032d111deeededd3113ca frontend/src/features/tbank-connection/index.ts
file 100644 673636a6275565755a383cba89a29c42c61ade16fd499308103aa9d9cbb32d73 frontend/src/features/tbank-connection/model/connection-boundary.ts
file 100644 a1324a729d5a4a4545a5431c170e50c205df0fc7a17aef3af3e7acfedd263087 frontend/src/features/tbank-connection/ui/TbankConnectionSection.test.tsx
file 100644 87b3e3d0c114166c6c9c5cde6a99bfdbd6b3a6ae342e744a0734fb6a7630870f frontend/src/features/tbank-connection/ui/TbankConnectionSection.tsx
file 100644 690d130a1ae2d4e49b1e05349ef70123a48baadee13f6778f058d3d02c439399 frontend/src/features/tbank-connection/ui/TbankConnectionSection.view.test.tsx
file 100644 5095f80b8a698c78beff4f6ee6688a76d058929813475298976458df1ec33c68 frontend/src/features/tbank-connection/ui/TbankConnectionSection.view.tsx
file 100644 09fd1fecdc3b3261f0e48072ef42a75bbaa92100861b4e4564ceab27044d5095 frontend/src/features/tbank-connection/ui/TbankConnectionSection.vm.ts
file 100644 ef66e1d01c4bcf752b1c77f4e27bd01f23afa5ce3527b5b13001fad1d03bacc0 frontend/src/features/tbank-connection/ui/TbankConnectionSectionLoaded.tsx
file 100644 ba06f55346657614504488b64c12f11c2fcde3f7f407f627a53cd378f10ed362 frontend/src/main.tsx
file 100644 8ca150b0224671f8aedf2e9c51d5d8cf07a99aa88e8ad2ad631869a0852eb3c4 frontend/src/pages/account-events/index.ts
file 100644 092266dce3058a0888f809b0b2edfd91fb93d0412ae21409021352cbb31747b6 frontend/src/pages/account-events/ui/AccountEventsPage.test.tsx
file 100644 ef4a005d7cd955db6e9c8caa525593417dbc836587801118f07026d9108b71bf frontend/src/pages/account-events/ui/AccountEventsPage.tsx
file 100644 c73bc8df3b9a16703a853823864df62525eb0808f6cf4306d46763a16909b426 frontend/src/pages/account-events/ui/AccountEventsPage.vm.ts
file 100644 7e68b58167a4b94e36a53bd6afa46f02a15812bbcc2134f8d9db895b36140677 frontend/src/pages/account-events/ui/components/AccountEventRow.view.tsx
file 100644 d107a120c5b0b2fcb5148a9a5295de9e70ca1143d9aa463c052099d8e0beae23 frontend/src/pages/account-events/ui/components/AccountEventsPageEmpty.view.tsx
file 100644 b592d310ef3f3f3903ec928ed80be8090ec03458b82df9595958d9ebe2fe24ce frontend/src/pages/account-events/ui/components/AccountEventsPageError.view.tsx
file 100644 2231260ea36c10973d99b04aa669ea005b5a170f717110f8c2918d8b753028aa frontend/src/pages/account-events/ui/components/AccountEventsPageLoaded.view.tsx
file 100644 87c15175cff3ffd21ee5600ac7bc6778cb9a5d62787a067a3140825ad70296d7 frontend/src/pages/account-events/ui/components/AccountEventsPageLoading.view.tsx
file 100644 6e4320e2d46dd1f9205da955915d1dcd8c52ddfc99dc0eefd529ed473560a0ca frontend/src/pages/account-income/index.ts
file 100644 948c2c317aa93c8a15cf7f5221980b8063a8326bf70641e7c2403d23eb6e2df7 frontend/src/pages/account-income/ui/AccountIncomePage.module.css
file 100644 5844d4515ce3fd04cf3d283039d9c5e396f6b2742ee6f01e0312c55c25562dc0 frontend/src/pages/account-income/ui/AccountIncomePage.test.tsx
file 100644 7d96d2fe6ccc62bb29e82b71dd26c82de4c7a6cd783c24177ab6dab7df712b9b frontend/src/pages/account-income/ui/AccountIncomePage.tsx
file 100644 0c98d08ec0c2ed37fccc6c573293b97a0fa61a7d0202c07f7d80eb56a30d88f9 frontend/src/pages/account-income/ui/AccountIncomePage.vm.ts
file 100644 881bf2279bc644094096ba09c377e5d5f47fd8d18fca4316377e867d69c6b5b6 frontend/src/pages/account-income/ui/components/AccountIncomePageError.view.tsx
file 100644 f961337393750024699e33eaeaf4f4223842a338a4c4dc7b609f2c61efd3c465 frontend/src/pages/account-income/ui/components/AccountIncomePageLoaded.view.tsx
file 100644 fb799544f998d4d9082e6decaa1e4e97784adc1196196bc5c85341a931dddb6d frontend/src/pages/account-income/ui/components/AccountIncomePageLoading.view.tsx
file 100644 97bf6d52cc7289a9814bdbb8a3180db40d5c56728c641846d29a83092b4f8063 frontend/src/pages/account-income/ui/components/IncomeBreakdown.view.tsx
file 100644 97a5cc4b73214a0785e9a73b0588ca2b9cb3b5df634e44320f643d0ae1f8b573 frontend/src/pages/account-income/ui/components/IncomeComparisons.view.tsx
file 100644 c825b843685692d9f22367a6b1d857026ea106f168fd6d3285b73d49dc87cd98 frontend/src/pages/account-income/ui/components/IncomeForecast.view.tsx
file 100644 b498969cec972dd25c738b0b2a5fab9fddfa2071fe403feba8b3defbe96a4e2f frontend/src/pages/account-income/ui/components/IncomeSummary.view.tsx
file 100644 b40e96330418d1143691d47632c097db9a3c964056660397faa72bd09283bb9a frontend/src/pages/account-income/ui/components/IncomeTrend.view.tsx
file 100644 52547737449ffd6c907d93f685698500ac63999a68f6ef2411d0abecde85e453 frontend/src/pages/account-operations/index.ts
file 100644 98d047e57b00cbdb22027e2f382f26c4fe77db07d59dd6c307f79ad03768e2af frontend/src/pages/account-operations/ui/AccountOperationsPage.test.tsx
file 100644 1429f228fe702a9e467270b8c86be761ad682f2b0e46f054772cb6850406eb64 frontend/src/pages/account-operations/ui/AccountOperationsPage.tsx
file 100644 8677eefcf98e2e48ca108224306d8f9d3fcdd3977cf4a91a0a571f1d459f6eaa frontend/src/pages/account-operations/ui/AccountOperationsPage.vm.ts
file 100644 694f220b6fbc3ef0bfd09478e40dead28efb4c0b474b2db3ae400c0ed106bb85 frontend/src/pages/account-operations/ui/components/AccountOperationsPageError.view.tsx
file 100644 ceefca357d7bf19b0cd53715dd9700bd4e7343ab436950e4efeb6c90b87833cf frontend/src/pages/account-operations/ui/components/AccountOperationsPageLoaded.view.tsx
file 100644 1b5538a2d1c750b1b89d68ca208f857377dac21acaedee4a0b61da519f0a27e8 frontend/src/pages/account-operations/ui/components/AccountOperationsPageLoading.view.tsx
file 100644 acb5e60d2da1d85cf7518a15778a710a1aa17e31859f0e4b6313121e2e711222 frontend/src/pages/account/index.ts
file 100644 adb8635ae3524b97355f18a235664373af718470edc62e036b36c1e75d6cacc2 frontend/src/pages/account/ui/AccountPage.module.css
file 100644 7f8e70fbe71345f7426d05b9f65090d3cd9241e49ef71ef77ca4a61a4e35fc3e frontend/src/pages/account/ui/AccountPage.test.tsx
file 100644 889a4ccc68936d880336524f419d959bac4360f4d34bcbae340a9bd6f28e746b frontend/src/pages/account/ui/AccountPage.tsx
file 100644 b1db7a7bb3c6aa9e07f97eae1e74d62ee7e29d878493b91533dc0ee4d9ba0df9 frontend/src/pages/account/ui/AccountPage.vm.test.tsx
file 100644 e90bb1f9993ded5071532ee34c8ee15c6b534fe112c503d2e6a212c3e63ad806 frontend/src/pages/account/ui/AccountPage.vm.ts
file 100644 e78ee3b110780be81ecbef6fdbf4931097c32434b5df82453f79cb0e291cfb8d frontend/src/pages/account/ui/components/AccountHeading.view.tsx
file 100644 d22cb260448fca772c985d2a29f565e83fad672f2d9b4b42fb92fbc5980d9532 frontend/src/pages/account/ui/components/AccountPageEmpty.view.tsx
file 100644 258ef783c093ab2640bfe3c312c27e52d74b9acd0402996a44b7f4ebb7c05960 frontend/src/pages/account/ui/components/AccountPageError.view.tsx
file 100644 1a8f7b88c2a5cbcada4443095aad386a181bc57911d6b80ebc593a7759b371bf frontend/src/pages/account/ui/components/AccountPageLoaded.view.tsx
file 100644 b19f2db05a01ca43f4992efde81d47a6d536459918ca56917054dfea9452cdf7 frontend/src/pages/account/ui/components/AccountPageLoading.stories.tsx
file 100644 981f088c47cbabfd57e9aa57290f27953fb66a284533941892dcbcac7b66af3c frontend/src/pages/account/ui/components/AccountPageLoading.view.tsx
file 100644 9c6480b81d4df06d111672a0cbe679bfe8dea5d8bdea7943850591034c6b46c9 frontend/src/pages/account/ui/components/CashBalances.view.tsx
file 100644 0fa5e2693c91a826af7eee7dae7700321f4f4fb1668baa1f176b210855ab6da2 frontend/src/pages/account/ui/components/IncomeSummary.view.tsx
file 100644 8bfd285a1b34557402ba03f5b2d6678e96831b2eba65122f877456da483d2a44 frontend/src/pages/account/ui/components/PortfolioRefreshState.stories.tsx
file 100644 fa07c51edb7eb2d0180de2ecbe0522ea9621ee49b605dbf1e6517b6c5f7d5d8f frontend/src/pages/account/ui/components/PortfolioRefreshState.view.tsx
file 100644 3a059b529cc2fc4477d81d3eae83dfe746d39b7bd11b8cb0ee8f599237a19606 frontend/src/pages/account/ui/components/PortfolioTotals.view.tsx
file 100644 19886c5bf31943776128441a9164a7cadbc59ed98906b00271195c982271620a frontend/src/pages/account/ui/components/PositionCard.stories.tsx
file 100644 57c1259d226a8cffd751cb12014f5bd79cdf7b45216c14ec246457f14ce31eb8 frontend/src/pages/account/ui/components/PositionCard.view.tsx
file 100644 9a608c85f49c8a4231790308c79201efb526aca2251348532032cbf4ba1f70fe frontend/src/pages/account/ui/components/PositionLogo.view.tsx
file 100644 b947e40bb3d3306dc010446f342c75d5c7c2a55e081f65a23b4ad8b23fdff50e frontend/src/pages/account/ui/components/PositionSection.view.tsx
file 100644 c81327808061e9d6180a682a52a132b3a9c944ea6c7ea7175d2e9e2c556bb6b1 frontend/src/pages/account/ui/components/RecentEvents.stories.tsx
file 100644 42f0b5bd6bb5d50ba156a8380c0b821e96c1cf57316d62e261eb6d2b0e793226 frontend/src/pages/account/ui/components/RecentEvents.view.tsx
file 100644 11e3b1de6a35f484d2f17cf6c079e160c61dfaf6329355b21671a6cd0b937383 frontend/src/pages/account/ui/components/RecentOperations.view.tsx
file 100644 d894b7b5d573339279d5b76fd67ee60fa4b3940883227f32e56e82f1b2a713b1 frontend/src/pages/accounts/index.ts
file 100644 f797ed0eb20092db09af055477d0d007818cf11e4266d429273222bd601830e5 frontend/src/pages/accounts/ui/AccountsPage.test.tsx
file 100644 b40e07504262fa525acacddb49324a83639aa0cf2a39f7ed131f7d711c9014ce frontend/src/pages/accounts/ui/AccountsPage.tsx
file 100644 8e704043780f80d6a5ee7eb3d62ab7cb7b9b2dd47f1bcc9949f60a576bb4a7fb frontend/src/pages/accounts/ui/AccountsPage.vm.ts
file 100644 14c04cf71bff77de0bb1e11a938d1b1a1dafe362a2e1f8ce2f075c7952349e68 frontend/src/pages/accounts/ui/components/AccountsPageEmpty.view.tsx
file 100644 0c4b155c4955ce4a397c5b79f8fd2408cb5c6ba02fe1064f7c919197ccb32f35 frontend/src/pages/accounts/ui/components/AccountsPageError.view.tsx
file 100644 7912cf312e941676265f9fac7319ae0affcd8aaac14690186191f82e6680e015 frontend/src/pages/accounts/ui/components/AccountsPageLoaded.view.tsx
file 100644 2386da6dc9fbe23b9a4379ad4ff8fd0cf539e5ea3b46b5b5c45c5cb74360bea2 frontend/src/pages/accounts/ui/components/AccountsPageLoading.view.tsx
file 100644 74899aba17ff330f749232f50ba0e251549bd7e8546090898fd8d829f19ed7bb frontend/src/pages/accounts/ui/components/AccountsRefreshState.view.tsx
file 100644 eecb1f89e2f1f7b6749585a6cd3adff86513a8bf5b2b53f3ead46de7ad3fc45b frontend/src/pages/login/index.ts
file 100644 da0abc69206621cddd71c4755edf7354a8f740df18807062e66e1e96e47d878e frontend/src/pages/login/ui/LoginPage.tsx
file 100644 6962cb472751d79b33ef31a46d18e057842320a062f39dad64a08cf38df338c3 frontend/src/pages/not-found/index.ts
file 100644 0621bd989fd15bb787e5bb8c07d95aab93a595a7aff2307b24bbe9c3cf82060c frontend/src/pages/not-found/ui/NotFoundPage.tsx
file 100644 97c0a1f58ca3592e9c4b0440b8ba6fe0c710eb53abd8b425cfdb806cfca14feb frontend/src/pages/register/index.ts
file 100644 94c558dc9d772afee6c9070ab74506a916308b4e333b0bc307285e295bf1cbd1 frontend/src/pages/register/ui/RegisterPage.tsx
file 100644 6081571ece8bfbb6fe1a497f8e8f9517c5a208f2cc3872dbbb4a8013fef78d1d frontend/src/pages/settings/index.ts
file 100644 943f183ea10396202a624eee5eac68d505dc258d0303c2cdcc18261bbd406d01 frontend/src/pages/settings/ui/SettingsPage.tsx
file 100644 059834d468b56472f3cb9a84e3c6ee53da3fd7d14d286ab03cf12ef66a159fb0 frontend/src/shared/api/authenticated-query.test.tsx
file 100644 2054ffb0279b1e72cfec24973fd212e25eef3d8e75a80f2cebaa8f496b0d02a2 frontend/src/shared/api/authenticated-query.ts
file 100644 21f632568273b6b192e1b9ebd4f63b7aeba65120b4efe75698e2fc0ec3c3988e frontend/src/shared/api/client.test.ts
file 100644 2454f54bf2d430feb765456aaf5ffb468fc068fb75bc13c6c257eb525683ba98 frontend/src/shared/api/client.ts
file 100644 c4aae54b591122701bc751f30feb57212f1f2df44adbcbfd77100afcef23a57c frontend/src/shared/api/generated/schema.d.ts
file 100644 c9b8bc978df49c35aa01a5997e2d79bcaa89f5ab363005e2019933a34ed88f53 frontend/src/shared/api/index.ts
file 100644 b33daae32b1a4b78b9f1c0535ac9f4bc91b277de718eca78385691b426e7e157 frontend/src/shared/api/problem-details.ts
file 100644 281f9aee4f254e85252f7688de7de1d0414ea2ee547a82d28fa4a1b7482b477f frontend/src/shared/api/spec/openapi.json
file 100644 c7b5bbc71bdbecc74dc8d71694fa766fafdd81effb5a4fa1206ef6e400cbf177 frontend/src/shared/i18n/i18n.ts
file 100644 bb3f6e68be414f50b1804a3a64e063ef7ad54f6d0fc2f0b330cd0eeaf25a0e02 frontend/src/shared/i18n/index.ts
file 100644 fa09ce4bde2469b7ead9f4655e78ba11910f506a25394f27e121a7a3503e14e7 frontend/src/shared/i18n/locales/ru.ts
file 100644 2ca092b2dcd83bca8c55d34c08c12c065721b7bd7382a8c87ff9fd9cac35a234 frontend/src/shared/i18n/zod.ts
file 100644 be879cdc1edc49f9356ac4358b5c4ed3c64f148a7d8c62049e477a36e81b289f frontend/src/shared/lib/display.test.ts
file 100644 c23021a1b968342cca8c3b163896991b65ae6d0a7c85469342d8a9ae2ede820b frontend/src/shared/lib/display.ts
file 100644 58a97412329011763f000f3690486e331b8060cad0b7687df8f35f5763019c7f frontend/src/shared/lib/index.ts
file 100644 e8609df757214d122d06d170d764745a02ba92fa2d64410d111176d88e4695ca frontend/src/shared/theme/Foundations.stories.tsx
file 100644 49f7368720c929acf09541dee9405d90f4ba41caf15aaee078d1527305d722a4 frontend/src/shared/theme/index.ts
file 100644 540661168457fdcf2fceaf201f741415cdf0b0d49b5a46de9c58719a2f2a5491 frontend/src/shared/theme/styles.css
file 100644 1ea0ca1aa2ed78a23a51cb22971b469672f3f560027824776e8110a4229703ce frontend/src/shared/theme/theme.test.ts
file 100644 5cce79e8fa932ab1f7130ff318a34515fb11c28e7c3c998aca4f603741e8bfdf frontend/src/shared/theme/theme.ts
file 100644 aa8648a568a935f59bd06e1c1e2aa7bc1a22d9d58db4edbbf89728cc55065f18 frontend/src/shared/theme/tokens.json
missing - - frontend/src/shared/theme/tokens.schema.ts
missing - - frontend/src/shared/theme/tokens.test.ts
file 100644 0ceb0e7f9658577e39056d8c69e235dcf5e84004d9208a06f8117d471e26ea04 frontend/src/shared/ui/AppShell.module.css
file 100644 68be717ac98fc04a3c9407c9eb3ce2cf8f62c975260d61d73e02fcbda1efbd55 frontend/src/shared/ui/AppShell.tsx
file 100644 cb97cbcf25dcd624bcd714d76b43f5972729a1426ee5cbe16ac32a761313390d frontend/src/shared/ui/Button.stories.tsx
file 100644 55a3a27849c7f0856c49179ef5542e842064dc30a4753180e91aa83ab8fd8214 frontend/src/shared/ui/LoadingSkeleton.stories.tsx
file 100644 2d1c5012c2fb93c83c5e7b8921485d392dbc5acbf0896703b32821329f9cbaed frontend/src/shared/ui/LoadingSkeleton.tsx
file 100644 f77300fa254afadeb65895adec96148f9e5b0943e2ed4ea81de258cf99b04048 frontend/src/shared/ui/StateView.stories.tsx
file 100644 de9d44409a715ce9c103e870edaf5b323127053844dcd8bef49c7854c6ccc0c2 frontend/src/shared/ui/StateView.test.tsx
file 100644 7414fa3de07b487c2fbcbf56f67925b418e64b586f9175add1576053fecfd0aa frontend/src/shared/ui/StateView.tsx
file 100644 3322a7129d6fca07f8d3b459d35508d8d7d1cbab22e8a2d49280c75c1813ac3f frontend/src/shared/ui/index.ts
file 100644 5440d1cee5598f9bdcc78ac203af6426b52ce8932bc6bd99366416a4de14a33d frontend/src/test/handlers.ts
file 100644 dd06c186d0d0a5ca218a296edd1f01a58c50f1433a16539c24692aa48eb15d20 frontend/src/test/query.tsx
file 100644 85c107a2f5429cd35c3a0b7ea7c9678934cf63038a66f42a0804d2e3465086cb frontend/src/test/server.ts
file 100644 6eb1bb94d573810cda4ea65da15957907d9c05d32880cb7217330e30a1b73c66 frontend/src/test/setup.ts
file 100644 6c887d8e2a3faf286443ec37d2cf6cef3582beb03339040ceff0412505e70ef6 frontend/src/test/storybook.test.tsx
file 100644 301c6165f2e9297f499870b4c6cf55e4f48aa15bf03a8fc98ea658c6eaa6c21d frontend/tsconfig.app.json
file 100644 975b855a3e1084cbd91c17718bf2099a00afbdad44dcc19b0b1d682f8cea5a10 frontend/tsconfig.json
file 100644 6cab0685be664373524de5ce6b2244b7d6cfdde28902ad3e3aa357f978b72299 frontend/tsconfig.node.json
file 100644 5c781c6de11dd929a9142b07467b51ea39f4886546804595d4075f430b59468a frontend/vite.config.ts
file 100644 7de4e28baa97782333946aa2f0a9ea7ddd4434498089079aad3fa79dbe615b03 frontend/vitest.config.ts
file 100644 39eb6f04b3a2e630f40985e5e4a64663bcf91b9422433dbcf093b6b3df94db81 specs/AGENTS.md
file 100644 8f71edf411818fb21b413c2172c63cc785cb6cd3716d7eaab9660daa9243c982 specs/openspec/changes/simplify-local-design-tokens/.openspec.yaml
file 100644 f9688ad20a5a8b85f5ce6b1df49e807efc8a554e8aae35ab67549b35699a05fe specs/openspec/changes/simplify-local-design-tokens/design.md
file 100644 deeccba5dbc3187111ab80c806f94580c4bc9c4b2f80d64c06d14c621756824b specs/openspec/changes/simplify-local-design-tokens/proposal.md
file 100644 911b9a4e900f37868a9959ff65f3f38ffa81473df974bba36d3313035829c8f6 specs/openspec/changes/simplify-local-design-tokens/specs/frontend-design-system/spec.md
file 100644 5aa52960b34d3ff8058d9da396ddc7c78f17f72b8692d795e14147add89d9819 specs/openspec/changes/simplify-local-design-tokens/tasks.md
file 100644 c5a7c8728824abcf525c4f26d3e7089c54396513d80d2ba4edba15c7a9ba5771 specs/openspec/config.yaml
file 100644 d4775e71e157bd144256ab62e40294a7f3d76173456c928b223a6b631d8ee629 specs/openspec/schemas/wealth-ledger-sdd/schema.yaml
file 100644 8d7749fe5bc32ef4acf70bcc4af22b95c615539fe866bc1de36571f3563b74d4 specs/openspec/schemas/wealth-ledger-sdd/templates/design.md
file 100644 d0aa6958b1e7abc3bae5516d3662e81caf9a46c6b34ee98045fb2b863a710a15 specs/openspec/schemas/wealth-ledger-sdd/templates/proposal.md
file 100644 8c01eb2f297af110cff4a12b998443684645fa54127d898a78b7765985ccb3a3 specs/openspec/schemas/wealth-ledger-sdd/templates/spec.md
file 100644 8722869157106bae35438834d4c05703dc3b3799df7377ec74ee905b8cdf477a specs/openspec/schemas/wealth-ledger-sdd/templates/tasks.md
file 100644 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 specs/openspec/specs/.gitkeep
file 100644 073d4e19163c6344a67c6cac99f11f60378d3ac6f5783c27ec88d2c92a67d713 specs/openspec/specs/agent-sdd-session-workflow/spec.md
file 100644 084ff97d258ab7c1e0eb6af27b36f054a8e16770813a3af5ac823f7385dbe53b specs/openspec/specs/api-contract-generation/spec.md
file 100644 d7ef01f72a89454a47e096b5f8666794d0d00d25f5a954aedb9ea2c3773f4d09 specs/openspec/specs/app-breadcrumb-navigation/spec.md
file 100644 a609567be6e205bd47d520acb67c9c6dbf382df541a91cfb1e52e831a9006b4b specs/openspec/specs/audit-remediation-workflow/spec.md
file 100644 25ea8975444c21aacfd78da0652c75c40906a68ba13b704cbe627ac5c8d9e3d8 specs/openspec/specs/backend-structured-logging/spec.md
file 100644 a5bbdc430022a3261383fb34430a1abe67e3811b40c36356ada24a250323faf8 specs/openspec/specs/brokerage-account-income-analytics/spec.md
file 100644 597db4690932fe06b735d94f690fe97ec95ad8ebfefb242a7e195b3a579f12bf specs/openspec/specs/brokerage-account-ledger/spec.md
file 100644 e04642cf3abdc694a879d036d28484289e31dc284843b47164af498eb5c8db7b specs/openspec/specs/brokerage-account-upcoming-events/spec.md
file 100644 8e7d826685c6e980efcf24356f5ae7800da349775d78be2c6c76183a31eba664 specs/openspec/specs/brokerage-accounts/spec.md
file 100644 ca75600c855a3bdbe46231279ce4bbfaacd9167ced1715c195797933f115b00b specs/openspec/specs/engineering-change-governance/spec.md
file 100644 4a63906e7dd2b83cc2fc7905444703c6eba0d4a0faa6d206362e70eb6e6798c9 specs/openspec/specs/frontend-data-resilience/spec.md
file 100644 9b761cfd70293f957a213de57d2b8099e225e89c2901b96546796b9be47c993c specs/openspec/specs/frontend-design-system/spec.md
file 100644 820f2cfdb583284cc6dad23d782ca8c720baa523800042a92b4daac6760e0f0f specs/openspec/specs/frontend-loading-presentation/spec.md
file 100644 07ce2f3cf0b128d9cb41b387c18977ca7e965b0cbbef9ea53b9354bc0337d35e specs/openspec/specs/frontend-quality-guardrails/spec.md
file 100644 e87e41adeaa0fa3859e29735009da5e8a6c13a3da26aab74c8fa3be2a33892e2 specs/openspec/specs/frontend-query-coherence/spec.md
file 100644 39f5543e5356755bc3512a99da866944eeb11def73c1cf05789e8b902aa6096f specs/openspec/specs/frontend-ui-catalog/spec.md
file 100644 3b249d74194004fde105ff6e80669b1f0df2365dda4f0b17b50ed2e4ec1f0297 specs/openspec/specs/frontend-ui-resilience/spec.md
file 100644 3a335f4d109e7944d6fd0ddb05a84e8690547bdec151aeabcd7ac736a2a0f06c specs/openspec/specs/multi-repository-delivery/spec.md
file 100644 de7a7853fd159c6a57487a9ea4e16f1f3dba8d7632d12daab0bdf8116c498342 specs/openspec/specs/openspec-authority-and-lifecycle/spec.md
file 100644 89bb963fba1f0b98d6be0f06b13f25d0cb138f8631e5e050a5ecf5ca749bdfd6 specs/openspec/specs/portfolio-view/spec.md
file 100644 8d370df3819339d9207220afbb953ca58a4ff5b0fe80c069a0348bf024ffe12e specs/openspec/specs/project-documentation/spec.md
file 100644 3694bb6bc69ec1926a545aa70f799a2581589ddce4519953c09ebe9fd71544d7 specs/openspec/specs/project-foundation/spec.md
file 100644 225cceba45eef078a39f93264575c633570ab3004fee989d81f02b0852693b2c specs/openspec/specs/tbank-connection/spec.md
file 100644 f4bf3fa2706da03a348fbde7219b8246a06f52405b91fd977bdb065b4d81ecf9 specs/openspec/specs/user-authentication/spec.md
file 100644 cbca6e5a6ac49a5cd70fcf560f2e52c2e5bd0f7df1f2ce5a5c75c42dfb9551b1 specs/openspec/specs/workspace-artifact-hygiene/spec.md
```
@@ -0,0 +1,75 @@
## Context
JSON v1 хранится в shared/theme и импортируется при сборке. Zod-схема и
большая часть token tests обслуживают неподдерживаемый сценарий произвольного
входного документа. App/Storybook уже используют общий public API.
## Goals / Non-Goals
**Goals:** прямое потребление локального JSON, сохранение адаптации и визуальных
значений, небольшой тест собственных преобразований.
**Non-Goals:** замена runtime-схемы новой системой типов или генератором,
изменение JSON/компонентов/провайдеров, удаление Zod из бизнес-форм,
внешняя загрузка токенов, редизайн и новые зависимости.
## Decisions
- Удалить tokens.schema.ts и связанные negative/document-shape tests.
theme.ts напрямую импортирует tokens.json, сохраняет public exports и
Mantine rem/rgba/getThemeColor adaptation.
- Использовать вывод типов JSON и существующие типы/helpers Mantine там,
где JSON расширяет литералы; не создавать параллельное полное описание
token contract. Type assertions при необходимости ограничены границей
адаптера и не считаются validation.
- Два кратких theme.test.ts сценария защищают собственные преобразования:
scale-aware размеры/тени и unscaled breakpoints; semantic color references.
Это оставшаяся полезная часть текущих тестов, без тестирования схемы.
- Формат JSON и правила редактирования сохраняются в as-built docs. Корректность
локальных изменений проверяется обычными repository gates, review и UI proof;
полный автоматический отказ для произвольного invalid JSON больше не обещается.
## UI/UX Applicability and Package
**Applicability and scope:** N/A: значения, стили, экраны, states, interactions
и responsive behavior не меняются; удаляется только runtime-проверка конфигурации.
Новый визуальный пакет не требуется.
## Delivery Map
| Area | Status | Architectural outcome or N/A reason |
| --- | --- | --- |
| frontend | affected | Прямой JSON adapter, компактный theme test и ownership docs |
| backend | N/A | API/auth/storage и денежная логика не затронуты |
| specs | affected | Уточнён контракт доверенного локального источника |
| docs | affected | As-built описание прямого адаптера и границ проверки |
### Contract Gate Decision
| Gate | Status | Reason |
| --- | --- | --- |
| runtime-openapi | N/A | Ни одна сторона API contract не меняется |
## Risks / Trade-offs
Произвольное некорректное значение не обязано вызывать раннюю ошибку загрузки.
Это согласованное упрощение для конфигурации репозитория, а не снятие проверки
на границе внешних данных: такого входа здесь нет. Если появится внешняя загрузка,
её trust boundary и validation должны проектироваться отдельно.
## Migration Plan
Сохранить baseline преобразований; удалить схему и её тесты, подключить JSON
напрямую; актуализировать документацию и проверить неизменность темы.
Прямое разрешение владельца распространяется на planning и delivery без
session records и повторных промежуточных approvals.
### Rollback Contract
| Field | Decision |
| --- | --- |
| Status | required |
| Trigger | Адаптер меняет представление либо потребителю необходим прежний runtime-отказ |
| Actions | Вернуть delivery diff и согласовать обратную spec delta; historical archives не менять |
| Data safety | JSON v1 и пользовательские данные остаются прежними |
| Verification | Прежние преобразования и обе сборки проходят проверки |
@@ -0,0 +1,31 @@
## Why
Токены поставляются вместе с приложением и редактируются в репозитории.
Полная проверка произвольного входного документа добавила сложность, хотя
загрузка сторонних токенов не предусмотрена. Владелец согласовал её удаление.
## What Changes
Упростить потребление локальных токенов, сохранив переносимый источник,
единую тему и прежнее представление приложения и каталога. Убрать гарантию
runtime-отказа на произвольные некорректные токены; оставить проверки собственных
преобразований темы. Формат и значения токенов не меняются.
Вне scope: внешняя загрузка токенов, редизайн, новые компоненты или зависимости,
продуктовые сценарии и изменение исторических архивов.
## Capabilities
### New Capabilities
Нет.
### Modified Capabilities
- `frontend-design-system`: доверенный локальный источник вместо обязательной
runtime-валидации произвольных документов; остальные инварианты сохраняются.
## Impact
Затронуты общий frontend-фундамент, актуальная спецификация и документация.
Пользовательское поведение, API и данные остаются прежними.
@@ -0,0 +1,57 @@
## MODIFIED Requirements
### Requirement: Приложение и каталог используют единый визуальный фундамент
Приложение и каталог SHALL использовать один источник темы и токенов: цвета
с семантическими ролями, типографику, интервалы, радиусы, тени и применимые
адаптивные параметры. Общий фундамент MUST быть независим от бизнес-сущностей
и уровня приложения; подключение провайдеров принадлежит приложению и runtime
каталога. Объявленные общие токены SHALL иметь единый переносимый машиночитаемый
источник значений, единиц, семантических ссылок и описаний, доступный без
приложения или UI-библиотеки. Адаптер приложения SHALL потреблять этот источник
без ручного дублирования значений. Перенос владения опубликованными шкалами
из UI-библиотеки в токены MUST сохранять их эффективные значения; не включённые
в контракт библиотечные defaults MAY оставаться у библиотеки. Выделение фундамента MUST сохранять текущий
внешний вид, светлую схему и поведение приложения без массовой замены локальных
значений оформления. Источник SHALL поставляться вместе с приложением как
доверенная конфигурация; загрузка внешних или пользовательских токенов
и отдельная runtime-валидация произвольных документов не поддерживаются.
#### Scenario: Компонент отображается в приложении и каталоге
- **WHEN** один компонент с одинаковыми props отображается в обоих runtime
- **THEN** он получает одинаковые тему, токены и общие визуальные правила
#### Scenario: Участник выбирает токен
- **WHEN** участник изучает фундамент дизайн-системы
- **THEN** доступны используемые шкалы, их значения, семантические роли и правила
применения без необходимости обращаться к данным приложения
#### Scenario: Владение темой перенесено
- **WHEN** приложение использует выделенный общий фундамент
- **THEN** исходные эффективные значения и внешний вид остаются прежними,
включая встроенные шкалы, глобальные стили и локальное оформление
#### Scenario: Токены использует другая система
- **WHEN** потребитель читает исходный файл обычным парсером данных
- **THEN** объявленные цвета, роли, типографика, интервалы, радиусы, тени и
адаптивные пороги доступны с определёнными единицами и ссылками без React,
Mantine, CSS runtime или запуска приложения; интеграция конкретного инструмента
не заявляется автоматически
#### Scenario: Изменён исходный токен
- **WHEN** валидное значение или семантическая ссылка изменены в едином источнике
- **THEN** приложение и каталог получают изменение через один адаптер, без
повторного редактирования визуального значения в коде темы
#### Scenario: Исходные токены некорректны
- **WHEN** разработчик вносит некорректное значение в локальный источник
- **THEN** отдельный runtime-валидатор не применяется; корректность изменения
проверяется при review и проверке представления, автоматическое отклонение
любого некорректного документа не гарантируется
@@ -0,0 +1,11 @@
## 1. Упрощение локальных токенов
- [x] 1.1 [frontend] Удалить runtime-схему и её тесты, подключить JSON напрямую, оставить два теста адаптера и актуализировать ownership docs.
- **Covers:** `frontend-design-system` — `Приложение и каталог используют единый визуальный фундамент`
- **Verification:** npm ci и npm run verify под .nvmrc; baseline/after theme и browser smoke Design System/Product/login; JSON values и provider contract неизменны.
- [x] 1.2 [specs] Оформить согласованный контракт доверенного источника и planning без изменения historical archives/state.
- **Covers:** `frontend-design-system` — `Приложение и каталог используют единый визуальный фундамент`
- **Verification:** openspec schema validate wealth-ledger-sdd --verbose; openspec validate simplify-local-design-tokens --type change --strict --store wealth-ledger-specs; openspec validate --specs --strict --store wealth-ledger-specs; semantic sync review.
- [x] 1.3 [docs] Описать прямое потребление локального JSON и фактические границы проверки.
- **Covers:** `frontend-design-system` — `Приложение и каталог используют единый визуальный фундамент`
- **Verification:** npm ci и npm run verify под docs .nvmrc; affected local links и соответствие реализации.
@@ -0,0 +1,2 @@
schema: wealth-ledger-sdd
created: 2026-09-09
@@ -0,0 +1,45 @@
# clarify-agent-context-routing — закрытие
**Дата:** 2026-09-09. **Статус:** архивирован; исходный план заменён последующим
решением пользователя. Итоговая задача реализована для frontend и backend.
## Основание
После первоначального переноса frontend conventions пользователь расширил
задачу: применить рекомендации исследований к обоим code repositories и убрать
обязательный OpenSpec/SDD. Решение закреплено в
[ADR 012](../../../../../docs/adr/012-task-driven-development-with-scoped-context.md).
Затем пользователь явно запросил завершение, архивацию и commits.
Proposal, Design, Tasks и delta specs сохранены как история первоначального
scope. Отметки Tasks относятся к прежнему этапу; они не означают соответствие
итоговой структуры старому плану или проведение независимой SDD Validation.
Архивация выполняется с `--skip-specs`: delta требует обязательных SDD-фаз
и исключает backend в planning scope, поэтому её перенос в main specs
не соответствует последующему решению пользователя.
## Поставленный результат
- Короткие root/local AGENTS и загрузка conventions по задаче, пути и границе.
- [Frontend router](../../../../../frontend/.ai/CONVENTIONS.md), SOURCES и четыре тематических руководства.
- [Backend router](../../../../../backend/.ai/CONVENTIONS.md), SOURCES и шесть тематических руководств.
- Общие engineering rules, HTTP contract workflow и docs/specs indexes.
- [18 routing cases](../../../../../docs/development/agent-context-evals.md) с безопасными исключениями и неприменимыми контрпримерами.
Код приложения, dependencies, runtime configuration и gates не менялись.
Основные specs и существовавшие архивы сохранены. OpenSpec остаётся optional;
эта архивация не возвращает обязательный lifecycle.
## Проверка
Проверки выполнены под `.nvmrc` соответствующих repositories:
- `frontend`: `npm run verify` — exit 0; 26 test files, 147 tests, сборки приложения и Storybook.
- `backend`: `npm run verify` — exit 0; 25 suites, 107 tests, Prisma generation и Nest build. Для Prisma cache потребовался повторный запуск с разрешённым доступом вне sandbox.
- `docs`: `npm run verify` — exit 0; Markdown lint и `git diff --check`.
- Root/local AGENTS, оба `.ai` дерева и specs index: Markdown lint — 19 файлов без ошибок; scoped navigation — 60 файлов, 413 links/anchors без ошибок.
- Все 18 routing cases проверены вручную. Это не эксперимент по качеству агента; результаты не доказывают first-pass acceptance или экономию токенов.
Runtime HTTP contract не менялся; отдельные contract/e2e проверки не запускались.
Известное ограничение общего backend e2e и непроверенная детализация validation
errors описаны в действующих документах; их исправление не входило в эту задачу.
@@ -0,0 +1,300 @@
## Context
Шесть frontend-local документов сейчас находятся в `frontend/docs/`.
Их README перечисляет темы, а local AGENTS одновременно служит входом и кратким
пересказом отдельных правил. Общая policy, context discipline, lifecycle,
architecture и as-built сведения уже имеют владельцев в отдельном `docs`.
Подход сохраняет это разделение и использует утверждённую Delta Spec
`specs/openspec-authority-and-lifecycle/spec.md` данного change.
Адресная проверка текущих источников уточнила основания решений:
- `frontend/biome.json` задаёт ошибки для declarations named components,
default exports и порядка exports с локальными исключениями. Он не задаёт
общего запрета непереведённых JSX-строк; TypeScript такого запрета не добавляет.
- `@testing-library/user-event` не объявлен напрямую в `frontend/package.json`,
но присутствует в lockfile через Storybook. Интерактивные тесты используют
`fireEvent` из объявленного `@testing-library/react`.
- Project-local schema `wealth-ledger-sdd` имеет version 5. Отслеживаемый Git
файл `specs/openspec/.cli-version` существует и содержит `1.10.0`.
ADR 010 описывает прежние schema v3, custom verifiers и dispatcher;
текущие lifecycle и entrypoints направляют к native OpenSpec и repo gates.
- Storybook уже поставлен. Workflow summary содержит прежнее число групп,
тогда как каталог описывает актуальную структуру у собственного владельца.
Это основания для уточнения документации, а не для изменения tooling или
исправления других main-spec requirements. Planning остаётся только в store
`wealth-ledger-specs`; исторические источники не становятся объектом миграции.
## Goals / Non-Goals
**Goals:** сохранить локальные правила при смене пути, определить единственного
владельца каждого решения о контексте и дать исполнителю точный способ отличать
обязательное чтение и review от автоматического enforcement и guidance.
Design фиксирует соответствие сохраняемого содержания конечным владельцам;
отдельный постоянный реестр для этого не нужен.
**Non-Goals:** изменение application runtime, API, auth, данных, dependencies,
schema graph или canonical gates; новый context framework, instruction layer,
checker, dispatcher, scoring либо token limit. Bundle-budget drift,
backend e2e, переносимость Completion Evidence и массовое переустройство
conventions остаются вне этого change.
## Decisions
### 1. Существующие источники сохраняют ownership
`frontend/.ai/` полностью заменяет `frontend/docs/` как место локальных правил.
В нём непосредственно лежат `README.md`, `CONVENTIONS.md`, `STRUCTURE.md`,
`API-LAYER.md`, `UI-PATTERN.md` и `TESTING.md`. Это relocation существующего
набора, без вложенного дерева, metadata, нового AGENTS или второго index.
Старые поддерживаемые копии и redirect stubs не остаются.
Frontend AGENTS направляет к `.ai/README.md` для выбора применимых документов,
сохраняет local gate и краткие boundaries. README приложения предоставляет
тот же вход и полезные прямые ссылки. Краткие pointers в AGENTS не заменяют
текст владельца и не требуют автоматически читать весь набор conventions.
Общая authority остаётся в `docs/development`: context discipline владеет
выбором и повторным определением inputs, lifecycle — входом по фазе и native
процедурой, governance — approvals, ролями и обязательностью проверок.
Architecture, ADR и as-built сведения остаются в своих текущих docs-разделах.
Frontend router ссылается на эти источники вместо копирования policy.
Альтернатива с двумя поддерживаемыми каталогами оставляет неоднозначный owner.
Новый registry или metadata layer создаёт дополнительную синхронизацию без
нового поведения; шесть существующих документов уже соответствуют границам.
### 2. Один локальный router выбирает объединение применимых тем
Таблица маршрутов принадлежит `frontend/.ai/README.md`. Она использует
содержание предстоящего решения, а не только расширение файла или название
задачи. Одновременно действующие triggers объединяются:
| Содержание изменения | Локальный документ в `.ai/` |
| --- | --- |
| Создание/перенос module или slice, cross-slice imports и public API boundaries | `STRUCTURE.md` |
| React declarations, props, exports, продуктовые строки и Zod messages | `CONVENTIONS.md` |
| Container/VM/view, формы, query states, skeleton, retry/recovery | `UI-PATTERN.md` |
| HTTP client, generated types, query/mutation, Bearer и refresh | `API-LAYER.md` |
| Изменение самих тестов либо поведения с testing risk trigger | `TESTING.md` |
Например, UI props и presentation требуют conventions и UI pattern; auth
refresh добавляет API и testing по security/concurrency risk. Наличие query
state во view само по себе не требует API rules, пока не затронуты получение
данных, query/mutation или transport boundary. Router не дублирует список
risk triggers: их полная policy остаётся в governance с указателем из TESTING.
### 3. Повторный routing расширяет существующую context discipline
В `docs/development/agent-token-efficiency.md` уточняется существующий раздел
Routing context. Фаза и affected areas определяются до supporting reads;
применимость inputs сверяется до первого зависимого решения, при новой области
или границе и перед handoff. Первая сверка работает и при отсутствии diff.
Перед handoff основанием служат фактические staged, unstaged и новые файлы
вместе с содержанием работы. Используется существующая inventory procedure
этого же документа: учитывается working content tracked и nonignored untracked
files, relevant ignored и workspace-owned inputs перечисляются явно.
Чужой concurrent diff учитывается при разграничении ownership, но не даёт
полномочий присвоить его текущему change. Второй manifest не вводится.
Новая граница в пределах approved scope означает загрузку её правил до
зависимой работы. Необходимость изменить intent, observable behavior,
architecture или Tasks означает возврат к соответствующей самой ранней
planning boundary. Само уточнение чтения не требует повторного approval;
расширение разрешённой работы следует существующей cascade policy.
В existing lifecycle достаточно коротких указателей на эту сверку в native
planning, Implementation/Validation и перед передачей результата. Отдельная
таблица фаз и контекста там не создаётся: владельцем mapping остаётся context
authority, а конкретные dependencies/contextFiles возвращает native instruction
разрешённого artifact или Apply.
При recovery восстанавливаются canonical artifacts и их approval bindings,
все обязательные native inputs, применимые risk inputs и согласованный Design
package текущей фазы. В том же контексте неизменившиеся прочитанные inputs
переиспользуются по действующей policy. Missing input, неоднозначная версия
или digest mismatch блокируют зависимое решение. Краткий handoff, внешний
каталог и число документов не заменяют обязательные источники.
### 4. Перенос сохраняет требования и действующие исключения
Следующая таблица относится только к этому Design: это соответствие исходного
содержания конечному owner для semantic review переноса. Она не становится
постоянным convention registry. Правила по возможности остаются в одноимённом
перенесённом документе; полезные краткие pointers между ними сохраняются.
| Исходное содержание | Конечный владелец | Что должно сохраниться |
| --- | --- | --- |
| `frontend/docs/README.md`: назначение набора, темы, общая policy и источник версий/scripts | `frontend/.ai/README.md` | Компактный router, ссылки на общие authorities; фактические dependencies и scripts определяет конфигурация проекта. |
| `frontend/docs/STRUCTURE.md` и FSD boundary из AGENTS | `frontend/.ai/STRUCTURE.md` | Локальные слои `app → pages → features → entities → shared`, без `widgets`; slice public API через `index.ts`, alias `@/` и запрет обхода public API. Владение theme/tokens, общий public API для app и Storybook, границы shared/ui и ссылка на дизайн-систему. |
| `frontend/docs/CONVENTIONS.md`: общие Must | `frontend/.ai/CONVENTIONS.md` | Biome style, type-only imports, правила enum/namespace, exact dependency versions, colocated tests, Conventional Commits и действующая pre-commit процедура. Public API и gate pointers сохраняют связь с владельцами. |
| `frontend/docs/CONVENTIONS.md`: i18n и Zod | `frontend/.ai/CONVENTIONS.md` | Продуктовые строки в locale через `useTranslation()`; Zod messages через `validationMessage(key)`. Обязательность сохраняется при semantic review без обещания общего автоматического запрета JSX-строк. |
| `frontend/docs/CONVENTIONS.md`: React component style | `frontend/.ai/CONVENTIONS.md` | Const-valued named components, export rules, `FC`, named props, `PropsWithChildren`, отсутствие inline props typing и destructuring parameters, читаемый нижний export block и прямые barrel re-exports. Storybook CSF default meta/direct story exports и конфигурационные default-export exceptions сохраняются в фактическом scope Biome; synthetic story data не подменяют i18n продукта. |
| File roles в `CONVENTIONS.md` и VM/view rules в `UI-PATTERN.md` | `frontend/.ai/CONVENTIONS.md` и `frontend/.ai/UI-PATTERN.md` соответственно | Container вызывает page VM; VM владеет data/auth/route inputs, state selection, domain grouping и derived presentation data; view получает props. Сохраняются запрет data/auth/router/navigation/VM hooks во view, допустимые translation/form-context/UI-only hooks и formatter одного значения. Разбиение по semantic responsibility, flat views и исключение для маленького tightly-coupled renderer не заменяются line-count rule. |
| `frontend/docs/API-LAYER.md` и API boundary из AGENTS | `frontend/.ai/API-LAYER.md` | Backend OpenAPI как authority; committed snapshot и generated types, offline generation/drift check, запрет ручного редактирования generated и дублирования transport DTO. Только typed shared clients, отсутствие прямых fetch в features/pages/entities и прямых T-Bank calls, slice ownership cache keys/policies. Bearer принадлежит shared authenticated fetch; auth endpoints исключены; single-flight refresh и один retry `UNAUTHORIZED`; query/mutation не задают Authorization. |
| `frontend/docs/UI-PATTERN.md`: формы и recovery | `frontend/.ai/UI-PATTERN.md` | React Hook Form, Zod resolver и FormProvider; form state в container/VM, context fields во view; блокирование действий при submit; field errors и сетевой Alert с сохранением ввода. Known API errors/retry локальны, непойманные route errors принадлежат route-keyed ErrorBoundary. |
| `frontend/docs/UI-PATTERN.md`: query states и skeleton | `frontend/.ai/UI-PATTERN.md` | Loading/error/empty/loaded без успешных данных; Client Cached Snapshot при refetch и его ошибке с progress, предупреждением и retry, отдельно от server stale/fallback. Page/section ownership structural skeleton, доступная busy-область, декоративные shapes без данных, интерактивности и фокуса; сохранение shell, neutral auth skeleton до ready. Mutation loading и cached-snapshot progress остаются контекстными indicators. |
| `frontend/docs/TESTING.md` | `frontend/.ai/TESTING.md` | Risk-based selection и ссылка на governance, observable behavior/AAA, validation без запроса при невалидной форме, route access/redirect и recovery cases, finance states и route-boundary reset. MSW только из Vitest setup, без browser worker/mock development mode; transport headers/retry через MSW. Один coverage-backed Vitest в gate, короткий локальный test run, coverage как anti-regression constraint без тестов ради процента. |
| Canonical gates и lifecycle safeguards в entrypoints | Local AGENTS affected repos и текущие `docs/development` authorities | Выбор Node по `.nvmrc`, repo-owned gates, fixed store, отдельные approvals, fingerprints, независимая Validation и Finalization. Их определения не копируются в новый локальный workflow. |
Неточность testing helper исправляется внутри TESTING: описывается используемый
`fireEvent` из Testing Library с ожиданием наблюдаемого async outcome.
Транзитивный `user-event` не объявляется отсутствующим на диске или напрямую
принятым проектом. Замена текста не меняет тестовую инфраструктуру и не требует
переписывания существующих тестов.
### 5. Enforcement описывается у владельца правила
В AGENTS и conventions различаются три категории: автоматическое блокирование
существующей проверкой, обязательная процедура/semantic review и guidance.
Автоматический статус указывается только для фактически проверяемого scope;
исключения описываются рядом. Component declarations и export rules сохраняют
Biome enforcement, а `FC`/props conventions и полнота i18n остаются обязательными
для review. Успех type/style checks не доказывает локализацию всех строк.
Раздел Guardrails общей governance уточняется в той же классификации:
определение Enforceable Engineering Invariant сохраняется для детерминированных
проверок, но отсутствие checker не переводит явно обязательную semantic policy
в guidance. Необязательные эстетические предпочтения остаются guidance.
Так общий owner и frontend instructions не дают противоположных указаний.
Новый автоматический запрет здесь не создаётся. Existing gates сохраняют свои
inputs и ответственность; structural validation не получает полномочий
подтверждать semantic compliance, approval или независимый PASS.
### 6. Текущая навигация обновляется без переписывания истории
Действующие входы обнаруживаются по старым путям и разрешаются относительно
файла-источника, включая локальные anchors. Текущий адресный inventory содержит
frontend AGENTS/README, UI-каталог и live ссылки на conventions/UI pattern в
remediation registry. Для registry меняются только эти адреса: Audit IDs,
dispositions, статусы, даты, формулировки findings и evidence links сохраняются.
В текущих main specs, schema и optional `.opencode/commands` ссылок на старые
frontend paths не найдено. Optional entrypoints уже направляют к lifecycle и
context authority, поэтому отдельная их редакция не нужна. Root AGENTS также
сохраняет действующий routing. При последующей адресной сверке новая активная
ссылка включается только в пределах разрешённой области; чужой approved
artifact нельзя переписывать для чистого результата поиска.
Historical OpenSpec archives, dated audits, прежнее Completion Evidence и
сохранённые датированные research/browser snapshots не редактируются.
Generated graph/cache outputs не являются поддерживаемыми instructions и не
регенерируются ради смены пути. Совпадение старого пути само по себе не означает
сломанный действующий маршрут. Отказ от redirect stub делает текущие пропуски
видимыми и сохраняет единственного владельца правил.
### 7. ADR и workflow summaries направляют к текущей процедуре
По правилу `docs/adr/README.md` замена решения оформляется новым ADR со ссылкой
на прежний. В `docs/adr` добавляется ADR 011
`011-native-openspec-lifecycle-with-repository-gates.md`, фиксирующий уже
действующий native lifecycle и repo-owned gates. Он содержит контекст, решение
и последствия этой замены, ссылки на governance/lifecycle/context authority
и на ADR 010; новый процесс или самостоятельный порядок команд не вводится.
В ADR 010 сохраняются исходное rationale и рассмотренные альтернативы, а
заменённые части про schema v3, custom verifiers и dispatcher получают явную
историческую квалификацию и ссылку на successor. Сохраняющийся выбор native
OpenSpec и независимых repo gates не объявляется отвергнутым. Index отражает
частичную замену. Такой подход предпочтительнее полного переписывания ADR 010
под сегодняшний текст или примечания без successor, нарушающего ADR policy.
Документация различает текущую schema version и наличие `.cli-version`;
удаление этого файла не утверждается. Его наличие также не выдаётся за
доказательство запуска прежнего verifier. Этот change не меняет version
contract, CLI pin, schema или untouched requirements о tooling.
Числовая структура Storybook принадлежит `docs/development/ui-catalog.md`.
`frontend-workflow.md` ссылается на этот owner без собственного числа групп.
Frontend entrypoints точно описывают уже поставленный каталог и его участие
в существующем gate. Использование каталога разрешено как существующей
инфраструктуры; ограничения на добавление новых инструментов сохраняются.
Reference каталога не заменяет отдельный Design approval и независимую Validation.
### 8. Доказательство опирается на routing scenarios и existing gates
Semantic review сопоставляет перенос с таблицей сохранения и применяет
утверждённые сценарии Delta Specs к получившимся маршрутам. Для UI-only,
API/auth, изменения тестов, структуры и строк проверяется состав применимых
документов; docs-only опечатка служит неприменимым контрпримером к обязательному
чтению всех frontend conventions.
Scope expansion рассматривается отдельно внутри approved границ и за ними;
handoff включает unstaged/new inputs, recovery — весь обязательный пакет,
а недоступный input должен оставлять явный blocker. Это проверка принятия
решений по содержанию, без unit tests, повторяющих Markdown, и без нового
checker. Ссылочная проверка и сохранение исторических bytes дополняют
canonical gates affected областей; они не подменяют независимую Validation.
## UI/UX Applicability and Package
**Applicability and scope:** `N/A`. Change меняет расположение и содержание
инженерных инструкций, не экраны, состояния, interactions или responsive
behavior приложения/каталога. Макеты, PNG/SVG exports и immutable visual reuse
inputs не нужны. Весь Design package — этот Markdown-файл.
## Delivery Map
| Area | Status | Architectural outcome or N/A reason |
| --- | --- | --- |
| frontend | `affected` | Единственный набор conventions непосредственно в `.ai/`, локальный router, точные инструкции и актуальная навигация; runtime, dependencies и gate не меняются. Область проходит свой canonical gate. |
| backend | `N/A` | Не меняются backend behavior, contract, implementation, local instructions или gate. Backend delivery и backend-only проверки не требуются этим change. |
| specs | `affected` | Canonical planning и уточнение capability `openspec-authority-and-lifecycle` по approved Delta Specs; native structural checks. Синхронизация main specs остаётся штатной completion operation, historical archives сохраняются. |
| docs | `affected` | Context discipline и lifecycle pointers, точная классификация обязательности, историческая квалификация ADR 010 с successor, workflow summary и действующие links. Общая as-built authority остаётся на месте; область проходит canonical docs gate и scoped link review. |
### Contract Gate Decision
| Gate | Status | Reason |
| --- | --- | --- |
| runtime-openapi | `N/A` | Ни backend runtime contract, ни frontend snapshot/generated types не меняются. Сохранение описания typed API и auth ownership не является изменением сторон API contract. |
## Risks / Trade-offs
- Перенос с сокращением может потерять оговорки. Одноимённые владельцы,
ограниченные точечные редакции и таблица сохранения делают semantic diff
проверяемым; массовая консолидация текста не нужна.
- Без redirect старый live link перестанет работать. Scoped review отличает
действующую навигацию от неизменяемых исторических ссылок и проверяет targets
после relocation, в том числе переходы между независимыми repositories.
- Ручной routing может пропустить вторую тему. Объединение triggers и повторные
сверки по фактическому scope снижают риск; автоматический анализ не обещается.
- Классификация без checker может быть воспринята как ослабление. Явная
mandatory-review категория и согласованный текст общей governance сохраняют
обязательность i18n, validation и прочих semantic требований.
- ADR может смешать историческое и действующее решение. Частичная замена,
successor и точные ссылки сохраняют rationale и отделяют текущую процедуру
от старой реализации; untouched spec drift не скрывается редакцией этого Design.
Build-versus-buy trigger отсутствует: общая tooling/infrastructure не вводится
и существенно не заменяется, используются уже принятые Markdown owners,
native OpenSpec и repository gates. Формальная Solution Assessment не нужна.
## Migration Plan
После отдельных planning approvals перенос выполняется как согласованный
documentation batch: сначала сохраняется одноимённый frontend набор и его
содержание, затем переключаются локальные и межрепозиторные входы. Target paths
и активная навигация должны сходиться до передачи реализации в Validation;
промежуточное смешение старых и новых путей не является завершённой поставкой.
В том же delivery приводятся в соответствие общая context discipline,
классификация enforcement и описание действующего workflow/ADR. Это изменение
существующих owners, без дублирования правил в schema или optional adapters.
Исторические artifacts сохраняются; canonical Proposal и Delta Specs этого
change не редактируются ради переноса ссылок, описывающих исходное состояние.
Проверяемая реализация передаётся отдельной Validation с фактическим inventory.
Canonical gates всех affected областей, independent verdict, semantic sync и
Completion Evidence остаются на своих lifecycle boundaries. Ошибка ссылки или
потерянное требование устраняется в реализации; противоречие утверждённому
approach возвращается к соответствующему planning artifact.
### Rollback Contract
| Field | Decision |
| --- | --- |
| Status | `N/A` |
| Reason | Обратимая документационная relocation не меняет auth/security behavior, данные, публичный API contract или труднообратимое решение. При откате связанный набор frontend/doc navigation возвращается согласованно средствами Git; пользовательские данные и runtime migration отсутствуют. |
@@ -0,0 +1,96 @@
## Why
Действующие инструкции не всегда однозначно показывают, какие локальные правила
нужны для конкретного изменения и когда выбор контекста следует пересмотреть.
Отдельные формулировки смешивают автоматическое блокирование с обязательным
review или описывают прежнюю процедуру как актуальную. Это создаёт риск пропуска
требований и неоправданной уверенности в успешных проверках.
Discovery завершена; первый этап согласован как уточнение существующих
источников и перенос frontend-local conventions. Сейчас нужно закрепить
границы этого этапа, сохранив действующие требования и lifecycle safeguards.
## What Changes
- Сохранить один актуальный набор всех шести frontend-local документов в
согласованной `frontend/.ai/` вместо `frontend/docs/`, с компактным входом по
типу изменения и работающей действующей навигацией. Общая engineering policy,
architecture, ADR и as-built документация остаются у общего docs authority.
- Сделать выбор локального контекста однозначным для изменений структуры и
импортов, компонентов и продуктовых строк, представления и восстановления
состояний, взаимодействия с сервером, а также тестов и поведения с testing
risk trigger. Несвязанная задача не требует чтения всех conventions.
- Закрепить в существующей context authority проверку маршрутизации до первого
зависимого решения, при обнаружении новой области или границы и перед handoff
по фактическому набору staged, unstaged и новых файлов. Расширение обнаруженного
scope не разрешает выходить за approved planning boundaries.
- Сохранить обязательное получение всех native contextFiles, применимых risk
inputs и согласованного Design package при работе и recovery независимо от
количества документов. Фаза определяет нужный контекст; новый workflow или
параллельный context map не возникает.
- Различать реально автоматизированные запреты, обязательные процедуры и
semantic review, а также необязательное guidance. Исправление неточного
обещания проверки не ослабляет обязательность локализации, правил валидации
и остальных действующих требований. Рекомендации по тестированию соответствуют
установленной инфраструктуре и используемым практикам.
- Обозначить исторический статус заменённых частей архитектурного решения и
направить к текущей принятой процедуре с сохранением rationale и правил
ведения ADR. Изменяемая информация о каталоге интерфейса принадлежит его
документу-владельцу; workflow ссылается на него. Использование уже поставленного
каталога не требует повторного согласования его добавления.
- Сохранить содержание всех переносимых требований к структуре и границам
модулей, локализации и валидации, стилю компонентов с действующими исключениями,
типизированному доступу к данным и авторизации, разделению данных и
представления, состояниям и доступности интерфейса, восстановлению после
ошибок и тестированию. Canonical gates, отдельные approvals, fingerprints,
независимая Validation и Finalization сохраняются.
Ожидаемый результат — применимые правила доступны до зависящего от них решения,
актуальные ссылки разрешаются, а перенос не оставляет двух поддерживаемых копий
и не теряет требований. Это относится как к отдельной UI-, API- или testing-задаче,
так и к расширению scope и recovery; неприменимые conventions остаются вне
обязательного контекста несвязанной задачи.
Вне scope:
- Изменение runtime-поведения frontend/backend и массовый рефакторинг conventions.
- Восстановление bundle budget checker, разрешение связанного spec drift,
восстановление backend e2e и переработка переносимости Completion Evidence.
- Новые instruction layers, реестры conventions, метаданные, context frameworks,
CI, hooks, dispatchers, checker frameworks, telemetry или dependency packages.
- Scoring, числовой token gate или ограничение числа обязательных документов.
- Перенос всей документации проекта в локальные инструкции frontend.
- Переписывание historical archives, dated audit snapshots и старого Completion
Evidence; их прежние пути сохраняют исторический контекст.
- Повторный аудит workspace и последующие этапы улучшения guardrails.
## Capabilities
### New Capabilities
Нет.
### Modified Capabilities
- `specs/openspec-authority-and-lifecycle/spec.md`: уточнить обязательный выбор
контекста по фазе и фактическому scope, моменты повторной проверки маршрутизации
и применимость локальных conventions без пропуска обязательных inputs или
обхода approval boundaries. Остальные действующие инварианты сохраняются.
## Impact
Изменение затрагивает AI-агентов, разработчиков, Change Owner и независимых
reviewers, которые планируют, выполняют и проверяют работу в workspace.
- **frontend** — расположение и маршрутизация локальных conventions, точность
инструкций и действующая навигация; поведение приложения не меняется.
- **docs** — context discipline, актуальные workflow summaries, навигация и
историческая квалификация заменённых частей ADR при сохранении общей authority.
- **specs** — canonical planning и уточнение существующего требования о выборе
контекста; без дублирующего planning store и изменения исторических artifacts.
- **backend — N/A**: первый этап не меняет его поведение, контракт, реализацию,
локальные инструкции или gate.
Согласование направления не является approval этого Proposal или downstream
artifacts. Каждый следующий artifact и переход роли сохраняет отдельную
границу canonical lifecycle.
@@ -0,0 +1,243 @@
## MODIFIED Requirements
### Requirement: Agent context следует текущей SDD-фазе
Project-owned agent workflow SHALL начинать каждую работу с определения текущей
SDD-фазы и affected delivery areas, а затем SHALL загружать canonical artifacts,
локальные routing instructions и supporting authority, применимые к этой фазе и
этим areas. Переход в новую agent-сессию после утверждения planning artifact
SHALL использовать canonical artifact и явное решение Change Owner как основание
продолжения. Workflow MUST NOT требовать полную историю предыдущей сессии или
отдельный handoff/context artifact, дублирующий OpenSpec authority. Supporting
material, не влияющий на текущую фазу, затронутую область, доменную семантику или
обязательный gate, SHALL загружаться только при появлении наблюдаемой
необходимости.
Workflow SHALL проверять применимость контекста до первого зависимого решения,
при обнаружении новой области или границы и перед handoff по фактическому scope,
включая staged, unstaged и новые файлы. Проверка SHALL учитывать содержание
изменения, а не только первоначальное описание задачи или наличие staging.
Обнаружение нового scope MUST NOT разрешать выход за утверждённые planning
boundaries: требующее пересмотра intent, behavior, architecture или Tasks
расширение SHALL возвращаться к самой ранней затронутой boundary до зависимой
работы.
Workflow MUST получать все возвращённые native contextFiles, применимые risk
inputs и согласованный Design package текущей фазы независимо от количества
документов. Это правило SHALL действовать и при recovery; уже прочитанные
неизменившиеся inputs в том же контексте MAY переиспользоваться по существующей
context discipline. Недоступный обязательный input MUST оставаться явным
блокером зависимого решения, а не заменяться историей чата или догадкой.
Маршрутизация MUST опираться на существующую context authority и MUST NOT
вводить параллельный workflow, scoring, числовой token gate или лимит числа
обязательных документов.
#### Scenario: Утверждённый artifact передаётся в новую сессию
- **WHEN** Change Owner явно утверждает текущий planning artifact и следующая agent-сессия продолжает change на разрешённой downstream-фазе
- **THEN** workflow восстанавливает работу из canonical change artifacts, owner approval и применимых routing instructions без обязательной передачи полной истории обсуждения или создания второго handoff artifact
#### Scenario: Работа ограничена одной delivery area
- **WHEN** текущая фаза затрагивает одну определённую delivery area и не требует cross-system или доменной оценки
- **THEN** workflow загружает authority и локальные instructions этой area, а нерелевантные architecture, domain и repository materials остаются lazy context
#### Scenario: Supporting material становится необходимым
- **WHEN** во время работы возникает вопрос, влияющий на domain semantics, cross-area architecture, audit disposition или обязательный gate
- **THEN** workflow загружает соответствующий authoritative material до принятия решения и не использует token-efficiency guidance как основание пропустить требуемый контекст
#### Scenario: Предстоит первое зависимое решение
- **WHEN** агент определил фазу и affected areas и готовится принять первое
решение, зависящее от локальных правил или иной authority
- **THEN** он определяет применимые inputs по содержанию задачи и получает их
до этого решения, даже если работа ещё не создала diff
#### Scenario: В пределах approved scope обнаружена новая граница
- **WHEN** текущая работа выявляет ранее неучтённую границу, уже разрешённую
утверждёнными planning artifacts
- **THEN** workflow пересматривает применимость контекста и получает недостающие
правила этой границы до зависимой работы, не требуя нового approval только
из-за уточнения набора чтения
#### Scenario: Новая область требует расширения approved scope
- **WHEN** обнаруженная область требует изменения утверждённого intent,
behavior, architecture или checklist
- **THEN** workflow загружает применимый контекст для оценки расхождения,
сообщает основание возврата к самой ранней затронутой planning boundary и
не начинает зависимую работу до требуемого approval
#### Scenario: Перед handoff есть изменения вне staged diff
- **WHEN** агент готовит handoff, а unstaged или новые файлы выявляют
применимую область или правило, отсутствующие в первоначальном scope
- **THEN** workflow учитывает их вместе со staged изменениями, пересматривает
обязательный контекст и передаёт фактический scope и оставшиеся blockers;
отсутствие staging не позволяет пропустить эту область
#### Scenario: Recovery требует нескольких обязательных источников
- **WHEN** новая сессия восстанавливает разрешённую фазу, для которой нужны
native contextFiles, risk inputs и согласованный Design package
- **THEN** она получает весь применимый набор и именно согласованную версию
Design, даже если краткий handoff перечисляет лишь часть источников или
документов больше, чем агент первоначально ожидал прочитать
#### Scenario: Обязательный материал недоступен
- **WHEN** необходимый для текущего решения input отсутствует либо согласованную
версию Design package нельзя однозначно восстановить
- **THEN** workflow сообщает конкретный blocker и не принимает зависимое решение
до восстановления input либо разрешения вопроса на нужной planning boundary
## ADDED Requirements
### Requirement: Frontend-local conventions имеют единый вход по содержанию изменения
Участник workflow SHALL получать один актуальный набор frontend-local
conventions через компактный router, выбирающий документы по содержанию
изменения. Router SHALL направлять создание или перенос module/slice и
cross-slice imports к правилам структуры; declarations, props, exports,
продуктовые строки и сообщения валидации — к conventions; разделение
container/VM/view, формы, query states, skeleton и retry/recovery — к правилам
UI; HTTP client, generated types, query/mutation, Bearer и refresh — к правилам
API; изменение самих тестов или поведения с testing risk trigger — к правилам
тестирования. При сочетании triggers участник MUST получать все применимые
документы, а не выбирать один взаимоисключающий маршрут.
Router MUST ссылаться на применимые общие authorities вместо пересказа их
содержания. Несвязанная задача MUST NOT требовать чтения всех conventions.
Перенос SHALL сохранять полезное содержание всех шести существующих документов,
обязательные требования и действующие исключения, не оставляя двух
поддерживаемых копий. Общая engineering policy, architecture, ADR и as-built
documentation MUST сохранять своих существующих владельцев.
Все применимые действующие ссылки SHALL разрешаться к актуальным владельцам
после переноса, включая ссылки из optional entrypoints, если они затронуты.
Historical archives, dated audit snapshots и прежнее Completion Evidence
MUST оставаться неизменными; их старые пути MUST NOT считаться действующей
навигацией, требующей переписывания.
#### Scenario: UI-only изменение не затрагивает данные и testing risk
- **WHEN** задача меняет только представление и props существующего компонента
без изменения структуры модулей, взаимодействия с сервером, тестов или
поведения с testing risk trigger
- **THEN** router направляет к UI rules и component conventions; правила API,
структуры и тестирования не становятся обязательными только из-за того,
что изменён frontend
#### Scenario: Меняются ownership авторизации или refresh
- **WHEN** задача затрагивает формирование авторизации запросов либо
координацию refresh
- **THEN** router включает правила API и тестирования по security/concurrency
risk, а применимый contract и domain context остаётся обязательным независимо
от того, меняется ли интерфейс
#### Scenario: Меняются сами тесты
- **WHEN** задача изменяет тесты без изменения production behavior
- **THEN** router включает testing conventions; отсутствие изменения
production behavior не отменяет этот trigger, а остальные документы
выбираются по фактически затронутым границам
#### Scenario: Создаётся slice с импортом другого slice
- **WHEN** задача создаёт или переносит slice либо меняет cross-slice import
- **THEN** router включает structural conventions и public API boundaries до
решения о размещении или импорте, даже если внешнее поведение сохраняется
#### Scenario: Меняются только продуктовые строки или сообщения валидации
- **WHEN** задача меняет продуктовый текст либо пользовательские сообщения
валидации без изменения структуры, UI-state logic, API и тестов
- **THEN** router включает conventions локализации и валидации, не требуя
остальных локальных документов при отсутствии их triggers
#### Scenario: Задача не относится к frontend conventions
- **WHEN** задача исправляет опечатку в общей документации, не меняет
frontend-local instructions и не затрагивает frontend behavior или boundaries
- **THEN** workflow использует применимые общие и docs-local authorities без
обязательного чтения frontend conventions
#### Scenario: Участник открывает действующую ссылку после переноса
- **WHEN** участник переходит к frontend-local conventions из действующей
навигации после их переноса
- **THEN** ссылка ведёт к единственному актуальному владельцу, где сохранены
применимые требования и исключения; прежнее расположение не содержит второй
поддерживаемой копии правил
#### Scenario: Старый путь встречается в historical artifact
- **WHEN** прежний путь к conventions встречается в historical archive,
dated audit snapshot или старом Completion Evidence
- **THEN** artifact и его историческая ссылка сохраняются без переписывания;
актуальный маршрут определяется действующими entrypoints
### Requirement: Активные инструкции точно описывают обязательность и способ обеспечения правил
Активные instructions SHALL различать автоматическое блокирование существующим
gate, обязательную процедуру или semantic review и необязательное guidance.
Инструкция MUST NOT обещать автоматическую проверку, которой нет в текущей
конфигурации. Отсутствие автоматической проверки MUST NOT превращать обязательное
требование локализации, валидации или другой действующий invariant в пожелание.
Успех structural checks MUST NOT объявляться semantic approval или независимым
Validation PASS.
Testing guidance SHALL соответствовать объявленным зависимостям и используемой
тестовой инфраструктуре; наличие пакета только как транзитивной зависимости
MUST NOT представляться как его прямое принятие проектом. Исправление guidance
в рамках этого этапа MUST NOT требовать нового dependency package или менять
runtime behavior.
Заменённые части действующего ADR SHALL иметь явную историческую квалификацию
и направление к текущей принятой процедуре с сохранением rationale и правил
ведения ADR. Документация MUST опираться на фактическое состояние, а не повторять
неподтверждённое утверждение об удалении или наличии инструмента. Числовые
детали UI-каталога SHALL принадлежать его документу-владельцу; workflow summaries
SHALL ссылаться на него. Использование уже поставленного каталога MUST NOT
трактоваться как новое добавление, требующее повторного approval установки.
#### Scenario: Автоматическая проверка не блокирует непереведённую строку
- **WHEN** текущие style и type checks допускают произвольную непереведённую
продуктовую строку
- **THEN** инструкции сохраняют обязательную локализацию и semantic review,
не обещают автоматический запрет такой строки и не считают успешный gate
доказательством выполнения этого требования
#### Scenario: Нарушение действительно блокируется существующим gate
- **WHEN** текущее tooling детерминированно отклоняет нарушение документированного
component style в применимом scope
- **THEN** инструкция описывает это правило как автоматически проверяемое
с сохранением действующих исключений, не переводя его в guidance
#### Scenario: Рекомендованный testing helper не объявлен напрямую
- **WHEN** прежнее guidance рекомендует helper из пакета, не объявленного
прямой dependency, а действующие тесты используют доступные средства
- **THEN** guidance согласуется с принятой инфраструктурой без добавления
пакета ради прежнего текста; транзитивная доступность описывается точно
#### Scenario: Часть ADR описывает заменённую процедуру
- **WHEN** описание схемы или orchestration в ADR отличается от текущей
принятой процедуры
- **THEN** участник может отличить историческую часть решения от действующей
процедуры и перейти к её authority; rationale и исторические artifacts
сохраняются, а неподтверждённое удаление инструмента не утверждается
#### Scenario: Workflow ссылается на существующий UI-каталог
- **WHEN** участник использует уже поставленный каталог как reference
- **THEN** он получает актуальные числовые детали у документа-владельца без
второй копии в workflow и без повторного approval добавления каталога;
требования к отдельному approval Design package и независимой Validation
продолжают действовать
@@ -0,0 +1,57 @@
## 1. Frontend-local conventions и входы
До первых delivery edits сохранить в рабочем контексте исходный inventory по
context authority, включая staged/unstaged/new inputs и bytes защищённых
historical paths для проверки 3.2. Новый versioned artifact для этого не нужен.
- [x] 1.1 [frontend] Перенести все шесть документов из `docs/` непосредственно в `.ai/`, сохранив одноимённых владельцев и содержание по таблице сохранения в Design; убрать прежний поддерживаемый набор без redirect stubs и второго registry.
- **Covers:** `openspec-authority-and-lifecycle` — `Frontend-local conventions имеют единый вход по содержанию изменения`
- **Verification:** Сопоставить исходные документы и перенос по каждой строке таблицы Design, включая FSD без widgets, i18n/validationMessage, React/Storybook exceptions, typed API/auth/refresh, VM/view, cached snapshot, skeleton accessibility, recovery и MSW/risk-based testing; проверить наличие только шести согласованных файлов непосредственно в `.ai/` и отсутствие второй поддерживаемой копии в `docs/`.
- [x] 1.2 [frontend] Сделать `.ai/README.md` компактным router с объединением triggers и ссылками на общие authorities; направить к нему `AGENTS.md` и README приложения, обновив их прямые ссылки на conventions и сохранив local gate и boundaries.
- **Covers:** `openspec-authority-and-lifecycle` — `Frontend-local conventions имеют единый вход по содержанию изменения`
- **Verification:** Проверить все пять маршрутов Design и сочетание нескольких triggers; проследить локальные входы до существующего target и применимого раздела, убедиться, что AGENTS/README не предписывают читать весь набор без triggers и не копируют общую policy.
- [x] 1.3 [frontend] Уточнить обязательность и enforcement в AGENTS и перенесённых conventions; согласовать TESTING с используемым `fireEvent` и объявленными зависимостями, а описание Storybook — с уже поставленной инфраструктурой.
- **Covers:** `openspec-authority-and-lifecycle` — `Активные инструкции точно описывают обязательность и способ обеспечения правил`
- **Verification:** Сверить автоматические утверждения и исключения с текущим `biome.json`, а testing guidance — с `package.json`, lockfile и интерактивными тестами; проверить сохранение mandatory i18n/validationMessage и React review conventions, точное описание транзитивного user-event, доступность существующего каталога без повторного approval установки и отсутствие изменений dependencies, runtime или gate.
## 2. Общая context authority и текущая документация
- [x] 2.1 [docs] Дополнить Routing context в `development/agent-token-efficiency.md` повторной сверкой до первого зависимого решения, при новой границе и перед handoff по staged/unstaged/new inputs; добавить короткие ссылки на эту процедуру в применимых фазах `development/openspec-authority-and-lifecycle.md`.
- **Covers:** `openspec-authority-and-lifecycle` — `Agent context следует текущей SDD-фазе`
- **Verification:** Проверить различие новой границы внутри approved scope и расширения intent/behavior/architecture/Tasks, полноту native contextFiles/risk inputs/Design package при recovery, reuse неизменившихся inputs и явный blocker при missing input; убедиться, что сохранены существующие fingerprints/approval boundaries и не созданы второй context map, workflow, scoring или лимит чтения.
- [x] 2.2 [docs] Уточнить Guardrails в `development/engineering-governance.md`: отделить автоматические Enforceable Engineering Invariants от обязательных процедур/semantic review и необязательного guidance, согласовав общую policy с frontend instructions.
- **Covers:** `openspec-authority-and-lifecycle` — `Активные инструкции точно описывают обязательность и способ обеспечения правил`
- **Verification:** Сопоставить governance с результатом 1.3: отсутствие checker не снимает явно обязательное требование, существующее автоматическое блокирование сохраняет свой статус, эстетические предпочтения остаются guidance; проверить сохранение canonical gates, approvals, fingerprints, независимой Validation и отсутствия новых checkers.
- [x] 2.3 [docs] Добавить ADR 011 `adr/011-native-openspec-lifecycle-with-repository-gates.md` о действующем native lifecycle и repo gates; обозначить частичную историческую замену ADR 010 и обновить ADR index, сохранив rationale и рассмотренные альтернативы.
- **Covers:** `openspec-authority-and-lifecycle` — `Активные инструкции точно описывают обязательность и способ обеспечения правил`
- **Verification:** Проверить соответствие `adr/README.md`, контекст/решение/последствия нового ADR, взаимные ссылки и указатели к текущим authorities; сверить формулировки schema v3/v5, прежних verifiers/dispatcher и сохранённого `.cli-version` с актуальными источниками, без изменения version contract, schema и untouched main requirements.
- [x] 2.4 [docs] Обновить текущие ссылки на frontend conventions в UI-каталоге и remediation registry; убрать дублируемое число групп каталога из frontend workflow, оставив ссылку на `development/ui-catalog.md` как owner этих сведений.
- **Covers:** `openspec-authority-and-lifecycle` — `Frontend-local conventions имеют единый вход по содержанию изменения`; `openspec-authority-and-lifecycle` — `Активные инструкции точно описывают обязательность и способ обеспечения правил`
- **Verification:** Проверить targets и anchors изменённых ссылок после переноса; в registry сравнить diff только live адресов conventions/UI pattern, сохранив Audit IDs, dispositions, статусы, даты, findings и evidence links; проверить отсутствие второй числовой сводки в workflow и сохранение Design approval/Validation при использовании существующего Storybook.
## 3. Implementation-owned проверка согласованного поведения
- [x] 3.1 [workspace] Пройти routing scenarios Delta Specs по итоговым instructions, проверяя выбор обязательного контекста и неприменимые контрпримеры без добавления тестов, повторяющих Markdown.
- **Covers:** `N/A` — задача не вводит самостоятельного observable behavior; это необходимая cross-area проверка frontend router и общей context discipline по решению 8 Design перед передачей реализации в независимую Validation.
- **Verification:** Для UI-only props/presentation проверить CONVENTIONS + UI-PATTERN без необоснованных API/STRUCTURE/TESTING; для auth/refresh — API-LAYER + TESTING и применимый contract/domain context; для изменения только тестов, slice/imports или строк — соответствующие triggers. Docs-only опечатка не должна требовать frontend conventions. Отдельно пройти первое решение без diff, supporting input по новой необходимости, новую границу внутри approved scope и расширение за его пределы, handoff с unstaged/new files, recovery полного обязательного пакета и missing-input blocker; сохранить observed outcomes в рабочем контексте без нового versioned registry или независимого verdict.
- [x] 3.2 [workspace] Проверить межрепозиторную навигацию и границы документационного переноса по фактическому inventory, включая сохранение исторических inputs и отсутствие непредусмотренных изменений.
- **Covers:** `N/A` — самостоятельное поведение не добавляется; согласованный перенос между независимыми репозиториями требует cross-area проверки ссылок и сохранения inputs по решениям 4 и 6 Design, которую один repository gate не покрывает.
- **Verification:** Найти старые paths в действующих entrypoints и проверить все применимые targets/anchors, включая optional entrypoints при наличии совпадений; отличить historical references и generated graph/cache outputs от live navigation. Сравнить historical archives, dated audits, прежнее Completion Evidence и датированные research/browser snapshots с исходным inventory по bytes; по staged/unstaged/new scope проверить сохранение root/optional routing при отсутствии затронутых ссылок, неизменность runtime/dependencies/gates и соблюдение read-only references policy.
## 4. Repository-owned delivery и canonical gates
Нормативный deliverable `specs` — canonical change, подготовленный Specification.
Design не предусматривает отдельную implementation-редакцию schema или main
specs: задача 4.3 проверяет эту repository-owned поставку. Semantic sync остаётся
на границе Finalization. Backend и `runtime-openapi` имеют `N/A` по Delivery Map.
Независимая Validation не является checkbox этой реализации.
- [x] 4.1 [frontend] Выполнить canonical frontend gate на итоговом frontend delivery после переноса и уточнения инструкций.
- **Covers:** `N/A` — новое observable behavior не вводится; repository-owned проверка обязательна для affected frontend delivery по Delivery Map, хотя runtime и gate остаются прежними.
- **Verification:** Из `frontend` выполнить `nvm use --silent`, lockfile install `npm ci` и `npm run verify`; сохранить exact commands, cwd, exit status, существенный observed output и relevant input inventory/fingerprint по context authority. Проверить, что tracked outputs не изменены самим non-mutating gate.
- [x] 4.2 [docs] Выполнить canonical docs gate на итоговой policy, ADR и действующей навигации.
- **Covers:** `N/A` — это необходимая repository-owned проверка affected docs delivery по Delivery Map, не отдельное spec-level поведение и не замена scoped link review из 2.4 и 3.2.
- **Verification:** Из `docs` выполнить `nvm use --silent`, lockfile install `npm ci` и `npm run verify`; сохранить exact commands, cwd, exit status, существенный observed output и relevant input inventory/fingerprint по context authority, без запуска auto-fix как доказательства корректности.
- [x] 4.3 [specs] Проверить полноту и целостность repository-owned canonical change на текущем native inventory и выполнить specs gate, сохраняя утверждённые planning inputs.
- **Covers:** `N/A` — нормативное specs delivery уже выражено canonical artifacts этого change; задача обеспечивает его обязательную repository-owned structural проверку по Delivery Map без дополнительной мутации ради участия specs, нового поведения или переноса completion operations в Implementation.
- **Verification:** Активировать frontend `.nvmrc` через `nvm use --silent` до перехода в `specs`; выполнить `openspec status --change clarify-agent-context-routing --json --store wealth-ledger-specs`, `openspec schema validate wealth-ledger-sdd --verbose`, `openspec validate clarify-agent-context-routing --type change --strict --json --store wealth-ledger-specs` и `openspec validate --specs --strict --store wealth-ledger-specs`. По native inventory сверить approval bindings с действующей нормализацией task progress; сохранить exact commands, cwd, exit status, существенный observed output и relevant input inventory/fingerprint. При изменённом planning input вернуть работу к ответственной boundary, не исправлять approved содержание неявно.
@@ -0,0 +1,2 @@
schema: wealth-ledger-sdd
created: 2026-09-09
@@ -0,0 +1,214 @@
## Context
Страница Brokerage Account уже разделяет получение данных в
`AccountPage.vm.ts` и отображение в `AccountPageLoaded.view.tsx`.
`PortfolioTotals` получает готовые `totalsByCurrency`; текущая сводка использует
Mantine `SimpleGrid` и `Paper`, вертикальную метку ISO над суммой, отступ 16 px
и верхнюю синюю границу 3 px. Максимальная ширина контента страницы — 960 px.
`shared/lib/display.ts` уже форматирует decimal string без числового
преобразования: сохраняет знак и дробные позиции, группирует целую часть.
`formatMoney` меняет только RUB на ₽. Query и VM уже сохраняют успешные данные
при refetch/error; `PortfolioRefreshState` отделён от server-side `isStale`.
Действующий initial skeleton содержит общий блок высотой 120 px вместо
геометрии валютных карточек.
Основание подхода — утверждённые [Proposal](proposal.md) и
[Delta Specs](specs/portfolio-view/spec.md). A1 из Discovery выбран владельцем;
ни выбор, ни прежние PNG сами по себе не были approval Design.
## Goals / Non-Goals
**Goals:** изменить представление сводки и её loading placeholder в существующем
page slice; сохранить строковую точность и валютную идентичность; использовать
нынешние primitives, theme tokens и границы VM/view.
**Non-Goals:** менять финансовые вычисления, транспортные типы, query/cache
policy, state machine, общие правила денежных суффиксов приложения или соседние
разделы страницы. Новые зависимости, инструментальные витрины и сервисы
форматирования не нужны.
## Decisions
### Ownership и поток данных
`PortfolioTotals` остаётся владельцем компактного представления каждого
переданного итога. Порядок, membership и ключи берутся из исходных валют;
никакой повторной агрегации, сортировки по знакам или дополнения валют нет.
VM продолжает выбирать loaded/empty по составу Portfolio, а не по итогам.
Пустой список итогов не создаёт карточек или отдельного empty-состояния.
Loaded view сохраняет своё место сводки после времени снимка и перед составом.
Его timestamp, refresh/error и stale sections продолжают получать прежние
props. Меняется только сводка и соответствующая область page-owned skeleton.
Это удерживает ответственность за устойчивость данных в query/VM, а за
геометрию — в presentation.
### Точное форматирование и валютная идентичность
Переиспользовать существующий `formatDecimal`, открыв его через публичный
`shared/lib` export без изменения алгоритма и поведения `formatMoney`.
Выбор метки и суффикса принадлежит сводке: RUB → ₽, USD → $, EUR → €;
для остальных валют метка и суффикс — исходный ISO-код. Например, USD/CAD
дают `$`/`CAD`, поэтому совпадающие мировые знаки не требуют каталога валют
или анализа всей коллекции. Неизвестные валюты не отбрасываются.
Полные названия трёх валют принадлежат существующему русскому i18n-каталогу.
Семантика карточки — пара название/значение (`dl`, `dt`, `dd` либо эквивалентная
связь): доступное название содержит «Российский рубль», «Доллар США» или «Евро»;
для прочих валют доступен ISO-код. Декоративный badge скрыт от accessibility
tree, полное текстовое значение остаётся доступным. Карточка не кнопка,
не получает tab stop и не требует tooltip/hover для определения валюты.
Числа не проходят через `Number`, `parseFloat`, округление или currency-mode
`Intl.NumberFormat`. Пробелы группировки и десятичная запятая сохраняются;
перенос — CSS layout, а не вставка символов в числовые данные. `-0.5` остаётся
`-0,5`, исходные дробные нули сохраняются. Замена ISO-кода в уже собранной строке
`formatMoney` отвергнута: она связывала бы сводку с деталями суффикса общего
formatter. Новый дубликат decimal formatter также не нужен.
### Компоновка средствами платформы
Mantine `Paper` и page-local CSS остаются основой карточек. Сетка сохраняет
колонки `base: 1`, `sm: 2`, `md: 3` (48 em / 62 em), с gap 10 px.
Одна валюта занимает одну ячейку сетки, как в A1; дополнительные валюты
заполняют следующие строки, без фиктивных пустых карточек.
Внутри карточки метка и сумма образуют переносимую строку. Естественная ширина
полного текста определяет вместимость, без JS-измерений и порогов по количеству
цифр. При недостатке места сумма переходит ниже метки и получает всю доступную
ширину. Ограничения `min-width: 0`, `max-width: 100%`, обычные переносы на
пробелах и `overflow-wrap: anywhere` защищают также длинную дробную часть.
Не задаются фиксированная высота, `nowrap`, ellipsis, clipping или уменьшение
шрифта ради вместимости. На переносимой строке знак минуса не отделяется
от первой цифры; остальные цифры и суффикс остаются текстом в исходном порядке.
Альтернативы: единая общая полоса не соответствует выбранным отдельным карточкам;
фиксированная двухстрочная раскладка теряет компактность обычных значений;
обрезание/скролл/округление нарушают Delta Specs. Нативный reflow сохраняет
простую архитектуру и работает при увеличении текста.
## UI/UX Applicability and Package
**Applicability and scope:** `new/changed` — сводка и её initial skeleton на
`/accounts/:accountId`. Навигация, heading, время, позиции, Cash Balance и
остальные секции на PNG служат контекстом; их внешний вид не является заданием
на редизайн. Наличие в runtime дополнительных секций не является расхождением.
**Package revision:** `a1-symbols-r1`. Один approval относится ко всему этому
Markdown и восьми локальным PNG ниже. Они доступны без Pencil, Storybook,
исходного чата или внешнего аккаунта.
**Reused source and differences:** это новый canonical пакет, а не ссылка на
approved archived reuse. PNG перенесены byte-for-byte из выбранных Discovery r3
exports; их первоначальные имена перечислены ниже. Исходный `.pen` не является
зависимостью пакета. Markdown дополняет изображения правилами для других валют,
accessibility, промежуточных ширин и произвольно длинных значений.
### Визуальная геометрия
Карточка белая, граница gray-3 1 px со всех сторон, radius 8 px, без отдельной
синей верхней полосы и тени. Padding 12 px; промежуток метка/сумма 10 px.
Слева badge: фон blue-0, текст blue-6, radius 4 px, минимум 40 × 24 px;
для ISO-кодов ширина растёт по тексту. Знак 18 px / 600. Сумма — 20 px / 600,
line-height 1.55, основной цвет dark-7. Используется системный font stack
приложения, без загрузки Roboto только ради совпадения canvas.
Обычная строка выравнивает сумму к правому краю. После перехода под метку
сумма использует всю ширину с чтением слева направо. Предпочтительны переносы
на пробелах группировки; длинная непрерывная дробь может переноситься между
цифрами. Точная точка переноса зависит от системного шрифта: PNG не задаёт
обязательный разрыв после конкретной цифры. Все цифры, минус и суффикс читаемы.
При базовом масштабе обычная карточка ориентировочно 57 px, три карточки
на mobile — 191 px вместе с двумя gap. Это design geometry, а не runtime
замер: критерием служат полные значения и уменьшение высоты относительно
прежнего UI при одинаковых ширине, шрифте, масштабе и данных. Длинные значения
и увеличение текста закономерно увеличивают высоту.
### Screens, states and interactions
| Состояние | Решение |
| --- | --- |
| Loaded, 1/3 валюты | Отдельные A1 карточки; символ слева, полная сумма справа при вместимости. |
| Loaded, больше валют / неизвестная валюта | Та же сетка без лимита количества; прочие валюты используют ISO-код в badge и суффиксе. |
| Длинные / отрицательные значения | Сумма уходит под badge и переносится; `-0,5 $` сохраняет минус; зелёный/красный тон по знаку не добавляется. |
| Initial loading | Три декоративных card placeholders с badge/amount shapes, в той же responsive сетке; число shapes не обещает три валюты. Существующий `LoadingSkeleton` владеет busy label, shapes скрыты от чтения и не фокусируются. Остальные semantic regions loading view сохраняются. |
| Refetch | Прежние карточки и `lastSuccessfulAt` плюс существующий compact progress; карточки не заменяются skeleton. |
| Client refresh error | Прежние значения и время плюс существующий orange warning и «Повторить». |
| Server stale | Прежний blue warning и время; само `isStale` не создаёт retry. |
| Error + stale | Оба самостоятельных предупреждения, карточки и время; retry принадлежит клиентской ошибке. |
| Retry → success | До ответа сохраняются значения; успешный ответ обновляет значения/время, снимает client error; stale зависит только от нового ответа. |
| Нет итогов при непустом составе | Нет карточек; состав и время остаются. |
| Пустые позиции и Cash Balance | Существующее empty-представление, без нулевых карточек. |
| Ошибка без успешных данных | Существующая error view; `404` ведёт к списку счетов без retry; ошибки подключения/токена сохраняют свои действия, временная/неизвестная ошибка допускает retry. |
Новых взаимодействий нет. Кнопка повтора сохраняет клавиатурную доступность
и существующую callback-связь с VM. Статические суммы не превращаются в live
region; обновление сообщает существующий status region.
**Responsive behavior:** на 320/390 px одна карточка в ряду; при 1440 px и
контенте 960 px — три. Между ними действуют указанные breakpoints. При любом
числе колонок переполнение значения переводит его ниже метки, а не расширяет
grid track. Одна валюта на узком экране занимает всю ширину. Loading использует
те же правила; refetch/error/stale не меняют геометрию самих итогов.
### Materials
| Material ID (Discovery source) | Relative path | Screen/state and purpose | SHA-256 |
| --- | --- | --- | --- |
| wide (ACV5F) | [visuals/a1-wide.png](visuals/a1-wide.png) | 1440, три обычных итога | `b406552e01098a5b95183d6a54bd59b3bf827387d33d73a6a453d83140b8da10` |
| narrow (j6ks4) | [visuals/a1-narrow.png](visuals/a1-narrow.png) | 390, три обычных итога | `8ee71bb406abaa980e08f57c45e007fb66183786dfd251c9280a6ff62205d922` |
| single (iwyJA) | [visuals/a1-single-wide.png](visuals/a1-single-wide.png) | 1440, одна валюта | `6f163dd4a4ba461df5e715bab5757f4698986aade56ae38f02585d9c8d366b28` |
| long-wide (Hbu1s) | [visuals/a1-long-wide.png](visuals/a1-long-wide.png) | 1440, длинные значения | `ecccaee124c72b0f4f22bd92b31ef97e260c830782761ed9e7aab4c33a7e0af0` |
| long-320 (vjlkn) | [visuals/a1-long-320.png](visuals/a1-long-320.png) | 320, перенос и сохранение всех цифр | `48d62eaa497819e6312f5ae56afa393f92c7bb0970543cd9b3abdd5960480534` |
| loading (DuAlU) | [visuals/a1-loading-narrow.png](visuals/a1-loading-narrow.png) | 390, placeholders сводки | `138b000b334f54294f6cd8e88d607619608b05620231267fc1dc0477e1f87f46` |
| refresh (aP2cU) | [visuals/a1-refresh-narrow.png](visuals/a1-refresh-narrow.png) | 390, сохранение данных при обновлении | `e453606105a96290a2d2358e039935c89187a90127ffe4228374ae6e16c9e252` |
| error-stale (z5EeB) | [visuals/a1-error-stale-narrow.png](visuals/a1-error-stale-narrow.png) | 390, отдельные предупреждения и retry | `c63c8a30cc932582f591422dd4871c6663cb55567218cee988d7119d77d4a4e3` |
## Delivery Map
| Area | Status | Architectural outcome or N/A reason |
| --- | --- | --- |
| frontend | `affected` | A1 totals и соответствующий skeleton; точное форматирование с локальными знаками и доступной идентичностью; сохранённые query/VM состояния. |
| backend | `N/A` | Готовые итоги, расчёты, Snapshot и HTTP/OpenAPI contract не меняются; серверная реализация не требуется. |
| specs | `affected` | Canonical planning, переносимый Design package и нормативное описание `portfolio-view` в рамках lifecycle. |
| docs | `affected` | После реализации `features/portfolio-view.md` описывает фактическую компактную сводку, валютные знаки и ограничения оценки. |
### Contract Gate Decision
| Gate | Status | Reason |
| --- | --- | --- |
| runtime-openapi | `N/A` | Ни одна сторона API contract не меняется; существующие decimal strings и исходные currency остаются входом представления. |
## Risks / Trade-offs
- Разные системные шрифты и zoom меняют вместимость. Нужен browser reflow review
обычных и длинных fixtures на узких и широких экранах; PNG не доказывает
отсутствие runtime overflow или доступность. Точность и полнота важнее высоты.
- Открытие существующего decimal formatter немного расширяет публичный
`shared/lib`, зато исключает дублирование денежных преобразований. Риск
защищается сохранением текущего `formatMoney` и regression cases для точных
цифр, дробных нулей, `-0.5` и валютной идентичности на presentation boundary.
- Значение `$` без доступного полного названия неоднозначно; связь пары
название/значение проверяется в accessibility tree. Цвет не несёт идентичность.
- Placeholder не знает количества валют заранее. Три формы повторяют ожидаемую
структуру, а успешный ответ определяет реальное число карточек.
- Геометрия и денежное представление требуют scoped acceptance проверки;
состояние refetch/error проверяется через существующие account-page tests,
без новой state machine или отдельной инфраструктуры тестирования.
## Migration Plan
После approval Design и затем Tasks представление и loading geometry
поставляются совместно с проверкой точности и сохранённых состояний. После
проверенной реализации docs фиксирует as-built поведение; canonical спецификация
синхронизируется в рамках штатного lifecycle. Миграции данных, переключатель
функции и координированный релиз backend не нужны.
### Rollback Contract
| Field | Decision |
| --- | --- |
| Status | `N/A` |
| Reason | Изменение обратимого frontend presentation без изменения auth, данных, API или другого труднообратимого решения. Обычный откат frontend delivery возвращает прежний вид без преобразования данных. |
@@ -0,0 +1,55 @@
## Why
Сводка итогов Portfolio на странице Brokerage Account занимает избыточную
высоту, особенно на узком экране, и отдаляет пользователя от состава портфеля.
Нужно сделать её компактнее, сохранив читаемость каждой суммы и понимание того,
к какой валюте она относится. В Discovery владелец выбрал направление A1:
отдельные компактные валютные карточки со знаками ₽, $ и €.
## What Changes
- Представить итоги Portfolio компактными отдельными карточками по валютам,
следуя выбранному направлению A1. На широком и узком экране обычная сводка
занимает меньше высоты, сохраняя читаемость и доступность значений.
- Для российского рубля, доллара США и евро показывать ₽, $ и € соответственно
в валютных метках и рядом с суммами вместо RUB, USD и EUR. Сохранить
однозначность валюты и её полное доступное название; другие валюты остаются
различимыми и не исключаются из сводки.
- Показывать длинные, дробные и отрицательные суммы полностью: без потери
цифр или знака, округления, сокращений до тысяч или миллионов, обрезания
и необходимости горизонтальной прокрутки. На узком экране допускать перенос
и увеличение высоты ради полного значения.
- Сохранить смысл каждой суммы: итог позиций и доступных денег в одной валюте,
без сложения разных валют и конвертации. Отсутствующие суммы не заменяются
нулями, рублёвый итог не становится общей стоимостью Portfolio, а сводка
не обещает полноту оценки при отсутствии стоимости отдельных позиций.
- Сохранить существующие состояния: первичное ожидание без вымышленных сумм;
прежние суммы и время успешного получения при фоновом обновлении и его
ошибке; раздельные сообщения об устаревшем снимке и ошибке обновления,
с применимым действием повтора. Существующее пустое состояние сохраняется.
Вне scope: изменение денежных расчётов, состава или источников данных;
редизайн навигации, заголовка, позиций и соседних разделов; изменение
форматирования денег во всём приложении. Выбор A1 задаёт направление этой
сводки; детальное визуальное решение подлежит отдельному согласованию Design.
## Capabilities
### New Capabilities
Нет.
### Modified Capabilities
- `specs/portfolio-view/spec.md`: добавить требования к компактному
представлению валютных итогов на странице Brokerage Account, символам
валют, доступной идентичности и полному отображению сумм на широком
и узком экране при сохранении существующего смысла итогов и состояний.
## Impact
Изменение затрагивает пользователей, просматривающих Portfolio одного
Brokerage Account на широких и узких экранах, и ограничено сводкой его
валютных итогов. При поставке обновляются пользовательское представление,
его нормативное описание и документация фактически доступного поведения.
Способ получения данных и финансовый смысл итогов сохраняются.
@@ -0,0 +1,251 @@
## ADDED Requirements
### Requirement: Компактная сводка валютных итогов Portfolio
На странице Brokerage Account клиент SHALL показывать каждый полученный
валютный итог Portfolio отдельной компактной карточкой. При достаточной
ширине карточки валютная метка и сумма MUST располагаться в одной строке;
сумма MUST оставаться основной читаемой величиной. Обычная сводка с одной
или тремя валютами MUST занимать меньше высоты, чем прежнее представление
с меткой над суммой при тех же значениях и ширине экрана.
На широком экране несколько карточек SHALL располагаться рядом при наличии
места; на узком экране MUST сохраняться одна карточка в ряду. Если полное
значение не помещается рядом с меткой, клиент MUST предоставлять ему полную
доступную ширину карточки и переносить содержимое с увеличением высоты.
Сводка MUST NOT обрезать суммы, заменять их многоточием или требовать
горизонтальной прокрутки для чтения карточек и значений.
#### Scenario: Обычная сводка на широком экране
- **WHEN** User открывает Portfolio с итогами `125000.50` RUB, `320.00` USD
и `9876.54321` EUR на широком экране с местом для трёх карточек
- **THEN** клиент показывает три отдельные карточки рядом, метку и полную
сумму в одной строке каждой карточки, с меньшей высотой сводки, чем при
прежнем размещении метки над суммой в тех же условиях
#### Scenario: Обычная сводка на узком экране
- **WHEN** User открывает тот же Portfolio с тремя валютными итогами
на узком экране
- **THEN** клиент показывает три карточки по одной в ряду, сохраняя полные
читаемые суммы и меньшую общую высоту сводки, чем при прежнем размещении
метки над суммой в тех же условиях
#### Scenario: Portfolio с одной валютой
- **WHEN** Portfolio содержит один валютный итог
- **THEN** клиент показывает одну компактную карточку без пустых карточек
для других валют; обычная сумма и метка помещаются в одну строку при
достаточной ширине на широком и узком экране
#### Scenario: Длинная сумма на узком экране
- **WHEN** User просматривает итог `123456789012.345678901` RUB при ширине
экрана 320 пикселей и значение не помещается рядом с валютной меткой
- **THEN** сумма получает полную доступную ширину карточки и при необходимости
переносится; все цифры и знак валюты читаемы без обрезания, многоточия
и горизонтальной прокрутки
### Requirement: Обозначение и доступная идентичность валюты в сводке
В валютных метках и суффиксах сумм сводки клиент MUST обозначать RUB знаком
`₽`, USD знаком `$`, EUR знаком `€` вместо видимых ISO-кодов этих валют.
Каждая сумма MUST оставаться однозначно связанной со своей исходной валютой,
включая чтение вспомогательными технологиями. Для RUB, USD и EUR MUST быть
доступны названия «Российский рубль», «Доллар США» и «Евро» соответственно.
Символ или цвет сами по себе MUST NOT быть единственным доступным способом
определения валюты.
Другие полученные валюты MUST оставаться в сводке с понятным обозначением
и доступной идентичностью. Если знак отсутствует или неоднозначен, клиент
MUST сохранять возможность различить исходную валюту по её названию или
ISO-коду, в том числе визуально; неизвестное полное название MUST NOT
подменяться вымышленным. Совпадающие знаки разных валют MUST NOT приводить
к объединению карточек или неразличимому представлению их валют.
#### Scenario: Рубль, доллар США и евро показаны знаками
- **WHEN** сводка содержит RUB, USD и EUR
- **THEN** метки показывают `₽`, `$` и `€`, а суммы заканчиваются теми же
знаками; видимые RUB, USD и EUR не подменяют выбранные знаки
#### Scenario: Валютная сумма читается вспомогательной технологией
- **WHEN** User читает валютный итог сводки вспомогательной технологией
- **THEN** ему доступны полное значение и однозначная валютная идентичность,
включая «Российский рубль», «Доллар США» или «Евро» для соответствующих валют
#### Scenario: Получена другая валюта
- **WHEN** сводка содержит валюту помимо RUB, USD и EUR
- **THEN** её отдельный итог остаётся доступным с понятным обозначением
и доступным названием либо исходным ISO-кодом при неизвестном названии
#### Scenario: У разных валют совпадает знак
- **WHEN** Portfolio содержит итоги двух исходных валют с одинаковым знаком
- **THEN** сводка показывает два отдельных итога, и User может различить
их валюты визуально и вспомогательной технологией без догадки по сумме
### Requirement: Полные точные значения и смысл валютной сводки
Сводка MUST показывать полученные валютные итоги без изменения их знака,
цифр и дробной точности. Для русской локали клиент SHALL сохранять группировку
разрядов пробелами и десятичную запятую, не округляя значение, не отбрасывая
дробные позиции и не сокращая сумму до тысяч, миллионов или экспоненциальной
записи. Требование MUST сохраняться для длинных, отрицательных значений
и отрицательных дробей с нулевой целой частью.
Каждая карточка MUST представлять только полученный итог позиций и Cash
Balance своей исходной валюты. Сводка MUST NOT конвертировать или складывать
разные валюты, показывать рублёвый итог как общую стоимость Portfolio,
повторно добавлять Cash Balance либо обещать полную оценку при наличии
позиций без вычислимой стоимости. Для отсутствующего валютного итога клиент
MUST NOT создавать нулевую карточку. Отсутствие итогов само по себе MUST NOT
обозначать пустой Portfolio, если позиции или Cash Balance присутствуют.
#### Scenario: Обычные суммы сохраняют дробные позиции
- **WHEN** в сводку поступают значения `125000.50` RUB, `320.00` USD
и `9876.54321` EUR
- **THEN** клиент показывает `125 000,50 ₽`, `320,00 $` и `9 876,54321 €`
с сохранением каждой исходной цифры и дробной позиции
#### Scenario: Длинные и отрицательные значения не теряют точность
- **WHEN** в сводку поступают `123456789012.345678901` RUB,
`-1234567.890123` USD и `987654321.000001` EUR
- **THEN** пользователь видит полные `123 456 789 012,345678901 ₽`,
`-1 234 567,890123 $` и `987 654 321,000001 €` без округления,
сокращений и потери отрицательного знака на широком и узком экране
#### Scenario: Отрицательная сумма меньше единицы
- **WHEN** полученный валютный итог равен `-0.5` USD
- **THEN** сводка показывает `-0,5 $`, сохраняя знак и дробную величину
#### Scenario: Несколько валют и денежные остатки
- **WHEN** полученные итоги RUB и USD уже включают позиции и Cash Balance
соответствующих валют
- **THEN** клиент показывает эти два итога без повторного добавления денег,
межвалютного итога или представления RUB как общей стоимости Portfolio
#### Scenario: Стоимость части позиций неизвестна
- **WHEN** Portfolio содержит позиции без вычислимой стоимости и доступные
валютные итоги, сформированные без этих позиций
- **THEN** сводка показывает только полученные итоги, не добавляет нули
за отсутствующие стоимости и не называет результат полной оценкой Portfolio
#### Scenario: Итоги отсутствуют при непустом Portfolio
- **WHEN** `totalsByCurrency` пуст, но `positions` или `cashBalances` непусты
- **THEN** клиент не создаёт валютные карточки и не заменяет доступный
состав Portfolio состоянием пустого портфеля
## MODIFIED Requirements
### Requirement: Состояния и ошибки Portfolio на клиенте
Клиент SHALL загружать Portfolio при открытии защищённого маршрута
`/accounts/:accountId`. Он MUST использовать успешный ответ в течение
30 секунд, без automatic retry, polling или refetch при возврате фокуса.
При загрузке без ранее полученного успешного ответа для этого же Brokerage
Account клиент MUST показывать структурное skeleton-состояние ожидаемого
содержимого, включая сводку, без вымышленных сумм. Область загрузки MUST
иметь доступное обозначение занятости; skeleton MUST NOT быть интерактивным
или получать клавиатурный фокус.
Истечение срока свежести MUST NOT само по себе скрывать уже доступный
Client Cached Snapshot. Во время фонового обновления и после его ошибки
клиент MUST сохранять ранее полученные данные того же Portfolio, включая
валютные итоги и исходное `lastSuccessfulAt`. При обновлении клиент MUST
показывать компактную неблокирующую индикацию без замены данных skeleton.
При ошибке обновления он MUST показывать локализованное неблокирующее
предупреждение о последних полученных данных и предоставлять действие
повторной попытки. Сохранённые данные MUST NOT считаться новым успешным
снимком или подтверждением текущей доступности Brokerage Account.
Клиент MUST показать штатное empty-состояние только при пустых `positions`
и `cashBalances`, без нулевых валютных карточек. Для показываемого состава
Portfolio клиент MUST сохранять время успешного получения снимка
`lastSuccessfulAt`, форматируемое в UTC. При `isStale: true` клиент MUST
показывать Snapshot, его исходное время и нейтральное предупреждение
о временной недоступности T-Bank. Это предупреждение само по себе MUST NOT
добавлять действие ручного обновления. Server-side stale и ошибка фонового
обновления Client Cached Snapshot MUST оставаться раздельными состояниями:
ошибка клиента MUST NOT менять или подменять `isStale`, а оба предупреждения
и действие повтора клиентского обновления MUST оставаться доступными при
одновременном наличии этих состояний.
При отсутствии ранее успешно полученного Client Cached Snapshot этого же
Portfolio и ответе `404` клиент MUST показать состояние недоступного счёта
с действием возврата к списку счетов без действия повтора. При отсутствии
Snapshot и `TBANK_NOT_CONNECTED`, `INVALID_TBANK_TOKEN`, `TBANK_UNAVAILABLE`
или иной ошибке клиент MUST показать безопасное error-состояние с доступными
для ошибки действиями; действие повтора разрешено только для временной
и неизвестной ошибки.
#### Scenario: Показ устаревшего Portfolio
- **WHEN** клиент получает успешный ответ с `isStale: true`
- **THEN** клиент показывает позиции, остатки, валютные итоги, время
последнего успешного снимка и нейтральное предупреждение; само это
предупреждение не добавляет действие ручного обновления
#### Scenario: Недоступный счёт на клиенте
- **WHEN** для текущего Portfolio отсутствует ранее успешно полученный
Client Cached Snapshot и клиент получает `404`
- **THEN** клиент не показывает Portfolio и предлагает вернуться к списку
Brokerage Account без действия повтора
#### Scenario: Первичная загрузка Portfolio
- **WHEN** клиент ожидает Portfolio этого Brokerage Account без ранее
полученного успешного ответа
- **THEN** он показывает структурный skeleton, включая место сводки,
с доступным обозначением загрузки, без вымышленных или нулевых сумм
и без фокусируемых placeholders
#### Scenario: Обновление после истечения срока свежести
- **WHEN** срок свежести показанного Portfolio истёк и началось фоновое
обновление данных того же Brokerage Account
- **THEN** клиент сохраняет прежние валютные итоги и время их успешного
получения, показывает неблокирующую индикацию обновления и не заменяет
доступный состав и сводку skeleton-состоянием
#### Scenario: Ошибка обновления после успешного ответа
- **WHEN** клиент уже показывает Client Cached Snapshot Portfolio,
а обновление этих же данных завершается ошибкой
- **THEN** клиент сохраняет данные, суммы и время прежнего снимка,
показывает неблокирующее предупреждение и действие повтора;
ошибка не делает сохранённый снимок свежим и не изменяет `isStale`
#### Scenario: Ошибка клиентского обновления при server-side stale
- **WHEN** клиент показывает Portfolio с `isStale: true`, а фоновое
обновление этого же Portfolio завершается ошибкой
- **THEN** прежние суммы и время остаются видимыми одновременно
с нейтральным server-side предупреждением, отдельным предупреждением
об ошибке клиентского обновления и его действием повтора
#### Scenario: Повторное обновление успешно
- **WHEN** после ошибки фонового обновления User запускает повторную
попытку и получает успешный Portfolio Snapshot
- **THEN** до ответа прежние суммы и время остаются видимыми, затем
клиент показывает значения и `lastSuccessfulAt` успешного ответа,
убирает предупреждение об ошибке клиентского обновления и показывает
server-side предупреждение только в соответствии с новым `isStale`
#### Scenario: Штатный пустой Portfolio на клиенте
- **WHEN** успешно полученный Portfolio содержит пустые `positions`
и `cashBalances`
- **THEN** клиент показывает существующее empty-состояние без валютных
карточек с вымышленными нулевыми итогами
@@ -0,0 +1,32 @@
## 1. Frontend: компактная сводка и сохранение состояний
Основа — утверждённый [Design](design.md), включая пакет `a1-symbols-r1`
и все восемь PNG из его Materials. Перед первой Node-командой активировать
frontend `.nvmrc` через `nvm use --silent`. Зависимости устанавливать из lockfile
через `npm ci`; новую инфраструктуру и зависимости не добавлять.
- [x] 1.1 [frontend] Открыть существующий `formatDecimal` через публичный `shared/lib` и использовать его в сводке с локальными знаками ₽/$/€, ISO-fallback и доступными названиями валют из i18n, сохранив поведение общего `formatMoney`.
- **Covers:** `portfolio-view` — `Обозначение и доступная идентичность валюты в сводке`; `portfolio-view` — `Полные точные значения и смысл валютной сводки`
- **Verification:** Scoped Vitest через `npm run test:run -- src/shared/lib/display.test.ts src/pages/account`: полные обычные и длинные decimal strings из Delta Specs, дробные нули, отрицательные значения и `-0.5`; видимые знаки и доступные полные названия RUB/USD/EUR; раздельные USD/CAD и неизвестный ISO; прежние результаты `formatMoney` вне сводки. Дополнить поведенческие тесты на существующей presentation boundary без копии алгоритма formatter в assertions.
- [x] 1.2 [frontend] Реализовать A1 в `PortfolioTotals` и page-local CSS: отдельные карточки, badge и сумма в строке при вместимости, responsive сетка и полный перенос длинной суммы под метку; сохранить исходные membership, порядок и смысл итогов.
- **Covers:** `portfolio-view` — `Компактная сводка валютных итогов Portfolio`; `portfolio-view` — `Обозначение и доступная идентичность валюты в сводке`; `portfolio-view` — `Полные точные значения и смысл валютной сводки`
- **Verification:** `npm run test:run -- src/pages/account`: одна/три/больше валют, отсутствие фиктивных карточек, отсутствие повторного добавления Cash Balance или общего межвалютного итога; при неизвестной стоимости позиции показываются только переданные суммы, без обещания полной оценки. Проверить семантическую связь названия с полным значением и отсутствие tab stop у карточки. Реальный reflow проверяется в 1.4, а не выводится из jsdom.
- [x] 1.3 [frontend] Заменить область сводки в page-owned initial skeleton тремя декоративными A1 placeholders и дополнить существующие account-page regression checks для сохранённых данных и состояний, не меняя query/VM policy.
- **Covers:** `portfolio-view` — `Состояния и ошибки Portfolio на клиенте`; `portfolio-view` — `Полные точные значения и смысл валютной сводки`
- **Verification:** `npm run test:run -- src/pages/account src/entities/portfolio`: initial busy label без сумм и фокусируемых shapes; refetch → error → retry → success сохраняет прежние суммы и UTC `lastSuccessfulAt` до успешного ответа; server stale отдельно и вместе с client error; `404` с/без Client Cached Snapshot; штатные действия ошибок подключения/токена/временной и неизвестной ошибки; empty по positions/cashBalances и непустой состав без totals. Переиспользовать имеющиеся assertions cache policy: 30 секунд, без automatic retry/polling/focus refetch; добавлять только недостающее покрытие затронутых инвариантов.
- [x] 1.4 [frontend] Проверить реализованный пакет в браузере, исправить расхождения в пределах Design и выполнить canonical frontend gate.
- **Covers:** `portfolio-view` — `Компактная сводка валютных итогов Portfolio`; `portfolio-view` — `Обозначение и доступная идентичность валюты в сводке`; `portfolio-view` — `Полные точные значения и смысл валютной сводки`; `portfolio-view` — `Состояния и ошибки Portfolio на клиенте`
- **Verification:** В существующей локальной витрине/приложении сравнить сводку и loading с `a1-symbols-r1` на 320, 390 и 1440 px и по обе стороны 48/62 em; использовать синтетические обычные, длинные, отрицательные значения, одну/три/дополнительные валюты и zoom 200%. Проверить все цифры и знаки, получение полной ширины под меткой, отсутствие обрезания и горизонтального overflow сводки (`scrollWidth <= clientWidth` для сводки/карточек), меньшую высоту обычных одной/трёх валют относительно прежнего UI при одинаковых данных, ширине, шрифте и масштабе. Проверить accessibility tree, клавиатурный retry, initial/refetch/error/stale и неизменность соседних sections. Сохранить observed measurements/screenshots как implementation evidence, не как новую revision Design. Затем после lockfile install выполнить `npm run verify` из frontend; runtime-openapi не требуется по Design.
## 2. Документация и canonical пакет
- [x] 2.1 [docs] После проверенной frontend реализации обновить `features/portfolio-view.md` описанием фактической компактной сводки, валютных знаков, точных переносимых сумм и ограничений оценки; сохранить согласованное описание состояний.
- **Covers:** `portfolio-view` — `Компактная сводка валютных итогов Portfolio`; `portfolio-view` — `Обозначение и доступная идентичность валюты в сводке`; `portfolio-view` — `Полные точные значения и смысл валютной сводки`; `portfolio-view` — `Состояния и ошибки Portfolio на клиенте`
- **Verification:** Scoped review соответствия документа реализованному UI и применимых локальных ссылок; из docs после `nvm use --silent` и `npm ci` выполнить `npm run verify`. Не переносить planning или будущие verification results в as-built документацию.
- [x] 2.2 [specs] Подготовить к поставке полный canonical change package: утверждённые planning artifacts, восемь Materials `a1-symbols-r1` и фактические отметки завершения implementation Tasks; сохранить неизменными approval-bound содержимое и относительную структуру пакета.
- **Covers:** `N/A` — задача не вводит отдельного пользовательского поведения; specs владеет поставкой уже подготовленного normative/UI пакета и checklist progress согласно Delivery Map. Main-spec sync и archive остаются последующей Finalization.
- **Verification:** Из specs под активированным frontend runtime выполнить `openspec schema validate wealth-ledger-sdd --verbose`, `openspec validate compact-portfolio-totals --type change --strict --store wealth-ledger-specs` и `openspec validate --specs --strict --store wealth-ledger-specs`. Сверить полный native Delta membership, prerequisite approval fingerprints и Tasks fingerprint с нормализацией только checklist progress; проверить все пути/SHA-256 материалов из Design. Не создавать независимый PASS, Completion Evidence или archive в этой task.
Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

@@ -0,0 +1,2 @@
schema: wealth-ledger-sdd
created: 2026-09-09
@@ -0,0 +1,147 @@
# Completion Evidence: simplify-income-page
## Outcome and authority
Finalization consumed independent **PASS** from Validation task
`01a085bd-2f84-7801-a9c1-d35abc346df1`, record
`.agents/sdd/changes/simplify-income-page/005-validation-2.md` (workspace-relative).
All nine implementation Tasks 1.1–1.4 and 2.1–2.5 are complete.
Canonical store: `wealth-ledger-specs`.
The owner authorized separate Validation/Finalization tasks, delivery commits and
archive in task `01a085af-72e7-78d3-b8a1-bfda579191a6`: «все ок, давай завершать».
This authorization supersedes the earlier request to leave delivery uncommitted.
No push or deployment is part of completion.
## Actual Delivery Map and existing delivery commits
| Area | Status and delivered result | Delivery commit |
| --- | --- | --- |
| frontend | affected: calendar draft/apply/reset form, flat individual payment cards, states and responsive fixes; income-owned trend/forecast consumers removed; regression tests updated | `26f229ba3e0f8335dde297bf6c4575f7014e9f28` |
| docs | affected: as-built calendar filters, flat payments, removed trend/forecast UI and unchanged analytics API | `7166ee51d181b04f3892ff6757a56b578ee9df64` |
| specs | affected: approved Proposal, Delta Specs, inline Design r2 and completed Tasks | `4c905c143368d799c142dbbce3a97d74108d6d28` |
| backend | N/A: existing endpoint, buckets, monetary calculations, events and persisted data unchanged; working tree clean | N/A |
`runtime-openapi`: N/A because neither API side changes; frontend continues using
the existing typed response and shared Future Cash Events remain available.
No empty backend commit or contract gate was required.
Delivery commits existed before this evidence was created. After committing,
re-expansion of all reviewed roots and explicit paths still matched the independent
PASS exactly, including raw content and modes; commit hooks did not alter inputs.
The subsequent evidence/archive/sync commit ID is reported outside this file to
avoid a self-reference.
## Verification commands and observed results
Commands below ran under the applicable `.nvmrc` via
`source ~/.nvm/nvm.sh && nvm use --silent`; specs used frontend runtime before
`cd ../specs`. Current Finalization repeated only `npm ls --depth=0` in frontend
and docs (both exit 0), native status/specs instructions (exit 0), and content
binding checks. Package/lock/runtime inputs match. No install or dependency change
occurred. Unchanged gates and independent review were consumed without repetition.
| cwd | Exact verification command | Observed result |
| --- | --- | --- |
| frontend | `npm run verify` | exit 0: OpenAPI generated comparison, strict TypeScript, Biome, 26 files / 147 tests and coverage, Knip, dependency boundaries, application and catalog builds passed; existing catalog chunk warning nonblocking |
| docs | `npm run verify` | exit 0: Markdown validation and whitespace checks passed; 44 Markdown files checked, 0 issues |
| specs | `openspec validate simplify-income-page --type change --strict --store wealth-ledger-specs` | exit 0: change valid |
| specs | `openspec validate --specs --strict --store wealth-ledger-specs` | exit 0: 25 passed / 0 failed |
| frontend | `npm run test:run -- src/pages/account-income/ui/AccountIncomePage.test.tsx src/pages/account-events/ui/AccountEventsPage.test.tsx` | independent Validation exit 0: 2 files / 35 tests |
| frontend | `node /tmp/validation-005-browser.cjs > /tmp/validation-005-browser.log 2>&1` | independent Validation exit 0; all acceptance assertions pass, zero page errors, contexts closed |
| frontend | `npm run dev -- --host 127.0.0.1 --port 5175 --strictPort` | Vite ready for browser proof; deliberately stopped with Ctrl-C, exit 130 |
| frontend, docs, specs | `git diff --check` | exit 0 in each affected repository |
Exact full command wrappers, cwd, exit codes and logs are retained at workspace
`.agents/sdd/changes/simplify-income-page/evidence-004/commands.json` and
`evidence-005/commands.json` under that same change envelope. Reused gate logs:
`evidence-004/frontend-verify.log`, `docs.log`, `specs-change.log`, `specs-main.log`.
Independent browser harness and observations: `evidence-005/browser.cjs`,
`browser.log`, `browser-result.json` and PNG captures. Detailed independent review:
`evidence-005/review.md`. These are evidence locators, not a competing planning store.
## Independent semantic and acceptance review
PASS; no Critical or Important findings. Review covered approved behavior and
inline package `simplify-income-page-ui-r2`: raw invalid dates retain text, focus,
URL and request count; calendar/keyboard apply, reset, browser navigation and
Moscow date boundary hold. Money signs, repeated IDs, server order, complete
membership validation and per-currency summaries remain intact. Cached snapshot,
empty/error/retry states and independent Future Cash Events navigation pass.
At 320/480/640/960 px all three fixture cards and complete descriptions remain
visible without horizontal overflow; mobile inputs meet 44 px. Actual Chrome
200% zoom measured outer width 960, inner width 480, DPR 2 and viewport height 406;
both calendars fit (bottom 401.5 and 327.70). Screenshots were independently
inspected. The shared Ledger consumer retains its prior clamp behavior.
Limits: browser proof used synthetic intercepted responses and installed Chrome,
not live brokerage data or every browser/zoom setting. A first harness assertion
included hidden inputs; the corrected visible-input selector passed on rerun.
These are not unresolved implementation findings.
## Approval and PASS input binding
Algorithm: `wealth-ledger-input-v1` from context authority. Each entry binds
workspace-relative path, kind, executable mode and SHA-256 payload; files are sorted
by UTF-8 path and NUL-framed. Only Tasks approval normalizes completed task prefixes;
verification includes raw Tasks. No unresolved Git conflicts were present.
Exact reviewed inventory and decision sources are preserved in workspace
`.agents/sdd/changes/simplify-income-page/005-inputs.json` and
`evidence-003/owner-decisions.json`. Inventory file raw SHA-256:
`2f207a30a2eb8f7d8924e5b1b8d98ad7d7f3ac62fbe9b380094741af3c131743`.
| Binding | Fingerprint |
| --- | --- |
| Proposal approval | `sha256:64879ac90bb35ca44922501abbb91d313cdea2d2a47a44bbd7a105d4ff572689` |
| Delta Specs approval | `sha256:d65555952f71f8c9080958adbc954589247ac53041baa3090b5c3da5383379c1` |
| Design approval | `sha256:000c2fcc1fce0e80805aa0b56380f3a616556461e49e6e86a5e721d2caa79490` |
| Tasks approval | `sha256:c99a5e5e5358f30c873b6231119f29d69c6edcf63897e9626d59a967bfd18d3e` |
| Raw planning | `sha256:41a86b7d134a978a19714c0c4d04f3cb51935d21a18db26d188f72794d030915` |
| Frontend gate, 209 entries | `sha256:f799913a03e5513838df53fb9f0f9c25548ec79558238a8b027aa35091e73b12` |
| Docs gate, 63 entries | `sha256:8a9c3bc06dc3a8ddb4f444ecc8a1b5c400804fb822c0cf5cb9d94259695001cf` |
| Specs gate, 347 entries | `sha256:509f2a7112061627fe876039b6bf69c1540d74ba0266e620bcc44eefd559631d` |
| Independent PASS, 620 entries | `sha256:3893b8301a80dfae92676a8bf2c3f78b294007bdd51b230ce35fb09d177d0d93` |
All four canonical artifact identities, native Delta Specs membership and approval
fingerprints match the original owner decisions. Explicit amendment source:
owner task `01a0855f-9b55-7d30-91ca-85636984ca6f`, turn
`01a0857c-1dec-7090-9102-1aeec4ce9c65`, including «Да, отдельная карточка каждой выплаты».
Reviewed roots are the complete frontend/, docs/, specs/ Git tracked and nonignored
untracked candidates from `git ls-files --cached --others --exclude-standard -z`,
union previous inventory entries (deletions remain missing). This covers planning,
relevant main baseline, dependency/config inputs and actual delivery.
Explicit workspace paths: root AGENTS.md; sdd-session, sdd-implementation,
sdd-validation SKILL.md files; sdd-session/references/workflow-protocol.md.
Each gate includes its local instructions. Explicit ignored frontend inputs are
.env, .env.local, .env.development, .env.development.local, .env.production and
.env.production.local; values are never copied into evidence.
Exclusions: .git, node_modules, ignored generated build/coverage/cache outputs,
this change's `.agents/sdd/changes/simplify-income-page/` navigation/evidence,
and its canonical completion-evidence.md. Expected archive/sync/evidence outputs
are verified separately; no unrelated input change is hidden by these exclusions.
## Semantic sync review and completion scope
Current native status and specs instructions resolved exactly one delta:
`specs/brokerage-account-income-analytics/spec.md` inside this change. The main
baseline is `openspec/specs/brokerage-account-income-analytics/spec.md` in the
canonical specs repository. All operation identities matched and all surviving
scenarios in the modified period requirement are retained.
Approved semantic diff: remove requirements for summary/trend visualization,
forecast display and instrument-grouped breakdown; add readable text summary,
income-page forecast exclusion and exact flat payment cards; modify explicit
period selection to include calendar controls, keyboard operation, validation and
no repeated period caption. Preserve the five other requirements and existing
Purpose byte-for-byte. The known old forecast wording in Purpose is explicitly
deferred by approved Design; it does not authorize restoring removed UI.
The package is wholly inline Design revision r2: no attachments or immutable reuse
inputs exist. Native archive must preserve Design and all planning bytes and
relative structure. No Audit ID applies, so no remediation registry mutation is
needed. New archive/main-spec outputs receive native strict checks; completion
record reports the observed archive path and separate completion commit.
@@ -0,0 +1,268 @@
## Context
Изменение ограничено presentation и page orchestration маршрута
`/accounts/:accountId/income`. Текущий `AccountIncomePage` выбирает состояния
через page VM; VM владеет периодом из URL, формой и запросами, а section views
отображают переданные данные. Верхняя форма использует native date inputs;
история событий уже использует установленный `@mantine/dates` DateInput.
Ответ `income-analytics` уже содержит totals, comparisons, instrument breakdown
и все включённые entries. VM соединяет entries с инструментами по непрозрачному
`instrumentKey`, проверяет валюту и receipt count. Денежные значения и порядок
инструментов приходят с backend. Дополнительная подписка income VM на `/events`
обслуживает только удаляемый прогноз. Trend view использует отдельную derived
модель из buckets того же ответа.
Ограничения: FSD `app → pages → entities → shared`, чистые section views,
существующие Mantine theme и i18n, без новой инфраструктуры или зависимостей.
Даты API и URL остаются календарными `YYYY-MM-DD` в Europe/Moscow;
формат отображения поля не меняет их смысл.
## Goals / Non-Goals
**Goals:** использовать существующий календарный контрол, сохранить явную
границу draft/applied period, отобразить полный состав дохода в отдельных
карточках выплат и удалить принадлежащие странице зависимости исчезающих разделов.
Сохранить проверки целостности membership и текущие денежные вычисления.
**Non-Goals:** изменение API, серверных buckets, денежных агрегатов, определения
Instrument, общих Ledger rows, обзора счёта, самостоятельной страницы будущих
событий, общей темы или навигации. Пагинация карточек, виртуализация,
дополнительные фильтры и новый каталог компонентов не вводятся.
## Decisions
### 1. Календарные поля адаптируются на границе page form
Используется Mantine DateInput с `valueFormat="DD.MM.YYYY"` и существующим
русским DatesProvider. Форма остаётся под управлением react-hook-form и Zod в
page VM; form-context/controlled-field adapter подключает DateInput к ней.
Общий date-range widget не создаётся: income требует обязательного периода,
запрета будущих дат и явных ошибок, тогда как Ledger допускает другие фильтры.
Импорта между page slices нет.
VM различает видимый черновик и применённые ISO-даты. Ручной ввод, в том числе
пустой или недопустимый текст, должен попасть в проверяемый черновик. Адаптер
учитывает raw input, календарный выбор и blur; автоматический откат текста
DateInput к последнему допустимому значению на blur отключается (`fixOnBlur=false`).
Парсинг допускает только существующую календарную дату формата поля, без
нормализации вроде 31 февраля в март. Успешный календарный выбор заменяет
черновик поля; валидированный submit переводит обе даты в ISO и обновляет URL.
Последнее внутреннее допустимое значение DateInput не заменяет ошибочный ввод.
Календарь ограничивает начало допустимым окончанием и сегодняшней московской
датой, окончание — допустимым началом и сегодняшней датой. Невалидное соседнее
поле не передаётся в min/max. Ограничения календаря помогают выбору, но Zod
остаётся проверкой обязательности, календарной корректности, будущих дат и
порядка. Для отсутствующей, некорректной или будущей даты ошибка принадлежит
самому полю; при обратном диапазоне — полю «По».
Адаптация календарной даты использует компоненты года, месяца и дня, как в
Ledger, без UTC-преобразования через `toISOString()` пользовательского Date.
«Сегодня» и default period определяются существующим московским правилом VM,
а не часовым поясом устройства. URL остаётся источником применённого периода;
его текущая нормализация, открытие и browser navigation сохраняются.
Сброс очищает ошибки и восстанавливает оба поля даже тогда, когда текущий URL
уже содержит default period, а пользователь изменил только черновик.
Альтернативы: native date inputs не обеспечивают согласованный контрол;
единый range picker меняет привычное взаимодействие двух полей; копирование
всего Ledger filter state переносит неподходящие income семантики. Выбран
существующий DateInput с локальной адаптацией формы.
### 2. Одна карточка соответствует одной выплате
`IncomePayments` отображает один семантический список: каждый Ledger Entry
передаётся существующему `LedgerEntryRow`. Нет внешней Paper-карточки Instrument,
заголовков валютных групп, instrument subtotal/total и receipt count на UI.
Как в списке позиций портфеля, карточки идут общей колонкой; импортов между
page slices и нового row renderer нет. LedgerEntryRow сохраняет дату, описание,
доступную identity, signed Money, характеристику выплаты и fallback изображения.
VM сначала проверяет opaque instrument keys, их непустоту и уникальность,
принадлежность каждой entry к известному instrumentKey, совпадение валюты и
receipt count. Валюта инструмента должна присутствовать среди totalsByCurrency.
Повреждённый ответ идёт целиком в error/retry, без частично заполненного списка.
Клиент не восстанавливает identity формулой и не пересчитывает денежные суммы.
После проверки VM отдаёт плоский список в исходном порядке `analytics.entries`.
В ключ строки входят opaque instrumentKey, operation ID, occurrence внутри
этой группы и logo source. Одинаковые IDs не схлопываются, в том числе между
инструментами. Валюта видна в каждой Money, а общие summary/comparisons по-прежнему
разделены по валютам. Серверный порядок не заменяется клиентской сортировкой.
### 3. Удаление секций завершает их локальное владение
Income page прекращает вызывать `useBrokerageEvents` и готовить forecast model.
Удаляются принадлежащие только этой странице forecast/trend views, derived
trend model и неиспользуемые стили/строки этих секций. Shared event entity,
её запрос и самостоятельная страница будущих событий сохраняются.
Backend продолжает возвращать buckets по прежнему контракту; frontend больше
не строит из них presentation для income page. Это позволяет ограничить
изменение потребителем без миграции transport types и соседних consumers.
Удаление полей API ради размера ответа — отдельное изменение контракта,
не необходимое для этого решения.
### 4. Границы состояний и доказательства остаются у страницы
Top-level loading/error и loaded composition остаются в существующей
container/VM/view структуре. Skeleton отражает новые регионы. Пустой результат
использует StateView; ошибка запроса или membership — существующий error/retry.
При фоновой загрузке того же периода валидный Client Cached Snapshot сохраняется
с неблокирующим progress, а после ошибки — с локальным предупреждением и retry
согласно frontend UI conventions. При отсутствии данных нового query используется loading, без старого результата.
Отдельной подписи применённого периода под формой нет; draft/applied boundary
сохраняется и видимые суммы меняются только после применения.
Проверка опирается на существующие page tests и MSW и защищает период и состав
денежных данных: draft/apply/reset/URL, недопустимый ручной ввод, московскую границу дня,
несколько валют, отрицательные и повторяющиеся выплаты, повреждённый membership
и retry. Удаляемые chart/forecast expectations заменяются наблюдаемым отсутствием
секций и запроса events от income page. Сохраняемые денежные/membership assertions
не удаляются вместе с Accordion. Адаптивность и keyboard flow оцениваются по
пакету ниже; новая test/browser инфраструктура для этого не требуется.
## UI/UX Applicability and Package
**Applicability and scope:** `new/changed` — income page: календарные фильтры,
плоский список выплат, последовательность секций и loading representation.
Общая оболочка, обзор счёта и самостоятельные «Будущие события» вне scope.
**Package revision:** `simplify-income-page-ui-r2`.
Пакет полностью inline; дополнительных attachments, external-only макетов
и immutable reuse inputs нет. Revision r2 отражает уточнение владельца:
убрать подпись периода, заголовки валют и внешние карточки Instrument,
переименовать раздел в «Выплаты», показывать отдельную карточку каждой выплаты.
**Screens, states and interactions:**
Loaded: существующая ширина content до 960 px, Stack gap `lg`, текущая светлая
Mantine theme. h1 страницы; h2 итогов, сравнений и «Выплаты». Нет h3/h4 групп
валют или инструментов. Суммы доступны текстом, не только цветом.
```text
Пассивный доход
┌ Период ──────────────────────────────────────────┐
│ С По │
│ [ДД.ММ.ГГГГ] [ДД.ММ.ГГГГ] │
│ [Применить] [Сбросить] │
└──────────────────────────────────────────────────┘
Получено купонами и дивидендами
[Итого в валюте · Купоны · Дивиденды]
[Остальные валюты отдельными карточками]
Налоговые операции не включены в этот результат.
Сравнения
[Предыдущий период · значения и разницы]
[Годом ранее · значения] или текст недоступности
Выплаты
[Ledger Entry: описание, дата, identity, signed Money]
[Следующая Ledger Entry, отдельная карточка]
```
Форма — одна Paper с border, radius `md`, padding `md`, видимым «Период».
Подписанные DateInput «С» и «По» имеют placeholder `ДД.ММ.ГГГГ`, штатный
календарь по focus/click, `valueFormat="DD.MM.YYYY"`, strict raw validation и
`fixOnBlur=false`. Действия отдельной строкой: «Применить» filled, «Сбросить»
default. Под формой нет повторяющей даты подписи. Неприменённый черновик не
меняет аналитику; ограничения и московское правило описаны в Decision 1.
«Выплаты» — один `ul`, одна LedgerEntryRow-карточка на `li`, gap `sm`.
Не добавляются внешние instrument cards, currency headings, instrument totals,
subtotals/count, chevron, раскрытие или внутренние scroll-панели. Метаданные и
logo fallback принадлежат существующему LedgerEntryRow. Все строки сразу видимы;
денежные значения не обрезаются, при нехватке ширины переносятся.
| Состояние | Представление |
| --- | --- |
| Initial loading / новый период без cached data | Busy skeleton повторяет heading, форму, summary/comparisons и отдельные строки выплат; shapes без фокуса, shell сохраняется. |
| Loaded | Итоги, сравнения и плоский список всех выплат; валюты указаны в Money, summary/comparisons не смешивают их. |
| Изменён draft | Поля отражают ввод; суммы, сравнения и выплаты сохраняют последний применённый результат. |
| Invalid draft | Ошибка под соответствующим полем, фокус на первом ошибочном поле, исходный текст и URL сохраняются, запроса нет. |
| Apply | Click или Enter проверяет raw draft; только успех меняет URL/query, повторный submit во время обработки блокируется. |
| Reset | Немедленно восстанавливает обе даты текущего московского месяца, очищает ошибки и синхронизирует URL даже при изменённом только draft. |
| Empty | Фильтры, «Купоны и дивиденды не найдены» / «За выбранный период подходящих исполненных выплат нет.», налоговое пояснение, сравнения; «Выплаты» без фиктивных карточек. |
| Error без результата / повреждённый membership | StateView «Не удалось рассчитать пассивный доход», пояснение, «Повторить» текущий applied period; частичные данные скрыты. |
| Фоновое обновление того же периода | Cached snapshot и draft сохраняются; progress не блокирует; при ошибке предупреждение с retry. |
| Нет year comparison | «Сравнение с годом ранее недоступно.», без вымышленных нулей. |
| Отрицательные суммы / неполная metadata | Исходный знак, доступная identity и существующий logo fallback; запись не исключается. |
Во всех состояниях отсутствуют trend, buckets UI, forecast и его состояния.
Keyboard order: «С» → «По» → «Применить» → «Сбросить»; Escape закрывает календарь.
Штатные доступные controls Mantine и focus indicator сохранены. Ошибки связаны
с inputs; суммы и неинтерактивные строки не получают tabIndex. Все продуктовые
подписи принадлежат русскому i18n.
**Responsive behavior:**
| Viewport | Форма | Выплаты |
| --- | --- | --- |
| От `48em` | Два равных поля, отдельная строка кнопок по содержимому. | Одна колонка LedgerEntryRow; identity и сумма используют существующий layout. |
| `36em`–`48em` | Поля рядом, кнопки переносятся при необходимости. | Одна колонка, текст и суммы переносятся без обрезки. |
| Менее `36em` | Поля и кнопки вертикально на всю ширину, touch target не менее 44 px. | Существующий mobile row: сумма под identity. |
На 320 px и при увеличенном масштабе страница не требует горизонтального scroll;
popover остаётся в viewport. Высота определяется данными, прокрутка общая,
пагинация, виртуализация и ограничения количества строк не вводятся.
**Reused source and differences:** immutable visual reuse `N/A`.
DateInput истории, LedgerEntryRow, StateView, LoadingSkeleton и theme —
implementation references. Поля income обязательны и запрещают будущие даты;
список выплат плоский. Код страницы портфеля не копируется между slices.
## Delivery Map
| Area | Status | Architectural outcome or N/A reason |
| --- | --- | --- |
| frontend | `affected` | Calendar form adapter, плоский список карточек выплат, composition/states и относящиеся к ним проверки; удаление только income-owned trend/forecast consumer code. |
| backend | `N/A` | Текущий income-analytics уже возвращает totals/comparisons, отсортированные instruments и полные entries с instrumentKey. Endpoint, buckets, денежные расчёты, events и хранение не меняются. |
| specs | `affected` | Canonical planning и последующая синхронизация согласованных delta requirements в capability brokerage-account-income-analytics по lifecycle. |
| docs | `affected` | После реализации актуализируется as-built описание пассивного дохода: календарные фильтры, отдельные карточки выплат, отсутствие подписи периода и trend/forecast UI и использование прежнего analytics response. |
### Contract Gate Decision
| Gate | Status | Reason |
| --- | --- | --- |
| runtime-openapi | `N/A` | API contract не меняется ни на одной стороне; frontend потребляет прежний типизированный ответ и прекращает отдельный вызов events только со страницы дохода. Snapshot/types не требуют регенерации. |
## Risks / Trade-offs
- Открытый список увеличивает высоту и число одновременно отрисованных строк.
Это ожидаемая цена согласованного полного состава выплат. Используется один
проход группировки в VM и обычный document flow; лимиты, скрытие строк или
виртуализация не подменяют согласованное поведение.
- Raw input DateInput, permissive calendar parsing и timezone conversion могут
незаметно подменить период. Локальный form adapter и строгая проверка видимого
черновика защищают эту границу; одних minDate/maxDate недостаточно.
- Замена Accordion может потерять повторяющиеся выплаты или ошибочные membership
guards при упрощении VM. Сохраняются исходный порядок, occurrence identity и
существующий отказ от повреждённого результата.
- Удаление forecast UI должно удалить и его query subscription; shared events
остаётся нужен самостоятельной странице и не удаляется вместе с consumer.
- В текущем main Purpose осталось упоминание отдельного ближайшего прогноза.
Согласованная delta явно удаляет требование его показа на income page.
Это известная устаревающая формулировка Purpose, не разрешение вернуть UI:
native sync сохраняет existing Purpose и незатронутые requirements. В этом
change Purpose не переписывается; его последующая редактура требует отдельного
разрешённого изменения. Упоминания buckets в сохраняемых серверных инвариантах
совместимы с неизменным API и не требуют их визуализации.
## Migration Plan
Поставка выполняется без серверной или data migration. Сначала обновляется
согласованная frontend composition и её адаптация формы в рамках существующих
границ; после удаления consumers очищаются их локальные неиспользуемые части.
Сохраняемые сценарии периода и membership проходят проверку вместе с новой
компоновкой. Затем docs отражает фактически поставленный поток. Синхронизация
main specs и archive остаются последующими canonical completion mechanics.
Предварительный backend rollout, изменение environment и установка новых
зависимостей не нужны. API остаётся совместимым со старым и новым frontend.
### Rollback Contract
| Field | Decision |
| --- | --- |
| Status | `N/A` |
| Reason | Обратимое read-only изменение frontend UI; auth/security, public API, persistent data и денежные вычисления не меняются. Возврат прежней frontend версии восстанавливает прежнее отображение на том же backend без преобразования или удаления данных. |
@@ -0,0 +1,43 @@
## Why
Страница «Пассивный доход» перегружена ненужными пользователю разделами
«Динамика» и «Ближайший прогноз». Выбор периода отличается от истории событий
портфеля, а просмотр выплат требует раскрывать аккордеоны и проходить лишние уровни группировки.
Нужно упростить страницу согласно запросу владельца продукта.
## What Changes
- Переработать верхний блок фильтров: выбирать начальную и конечную даты
календарным контролом с тем же взаимодействием и форматом `ДД.ММ.ГГГГ`,
что в истории событий портфеля. Сохранить явное применение, сброс периода,
проверку допустимости дат и восстановление применённого периода из URL.
Не дублировать даты отдельной подписью под фильтрами.
- Убрать со страницы блок «Динамика».
- Заменить «По инструментам» разделом «Выплаты»: одна сразу видимая карточка
на каждую выплату, без внешних блоков инструментов, их итогов и количества
выплат и без отдельных заголовков валют.
- Убрать со страницы блок «Ближайший прогноз».
Ожидаемый результат: пользователь выбирает период привычным способом и сразу
видит итоги, сравнения и все выплаты, образующие полученный доход.
Non-goals: изменение расчётов дохода, правил отбора выплат, разделения валют,
сравнений периодов, обзора счёта или самостоятельной страницы будущих событий.
## Capabilities
### New Capabilities
Нет.
### Modified Capabilities
- `specs/brokerage-account-income-analytics/spec.md`: унифицировать выбор
периода, убрать требования показывать динамику и ближайший прогноз на
странице дохода, представить каждую выплату отдельной карточкой без группировочных блоков.
## Impact
Изменение затрагивает пользователей страницы дохода выбранного брокерского
счёта, её интерфейс, нормативное описание поведения и пользовательскую
документацию. Источники данных и денежные расчёты сохраняются.
@@ -0,0 +1,130 @@
## REMOVED Requirements
### Requirement: Analytics summary and trend remain readable per currency
**Reason**: По согласованному Proposal блок «Динамика» удаляется вместе со сценариями дневной, недельной и месячной группировки и переключения валюты графика. Сохранение этих сценариев требовало бы показывать удаляемый блок.
**Migration**: Требования к читаемым итогам, разделению валют и объяснению налоговой границы перенесены в `Analytics summary remains readable per currency`. Удаление визуализации не меняет денежные расчёты.
### Requirement: Forecast is separate from realized income
**Reason**: По согласованному Proposal со страницы удаляется весь блок «Ближайший прогноз» вместе со сценариями отображения его сумм, событий и локальной ошибки.
**Migration**: `Income page excludes upcoming forecasts` сохраняет отделение будущих событий от полученного дохода, независимость аналитики от доступности прогноза и доступность самостоятельной страницы «Будущие события».
### Requirement: Instrument breakdown is traceable to exact Ledger Entries
**Reason**: По уточнению владельца удаляются внешние карточки Instrument с subtotals, combined total и receipt count, а также валютные группы. Прежние scenarios группировочного breakdown больше не описывают требуемый UI.
**Migration**: `Income payments remain traceable to exact Ledger Entries` заменяет grouping UI плоским разделом «Выплаты», сохраняя точный состав entries, signed Money, повторяющиеся IDs и внутренние membership guards.
## 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
## ADDED Requirements
### 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 включать записи вне аналитического итога. Отказ от группировочного UI MUST NOT отменять проверки принадлежности entries к instrumentKey, валюты и receipt count ответа или приводить к пересчёту денежных итогов на клиенте.
#### Scenario: Каждая выплата показана отдельной карточкой
- **WHEN** выбранный период содержит несколько включённых Ledger Entries одного или разных Instrument
- **THEN** раздел «Выплаты» сразу показывает отдельную карточку каждой записи в исходном порядке без внешних группировочных блоков, раскрытия, итогов инструмента и receipt count на UI
#### Scenario: Список воспроизводит точный состав дохода
- **WHEN** User просматривает загруженный раздел «Выплаты»
- **THEN** он видит все и только включённые coupon и dividend Ledger Entries применённого периода с датой, описанием, доступной identity и исходной суммой
#### Scenario: Отрицательная корректировка и повторяющиеся IDs сохраняются
- **WHEN** доход содержит отрицательную корректировку либо несколько включённых 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** страница показывает итоги и существующие сравнения, но не показывает блок «Динамика», график, его переключатель валюты или текстовый список временных buckets
### 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 сохраняться.
#### 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,32 @@
## 1. Frontend behavior and presentation
- [x] 1.1 [frontend] Перевести форму периода income page на управляемые Mantine DateInput, сохранив отдельные draft и applied значения, строгую проверку видимого ввода, московскую границу сегодняшнего дня, явное применение, немедленный сброс и восстановление из URL.
- **Covers:** `brokerage-account-income-analytics` — `User explicitly applies an analytics period represented in the URL`
- **Verification:** Page UI tests подтверждают формат `ДД.ММ.ГГГГ`, календарный и клавиатурный ввод, отсутствие запроса и сохранение результата/URL при invalid draft, применение допустимого периода, reset при изменённом draft и восстановление applied period из URL.
- [x] 1.2 [frontend] Заменить Accordion breakdown плоским разделом «Выплаты» с отдельной LedgerEntryRow-карточкой каждой entry, без внешних instrument cards, их итогов/count и заголовков валют; сохранить исходный порядок entries, opaque membership guards, signed payments и устойчивые keys повторяющихся entries.
- **Covers:** `brokerage-account-income-analytics` — `Instrument breakdown is traceable to exact Ledger Entries`; `brokerage-account-income-analytics` — `Income payments remain traceable to exact Ledger Entries`
- **Verification:** Page/component tests подтверждают одну карточку на entry в исходном порядке, отсутствие instrument/currency groups и instrument totals/count; несколько валют, отрицательные корректировки, повторяющиеся IDs и повреждённый membership сохраняют согласованные outcomes без disclosure action.
- [x] 1.3 [frontend] Удалить из income page trend и forecast composition, derived models, events query subscription и ставшие неиспользуемыми локальные views/styles/i18n, сохранив текстовые totals, currency separation, comparisons и независимую страницу Future Cash Events.
- **Covers:** `brokerage-account-income-analytics` — `Analytics summary and trend remain readable per currency`; `brokerage-account-income-analytics` — `Forecast is separate from realized income`; `brokerage-account-income-analytics` — `Analytics summary remains readable per currency`; `brokerage-account-income-analytics` — `Income page excludes upcoming forecasts`
- **Verification:** Page/route tests подтверждают текстовые итоги и сравнения без «Динамики» и «Ближайшего прогноза», отсутствие запроса events со страницы income, устойчивость realized analytics при недоступности events API и прежнюю доступность самостоятельной страницы «Будущие события».
- [x] 1.4 [frontend] Привести loaded, loading, empty, error и cached-snapshot presentation к inline UI/UX package revision `simplify-income-page-ui-r2`, включая отсутствие отдельной подписи периода, семантическую иерархию, keyboard focus, field errors и responsive layout фильтров и отдельных карточек выплат.
- **Covers:** `brokerage-account-income-analytics` — `User explicitly applies an analytics period represented in the URL`; `brokerage-account-income-analytics` — `Instrument breakdown is traceable to exact Ledger Entries`; `brokerage-account-income-analytics` — `Income payments remain traceable to exact Ledger Entries`; `brokerage-account-income-analytics` — `Analytics summary remains readable per currency`; `brokerage-account-income-analytics` — `Income page excludes upcoming forecasts`
- **Verification:** UI tests проверяют доступные labels/errors, порядок действий и state selection; scoped review на ширинах 320 px, менее `36em`, `36em`–`48em` и от `48em` подтверждает отсутствие горизонтального scroll, перенос длинных названий и сумм, 44 px touch targets и соответствие package revision.
## 2. Affected repository delivery
- [x] 2.1 [frontend] Выполнить canonical frontend gate после завершения income page delivery.
- **Covers:** `N/A` — non-mutating gate проверяет frontend implementation уже покрытых Requirements и UI/UX package, не создавая самостоятельного observable behavior.
- **Verification:** Под Node 22 выполнить `npm run verify` в `frontend`.
- [x] 2.2 [docs] Обновить as-built описание пассивного дохода: календарные draft/applied фильтры, отдельная карточка каждой выплаты без instrument/currency groups и подписи периода, сохранённые totals/comparisons и отсутствие trend/forecast UI при неизменном income-analytics API.
- **Covers:** `brokerage-account-income-analytics` — `Analytics summary and trend remain readable per currency`; `brokerage-account-income-analytics` — `Forecast is separate from realized income`; `brokerage-account-income-analytics` — `User explicitly applies an analytics period represented in the URL`; `brokerage-account-income-analytics` — `Instrument breakdown is traceable to exact Ledger Entries`; `brokerage-account-income-analytics` — `Income payments remain traceable to exact Ledger Entries`; `brokerage-account-income-analytics` — `Analytics summary remains readable per currency`; `brokerage-account-income-analytics` — `Income page excludes upcoming forecasts`
- **Verification:** `docs/features/brokerage-account-income-analytics.md` описывает только фактически поставленный flow, сохраняет корректные локальные ссылки и не копирует Proposal, Delta Specs, Design или Tasks.
- [x] 2.3 [docs] Выполнить canonical docs gate после as-built update.
- **Covers:** `N/A` — non-mutating gate проверяет affected docs delivery и ссылочную целостность, не добавляя product behavior.
- **Verification:** Под Node 22 выполнить `npm run verify` в `docs`.
- [x] 2.4 [specs] Сверить завершённые frontend и docs изменения и отмеченные Tasks со всеми operation sections canonical delta, не изменяя approved semantics и сохраняя backend и runtime-openapi как `N/A`.
- **Covers:** `brokerage-account-income-analytics` — `Analytics summary and trend remain readable per currency`; `brokerage-account-income-analytics` — `Forecast is separate from realized income`; `brokerage-account-income-analytics` — `User explicitly applies an analytics period represented in the URL`; `brokerage-account-income-analytics` — `Instrument breakdown is traceable to exact Ledger Entries`; `brokerage-account-income-analytics` — `Income payments remain traceable to exact Ledger Entries`; `brokerage-account-income-analytics` — `Analytics summary remains readable per currency`; `brokerage-account-income-analytics` — `Income page excludes upcoming forecasts`
- **Verification:** Requirement-by-requirement comparison с Proposal, Delta Specs, Design package revision `simplify-income-page-ui-r2` и delivery не оставляет unchecked implementation scope или поведения вне approved artifacts; найденный semantic mismatch маршрутизируется в ответственную planning boundary.
- [x] 2.5 [specs] Выполнить canonical native strict validation change и main specs после обновления task progress.
- **Covers:** `N/A` — structural validation является обязательным gate affected specs repository и не представляет новое пользовательское поведение.
- **Verification:** Под Node 22 выполнить `openspec validate simplify-income-page --type change --strict --store wealth-ledger-specs` и `openspec validate --specs --strict --store wealth-ledger-specs` из `specs`.
@@ -0,0 +1,2 @@
schema: wealth-ledger-sdd
created: 2026-09-12
@@ -0,0 +1,40 @@
# Completion Evidence: add-manual-bank-cards
## Finalization Authority
Change Owner authorized Finalization, canonical sync and archive after a current
independent read-only Validation PASS. The PASS found no Critical or Important
findings after the monthly-summary presentation correction.
## Delivery
| Area | Result | Delivery commit |
| --- | --- | --- |
| frontend | Manual card screens and summaries; Finalization correction shows exact expenses, refunds and net totals with a regression test. | `ff67a76`, `d3ac8c2`, `734c5bd` |
| backend | Bank-card persistence, calculations, API and isolated verification. | `e6f58ef` |
| docs | Cards overview states the three displayed summary totals. | `3a27268` |
| specs | Approved legacy Proposal, Delta Specs, Design, completed Tasks and Finalization evidence; native sync creates canonical behavior spec. | `834f2d1` plus finalization/archive commit |
## Observed Verification
| Area | Command or review | Result |
| --- | --- | --- |
| frontend | `npm run test:run -- src/entities/bank-card/ui/BankSummary/BankSummary.view.test.tsx` | PASS: 1 file, 1 test |
| frontend | `npm run typecheck` | PASS |
| docs | `npm run verify` | PASS |
| specs | `openspec schema validate wealth-ledger-sdd --verbose` | PASS |
| specs | `openspec schema validate wealth-ledger-sdd-v2 --verbose` | PASS |
| specs | `openspec validate add-manual-bank-cards --type change --strict --store wealth-ledger-specs` | PASS |
| specs | `openspec validate --specs --strict --store wealth-ledger-specs` | PASS: 25 specs, 0 failed |
| all affected areas | `git diff --check` | PASS |
## Independent Validation
PASS. The review compared Proposal, all 17 Delta Requirements, Design and
completed Tasks with current backend calculations/DTOs, shared frontend summary
rendering and cards documentation. It verified that `/cards` and `/cards/:id`
share the corrected summary view and found no Critical or Important findings.
Known non-blocking limitation: a plain Prisma fresh migration deploy still needs
`RUST_LOG=info`; the verified migration path and its evidence remain recorded in
the change Tasks.
@@ -0,0 +1,445 @@
## Context
**Revision:** 1. **Status:** согласован пользователем для подготовки Tasks;
реализация не начата.
Основание — [Proposal](proposal.md) и [Delta Specs](specs/manual-bank-cards/spec.md)
с ответами пользователя Q1–Q9. Здесь определён способ реализации; проверяемое
поведение и денежные правила принадлежат Delta Specs.
WealthLedger уже использует React/Mantine, TanStack Query, NestJS,
Prisma и локальную SQLite. Брокерские сведения приходят из T-Bank и временно
кэшируются; пользовательские банковские операции станут долговременными
данными. Их источник и правила доступа отличаются от брокерского журнала.
Подход опирается на существующие границы: [контейнеры](../../../../docs/architecture/containers.md),
[Money](../../../../docs/glossary.md#money), [backend data](../../../../backend/.ai/conventions/backend-data.md)
и [frontend data](../../../../frontend/.ai/conventions/frontend-data.md).
Ни внешний банк, ни новый сервис для ручного учёта не требуются.
## Goals / Non-Goals
**Goals:** один владелец ручного учёта, точные денежные расчёты, согласованные
переводы и возвраты, безопасные исправления и повторы, сохранение истории
между запусками, два экрана в существующей визуальной системе.
**Non-Goals:** новый стек хранения, универсальная бухгалтерская платформа,
двойная запись, event sourcing, серверный кэш банковского учёта, отдельный
изменяемый баланс или конвертация валют. Продуктовые границы перечислены
в Proposal; этот Design их не расширяет.
## Decisions
### 1. Отдельная область ручного банковского учёта
Backend получает один `BankCardsModule` с controller, сервисом сценариев,
DTO и чистыми функциями расчёта. Сервис использует существующий `PrismaService`.
Controller отвечает за транспорт, сервис — за владение данными, связи
операций, проверки и транзакции. Generic repository, интерфейс для одной
реализации и отдельные сервисы на каждый арифметический шаг не нужны.
`JwtAuthGuard` и `request.user.sub` остаются источником локальной идентичности.
Каждая карта, категория и операция выбирается с ограничением по владельцу;
все дополнительные ссылки проверяются в том же сценарии. Неизменяемый
владелец связанных записей не определяется из входного `userId`.
`TbankAccessService` не используется: он устанавливает доступ через подключение
и счета брокера. Ручные карты доступны без T-Bank. Замена или удаление
брокерского подключения не удаляет и не инвалидирует ручную историю;
logout по-прежнему очищает данные сессии в браузере.
### 2. Три сохраняемые модели
| Модель | Данные и ответственность |
| --- | --- |
| `BankCard` | `id`, `userId`, имя, необязательные банк и последние четыре цифры, начальный остаток в копейках, дата начала учёта, `version`, `archivedAt`, неизменяемый fingerprint создания, служебные даты |
| `ExpenseCategory` | `id`, `userId`, название, нормализованный ключ уникальности названия, необязательный неизменяемый `seedKey`, `version` |
| `BankCardOperation` | `id`, `userId`, вид, `cardId`, необязательные `targetCardId`, `categoryId`, `originalExpenseId`, положительная сумма в копейках, дата операции, описание, `version`, `deletedAt`, неизменяемый fingerprint создания, служебные даты |
Новые таблицы ссылаются на существующий `User`. Foreign keys сохраняют
ссылочную целостность; ограничения уникальности защищают идентификаторы
и категории при гонках. Совпадение владельцев, допустимые сочетания полей
и финансовые зависимости проверяются сервисом внутри транзакции.
Удаление карты или категории каскадом не применяется.
При первой карте её создание и начальный набор пользовательских категорий
сохраняются согласованно. `seedKey` позволяет повторно использовать начальные
категории после переименования и удерживает роль категории по умолчанию.
Если первая запись категории создаётся раньше карты, тот же сценарий сначала
обеспечивает начальный набор. Чтение не создаёт данных, повторная инициализация
не возвращает старые названия и не добавляет дубли.
Перевод — одна `BankCardOperation`: `cardId` задаёт источник, `targetCardId`
получателя. Две строки истории являются представлениями одной записи
относительно выбранной карты. Изменение или удаление этой записи затрагивает
оба эффекта; отдельного сохраняемого состояния «половина перевода» нет.
Возврат ссылается на расход через `originalExpenseId`. Его карта проверяется
по исходному расходу, а категория получается через эту связь. Собственная
копия категории в возврате не хранится, поэтому исправление категории покупки
не оставляет возвраты в прежней группе. Вид операции и ссылка возврата
на исходную покупку после создания неизменны.
### 3. Копейки в хранилище, Money на внешней границе
Начальный остаток и суммы операций сохраняются как `BigInt` целых копеек.
Входные денежные значения проверяются по диапазону и точности Delta Specs
до преобразования и записи. Расчёты используют `bigint`; результат не проходит
через `Number`, `parseFloat` или SQLite `SUM` с ограничением 64-битного итога.
Вычисленный баланс может выходить за диапазон отдельного сохраняемого значения.
Публичный контракт остаётся Money `{ currency, value }`, где `value` — точная
decimal string. Ограничение двух знаков относится к новому вводу RUB,
а не к существующим брокерским Money. Frontend использует существующий
[formatMoney](../../../../frontend/src/shared/lib/display.ts) и не складывает суммы.
Уже есть [decimal helpers](../../../../backend/src/tbank/decimal.ts) и
`MoneyDto` в [portfolio.dto.ts](../../../../backend/src/tbank/portfolio.dto.ts).
Общие parsing/formatting и денежный DTO получают общий owner вне `tbank`;
protobuf-преобразование остаётся у интеграции. Существующие потребители
продолжают использовать ту же точную арифметику и тот же публичный Money-контракт.
Новая денежная библиотека и две копии парсера не нужны.
### 4. Баланс и сводка вычисляются из первичных записей
| Вид | Эффект на баланс | Эффект на месячную сводку |
| --- | --- | --- |
| Расход | Минус сумма у `cardId` | Расход по категории и дате покупки |
| Пополнение | Плюс сумма у `cardId` | Не включается |
| Перевод | Минус у источника, плюс у получателя | Не включается у обеих сторон |
| Возврат | Плюс сумма у карты исходного расхода | Возврат по категории покупки и собственной дате |
Backend выбирает минимальные необходимые поля и выполняет точный fold.
Баланс использует начальный остаток и всю неудалённую историю карты; месячная
сводка использует относящиеся к месяцу расходы и возвраты. Начальный остаток
не создаёт синтетическое поступление. Архивная карта остаётся в исторической
сводке «Все карты», чтобы архивирование не переписывало месячные итоги.
Внутри одного ответа связанные первичные данные читаются согласованно.
Если требуется несколько зависимых чтений, они используют одну read-транзакцию;
независимые запросы интерфейса не получают общей транзакции ради внешнего вида.
Frontend после подтверждённых изменений перечитывает связанные представления.
Цена подхода — O(N) просмотр нужной ручной истории при расчёте. Для первой
версии это проще второго сохраняемого баланса, его восстановления и
инвалидации. В реализации у fold будет комментарий `ponytail:` с этим
пределом и переходом к измеренной оптимизации при росте истории. Пагинация
журнала не используется для ограничения финансового расчёта.
Даты ручного учёта сохраняются как проверенные календарные `YYYY-MM-DD`,
а не как фиктивный момент полуночи. Сегодня и текущий месяц определяются
в Europe/Moscow; выбранный месяц — явный параметр. Это сохраняет календарный
смысл введённого дня независимо от часового пояса браузера.
### 5. Транзакции защищают связанные изменения
Проверка владельцев и активности всех старых и новых ссылок, проверка дат,
финансовых зависимостей и запись выполняются в одной DB-транзакции.
Изменение начальной даты карты проверяет всю зависимую историю, включая
переводы, в которых карта является получателем. Сетевых запросов к банкам
внутри транзакции нет.
При создании, изменении или удалении возврата исходный расход участвует
в условном обновлении версии внутри той же транзакции. Проверка оставшейся
доступной к возврату суммы выполняется после получения права на изменение.
При редактировании сумма остальных неудалённых возвратов складывается
с новой суммой текущего: редактируемая запись не учитывается дважды.
Так два возврата и одновременное уменьшение исходного расхода не обходят
общий лимит, а изменение описания полного возврата остаётся допустимым.
Неудачная проверка откатывает всё действие.
Операции, редактирование метаданных и архивирование согласуются средствами
SQLite-транзакций. Конфликт занятой базы или сериализации не превращается
в частичный успех. Для кратковременного DB-конфликта допускается ограниченный
повтор всей транзакции с тем же идентификатором намерения; пользовательский
конфликт версии не повторяется скрыто. In-memory mutex и глобальный
transaction manager не вводятся.
Редактирование и удаление используют `version`: условное обновление по
идентификатору, владельцу и ожидаемой версии. Устаревшая форма получает
конфликт; frontend сохраняет ввод и выполняет повторный GET для получения
актуальной записи, а не ожидает финансовый DTO внутри ответа об ошибке
и не затирает изменение из другой вкладки. Начальный остаток и дата начала также защищены
версией карты. Принцип уже используется в
[AuthService](../../../../backend/src/auth/auth.service.ts) для conditional update;
финансовые правила остаются у нового владельца.
### 6. Повтор создания не создаёт вторую карту или операцию
При открытии формы новой карты или операции клиент создаёт UUID будущей записи
и сохраняет его до определённого результата. Повтор после сетевой ошибки
и общий auth-retry используют тот же UUID. Это идентификатор пользовательского
действия, а не транспортный `X-Request-ID`.
Запись хранит неизменяемый fingerprint нормализованного исходного ввода,
рассчитанный стандартным `node:crypto`. Уникальный идентификатор защищён
ограничением базы. Повтор с тем же владельцем, UUID и fingerprint не применяет
создание второй раз; другой ввод с тем же UUID даёт конфликт.
После проверки сессии и формы входа уже сохранённое намерение разрешается
до проверки изменяемых условий нового создания: архива, текущей даты
и доступного лимита возврата. Полный возврат с потерянным ответом не становится
новым возвратом с нулевым лимитом, а поздний повтор создания архивированной
карты возвращает её актуальное состояние.
При коллизии создания сервис перечитывает результат и проверяет владельца
и fingerprint, не полагаясь только на предварительный lookup.
Редактирование не меняет fingerprint создания. Удаление оставляет tombstone
с идентификатором и fingerprint: запоздалый повтор не воскрешает запись.
Для уже изменённой записи возвращается её актуальное состояние; для удалённой
— понятный безопасный отказ без нового сохранения. После определённого
отказа пользователь может сознательно начать новое действие с новым UUID;
автоматическая смена UUID при неопределённом исходе запрещена.
В обеих моделях fingerprint остаётся неизменным; общего журнала команд
или отдельной таблицы idempotency не требуется.
Удаление операций логическое (`deletedAt`), не полное версионирование или бухгалтерский
журнал аудита. Повтор удаления своей уже удалённой операции безопасен.
Восстановление удалённых операций отдельным пользовательским сценарием
в первую версию не добавляется. Архив карты обратим; для финансовых
исправлений архивную карту сначала возвращают в активный список.
### 7. HTTP и frontend сохраняют существующие границы
Новый API размещается под `/api/v1/bank-cards`. Внутри этой области находятся
ресурсы карт, категорий, операций и месячной сводки. Одно представление
перевода изменяется по идентификатору операции независимо от того, из какой
карты открыта форма. Для выбора исходного расхода возврата доступно чтение
расходов карты за всю историю, без ограничения месяцем открытого журнала.
Runtime DTO валидируют форму, сервис — связанные ограничения. Чужие
и отсутствующие ресурсы не раскрывают финансовых сведений. Ошибки используют
существующий filter и стабильные коды для невалидного ввода, отсутствия
ресурса, архива, конфликта версии, повторного намерения и зависимых возвратов.
Конкретная исполняемая форма ответа принадлежит backend OpenAPI, а не
дублирующей таблице DTO в спецификациях.
Frontend получает типы из нового runtime OpenAPI через существующий
[contract workflow](../../../../docs/development/runtime-openapi-verification.md).
Используются `apiClient`/`apiQueryClient`, session-aware queries и общий auth
middleware. Свой Bearer header, refresh loop или вторую систему запросов
новая область не вводит.
После успешного изменения данных инвалидируются банковские GET-запросы:
список, детали, категории, страницы истории, выбор исходного расхода и
все месячные сводки. Такой scope покрывает обе стороны перевода, изменение
даты между месяцами и перегруппировку связанных возвратов. Брокерские кэши
не затрагиваются. Оптимистический пересчёт денежных сумм не используется;
ошибка mutation сохраняет прежние данные и введённую форму.
Композиции принадлежат `pages/cards` и `pages/card`; каждая финансовая страница
имеет container, VM и presentation views. Общие для обеих страниц banking
queries и представления принадлежат `entities/bank-card`. Только реально
повторяющиеся формы получают общего владельца; универсальный ledger,
новый слой widgets и отдельный feature на каждое поле не нужны.
## UI/UX Applicability and Package
**Applicability and scope:** `new/changed` — новый раздел «Карты», список,
страница карты, формы карты/операций/категорий и месячная сводка.
**Package revision:** 1. Пакет полностью находится в этом Markdown: схемы
композиции, состояния, действия и адаптивность. Отдельные PNG/SVG и внешние
макеты для согласования этой версии не требуются; self-hash не используется.
**Screens, states and interactions:**
| Экран | Состав и основные действия |
| --- | --- |
| `/cards?month=YYYY-MM` | Заголовок «Карты», «Добавить карту», список активных карт, переключатель показа архива, месячная сводка всех карт, «Категории» после появления первой карты |
| `/cards/:cardId?month=YYYY-MM` | Крошки «Карты / Название», баланс, «Добавить операцию», изменение сведений/архив, сводка и журнал выбранного месяца |
| Архивная карта | Явная отметка архива, доступные баланс/история/сводки, действие «Вернуть из архива»; финансовый ввод и исправления недоступны |
```text
Карты [Добавить карту]
[Повседневная 24 800 ₽] [Запасная 10 000 ₽]
Баланс по внесённым операциям [Показать архив]
Расходы по всем картам [Сентябрь 2026]
Потрачено Возвращено После возвратов
8 500 ₽ 1 000 ₽ 7 500 ₽
Продукты … ₽ Кафе … ₽ Такси … ₽
[Категории]
Карты / Повседневная
Повседневная 24 800 ₽ [Добавить операцию]
Баланс по внесённым операциям [Изменить карту]
Расходы за месяц [Сентябрь 2026]
Потрачено / Возвращено / После возвратов
Категории и суммы
Операции
12.09 Расход · Кафе −500 ₽
11.09 Перевод на «Запасная» −2 000 ₽
10.09 Возврат · Покупки +1 000 ₽
[Назад] [Далее]
```
Главная иерархия карты — название, баланс, действие, журнал. Начальные
декоративные изображения банков не загружаются: название и необязательные
последние цифры различают карты без внешнего каталога. Связанные ссылки
перевода объясняют направление; одна карта не имитирует брокерский счёт.
Сводка показывает расходы, возвраты и разницу отдельно. Отрицательный итог
месяца или категории отображается со знаком и поясняется возвратами;
процентный круговой график для таких величин не используется. Баланс
остаётся текущим при смене месяца. Журнал упорядочен по дате операции,
затем времени создания и идентификатору; переходы не обещают неизменяемый
снимок истории между конкурентными изменениями.
| Форма | Ввод и взаимодействие |
| --- | --- |
| Карта | Название, необязательные банк и последние четыре цифры, начальный остаток, дата начала. Пояснение: «Остаток до операций с выбранной даты». Валюта RUB фиксирована |
| Расход | Сумма, дата, предвыбранная категория по умолчанию (изначально «Другое»), необязательное описание |
| Пополнение | Сумма, дата, необязательное описание; без категории расходов |
| Перевод | Карта-источник и другая активная карта-получатель, сумма, дата, описание; одно подтверждение для обеих сторон |
| Возврат | Исходный расход за любой период, дата, сумма не выше доступной к возврату, описание; карта и категория берутся из покупки. При редактировании лимит равен сумме покупки минус остальные неудалённые возвраты |
| Категории | Начальные и свои категории одним списком; добавление и переименование, без вложенности и удаления |
Выбор возврата не ограничивается текущим месяцем: он показывает дату,
описание/категорию покупки, сумму и оставшуюся доступную сумму. Добавление
категории из формы расхода сохраняет уже введённые поля. Изменение операции
сохраняет её вид; ограничения связанных возвратов объясняются у действия.
Перед удалением операции подтверждение показывает сумму и последствия,
для перевода — обе карты. При конфликте версии или зависимых возвратах
форма сохраняет ввод и предлагает перечитать затронутую запись. Архивирование
не удаляет историю и имеет явное обратное действие.
**States and recovery:**
| Состояние | Представление и восстановление |
| --- | --- |
| Initial loading | Структурный skeleton соответствующих областей, доступное состояние занятости, без фиктивных цифр и фокуса на декорациях |
| Нет карт | «Добавьте первую карту» и действие добавления |
| Пустой месяц | Нет операций/расходов за месяц, известные нулевые суммы, сохранённый текущий баланс |
| Ошибка без данных | Понятное сообщение и повторная загрузка, без подмены данных нулями |
| Фоновое обновление или его ошибка | Последние успешные данные остаются; рядом progress или предупреждение и retry |
| Локальная ошибка сводки | Доступная карта и журнал остаются видимыми |
| Invalid / pending формы | Ошибки полей без запроса; при отправке действия формы блокируются, включая закрытие |
| Ошибка сохранения | Alert, сохранённый ввод и безопасный повтор того же действия; форма закрывается только после определённого успеха |
| Чужая/отсутствующая карта | Нейтральное «Карта не найдена» и переход к списку без раскрытия чужих данных |
Формы используют Mantine Modal, React Hook Form, Zod и FormProvider.
Модальное окно удерживает фокус и возвращает его вызвавшему действию.
Поля имеют видимые подписи, кнопки — доступные названия, суммы — знак
и текст вида операции независимо от цвета. Продуктовые строки принадлежат
существующему русскому i18n-словарю.
**Responsive behavior:** на широком экране карты размещаются в две колонки,
на узком — в одну; заголовки и действия переносятся. Формы становятся
одноколоночными и помещаются по ширине экрана. Длинные названия и суммы
не скрывают действия; журнал и формы не перекрываются нижней навигацией.
Пятый пункт навигации «Карты» проверяется на ширине 320–375 px. Фокус,
клавиатурное управление и reduced motion сохраняются.
**Reused source and differences:** это новый пакет на текущей общей теме,
а не повторное использование архивного UI-пакета. Поэтому immutable locator
чужого change не требуется. Переиспользуются реальные элементы реализации:
- [AppShell](../../../../frontend/src/shared/ui/AppShell.tsx) — один пункт
«Карты» после «Счета» для desktop и mobile; текущие разделы сохраняются.
- [ProtectedLayout](../../../../frontend/src/app/routes/ProtectedLayout.tsx)
и [AppRoutes](../../../../frontend/src/app/routes/AppRoutes.tsx) — локальная
сессия и два lazy-маршрута; крошки и заголовок документа расширяются у
существующих владельцев.
- [EntryCard](../../../../frontend/src/shared/ui/EntryCard.tsx) — представление
строки операции с banking mapping. `LedgerEntryRow` и брокерские DTO
напрямую не переиспользуются.
- [AccountOperationsPage VM](../../../../frontend/src/pages/account-operations/ui/AccountOperationsPage.vm.ts)
и [форма периода дохода](../../../../frontend/src/pages/account-income/ui/components/IncomePeriodForm.view.tsx)
— примеры организации URL/форм, без cross-page imports.
- [Дизайн-система](../../../../docs/development/design-system.md) — текущие
светлая тема, семантические цвета и размеры. Новые шрифты, палитра и
глобальная переработка оформления не вводятся.
## Delivery Map
| Area | Status | Architectural outcome or N/A reason |
| --- | --- | --- |
| frontend | `affected` | Раздел карт, операции/категории/сводки, формы и состояния, generated client, локальная навигация и query invalidation |
| backend | `affected` | Владелец ручного учёта, хранение, транзакции, точные расчёты, API и общие Money-компоненты без изменения брокерской семантики |
| specs | `affected` | Новый контракт `manual-bank-cards` и SDD-артефакты этого change; текущие инвестиционные requirements сохраняются |
| docs | `affected` | После поставки: область банковских карт, глоссарий, обзор продукта/навигация, расширение описания хранимых данных и восстановление |
### Contract Gate Decision
| Gate | Status | Reason |
| --- | --- | --- |
| runtime-openapi | `required` | Новый backend API потребляется frontend; runtime document, snapshot и generated types должны совпадать |
## Solution Assessment
Оценка обязательна из-за новых долговременных финансовых данных, транзакций
и миграции. Выбирается существующая инфраструктура, с небольшой доменной
реализацией внутри приложения.
| Вариант | Fit, зрелость и безопасность | Эксплуатация, ограничения и миграция | Решение |
| --- | --- | --- | --- |
| Установленные Prisma + SQLite | Соответствуют локальному приложению; существующий DB provider, FK, unique constraints и транзакции | Без отдельного сервера; добавление таблиц сохраняет действующие данные; остаётся известная проблема migration engine | Использовать |
| Другой сервер БД, например PostgreSQL | Зрелые средства конкурентной записи; текущая нагрузка не требует смены SQLite | Новый сервис, конфигурация, резервирование и миграция всей базы | Не добавлять в эту итерацию |
| Browser storage | Нативное средство, но не соответствует сохранению серверных пользовательских данных и доступу через существующую сессию | Появляется второй источник истины и отдельная миграция из браузера | Не использовать для финансовой истории |
| Prisma Decimal поверх SQLite | Удобное имя типа не доказывает точный путь хранения | В установленном adapter наблюдалось преобразование Decimal через `parseFloat`; BigInt сохраняет целые значения | Хранить копейки в BigInt; проверить реальный roundtrip |
| Изменяемый cached balance или event-sourced ledger | Возможны при иных объёмах и требованиях аудита; сейчас добавляют второе состояние или инфраструктуру событий | Дополнительные восстановление, проекции, invalidation и миграционные обязательства | Вычислять из трёх простых моделей |
Наблюдение adapter относится к установленной зависимости, зафиксированной
в [backend package-lock](../../../../backend/package-lock.json), и её подключению
в [PrismaService](../../../../backend/src/prisma/prisma.service.ts).
Это не заменяет будущую проверку сохранения значений больше `2^53` через
реальную БД и не требует изменения версии зависимости на этапе Design.
## Risks / Trade-offs
- Пропущенные ручные движения приводят к расхождению с банком. Отрицательный
учётный баланс допустим; приложение не проверяет банковский доступный лимит.
- Возврат прошлой покупки может дать отрицательный месячный итог. UI показывает
расходы и возвраты отдельно, а не скрывает это округлением или обнулением.
- Редактирование истории меняет прошлые сводки. Здесь нет неизменяемого
бухгалтерского аудита; soft deletion предотвращает повторное применение
удалённой записи, но не является полной историей всех её редакций.
- Точный fold имеет O(N) стоимость. Нужную оптимизацию выбирают после измерения
фактической истории, сохраняя одного владельца расчёта.
- В [текущих ограничениях проверки](../../../../docs/development/verification.md#известное-ограничение-сквозной-проверки)
зафиксирован `Schema engine error` Prisma 7.9.1/Node 22 на новой SQLite.
В этой фазе он не воспроизводился и не признан устранённым. Финансовую
поставку нельзя признать проверенной без реальной DB/migration границы.
- Существующая ValidationPipe не доказывает обещанные структурированные
field errors — это отмечено в [HTTP API](../../../../docs/architecture/http-api.md#error-contract).
Формы имеют локальную валидацию и безопасный общий ответ об ошибке;
исполняемое поведение нового API проверяется на настроенном HTTP-приложении.
## Migration Plan
Сначала подтверждаются сохранность действующей базы и применимость известных
ограничений миграций на изолированной копии. Backup включает согласованный
снимок SQLite; ключ T-Bank сохраняется отдельно и не выводится в отчёты.
Работающая база и её WAL не копируются несогласованными частями: используется
корректный SQLite backup либо остановленное приложение.
Изменение схемы additive: три таблицы, индексы и связи с `User`.
Существующие строки User, Session и TbankConnection не переписываются.
Применённые migrations не редактируются; schema reset и обход migration
history не используются. Категории создаются для владельца при первом вводе,
без массового изменения всех существующих профилей.
Новый backend и его публичный контракт готовятся до включения нового
frontend-потребителя. Перенос общих Money-компонентов сохраняет действующий
контракт брокерских сценариев. Документация описывает поставленный результат
после проверки, а не объявляет будущие возможности готовыми.
Если migration engine мешает проверить additive-переход, сначала решается
это подтверждённое препятствие в допустимых границах. Unit и OpenAPI success
не подменяют миграцию на изолированной реальной базе. Условие пересмотра
известного blocker сохраняется у [владельца проверки](../../../../docs/development/verification.md#известное-ограничение-сквозной-проверки).
### Rollback Contract
| Field | Decision |
| --- | --- |
| Status | `required` |
| Trigger | Регрессия входа/действующих данных, неверные финансовые результаты, нарушение изоляции владельцев или неуспешная миграция |
| Actions | Прекратить новые записи, сохранить актуальную базу; вернуть совместимую предыдущую версию приложения, сохранив добавленные таблицы. Не выполнять destructive down-migration |
| Data safety | Проверенный backup до миграции и актуальный снимок после остановки записей сохраняются отдельно. Старый backup не восстанавливается поверх новой истории как штатный rollback: это потеряло бы введённые операции |
| Verification | На disposable копии подтверждаются сохранность действующих пользователей/сессий/подключения, совместимость старого приложения с расширенной схемой, доступность новых записей после возврата новой версии и восстановление согласованного снимка |
Более детальная последовательность доставки и команды проверок принадлежат
следующему артефакту Tasks. Этот документ не разрешает менять пользовательскую
базу или начинать реализацию до согласования подготовленного решения.
@@ -0,0 +1,111 @@
## Why
WealthLedger показывает инвестиции и пассивный доход, но пользователю также
нужен раздел личных банковских карт: сколько денег на каждой карте, какие
операции происходили и к каким категориям относятся повседневные траты.
Первая итерация позволяет самостоятельно добавить карты и вести операции.
Пользователь выбрал последовательную подготовку SDD-артефактов: сначала
концепция и требования, затем Design, Tasks и реализация.
## What Changes
**Статус:** ответы Q1–Q9 согласованы. [Delta Specs](specs/manual-bank-cards/spec.md)
дополнены правилами целостного ручного учёта для рассмотрения вместе с Design.
Конкретизация требований ниже не является дословным ответом пользователя
на каждый частный случай и не означает начала реализации.
### Согласованный результат
- Q1: каждая вручную добавленная карта имеет независимые баланс и историю.
- Q2: баланс рассчитывается из начального остатка и внесённых операций.
- Q3: первая версия поддерживает дебетовые карты в рублях.
- Q4: доступны расход, пополнение, перевод между своими картами и возврат
покупки. Перевод вводится одним действием и не считается расходом.
- Q5: есть начальный набор категорий; пользователь может добавлять
и переименовывать свои категории без вложенных подкатегорий.
- Q6: журнал дополняется месячной сводкой расходов по категориям для выбранной
карты и всех банковских карт пользователя вместе.
- Q7: отрицательный учётный баланс допустим, в том числе до внесения
пропущенного пополнения.
- Q8: операции можно исправлять и удалять с пересчётом результатов;
ненужную карту можно убрать в архив с сохранением истории.
- Q9: возврат уменьшает расходы месяца, в котором он произошёл;
сводка отдельно показывает расходы, возвраты и итог после возвратов.
### Целостность пользовательского сценария
Пользователь задаёт начало учёта и начальный остаток, вносит операции
с категориями и датами, исправляет ошибки и видит согласованные баланс,
историю и месячные сводки. Частичные возвраты связаны с исходной покупкой.
Архив скрывает неиспользуемую карту из обычного списка, сохраняя её вклад
в исторические сводки; для новых операций и финансовых исправлений карту
нужно вернуть из архива.
Раздел банковских карт доступен после входа в WealthLedger независимо
от подключения инвестиционного счёта. Пользовательские данные сохраняются
при повторном входе и перезапуске приложения, остаются доступны только
владельцу и защищены от дублирования при повторной отправке и от потери
одновременных изменений.
Повтор создания карты или операции после потерянного ответа сохраняет один
результат, включая начальный остаток карты.
### Термины
**Карта:** добавленная пользователем дебетовая банковская карта в рублях,
для которой он отдельно ведёт баланс и историю операций. Она не является
брокерским счётом или общим счётом нескольких карт.
**Баланс карты:** сумма денег по данным ручного учёта, рассчитанная из начального
остатка и внесённых операций. Она не подтверждает текущий остаток в банковском
приложении: пропущенные движения могут привести к расхождению.
**Итог расходов после возвратов:** расходы выбранного месяца за вычетом
возвратов, произошедших в этом месяце; итог может быть отрицательным.
После реализации определения дополнят существующий
[глоссарий](../../../../docs/glossary.md); отдельный постоянный глоссарий
не создаётся.
### Границы первой итерации
Не входят автоматическая загрузка данных из банков, кредитные карты, другие
валюты, общий денежный баланс нескольких карт, объединение банковских денег
с инвестициями, прогнозы и бюджеты. Не предусмотрены вложенные подкатегории,
удаление категорий и безвозвратное удаление карт.
Произвольной операции «исправить баланс» нет: расхождение исправляется
изменением начального остатка либо внесением или исправлением реальной
операции. Месячная сводка всех карт показывает траты и возвраты, сохраняя
независимый баланс каждой карты.
## Capabilities
### New Capabilities
- `specs/manual-bank-cards/spec.md`: ручное добавление и архив банковских карт,
точный баланс по операциям, расходы и категории, пополнения, переводы,
связанные возвраты, исправления и месячные сводки.
### Modified Capabilities
Нет. Новый раздел сохраняет поведение существующих инвестиционных
представлений и не расширяет понятие Brokerage Account банковскими картами.
## Impact
- Пользователь получает учёт банковских карт и повседневных трат с историей
и возможностью исправить введённые данные.
- Frontend: отдельный раздел карт, формы операций и исправлений, архив,
категории и месячные сводки.
- Backend: сохранение пользовательского ручного учёта, точный расчёт
и защита целостности, приватности и одновременных изменений.
- Specs: проверяемый контракт новой возможности через SDD.
- Docs: описание новой предметной области и обновление глоссария при поставке.
Существующие [брокерские счета](../../../../docs/domains/accounts/overview.md)
получаются через инвестиционное подключение; банковские счета исключены их
[контрактом](../../specs/brokerage-accounts/spec.md).
Текущий [журнал операций](../../../../docs/domains/operations/overview.md)
описывает историю брокера для чтения и не рассчитывает остаток после операции.
Правила нового ручного учёта определяют Delta Specs этого изменения.
@@ -0,0 +1,688 @@
## Purpose
Позволяет пользователю вручную вести дебетовые банковские карты в рублях:
сохранять операции, видеть независимые балансы, исправлять записи и оценивать
месячные расходы по категориям с учётом возвратов.
## Статус согласования
[Proposal](../../proposal.md) фиксирует согласованные ответы Q1–Q9.
Эта редакция раскрывает их в целостный предлагаемый контракт для рассмотрения
вместе с Design. Ограничения полей, даты, категории и правила целостности ниже
являются конкретизацией требований, а не отдельными цитатами пользователя.
Открытых продуктовых развилок в этой редакции нет; наличие спецификации
не означает, что соответствующее поведение уже реализовано.
## ADDED Requirements
### Requirement: Отдельный раздел ручного учёта банковских карт
Система SHALL предоставлять авторизованному пользователю раздел банковских
карт на `/cards` и просмотр отдельной своей карты на `/cards/:id`.
Раздел MUST работать без подключения T-Bank и не зависеть от доступности
инвестиционных данных. Банковские карты MUST NOT становиться брокерскими
счетами, а банковские суммы и операции MUST NOT включаться в инвестиционный
портфель, пассивный доход или прогноз выплат.
Первая версия SHALL поддерживать только ручной учёт дебетовых карт в RUB.
Автоматическая загрузка из банков, кредитные карты, другие валюты, общий
денежный баланс нескольких карт, прогнозы и бюджеты не входят в возможность.
#### Scenario: Работа без инвестиционного подключения
- **WHEN** пользователь с действующей локальной сессией, но без подключения T-Bank, открывает раздел карт
- **THEN** он может просматривать и вести свои банковские карты без перехода к подключению брокера
#### Scenario: Независимость от инвестиционных данных
- **WHEN** пользователь сохраняет банковское пополнение или расход, а инвестиционные данные временно недоступны
- **THEN** банковская операция сохраняется по правилам ручного учёта и не меняет инвестиционные суммы или пассивный доход
### Requirement: Ручное добавление независимых банковских карт
Система SHALL позволять пользователю добавить карту с названием, начальным
остатком и датой начала учёта. Название MUST содержать от 1 до 100 символов
после удаления внешних пробелов. Название банка необязательно и MUST содержать
не более 100 символов после удаления внешних пробелов. Последние четыре цифры
карты необязательны; указанное значение MUST состоять ровно из четырёх цифр.
Полный номер карты, срок действия, PIN и код безопасности не запрашиваются
и не сохраняются как реквизиты этой возможности.
Каждая карта MUST иметь собственную идентичность, баланс и историю независимо
от совпадения банка, названия или последних цифр. Пользователь SHALL иметь
возможность изменить указанные сведения о своей карте с теми же ограничениями.
Финансовые изменения начального остатка и даты начала подчиняются отдельным
требованиям исправления и архива.
#### Scenario: Добавление карты с начальным остатком
- **WHEN** пользователь сохраняет карту с допустимым названием, датой начала учёта и начальным остатком 10 000 ₽
- **THEN** карта появляется в его списке с балансом 10 000 ₽ и без вручную внесённых операций
#### Scenario: Необязательные сведения не заполнены
- **WHEN** пользователь добавляет карту без названия банка и последних четырёх цифр
- **THEN** отсутствие этих сведений не препятствует сохранению карты при корректности обязательных значений
#### Scenario: Несколько карт одного банка
- **WHEN** пользователь добавляет две карты одного банка с одинаковым названием и начальными остатками 1 000 ₽ и 2 000 ₽
- **THEN** система сохраняет две самостоятельные карты с раздельными балансами и историей
#### Scenario: Некорректные сведения карты
- **WHEN** название после удаления внешних пробелов пустое или длиннее 100 символов, название банка длиннее 100 символов либо указанные последние цифры не состоят ровно из четырёх цифр
- **THEN** система отклоняет сохранение, объясняет ошибку соответствующего поля и не создаёт или не изменяет карту
### Requirement: Точные денежные значения и проверка до сохранения
Система MUST принимать денежные значения только в RUB, с точностью не более
двух знаков после десятичного разделителя, без округления введённой суммы.
Сумма расхода, пополнения, перевода или возврата MUST быть строго положительной
и не превышать 92 233 720 368 547 758,07 ₽. Начальный остаток MUST находиться
в диапазоне от −92 233 720 368 547 758,08 ₽ до 92 233 720 368 547 758,07 ₽
включительно; ноль и отрицательный начальный остаток допустимы.
Значение, не представляющее корректную денежную сумму, неподдерживаемая
валюта, лишние десятичные знаки или выход за диапазон MUST приводить
к понятной ошибке без частичного сохранения. Применение ограничения
к каждому денежному вводу MUST NOT приводить к округлению, переполнению
или ограничению точного результата сложения уже сохранённых сумм.
#### Scenario: Нулевой и отрицательный начальный остаток
- **WHEN** пользователь сохраняет отдельные карты с начальными остатками 0 ₽ и −100 ₽
- **THEN** система сохраняет обе карты и показывает соответствующие известные балансы
#### Scenario: Неположительная сумма операции
- **WHEN** пользователь пытается внести операцию с суммой 0 ₽ или −1 ₽
- **THEN** система отклоняет операцию, сохраняя прежние историю, балансы и сводки
#### Scenario: Точность не исправляется округлением
- **WHEN** пользователь пытается сохранить начальный остаток или операцию со значением 1,001 ₽
- **THEN** система сообщает о допустимых двух десятичных знаках и не заменяет введённое значение на 1 ₽
#### Scenario: Границы денежных значений
- **WHEN** пользователь указывает верхнюю допустимую сумму операции или нижний допустимый начальный остаток
- **THEN** сами эти граничные значения принимаются, а выход за соответствующую границу на одну копейку отклоняется без изменения данных
#### Scenario: Некорректная сумма или валюта
- **WHEN** поступает попытка сохранить нечисловую сумму либо сумму в валюте, отличной от RUB
- **THEN** система объясняет ошибку и не сохраняет карту, операцию или изменение существующих данных
### Requirement: Даты начала учёта и ручных операций
Дата начала учёта и дата операции MUST быть существующими календарными датами
без времени. Сегодня определяется по Europe/Moscow. Дата начала учёта
MUST NOT быть позже сегодняшней даты. Начальный остаток означает деньги
до всех вручную внесённых операций начиная с указанной даты включительно.
Дата операции MUST находиться между датой начала учёта затронутой карты
и сегодняшним днём включительно. Для перевода ограничение MUST выполняться
для обеих карт. Ввод прошедших операций внутри этого интервала SHALL быть
доступен. Необязательное описание операции MUST содержать не более
500 символов. Сохранённая календарная дата MUST NOT сдвигаться при смене
часового пояса устройства.
#### Scenario: Операция в первый день учёта
- **WHEN** при начальном остатке 1 000 ₽ на 1 сентября пользователь вносит расход 100 ₽ с датой 1 сентября
- **THEN** расход учитывается после начального остатка и баланс становится 900 ₽
#### Scenario: Прошедшая операция
- **WHEN** пользователь вносит операцию задним числом не раньше начала учёта и не позже сегодняшнего дня
- **THEN** операция сохраняется с указанной датой и изменяет текущий баланс и сводку соответствующего месяца
#### Scenario: Недопустимая дата или описание
- **WHEN** дата не существует, находится до начала учёта или в будущем либо описание длиннее 500 символов
- **THEN** система объясняет ошибку и не сохраняет изменение
#### Scenario: Перевод до начала учёта получателя
- **WHEN** дата перевода допустима для карты-источника, но предшествует началу учёта карты-получателя
- **THEN** перевод отклоняется без изменения обеих карт
#### Scenario: Московская календарная граница
- **WHEN** на устройстве ещё предыдущий день, а в Europe/Moscow уже наступил следующий
- **THEN** допустимая сегодняшняя дата и текущий месяц определяются по Europe/Moscow, а сохранённые даты операций остаются прежними
### Requirement: Баланс определяется начальным остатком и внесёнными операциями
Баланс карты MUST равняться начальному остатку плюс денежное влияние всех
её сохранённых и не удалённых операций: пополнение и возврат увеличивают
баланс, расход уменьшает, перевод уменьшает баланс источника и увеличивает
баланс получателя. Начальный остаток MUST учитываться ровно один раз
и MUST NOT считаться расходом, возвратом или пополнением.
Баланс MUST рассчитываться точно и может быть отрицательным. Недостаточный
учётный остаток MUST NOT препятствовать сохранению корректного расхода
или перевода. Баланс MUST NOT зависеть от выбранного месяца, категории,
показанной страницы истории или нахождения карты в архиве.
Система SHALL представлять баланс как результат ручного учёта и MUST NOT
выдавать его за подтверждённый банком остаток или обещать полноту невнесённых
операций. Отдельной произвольной операции корректировки баланса SHALL NOT быть.
#### Scenario: Последовательные расходы с копейками
- **WHEN** при начальном остатке 1 ₽ сохраняются расходы 0,10 ₽ и 0,20 ₽
- **THEN** баланс составляет ровно 0,70 ₽
#### Scenario: Полное расходование и отрицательный остаток
- **WHEN** при балансе 100 ₽ пользователь сохраняет расход 100 ₽, затем расход 50 ₽
- **THEN** первая операция даёт известный баланс 0 ₽, вторая сохраняется и даёт баланс −50 ₽
#### Scenario: Начальный остаток и страница истории
- **WHEN** при начальном остатке 1 000 ₽ и расходах 100 ₽ и 200 ₽ пользователь повторно открывает карту и видит только расход 200 ₽
- **THEN** текущий баланс остаётся 700 ₽: учитываются обе операции, а начальный остаток не добавляется повторно
#### Scenario: Итог превышает границу отдельного ввода
- **WHEN** на карту с начальным остатком 92 233 720 368 547 758,07 ₽ сохраняется пополнение 0,01 ₽
- **THEN** баланс составляет точно 92 233 720 368 547 758,08 ₽ без переполнения, округления или ограничения верхней границей ввода
### Requirement: Ручное внесение расходов и пополнений
Система SHALL позволять вручную внести расход или пополнение по своей активной
карте с суммой, датой и необязательным описанием. Расход MUST иметь категорию;
если пользователь не выбирает другую, применяется его категория по умолчанию.
После успешного сохранения операция MUST появиться в истории соответствующей
карты и ровно один раз изменить её баланс.
Расход SHALL уменьшать баланс на свою сумму, пополнение SHALL увеличивать.
Категория MUST NOT изменять денежное влияние расхода. Пополнение MUST NOT
включаться в расходы, возвраты или пассивный инвестиционный доход.
#### Scenario: Расход с категорией
- **WHEN** при балансе 10 000 ₽ пользователь сохраняет расход 500 ₽ с категорией «Кафе»
- **THEN** история показывает расход 500 ₽ с этой категорией, а баланс составляет 9 500 ₽
#### Scenario: Категория по умолчанию
- **WHEN** пользователь сохраняет расход, не выбрав другую категорию
- **THEN** расход сохраняется с принадлежащей пользователю категорией по умолчанию и отображается под её текущим названием
#### Scenario: Пополнение карты
- **WHEN** при балансе −500 ₽ пользователь сохраняет пополнение 2 000 ₽
- **THEN** баланс становится 1 500 ₽, история содержит пополнение, а расходы и возвраты не меняются
#### Scenario: Независимость другой карты
- **WHEN** при балансах A 1 000 ₽ и B 2 000 ₽ пользователь сохраняет расход 125,50 ₽ по A
- **THEN** баланс A становится 874,50 ₽, а баланс и история B остаются прежними
### Requirement: Перевод между своими картами как одно действие
Система SHALL позволять одним действием внести перевод между двумя разными
активными картами одного пользователя. Сумма перевода MUST одинаково уменьшать
баланс источника и увеличивать баланс получателя. История каждой стороны
MUST показывать направление и другую карту этого перевода.
Перевод MUST сохраняться, исправляться и удаляться целиком для обеих сторон.
Нельзя сохранять только одну сторону, переводить на ту же или чужую карту.
Перевод MUST NOT увеличивать расходы или возвраты в сводках одной или всех карт.
#### Scenario: Перевод между картами
- **WHEN** при балансах A 5 000 ₽ и B 1 000 ₽ пользователь сохраняет перевод 2 000 ₽ из A в B
- **THEN** баланс A становится 3 000 ₽, баланс B — 3 000 ₽, обе истории показывают стороны одного перевода, а расходы не меняются
#### Scenario: Недостаточный остаток источника
- **WHEN** при балансах A 100 ₽ и B 0 ₽ пользователь сохраняет перевод 150 ₽ из A в B
- **THEN** перевод сохраняется целиком, баланс A становится −50 ₽, а баланс B — 150 ₽
#### Scenario: Недопустимый получатель
- **WHEN** получателем указана та же карта или карта другого пользователя
- **THEN** система отклоняет перевод без изменения историй, балансов и сводок
#### Scenario: Сбой сохранения перевода
- **WHEN** перевод не удаётся сохранить
- **THEN** система не подтверждает успех и не оставляет сохранённой только одну его сторону
### Requirement: Возврат связан с исходным расходом
Возврат SHALL ссылаться на существующий не удалённый расход той же карты.
При внесении возврата пользователь SHALL иметь возможность выбрать исходный
расход карты за любой период учёта, независимо от месяца открытого журнала.
Частичные и несколько возвратов допустимы; сумма всех не удалённых возвратов
MUST NOT превышать текущую сумму исходного расхода. Дата возврата MUST быть
не раньше даты расхода и соответствовать общим ограничениям даты карты.
При редактировании возврата проверка общей возвращённой суммы MUST заменять
его прежнюю сумму новой, не учитывая обе версии одновременно.
Возврат MUST увеличивать баланс карты и наследовать категорию исходного
расхода. Отдельный выбор категории возврата SHALL NOT предоставляться.
Изменение категории исходного расхода MUST менять категорию связанных
возвратов и результаты затронутых месячных сводок, сохраняя их суммы и даты.
#### Scenario: Частичные возвраты
- **WHEN** по расходу 1 000 ₽ пользователь последовательно сохраняет возвраты 300 ₽ и 700 ₽
- **THEN** оба возврата связаны с расходом, увеличивают баланс суммарно на 1 000 ₽ и используют его категорию
#### Scenario: Выбор покупки прошлого месяца
- **WHEN** пользователь открывает сентябрьский журнал и вносит возврат по августовской покупке
- **THEN** форма позволяет найти и выбрать исходный августовский расход этой карты без ограничения сентябрьским журналом
#### Scenario: Исправление полностью возвращённой покупки
- **WHEN** по расходу 500 ₽ сохранён единственный возврат 500 ₽ и пользователь изменяет только описание возврата
- **THEN** исправление сохраняется: общая возвращённая сумма остаётся 500 ₽, а прежняя и новая версии не складываются как два возврата
#### Scenario: Превышение возвращаемой суммы
- **WHEN** при расходе 1 000 ₽ и сохранённом возврате 800 ₽ пользователь пытается сохранить ещё 300 ₽
- **THEN** система отклоняет новый возврат без изменения существующих записей и сумм
#### Scenario: Недопустимая связь или дата возврата
- **WHEN** возврат ссылается на удалённую операцию, операцию другого вида или другой карты либо датирован раньше расхода
- **THEN** система отклоняет возврат без изменения истории, баланса и сводок
#### Scenario: Изменение категории покупки
- **WHEN** пользователь меняет категорию расхода с «Покупки» на «Здоровье», а возврат по нему произошёл в другом месяце
- **THEN** расход и возврат учитываются в категории «Здоровье» в своих месяцах с прежними денежными суммами
### Requirement: Общие для пользователя категории расходов
Система SHALL предоставлять каждому пользователю собственный общий для всех
его карт список категорий: «Продукты», «Кафе», «Такси», «Транспорт», «Жильё»,
«Здоровье», «Покупки», «Развлечения», «Другое». «Другое» является начальной
категорией по умолчанию. Категории MUST быть доступны до первого расхода.
Начальный набор MUST создаваться при успешном добавлении первой карты
пользователя и не дублироваться при добавлении следующих карт.
Пользователь SHALL иметь возможность добавлять и переименовывать любые свои
категории, включая начальные. Название MUST содержать от 1 до 80 символов
после удаления внешних пробелов и быть уникальным среди категорий пользователя
без учёта регистра. Переименование MUST сохранять идентичность категории:
все старые расходы, возвраты и сводки показывают её текущее название.
Переименование категории по умолчанию MUST сохранять её роль.
Удаление категорий и вложенные подкатегории SHALL NOT предоставляться в V1.
#### Scenario: Начальный набор и повторный вход
- **WHEN** пользователь сохраняет первую карту, затем добавляет следующую карту и повторно входит в приложение
- **THEN** после первой карты ему доступен начальный набор категорий до первого расхода, а дальнейшие действия не создают дубликатов
#### Scenario: Категория доступна на всех своих картах
- **WHEN** пользователь создаёт категорию «Питомец»
- **THEN** она доступна для расходов по всем его картам и не появляется у другого пользователя
#### Scenario: Переименование используемой начальной категории
- **WHEN** пользователь переименовывает категорию «Другое» в «Остальное»
- **THEN** прежние записи отображаются как «Остальное», суммы не меняются, а новые расходы без другого выбора используют ту же категорию «Остальное»
#### Scenario: Дублирующееся или недопустимое название
- **WHEN** пользователь пытается добавить « кафе » при существующей «Кафе», сохранить пустое после удаления пробелов название или название длиннее 80 символов
- **THEN** система объясняет ошибку и не изменяет список категорий
### Requirement: Исправление и удаление операций с согласованным пересчётом
Пользователь SHALL иметь возможность изменить сумму, дату, описание
и применимые реквизиты сохранённой операции либо удалить её. Вид операции
и связь возврата с исходным расходом MUST быть неизменны после создания.
Все затронутые карты MUST быть активны; каждое исправление MUST соблюдать
те же денежные, календарные и пользовательские ограничения, что и создание.
Обычный расход или пополнение SHALL быть переносимы на другую свою активную
карту. Расход с не удалёнными возвратами MUST NOT переноситься на другую карту,
удаляться, уменьшаться ниже суммы возвратов или сдвигаться на дату позже
любого связанного возврата. Для такого действия сначала исправляются
или удаляются мешающие возвраты. Возврат остаётся на карте исходного расхода.
Исправление или удаление MUST пересчитать балансы, историю и все затронутые
месячные сводки; изменение даты переносит влияние между соответствующими
месяцами. Исправление сторон перевода MUST учитывать его прежние и новые
карты. Удаление перевода MUST удалять обе стороны одним действием.
Повторное удаление уже удалённой собственной операции SHALL быть
безрезультатным успешным действием без повторного денежного влияния.
#### Scenario: Изменение суммы и месяца расхода
- **WHEN** пользователь меняет расход 100 ₽ за август на 150 ₽ за сентябрь
- **THEN** баланс дополнительно уменьшается на 50 ₽, август перестаёт учитывать 100 ₽, а сентябрь учитывает 150 ₽
#### Scenario: Перенос пополнения на другую карту
- **WHEN** пользователь переносит пополнение 500 ₽ с A на свою активную B с допустимой датой
- **THEN** баланс A уменьшается на 500 ₽, баланс B увеличивается на 500 ₽, а запись относится к B
#### Scenario: Изменение получателя перевода
- **WHEN** перевод 200 ₽ из A в B исправляется на перевод 300 ₽ из A в C и все три карты активны
- **THEN** прежнее влияние на B отменяется, A уменьшается дополнительно на 100 ₽, C увеличивается на 300 ₽, а обе стороны исправленного перевода сохраняются согласованно
#### Scenario: Исправление расхода ограничено возвратами
- **WHEN** для расхода 1 000 ₽ существует возврат 600 ₽ и пользователь пытается удалить расход, перенести его на другую карту, уменьшить до 500 ₽ или сдвинуть позже даты возврата
- **THEN** система отклоняет действие и объясняет необходимость сначала исправить связанные возвраты
#### Scenario: Удаление возврата и затем расхода
- **WHEN** пользователь удаляет все возвраты расхода, а затем сам расход
- **THEN** влияние каждой удалённой записи отменяется в балансе и в её месячной сводке, а ограничение удаления исходного расхода больше не действует
#### Scenario: Удаление перевода из любой стороны
- **WHEN** пользователь удаляет перевод из истории получателя и затем повторяет удаление
- **THEN** обе стороны удаляются при первом действии, денежное влияние отменяется один раз, а повторное удаление ничего не меняет
#### Scenario: Смена вида или исходного расхода
- **WHEN** пользователь пытается превратить расход в пополнение либо связать существующий возврат с другим расходом
- **THEN** изменение отклоняется без подмены вида операции или её исходной связи
### Requirement: Исправление начала ручного учёта
Пользователь SHALL иметь возможность изменить начальный остаток и дату начала
учёта активной карты. Изменение MUST проверять всю затронутую сохранённую
историю, включая стороны переводов и связанные возвраты, и отклоняться
без частичного результата, если после него какая-либо операция нарушает
действующие ограничения. Удалённые операции не ограничивают новую дату.
Изменение начального остатка SHALL пересчитывать текущий баланс, сохраняя
суммы, даты и категории операций. Оно MUST NOT создавать отдельную операцию
или изменять месячные расходы и возвраты. Пропущенное движение SHALL
исправляться внесением реальной операции с допустимой датой.
#### Scenario: Исправление начального остатка
- **WHEN** при начальном остатке 1 000 ₽ и расходах 300 ₽ пользователь меняет начальный остаток на 1 200 ₽
- **THEN** баланс становится 900 ₽, история операций не пополняется корректировкой, а месячные расходы остаются 300 ₽
#### Scenario: Новое начало противоречит истории
- **WHEN** пользователь переносит дату начала учёта с 1 на 10 сентября, а по карте сохранён расход или перевод от 5 сентября
- **THEN** система отклоняет изменение и сохраняет прежние дату, баланс и всю историю
### Requirement: Архив карт сохраняет историю
Пользователь SHALL иметь возможность архивировать свою карту при любом
балансе, включая положительный, нулевой и отрицательный, и вернуть её
из архива. Архивация MUST NOT менять деньги или операции. Обычный список
SHALL показывать активные карты; переключатель архива SHALL открывать
архивные карты и их историю.
Пока карта архивна, система MUST запрещать создание, редактирование
и удаление затрагивающих её операций, а также изменение начального остатка
и даты начала учёта. Для исправления или новой операции карту сначала
возвращают из архива. Перевод или возврат MUST требовать активного состояния
всех затронутых карт. Изменение описательных сведений карты само по себе
не является финансовой операцией.
Архивная карта MUST продолжать участвовать в исторических сводках «Все карты».
Безвозвратное удаление карты SHALL NOT предоставляться в первой версии.
#### Scenario: Архивирование с ненулевым остатком
- **WHEN** пользователь архивирует карту с балансом −500 ₽ и сохранённой историей
- **THEN** карта скрывается из обычного списка, остаётся доступной через переключатель архива с теми же балансом и историей и сохраняет вклад в сводки всех карт
#### Scenario: Финансовое действие по архивной карте
- **WHEN** пользователь пытается внести, исправить или удалить операцию либо изменить начало учёта архивной карты
- **THEN** система отклоняет действие без изменения данных и объясняет необходимость вернуть карту из архива
#### Scenario: Возврат или перевод затрагивает архив
- **WHEN** возврат относится к расходу архивной карты либо хотя бы одна прежняя или новая сторона изменяемого перевода архивна
- **THEN** финансовое действие отклоняется до возвращения всех затронутых карт из архива
#### Scenario: Возвращение карты
- **WHEN** пользователь возвращает карту из архива
- **THEN** она снова видна в обычном списке с сохранёнными данными и допускает финансовые действия по общим правилам
### Requirement: Месячный журнал и сводка расходов по категориям
Система SHALL показывать историю и месячную сводку выбранной карты, а также
месячную сводку всех банковских карт пользователя. По умолчанию выбирается
текущий месяц Europe/Moscow; пользователь SHALL иметь возможность явно выбрать
другой месяц. История карты и её сводка MUST использовать один выбранный месяц.
Операции SHALL отображаться по убыванию календарной даты со стабильным порядком
внутри даты при повторном чтении неизменившихся данных и доступом к следующим
страницам истории.
Сводка MUST показывать расходы, возвраты и итог после возвратов, а также
соответствующие суммы по категориям. Расход входит в месяц собственной даты,
возврат — в месяц даты возврата, даже если покупка была раньше.
Итог MUST равняться расходам минус возвраты и может быть отрицательным.
Начальные остатки, пополнения и переводы MUST NOT включаться в эти суммы.
Сводка MUST учитывать все подходящие не удалённые записи, а не только
текущую страницу истории. «Все карты» MUST включать активные и архивные карты
владельца. Выбор месяца и охвата MUST NOT изменять текущие балансы.
При отсутствии расходов и возвратов SHALL показываться известные нули.
#### Scenario: Месяц по умолчанию и переключение
- **WHEN** пользователь открывает карту в сентябре по Europe/Moscow и затем выбирает август
- **THEN** сначала история и сводка относятся к сентябрю, после выбора обе относятся к августу, а текущий баланс остаётся прежним
#### Scenario: Сводка расходов и переводов
- **WHEN** в месяце сохранены расходы «Кафе» 500 ₽ и «Такси» 300 ₽, пополнение 2 000 ₽ и перевод 1 000 ₽ без возвратов
- **THEN** сводка показывает расходы 800 ₽, возвраты 0 ₽, итог 800 ₽ и категории 500 ₽ и 300 ₽ без включения пополнения или перевода
#### Scenario: Возврат покупки прошлого месяца
- **WHEN** расход «Покупки» 1 000 ₽ совершён в августе, возврат 600 ₽ — в сентябре и других операций этих видов нет
- **THEN** август показывает расходы 1 000 ₽, возвраты 0 ₽ и итог 1 000 ₽, а сентябрь — расходы 0 ₽, возвраты 600 ₽ и итог −600 ₽ в той же категории
#### Scenario: Все карты включают архивные
- **WHEN** расходы активной A за месяц составили 800 ₽, архивной B — 200 ₽, а между ними ранее был перевод 1 000 ₽
- **THEN** общая сводка показывает расходы 1 000 ₽ и не добавляет перевод, независимо от переключателя архива в списке карт
#### Scenario: Несколько страниц и равные даты
- **WHEN** операции выбранного месяца занимают несколько страниц и часть операций имеет одинаковую дату
- **THEN** пользователь может просмотреть все страницы в стабильном порядке, а сводка и баланс учитывают все соответствующие записи независимо от открытой страницы
### Requirement: Изоляция пользовательских финансовых данных
Карты, категории, операции и связи между ними MUST принадлежать локальному
пользователю WealthLedger. Система MUST предоставлять чтение, создание,
изменение, удаление, архив и восстановление только владельцу с действующей
сессией. Идентификатор сам по себе MUST NOT подтверждать доступ.
Проверка владельца MUST распространяться на каждую упомянутую карту,
категорию и операцию, включая обе стороны перевода, исходный расход возврата
и новые связи при редактировании. Отказ MUST NOT раскрывать чужие финансовые
сведения или менять данные любой стороны. Сводки MUST включать только
данные текущего пользователя.
#### Scenario: Нет действующей сессии
- **WHEN** пользователь без действующей сессии пытается прочитать карты, сводку или изменить финансовые данные
- **THEN** система отказывает в доступе без раскрытия и изменения сохранённых данных
#### Scenario: Чужая карта или категория
- **WHEN** пользователь обращается к чужой карте или пытается назначить своей операции чужую категорию
- **THEN** система отклоняет действие, не раскрывает чужие сведения и не меняет баланс, историю или категории
#### Scenario: Чужая связь перевода или возврата
- **WHEN** пользователь подставляет чужую карту в перевод или чужой расход в возврат при создании либо редактировании
- **THEN** система отклоняет всё действие без сохранения частичного результата и без раскрытия данных другого пользователя
#### Scenario: Чужие исправления и архив
- **WHEN** пользователь пытается изменить или удалить чужую операцию, переименовать чужую категорию либо архивировать или восстановить чужую карту
- **THEN** система отказывает и сохраняет все данные владельца без изменений
### Requirement: Согласованные изменения и защита повторной отправки
Успешное финансовое изменение MUST согласованно сохранять операции, связи,
денежное влияние и пересчитанные результаты для всех затронутых карт.
Система MUST NOT подтверждать успех несохранённого действия или оставлять
частичный результат. Независимые одновременные операции MUST сохранять
влияние каждой успешной записи, не затирая друг друга.
Повторная отправка одного намерения создать карту или операцию с тем же
содержанием MUST приводить не более чем к одному соответствующему результату
и однократному учёту начального остатка или денежного влияния операции.
Иное содержание при повторном использовании того же намерения MUST приводить
к конфликту без изменения данных. После исправления или удаления результата
поздняя повторная отправка исходного создания MUST NOT восстанавливать
старые значения, удалённую запись или её денежное влияние.
Ответ на повтор MUST показывать актуальное состояние ранее созданной карты
или операции либо сообщать, что операция удалена, не выдавая старые данные
за текущий результат. Уже сохранённое намерение MUST NOT проверяться
как новая финансовая запись и повторно расходовать доступную сумму возврата.
Архивирование ранее созданной карты MUST NOT превращать повтор её создания
в создание другой карты или возвращение из архива.
Редактирование или удаление существующей операции и изменение карты,
основанные на её устаревшем состоянии, MUST завершаться понятным конфликтом
без перезаписи более свежих изменений. Повторное удаление уже удалённой
операции не считается перезаписью и остаётся безрезультатным действием.
Проверки суммы возвратов, состояния архива, дат и других ограничений
MUST сохраняться при одновременных действиях.
#### Scenario: Повтор после потерянного ответа
- **WHEN** создание расхода 100 ₽ успешно сохранено, ответ потерян и пользователь повторяет то же намерение с тем же содержанием
- **THEN** история содержит один расход, баланс изменён один раз, а повтор не создаёт дубликат
#### Scenario: Повтор создания карты после архивации
- **WHEN** карта с начальным остатком 1 000 ₽ создана, ответ потерян, карта затем архивирована и приходит повтор её исходного создания
- **THEN** система показывает актуальную архивную карту, не создаёт вторую карту, не дублирует начальный остаток и не возвращает карту из архива
#### Scenario: Повтор полного возврата
- **WHEN** возврат 500 ₽ полностью покрывает расход 500 ₽, успешно сохранён, но ответ потерян и пользователь повторяет то же намерение создания возврата
- **THEN** система возвращает сохранённый возврат без новой записи, повторного изменения баланса или ошибки о превышении суммы покупки
#### Scenario: Изменённое содержание того же намерения
- **WHEN** после сохранения расхода 100 ₽ повтор того же намерения содержит сумму 200 ₽
- **THEN** система сообщает о конфликте и сохраняет исходный результат без дополнительной операции
#### Scenario: Позднее создание после исправления или удаления
- **WHEN** сохранённый расход 100 ₽ исправлен на 150 ₽ или удалён, после чего приходит повтор его исходного создания
- **THEN** система показывает актуальный расход 150 ₽ либо сообщает об удалении; прежняя сумма или удалённая запись не восстанавливаются и дополнительного денежного влияния нет
#### Scenario: Два независимых расхода
- **WHEN** при балансе 1 000 ₽ расходы 100 ₽ и 200 ₽ успешно сохраняются одновременно как разные намерения
- **THEN** история содержит оба расхода, а баланс составляет 700 ₽
#### Scenario: Два возврата превышают покупку вместе
- **WHEN** для расхода 1 000 ₽ одновременно сохраняются независимые возвраты 600 ₽ и 600 ₽
- **THEN** система не допускает общей сохранённой суммы 1 200 ₽: конфликтующее действие отклоняется без частичного результата
#### Scenario: Исправление по устаревшим данным
- **WHEN** операция или карта изменилась после того, как пользователь начал её редактирование, а он отправляет изменение прежнего состояния
- **THEN** система сообщает о конфликте, сохраняет более свежие данные и позволяет получить актуальное состояние для нового исправления
#### Scenario: Удаление по устаревшим данным
- **WHEN** пользователь пытается удалить существующую операцию, которую другой успешный запрос уже изменил
- **THEN** система отклоняет устаревшее удаление без потери более свежего исправления
#### Scenario: Архивирование одновременно с финансовым изменением
- **WHEN** архивирование карты пересекается с созданием или исправлением затрагивающей её операции
- **THEN** результаты соответствуют последовательному выполнению допустимых действий и не допускают финансовую запись, выполненную после вступившего в силу архива
### Requirement: Сохранённый ручной учёт переживает повторный вход и перезапуск
Успешно сохранённые карты, начальные остатки, даты начала, операции,
категории, архивное состояние и связи возвратов и переводов MUST оставаться
доступными владельцу после обновления страницы, выхода, повторного входа
и обычного перезапуска приложения. Эти действия MUST NOT создавать
дубликаты, восстанавливать удалённые операции или сбрасывать балансы.
Защита повторной отправки сохранённых намерений MUST сохранять действие
после этих переходов.
#### Scenario: Повторный вход
- **WHEN** пользователь выходит и снова входит после сохранения карты с начальным остатком 1 000 ₽ и расходом 100 ₽
- **THEN** он видит ту же карту, дату начала, расход с его текущей категорией и баланс 900 ₽
#### Scenario: Перезапуск с переводом, возвратом и архивом
- **WHEN** приложение перезапускается после сохранения переводов, связанных возвратов, переименования категорий и архивирования карты
- **THEN** владелец получает прежние связи, названия, архивное состояние и рассчитанные суммы без дубликатов или потери истории
#### Scenario: Повтор создания после перезапуска
- **WHEN** после перезапуска повторяется намерение создания ранее сохранённой, а затем удалённой операции
- **THEN** удалённая операция и её денежное влияние не восстанавливаются
### Requirement: Доступные формы и различимые состояния без ложного успеха
Раздел SHALL различать отсутствие карт, отсутствие операций за месяц,
известный нулевой баланс, загрузку и ошибку получения данных. Ошибка
MUST NOT подменяться пустым списком или нулевой суммой. Пользователь SHALL
иметь возможность повторить неуспешную загрузку.
Формы карт, операций, исправлений и категорий MUST иметь доступные названия
полей и действий, управление с клавиатуры и понятные ошибки, связанные
с соответствующим полем или действием. Обязательность, направление денег
и ошибки MUST NOT передаваться только цветом.
Во время сохранения, ошибки и конфликта введённый черновик MUST оставаться
доступным до явного закрытия или успешного завершения. Пока результат
сохранения не подтверждён, система MUST NOT показывать введённую сумму
как подтверждённый новый баланс или выдавать действие за успешное.
При неопределённом результате после потери ответа повтор SHALL использовать
защиту того же намерения создания. После подтверждённого изменения
отображаемые история, балансы и сводки MUST согласовываться с сохранёнными
данными всех затронутых карт.
#### Scenario: Пустой список
- **WHEN** раздел успешно загружен и у пользователя ещё нет карт
- **THEN** система показывает штатное пустое состояние с доступным действием добавления карты
#### Scenario: Нет операций при ненулевом балансе
- **WHEN** в выбранном месяце нет операций, но текущий баланс карты составляет 10 000 ₽
- **THEN** история сообщает об отсутствии операций за месяц, сводка содержит нули, а текущий баланс остаётся 10 000 ₽
#### Scenario: Ошибка загрузки
- **WHEN** загрузка карт, истории или сводки завершается ошибкой
- **THEN** система показывает ошибку и действие повтора, не выдавая её за отсутствие данных или нулевой результат
#### Scenario: Доступный ввод и проверка
- **WHEN** пользователь управляет формой с клавиатуры или средством чтения экрана и вводит некорректную сумму
- **THEN** названия полей и действий доступны, ошибка суммы определима без цвета, а введённые значения остаются для исправления
#### Scenario: Ожидание и неопределённый результат
- **WHEN** пользователь отправляет перевод и ответ ещё не получен либо потерян
- **THEN** черновик сохраняется, неподтверждённые балансы не выдаются за сохранённые, а повтор того же создания не порождает второй перевод
#### Scenario: Ошибка или конфликт исправления
- **WHEN** сохранение исправления отклоняется из-за ошибки или устаревшего состояния
- **THEN** пользователь видит причину, сохраняет черновик и может получить актуальные данные для следующего действия без перезаписи чужого успешного изменения
@@ -0,0 +1,314 @@
# Реализация ручного учёта банковских карт
**Статус:** согласованный объём реализован и проверен 12 сентября 2026 года.
Независимое ревью завершено; change остаётся активным, без архивирования
и синхронизации в основные specs.
Основание — согласованные [Design revision 1](design.md), включая полностью
inline UI/UX package revision 1, и [Delta Specs](specs/manual-bank-cards/spec.md).
Внешних визуальных материалов или дополнительных пакетов для начала нет.
Checkbox закрывается только после выполнения действия и его Verification.
Verification ниже задаёт критерии; наблюдённые результаты отмечаются отдельно.
Перед Node-командами из affected repo выполняются
`source ~/.nvm/nvm.sh && nvm use --silent`; для specs используется frontend
`.nvmrc`. Git-команды выполняются из соответствующего независимого репозитория.
DB/HTTP-проверки используют уникальную временную SQLite с вымышленными данными
и явно заданной `DATABASE_URL`, без доступа к пользовательской базе или T-Bank.
Общий `npm run test:e2e` с фиксированной `/tmp/wealth-ledger-e2e.db` для них
не используется. Рабочая финансовая база не сбрасывается и не заменяется
ради выполнения проверки.
Задачи одного раздела выполняются по указанным зависимостям. Задача 1.2
может идти независимо от 1.1; остальные параллельные работы допустимы только
при закрытых зависимостях и раздельном владении изменяемыми файлами.
## 1. Основа хранения и точных денег
- [x] 1.1 [backend] Подтвердить применимость выбранного Solution Assessment: существующие Prisma/SQLite и migration boundary под Node 22; зафиксировать исходную версию схемы/приложения для проверки перехода и отката.
- **Covers:** `N/A` — техническое предусловие нового долговременного хранения из Design, без самостоятельного пользовательского поведения или смены выбранной архитектуры.
- **Verification:** `npx prisma validate`; существующие migrations применяются через `npx prisma migrate deploy` с `DATABASE_URL` каждой уникальной временной базы. Для снятия известного blocker нужны три последовательных успешных fresh-DB применения согласно [правилу проверки](../../../../docs/development/verification.md#известное-ограничение-сквозной-проверки). При ошибке сохранить версию и точный failure, не закрывать задачу и не подменять migration proof через `db push`, reset или ручной обход migrations.
- **Результат 2026-09-12:** Node `22.21.0`, Prisma/client `7.9.1`, engine `e922089b7d7502aff4249d5da3420f6fa55fc6ad`; `prisma validate` — passed. Исходный backend `8ec172f48eefa2237dc0c16919fbb2e726b39a65`, frontend `427ca23a433a20be25b1f4373f32c9e1f6f8c400`; оба working tree были чистыми. Обычный fresh deploy воспроизвёл `Error: Schema engine error:` без деталей, в том числе вне sandbox. С `RUST_LOG=info` штатный `npx prisma migrate deploy` вне sandbox прошёл три раза подряд: `/tmp/wealthledger-bank-cards-migrations-logging.rFEHKw/fresh-{1,2,3}.db`, логи `migrate-{1,2,3}.log` рядом. В каждом применены обе исходные migrations. Это подтверждает путь с указанной переменной окружения; запуск без неё остаётся ограничением. Версии зависимостей, применённые migrations и пользовательская база не менялись.
- [x] 1.2 [backend] Выделить общего владельца decimal parsing/formatting и MoneyDto, сохранить protobuf-преобразование у T-Bank и добавить точное преобразование допустимого RUB-ввода в копейки.
- **Covers:** `manual-bank-cards` — `Точные денежные значения и проверка до сохранения`.
- **Verification:** существующие decimal и затронутые брокерские calculation tests проходят до и после переноса; targeted Jest проверяет signed-64-bit границы, отрицательный/нулевой начальный остаток, строго положительную сумму операции, запрет лишней точности и точный Money roundtrip. `npm run typecheck:app`; прежний Money wire contract сохраняется.
- **Результат 2026-09-12:** общий owner `src/common/decimal.ts`, `money.dto.ts`, `rub-money.ts`; protobuf-преобразование осталось в T-Bank. До переноса `npm test -- --runInBand src/tbank` — 24 suites / 191 tests passed; после `npm test -- --runInBand 'src/tbank|src/common/rub-money'` — 25 suites / 213 tests passed. Targeted ESLint, `npm run typecheck:app`, `git diff --check` — passed. `npm run test:openapi-contract` до добавления банковских endpoints подтвердил неизменность прежнего контракта, но проверка обнаружила безусловную замену внешнего `DATABASE_URL` в `test/e2e-env.cjs` на фиксированную временную базу. Этот запуск не принимается как доказательство требуемой изоляции; после исправления test setup сравнение повторяется на уникальной базе. Пользовательская база не использовалась. Новый banking API требует отдельного финального сравнения.
- [x] 1.3 [backend] После 1.1 добавить BankCard, ExpenseCategory и BankCardOperation, additive migration, связи, ограничения уникальности, индексы, версии, tombstones и fingerprints из Design.
- **Covers:** `manual-bank-cards` — `Сохранённый ручной учёт переживает повторный вход и перезапуск`; `manual-bank-cards` — `Точные денежные значения и проверка до сохранения`.
- **Verification:** `npx prisma validate`, `npx prisma generate`, `npm run typecheck:app`; migration на изолированной базе с representative User/Session/TbankConnection сохраняет их данные. Реальная DB-проверка подтверждает FK/unique constraints и точное хранение копеек больше `2^53` и обеих граничных величин; применённые migrations не переписаны.
Если для устранения подтверждённого migration blocker потребуется другой
подход или изменение согласованного стека, это возвращает решение в Design.
Успех unit/Swagger-проверки не разрешает закрыть 1.1 или 1.3 без DB-доказательства.
Результат 1.3: additive migration `20260912090000_add_manual_bank_cards`
(существующие migrations неизменны), `prisma validate/generate` и app typecheck
— passed. Реальный adapter/Prisma roundtrip подтвердил `9007199254740993n`
и обе signed64 границы; configured DB-тесты подтвердили FK/unique.
Переход с исходной схемы сохранил representative User/Session/TbankConnection
в проверке сохранности 3.4.
## 2. Backend: пользовательский учёт и денежные изменения
- [x] 2.1 [backend] После 1.2–1.3 создать BankCardsModule и сервис сценариев: добавление карт с защитой повторов, пользовательские категории, начальный набор и переименование с сохранением идентичности и default role.
- **Covers:** `manual-bank-cards` — `Ручное добавление независимых банковских карт`; `manual-bank-cards` — `Общие для пользователя категории расходов`; `manual-bank-cards` — `Изоляция пользовательских финансовых данных`; `manual-bank-cards` — `Согласованные изменения и защита повторной отправки`.
- **Verification:** targeted service/DB cases проверяют ограничения сведений карты, собственника, atomic card+seed creation, concurrent first-card creation без дублей категорий, регистронезависимую уникальность, rename default category и same-intent create карты с тем же/другим содержанием. Чтение не создаёт данных и не обращается к T-Bank.
- [x] 2.2 [backend] После 1.2 реализовать чистые правила календарных дат и точные расчёты баланса и месячных расходов из первичных записей, сохранив O(N) подход Design.
- **Covers:** `manual-bank-cards` — `Даты начала учёта и ручных операций`; `manual-bank-cards` — `Баланс определяется начальным остатком и внесёнными операциями`; `manual-bank-cards` — `Месячный журнал и сводка расходов по категориям`.
- **Verification:** targeted Jest проверяет `1 − 0,10 − 0,20 = 0,70`, отрицательный баланс, итог выше предела отдельного ввода, московскую границу дня/месяца, реальные календарные даты, возврат прошлого месяца и исключение переводов/пополнений из расходов. Расчёт не зависит от страницы журнала; комментарий `ponytail:` фиксирует предел O(N) и условие оптимизации.
- [x] 2.3 [backend] После 2.1–2.2 реализовать создание расходов и пополнений с проверкой карты/категории/дат, транзакцией и разрешением сохранённого намерения до повторной проверки изменяемых условий.
- **Covers:** `manual-bank-cards` — `Ручное внесение расходов и пополнений`; `manual-bank-cards` — `Согласованные изменения и защита повторной отправки`; `manual-bank-cards` — `Изоляция пользовательских финансовых данных`.
- **Verification:** service/DB cases проверяют default category, сохранение при отрицательном остатке, отказ чужим ссылкам и недопустимому вводу без частичной записи; same UUID не создаёт дубликат, другой payload даёт конфликт, две независимые успешные траты сохраняются обе.
- [x] 2.4 [backend] После 2.3 реализовать перевод одной записью с проверкой обеих карт и дат начала учёта, двумя проекциями истории и общим денежным эффектом.
- **Covers:** `manual-bank-cards` — `Перевод между своими картами как одно действие`; `manual-bank-cards` — `Даты начала учёта и ручных операций`; `manual-bank-cards` — `Изоляция пользовательских финансовых данных`.
- **Verification:** реальная DB-проверка подтверждает обе стороны перевода, допустимый отрицательный остаток источника и отсутствие частичного результата при ошибке; отклоняются одинаковые, чужие и архивные карты и дата до начала учёта любой стороны. Сумма не включается в расходы одной или всех карт.
- [x] 2.5 [backend] После 2.3 реализовать связанные частичные и повторные возвраты, наследование категории покупки, общий лимит и чтение доступных исходных расходов за любой период.
- **Covers:** `manual-bank-cards` — `Возврат связан с исходным расходом`; `manual-bank-cards` — `Согласованные изменения и защита повторной отправки`; `manual-bank-cards` — `Изоляция пользовательских финансовых данных`.
- **Verification:** DB cases проверяют несколько возвратов, исходный расход прошлого месяца, чужую/удалённую покупку, дату до покупки и два конкурирующих возврата с превышением общего лимита. Повтор успешно сохранённого полного возврата после потерянного ответа возвращает существующий результат без ошибки нового лимита и без дубликата.
- [x] 2.6 [backend] После 2.4–2.5 реализовать version-protected исправление и логическое удаление всех видов операций с проверкой прежних и новых связей, сохранением creation fingerprint и запретом изменения вида/исходной покупки возврата.
- **Covers:** `manual-bank-cards` — `Исправление и удаление операций с согласованным пересчётом`; `manual-bank-cards` — `Возврат связан с исходным расходом`; `manual-bank-cards` — `Согласованные изменения и защита повторной отправки`.
- **Verification:** DB cases покрывают перенос расхода/пополнения между картами, смену получателя и удаление целого перевода, смену месяца/категории, ограничения исходного расхода с возвратами, edit refund с исключением собственной прежней суммы, stale edit/delete и повтор delete. Late create retry после edit/delete не возвращает старые суммы и не воскрешает запись; гонка refund с уменьшением покупки сохраняет общий лимит.
- [x] 2.7 [backend] После 2.6 реализовать изменение сведений и начала учёта карты, архив и возврат из архива с проверкой всей затронутой истории и version conflict.
- **Covers:** `manual-bank-cards` — `Ручное добавление независимых банковских карт`; `manual-bank-cards` — `Исправление начала ручного учёта`; `manual-bank-cards` — `Архив карт сохраняет историю`; `manual-bank-cards` — `Согласованные изменения и защита повторной отправки`.
- **Verification:** DB cases проверяют opening balance без новой операции/изменения месячных трат, недопустимое новое начало с входящим переводом, архив при любом остатке, запрет create/edit/delete по архиву, unarchive и stale card edit. Гонка архива или смены даты с финансовым изменением даёт последовательный допустимый результат; повтор create архивированной карты возвращает ту же архивную карту.
Результат раздела 2: `src/bank-cards/bank-card.calculations.spec.ts` — 23
pure tests passed; полный backend gate — 33 suites / 253 tests passed.
`test/bank-cards.e2e-spec.ts` — 18 configured HTTP/SQLite tests passed
(`/tmp/bank-cards-http.log`): атомарные повторы/переводы, независимые траты,
конкурирующие возвраты и уменьшение покупки, смена начала/архив одновременно
с операцией, версии, foreign ownership, исправления/удаления, категории и
точные денежные границы. Fixtures вымышленные, URL уникальный; setup сохраняет
внешний `DATABASE_URL`, банковский suite запрещает shared/user DB.
## 3. Backend: чтение, HTTP и доказательство сохранности
- [x] 3.1 [backend] После раздела 2 подключить расчёты к согласованным чтениям списка/деталей карт, месячного журнала, исходных расходов возврата и сводок одной/всех карт.
- **Covers:** `manual-bank-cards` — `Баланс определяется начальным остатком и внесёнными операциями`; `manual-bank-cards` — `Месячный журнал и сводка расходов по категориям`; `manual-bank-cards` — `Архив карт сохраняет историю`; `manual-bank-cards` — `Возврат связан с исходным расходом`.
- **Verification:** integration cases проверяют пагинацию и стабильный порядок равных дат, весь набор для баланса/сводки, отрицательный net, архив в общей сводке, перенос между месяцами и перегруппировку связанных возвратов после изменения категории покупки. Связанные данные одного ответа согласованы; отсутствуют server cache и materialized balance.
- [x] 3.2 [backend] После 3.1 завершить controller/runtime DTO под `/api/v1/bank-cards`, подключить существующие JWT/filter и сформировать полный runtime OpenAPI для frontend.
- **Covers:** `manual-bank-cards` — `Отдельный раздел ручного учёта банковских карт`; `manual-bank-cards` — `Точные денежные значения и проверка до сохранения`; `manual-bank-cards` — `Даты начала учёта и ручных операций`; `manual-bank-cards` — `Изоляция пользовательских финансовых данных`.
- **Verification:** configured HTTP cases проверяют фактические validation/status/error codes, отсутствие сессии, чужие связи, архив/version/refund conflicts, безопасность ответов и работу без T-Bank. Runtime OpenAPI отражает действующие DTO и Money; сырые Prisma records не выдаются вместо оговорённого контракта, финансовые DTO не попадают в ответы с ошибками.
- [x] 3.3 [backend] После 3.2 завершить runnable configured HTTP/DB проверки критических сценариев и конкурентных изменений в отдельной временной базе, дополнив уже созданные cases вместо их дублирования.
- **Covers:** `manual-bank-cards` — `Согласованные изменения и защита повторной отправки`; `manual-bank-cards` — `Изоляция пользовательских финансовых данных`; `manual-bank-cards` — `Исправление и удаление операций с согласованным пересчётом`; `manual-bank-cards` — `Доступные формы и различимые состояния без ложного успеха`.
- **Verification:** с уникальной `DATABASE_URL` выполнить `NODE_ENV=development NODE_OPTIONS=--experimental-vm-modules npx jest --config ./test/jest-e2e.json --runInBand bank-cards`. Проверяемые исходы: один результат concurrent same-intent create карты/операции, сохранность обеих независимых трат, no partial transfer/refund, stale conflicts, архивные/чужие ссылки, loss-of-response replay и отсутствие ложного успеха. Mock Prisma не заменяет DB cases.
- [x] 3.4 [backend] После 3.3 доказать долговременность ручных данных, корректный backup/restore и совместимость отката приложения с расширенной схемой без удаления новой истории.
- **Covers:** `manual-bank-cards` — `Сохранённый ручной учёт переживает повторный вход и перезапуск`; `manual-bank-cards` — `Согласованные изменения и защита повторной отправки`.
- **Verification:** на disposable базах перезапуск сохраняет карты, операции, связи, переименованные категории, архив, tombstones и fingerprints; повтор после перезапуска не создаёт дубликат. Проверяются согласованный SQLite backup/restore, сохранность User/Session/TbankConnection, запуск исходной версии приложения с additive schema и доступность новых записей после возврата новой версии. Старый backup не восстанавливается поверх новой истории как штатный rollback.
Результат раздела 3: те же 18 HTTP/DB tests подтверждают чтения, пагинацию,
month/category regrouping, validation/status и безопасные ошибки без T-Bank.
Дополнительный процессный сценарий `node /tmp/bank-cards-durability.cjs` — passed:
исходный backend собран из Git archive `8ec172f48eefa2237dc0c16919fbb2e726b39a65`
в `/tmp/wealthledger-bank-baseline.8hcDz3`; миграция сохранила старые таблицы,
перезапуск — категории/архив/возвраты/tombstones/fingerprints, старое приложение
запустилось с расширенной схемой, новое вновь прочитало историю, согласованный
SQLite backup восстановлен и прочитан приложением. Свидетельство:
`/var/folders/j_/h2ft23gd3qq35ps3v5bcftfm0000gn/T/wealthledger-bank-durability-hPvsEO/result.json`.
Runnable версия сохранена в `backend/test/bank-cards-durability.cjs`; ей
передаётся каталог отдельно собранной исходной версии. Проверки создают свои
временные базы; старый backup не записывается поверх новой истории.
## 4. Контракт и слой данных frontend
- [x] 4.1 [frontend] После 3.2 обновить committed OpenAPI snapshot из настроенного backend runtime document и сгенерировать frontend types, сохранив backend единственным editable HTTP source.
- **Covers:** `N/A` — производные артефакты двустороннего контракта из Design обеспечивают типизированного потребителя, но не вводят отдельного пользовательского поведения.
- **Verification:** `npm run openapi:generate`, `npm run openapi:check`, `npm run typecheck`; snapshot получен по [существующему workflow](../../../../docs/development/runtime-openapi-verification.md), generated types не исправлялись вручную, существующие инвестиционные Money-контракты сохранены.
- [x] 4.2 [frontend] После 4.1 добавить banking queries/mutations у domain owner: session-aware transport, стабильный create UUID, version inputs, безопасные retries и invalidation всех затронутых банковских GET-представлений.
- **Covers:** `manual-bank-cards` — `Согласованные изменения и защита повторной отправки`; `manual-bank-cards` — `Изоляция пользовательских финансовых данных`; `manual-bank-cards` — `Отдельный раздел ручного учёта банковских карт`; `manual-bank-cards` — `Доступные формы и различимые состояния без ложного успеха`.
- **Verification:** Vitest/MSW через `npm run test:run -- src/entities/bank-card` проверяют одинаковый UUID при network/auth retry, отсутствие optimistic balance, invalidation обеих карт/всех месяцев/исходных расходов, rename категории, сохранение данных при ошибке, GET после version conflict, logout isolation и независимость от T-Bank connection mutation. Свой refresh loop и второй query cache отсутствуют.
Результат 4.2: `src/entities/bank-card/api/banking.test.tsx` — 10 MSW tests
passed; typecheck, focused Biome и dependency boundaries — passed. Повторы
сохраняют UUID, подтверждённые mutations инвалидируют только все banking GETs,
конфликты вызывают перечитывание, ошибки не меняют суммы оптимистически.
Дополнены реальные feature tests AuthProvider/TbankConnectionSection: 10 tests
passed доказывают очистку banking cache при logout/refresh failure и его
сохранение без invalidation при connect/replace/delete T-Bank.
Результат 4.1: snapshot экспортирован из configured runtime Nest/Swagger
(42 paths) на уникальной временной SQLite, включая банковские 409/410.
`npm run openapi:generate`, `npm run openapi:check`, `npm run typecheck` — passed;
generated types не редактировались вручную. После финального добавления
описаний 409/410 snapshot/types повторно сгенерированы, `openapi:check` passed.
## 5. Экраны и формы из UI/UX package revision 1
Все задачи раздела используют существующие Mantine-тему, i18n, container/VM/view,
RHF/Zod и shared UI из Design. Проверки формы и состояний выполняются через
видимый результат и сеть MSW; отдельный тест декларативной обёртки не нужен.
- [x] 5.1 [frontend] После 4.2 собрать `/cards`: навигацию, список активных карт и архива, месячную сводку всех карт и форму добавления карты с начальным остатком/датой.
- **Covers:** `manual-bank-cards` — `Отдельный раздел ручного учёта банковских карт`; `manual-bank-cards` — `Ручное добавление независимых банковских карт`; `manual-bank-cards` — `Месячный журнал и сводка расходов по категориям`; `manual-bank-cards` — `Доступные формы и различимые состояния без ложного успеха`.
- **Verification:** `npm run test:run -- src/pages/cards` и targeted route tests проверяют login/returnTo без T-Bank, обязательные/необязательные поля, точный отрицательный остаток, no request при invalid, блокировку pending, сохранение ввода при ошибке и один create UUID при повторе. Loaded/empty/error/skeleton и архивный переключатель соответствуют Design; сводка всех карт включает архив независимо от фильтра списка.
- [x] 5.2 [frontend] После 5.1 собрать `/cards/:id`: крошки и title, текущий баланс, общий выбор месяца для журнала/сводки, пагинацию и представления четырёх видов операций.
- **Covers:** `manual-bank-cards` — `Баланс определяется начальным остатком и внесёнными операциями`; `manual-bank-cards` — `Месячный журнал и сводка расходов по категориям`; `manual-bank-cards` — `Архив карт сохраняет историю`; `manual-bank-cards` — `Доступные формы и различимые состояния без ложного успеха`.
- **Verification:** `npm run test:run -- src/pages/card` проверяет month/URL и страницу, неизменный текущий баланс, расходы/возвраты/net со знаком, понятные стороны перевода, локальную ошибку сводки, cached snapshot при refetch failure и безопасное состояние отсутствующей карты. Frontend форматирует Money без собственной финансовой арифметики.
- [x] 5.3 [frontend] После 5.2 добавить формы создания расхода, пополнения и перевода с общими проверками денежного ввода/дат и сохранением одного намерения до определённого результата.
- **Covers:** `manual-bank-cards` — `Ручное внесение расходов и пополнений`; `manual-bank-cards` — `Перевод между своими картами как одно действие`; `manual-bank-cards` — `Точные денежные значения и проверка до сохранения`; `manual-bank-cards` — `Даты начала учёта и ручных операций`; `manual-bank-cards` — `Доступные формы и различимые состояния без ложного успеха`.
- **Verification:** RTL/MSW cases проверяют default category, положительную сумму/копейки без потери больших значений, даты обеих карт перевода, отрицательный итог без запрета реальной траты, visible labels, pending/ошибки с сохранённым вводом и повтор одного перевода без дубликата. Новое подтверждённое состояние приходит из банковских queries.
- [x] 5.4 [frontend] После 5.3 добавить создание и редактирование возврата с выбором исходной покупки за всю историю карты и корректным лимитом текущей записи.
- **Covers:** `manual-bank-cards` — `Возврат связан с исходным расходом`; `manual-bank-cards` — `Исправление и удаление операций с согласованным пересчётом`; `manual-bank-cards` — `Месячный журнал и сводка расходов по категориям`; `manual-bank-cards` — `Согласованные изменения и защита повторной отправки`.
- **Verification:** RTL/MSW journey выбирает покупку прошлого месяца, записывает частичный возврат и видит его в месяце получения; полный возврат позволяет исправить описание, исключая собственную старую сумму из лимита. Проверяются превышение, stale source, immutable purchase link, архив, network retry полного возврата и сохранение формы без ложного успеха.
- [x] 5.5 [frontend] После 5.3–5.4 добавить остальные исправления/удаления операций, изменение сведений и начала учёта карты, архив и возврат из архива с понятными конфликтами.
- **Covers:** `manual-bank-cards` — `Исправление и удаление операций с согласованным пересчётом`; `manual-bank-cards` — `Исправление начала ручного учёта`; `manual-bank-cards` — `Архив карт сохраняет историю`; `manual-bank-cards` — `Доступные формы и различимые состояния без ложного успеха`.
- **Verification:** RTL/MSW проверяет edit с version и последующим GET при конфликте, сохранение черновика, перенос между картами/месяцами, подтверждение удаления перевода с обеими картами, запреты связанного расхода и архива, opening date error и обратимый архив. После успешного действия все затронутые видимые суммы соответствуют новым серверным данным.
- [x] 5.6 [frontend] После 5.3 добавить управление начальными и своими категориями и создание категории из формы расхода без потери введённой операции.
- **Covers:** `manual-bank-cards` — `Общие для пользователя категории расходов`; `manual-bank-cards` — `Ручное внесение расходов и пополнений`; `manual-bank-cards` — `Доступные формы и различимые состояния без ложного успеха`.
- **Verification:** RTL/MSW проверяет add/rename, уникальность без регистра и длину, переименование default category без утраты роли, обновление названий в истории/сводке и неизменный черновик расхода при добавлении категории. Удаление и вложенные подкатегории отсутствуют согласно scope.
Результат 5.1–5.6: реализованы страницы, четыре вида операций, исправления,
удаление, архив и категории. RTL/MSW покрывают точный ввод и UUID после ошибки,
month/page/returnTo, неизменный текущий баланс, полный возврат с собственным
лимитом, stale version с явным принятием новой версии, opening-date conflict,
архив, обе стороны удаления перевода и вложенное создание категории.
Финальный frontend gate включает 43 test files / 263 passed tests.
Браузер обнаружил потерю нового имени категории после reset неконтролируемого
поля: поле переведено на RHF Controller, исправление проверено RTL и настоящим
PATCH/GET. Независимое ревью обнаружило скрытые ошибки вспомогательных GET
карт/категорий: добавлены локальные сообщения и retry, две regression-проверки
и browser recovery passed. Существующие authentication/инвестиционные tests
прошли; returnTo сохраняет карточку и query выбранного месяца.
## 6. Проверка пользовательского результата и документация
- [x] 6.1 [frontend] После 3.3–3.4 и разделов 4–5 проверить UI package в браузере с изолированным backend и подготовить Storybook-примеры новых собственных представлений на вымышленных данных.
- **Covers:** `manual-bank-cards` — `Доступные формы и различимые состояния без ложного успеха`; `manual-bank-cards` — `Отдельный раздел ручного учёта банковских карт`; `manual-bank-cards` — `Сохранённый ручной учёт переживает повторный вход и перезапуск`.
- **Verification:** реальный UI journey без T-Bank: две карты → расход/пополнение → перевод → возврат прошлой покупки → исправление/удаление → архив/unarchive → reload/login. Проверить широкий экран и 320–375 px, пять пунктов навигации, длинные суммы/названия, keyboard/focus return, reduced motion и loading/empty/error/refetch recovery. Сохранить краткие результаты и локальные визуальные свидетельства; Storybook build не подменяет браузерную проверку.
- [x] 6.2 [docs] После подтверждения реализованного поведения обновить документацию: область банковских карт, глоссарий, обзор продукта и тематическую навигацию, хранимые данные, денежные правила и безопасное восстановление.
- **Covers:** `N/A` — as-built объяснение поставленного поведения и восстановления из Delivery Map; новая норма не вводится и не копируется параллельно Delta Specs.
- **Verification:** пройти путь README → банковские карты → понятия/расчёты/исправления и восстановление; сверить текст с поставленными flows и ограничениями, включая ручной источник баланса, отрицательные итоги, архив, повторы и сохранение новой истории при rollback. Ссылки ведут на существующие targets; исторические материалы не переписываются, обещания неподтверждённой миграции не добавляются.
Результат 6.1: реальный браузер работал с отдельным BrowserContext и backend
на уникальной временной SQLite, с вымышленным пользователем без T-Bank.
Через UI выполнены две карты (1 000 ₽ и 0 ₽), августовский расход 100 ₽,
сентябрьские пополнение 200 ₽, перевод 150 ₽ и возврат 60 ₽: баланс A 1 010 ₽,
B 150 ₽, сентябрьский net −60 ₽. Возврат исправлен до полного 100 ₽,
его описание изменено, затем сумма возвращена к 60 ₽. Исправление покупки
100 → 150 ₽ дало A 960 ₽; удаление перевода с подтверждением обеих карт —
A 1 110 ₽ и B 0 ₽. Архив сохранил историю и общий net; unarchive вернул действия.
Добавление категории из расхода сохранило сумму 12,34 ₽ и описание черновика;
переименование «Другое» сохранило default role. Проверены Escape и возврат
фокуса к действию, visible labels, initial loading/empty, ошибки GET и retry,
сохранённый net при неудачном refetch, reduced motion. Logout → protected URL →
login вернул ту же карту и август; reload сохранил баланс 1 110 ₽ и расход 150 ₽.
Экран 1440 px и 320/375 px проверен визуально: пять пунктов навигации,
длинное название и точная предельная сумма доступны без горизонтального overflow.
Локальные screenshots итогового UI:
`/tmp/wealthledger-bank-cards-final-desktop.png`,
`/tmp/wealthledger-bank-cards-final-mobile.png`,
`/tmp/wealthledger-bank-cards-final-mobile-long.png`,
`/tmp/wealthledger-bank-card-final-mobile.png`;
состояние cached error — `/tmp/wealthledger-bank-cards-cached-error.png`.
Добавлены две изолированные story groups и 9 примеров на вымышленных данных;
storybook dependency isolation и build passed.
Результат 6.2: обновлены README/overview, glossary, область cards, описание
хранения, локальная миграция/восстановление и UI-каталог. Маршруты README → cards →
Money/восстановление проверены; объяснения сверены с Delta Specs, реализацией
и наблюдёнными flows. Новых Mermaid-диаграмм и изменений исторических ADR нет.
## 7. Финальные gates и состояние SDD-артефактов
Backend `npm run verify` — passed (`/tmp/bank-cards-verify.log`): lint без
warnings, app/test TypeScript, 33 suites / 253 tests, Prisma generate и build.
Последующий итоговый HTTP-прогон и independent review описаны ниже;
успех gates не является заменой semantic review.
После независимого review исправлен P2 в DTO: явный `null` для Money,
архива и non-null полей отклоняется; nullable реквизиты очищаются по-прежнему.
Configured HTTP/SQLite — 19/19 passed, включая 400 без изменения данных/версий;
OpenAPI parity на уникальной базе — passed, wire shape не изменился.
Итоговый `npm run verify` повторно passed
(`/tmp/bank-cards-final-backend-verify.log`). Сохранённый runnable durability
сценарий повторно passed на итоговом backend:
`/var/folders/j_/h2ft23gd3qq35ps3v5bcftfm0000gn/T/wealthledger-bank-durability-dXSTmz/result.json`.
Reviewer перепроверил исправление и не нашёл других backend findings.
Gate применяется к итоговому состоянию соответствующего repo. Уже прошедшие
проверки не повторяются без новых изменений, сбоев или оставшегося риска.
Если финальная правка меняет проверенную границу, повторяется её нужный gate.
- [x] 7.1 [backend] После разделов 1–3 выполнить canonical backend gate и сохранить результаты DB/HTTP/durability проверок для того же состояния кода.
- **Covers:** `N/A` — обязательная проверка backend delivery из Design, дополняющая уже покрытые поведенческие и DB cases без новой возможности.
- **Verification:** `npm run verify`; отдельные результаты 3.3–3.4 остаются применимы к итоговому коду. Type/lint/unit/build не объявляются доказательством транзакций или миграции, если DB-проверка не запускалась успешно.
- [x] 7.2 [frontend] После 6.1 выполнить canonical frontend gate для итоговых API types, UI, i18n, stories и dependency boundaries.
- **Covers:** `N/A` — обязательный frontend delivery gate из Design, без добавления самостоятельного пользовательского поведения.
- **Verification:** `npm run verify` включает OpenAPI drift, typecheck, lint, coverage, Knip, dependency boundaries и обе сборки. Отдельный результат браузерной проверки 6.1 сохраняет применимость; процент покрытия и Storybook build не заменяют его.
- [x] 7.3 [workspace] После 4.1, 7.1 и 7.2 подтвердить двусторонний runtime OpenAPI contract по итоговому backend и frontend snapshot/types.
- **Covers:** `N/A` — межрепозиторная verification из Contract Gate Decision, без владения кодом приложений или отдельным поведением.
- **Verification:** из backend выполнить `npm run test:openapi-contract`; использовать актуальный frontend `openapi:check` из 7.2, не повторяя его при неизменном snapshot/types. Проверка не требует T-Bank и не изменяет snapshot автоматически.
- [x] 7.4 [docs] После 6.2 выполнить canonical docs gate, ручную смысловую сверку и проверку затронутых маршрутов чтения.
- **Covers:** `N/A` — проверка связности as-built документации из Delivery Map, без самостоятельного нормативного поведения.
- **Verification:** из docs выполнить `npm run verify`; проверить новые Markdown-файлы и затронутые внешние относительно docs ссылки. Если изменены Mermaid-диаграммы, отдельно проверить доступным локальным renderer их вид либо явно сохранить ограничение; новый стек для этого не устанавливать.
- [x] 7.5 [specs] После всех implementation-owned задач сверить поставленный результат со всеми Delta Requirements и обновить этот Tasks только по выполненным действиям и наблюдённым проверкам, затем выполнить canonical validation.
- **Covers:** `N/A` — поддержание существующих SDD-артефактов и traceability реализации из Delivery Map; новые требования, архитектура и отдельный журнал доказательств не создаются.
- **Verification:** каждое требование `manual-bank-cards` связано с выполненной задачей и подходящим доказательством; все оставшиеся ограничения названы. Выполнить `openspec validate add-manual-bank-cards --type change --strict --store wealth-ledger-specs`, `openspec validate --specs --strict --store wealth-ledger-specs` и `git diff --check`, отдельно просмотрев новые файлы и ссылки. Approval и завершение реализации не выводятся только из наличия файлов.
Независимая проверка результата и завершение lifecycle выполняются после
implementation-owned задач; они не маскируются checkbox, который исполнитель
не может закрыть сам. Этот план не включает преждевременную синхронизацию
нового capability в основные specs, архивирование или изменение истории.
Такие действия относятся к последующему завершению выбранного SDD lifecycle.
Итоговые результаты 7.2–7.4: frontend `npm run verify` — passed
(`/tmp/bank-cards-final-frontend-verify.log`): OpenAPI drift, TypeScript,
Biome, 43 files / 263 tests с coverage, Knip, обе dependency проверки,
app и Storybook builds. Backend `npm run test:openapi-contract` — passed
на уникальной базе (`/tmp/bank-cards-contract.log`); последний DTO fix
не меняет wire shape, frontend snapshot/types после этого не изменялись.
Docs `npm run verify` — passed, 54 Markdown-файла, 0 issues.
Независимое ревью backend/frontend/docs завершено: найденные null-validation,
category-form и auxiliary-query recovery дефекты исправлены и перепроверены;
открытых actionable findings нет. Обычный Prisma fresh migrate deploy без
`RUST_LOG=info` по-прежнему имеет указанное в 1.1 ограничение; причина
не установлена. Подтверждён только штатный путь с переменной окружения.
Предупреждение Storybook/Vite о размере chunks не препятствует сборке.
Результат 7.5: все 17 Delta Requirements сверены с реализацией и Covers/проверками
выше; 29 implementation-owned задач завершены. `openspec validate
add-manual-bank-cards --type change --strict --store wealth-ledger-specs` — valid;
`openspec validate --specs --strict --store wealth-ledger-specs` — 25 passed,
0 failed. Итоговый routing повторён по modified/untracked файлам четырёх repo;
новые файлы и ссылки просмотрены, `git diff --check` — passed в каждом repo.
Proposal, Delta Specs, Design, основные specs и архивы не изменялись.
Последующие проверки перед commit: по запросу пользователя все четыре нативных
поля дат/месяца заменены на Mantine DateInput/MonthPickerInput. Сохранение
календарных дат, month/URL и календарь на 320 px проверены в браузере
на новой временной базе; будущие даты недоступны. Полный frontend
`npm run verify` повторно passed: 43 files / 263 tests и обе сборки
(`/tmp/bank-mantine-verify.log`).
После отдельного разрешения пользователя миграция применена к рабочей SQLite
через `RUST_LOG=info npx prisma migrate deploy`. До применения создан
согласованный SQLite backup; после — проверены integrity/FK и неизменность
User/Session/TbankConnection. Это обновление рабочей среды, не тестовые записи
в пользовательской базе. Backup исключён из Git; ограничение обычного запуска
Prisma без `RUST_LOG=info` остаётся прежним.
@@ -0,0 +1,2 @@
schema: wealth-ledger-sdd
created: 2026-09-16
@@ -0,0 +1,31 @@
# Completion Evidence: containerized-application-delivery
## Outcome
The local and disposable single-server container delivery path is implemented
and verified. External publication and target-server gates remain unavailable in
the current environment and are recorded as blockers rather than represented as
successful results.
## Observed Results
- Frontend and backend development/production images build and run locally.
- Development HMR/watch, same-origin API routing, production SPA fallback,
readiness, persistence, migration failure handling, backup and isolated
restore were verified on disposable data.
- Docs verification and Compose configuration checks passed.
- No production data, secrets, registry credentials or real server were used.
## External Blockers
The following approved implementation tasks remain externally blocked:
- Docker-capable Gitea runner acceptance;
- registry publisher and server pull-only credentials;
- authenticated candidate publication and digest pull;
- target-server Docker/Compose verification;
- first server rollout pair confirmation.
These tasks remain unchecked in the archived checklist. The change is archived
as externally blocked after owner authorization; this is not a claim that the
external gates passed and does not authorize production rollout.
@@ -0,0 +1,247 @@
## Context
Frontend и backend выпускаются из независимых Git-репозиториев. Frontend —
Vite SPA, backend — NestJS API с embedded SQLite, Prisma migrations, native
Node dependencies и runtime `.proto` contracts. Браузерная сессия использует
Bearer access token и Secure httpOnly refresh-cookie с path `/api/v1/auth`.
На production-сервере уже работает Caddy. Он должен знать только домен и один
loopback port WealthLedger; TLS и server-wide routing остаются вне приложения.
Краткий простой обновляемого компонента допустим. Текущие кэши, rate limiter и
connection generation допускают только один активный backend instance.
Root workspace связывает независимые checkout, но его CI по действующему
контракту выполняет только commitlint. Поэтому orchestration принадлежит root,
а сборка и публикация каждого image — репозиторию компонента.
## Goals / Non-Goals
**Goals:**
- запускать весь application runtime локально через Docker с Linux, общей
container network, единым browser origin и быстрым frontend/backend watch;
- поставлять отдельные immutable frontend и backend images;
- публиковать наружу только frontend port, через который доступны SPA и API;
- хранить SQLite и backend secrets вне images и сохранять их при recreation;
- применять Prisma migrations до готовности новой версии backend;
- обновлять frontend или backend отдельно с явной парой установленных версий;
- дать оператору воспроизводимые backup, rollback и restore boundaries.
**Non-Goals:**
- zero-downtime, blue-green, несколько backend replicas и высокая доступность;
- PostgreSQL, Kubernetes, container orchestration вне одного сервера;
- управление Caddy, DNS, TLS, host firewall или provisioning сервера;
- изменение HTTP API, session semantics или пользовательского интерфейса;
- общий release номер для независимых репозиториев.
## Decisions
Уточнение Change Owner от 2026-09-16: обязательная настройка digests отменена.
Production принимает `FRONTEND_IMAGE`/`BACKEND_IMAGE` с release version tags.
Публикация сохраняет запрет перезаписи release tags; `latest` не используется.
### 1. Docker Compose владеет application topology
Root workspace получает production Compose и development override. Production
Compose использует опубликованные image references; development override
заменяет их local development targets, подключает исходники и отдельное local
data storage. Новые custom orchestration scripts не создаются: lifecycle
выражается штатными Compose commands и документированной последовательностью.
Production содержит два long-running services:
- `frontend`: nginx со статической SPA и proxy `/api/` в `backend:3000`;
- `backend`: один NestJS process с доступом к persistent SQLite storage.
Одноразовый migration service использует тот же backend image и data storage,
завершается до старта новой версии backend. Наружу публикуется только nginx на
настраиваемый loopback host port. Caddy проксирует весь домен на этот port и не
маршрутизирует `/api/` отдельно.
Development сохраняет те же component и network boundaries, но frontend target
запускает Vite с proxy `/api/` в `backend:3000`, а backend target запускает Nest
watch. Browser видит только frontend port. Source mounts не включают host
`node_modules`; Linux dependencies принадлежат images/volumes. Изменение
manifest или lockfile требует rebuild соответствующего development image.
Отдельный nginx gateway в development не добавляется: он создал бы третий
постоянный service, но всё равно использовал бы отличающуюся proxy-конфигурацию
для Vite HMR. Production image и его nginx routing проверяются отдельным
production Compose smoke scenario.
### 2. Frontend и backend имеют собственные multi-stage images
Frontend image собирает SPA точной project Node version и переносит только
build output и nginx configuration в version-pinned nginx runtime. Production
SPA использует same-origin API base. nginx поддерживает SPA history fallback,
передаёт `/api/` без переписывания prefix и повторно разрешает Docker DNS имя
backend после recreation.
Backend image собирается под Linux точной project Node version. Runtime содержит
compiled application, production dependencies, generated Prisma client,
migrations и T-Bank `.proto` contracts по пути, который использует production
process. Prisma CLI становится явным runtime tool backend image; test, lint и
compiler tooling в runtime не переносится. Process работает не от root и пишет
только в выделенный data path.
Оба images получают OCI metadata с repository revision и version. Base images
и package installs закрепляются воспроизводимыми версиями; новая platform или
architecture не обещается без отдельной проверки.
### 3. Gitea Container Registry хранит независимые release images
Frontend и backend workflows публикуют собственный image после push защищённого
release tag `vX.Y.Z`. Publish job повторяет канонический repository gate,
проверяет совпадение tag с package version и только затем отправляет semver tags в Gitea Container Registry. `latest` не используется.
Server deployment фиксирует полные frontend/backend image references с
semver tags; оператор выбирает опубликованную версию каждого компонента. CI использует отдельный secret с
package write permission, сервер — отдельный credential только для pull. Ни один
credential не хранится в repository или image. Возможность встроенного Gitea
workflow token публиковать packages не предполагается без проверки фактической
server version.
Root не получает image workflow: каждый repository самостоятельно доказывает и
публикует свой artifact, а Compose только фиксирует установленную пару image references.
### 4. Same-origin routing сохраняет действующую auth boundary
Production frontend nginx и development Vite направляют исходный `/api/...` в
backend без изменения path. Frontend client использует relative API base.
Backend получает `FRONTEND_ORIGIN`, равный browser-visible origin; production
работает с `NODE_ENV=production`, поэтому refresh-cookie остаётся Secure.
nginx передаёт host, forwarding headers и request ID context, не журналирует
Authorization, Cookie или request body. Backend port доступен только Compose
network. Server host публикует frontend на loopback, чтобы Caddy оставался
единственным внешним listener.
### 5. Readiness опирается на существующий runtime contract
Migration service должен завершиться успешно до запуска backend. Backend
healthcheck подтверждает завершённый HTTP-ответ от существующего auth route;
статус `401` без cookie является допустимым доказательством готового HTTP/auth
boundary. Новый публичный health endpoint не добавляется. Frontend healthcheck
получает статический root и зависит от healthy backend при полном старте.
Compose `--wait` используется как bounded readiness gate. Он не является
автоматическим rollback и не обещает отсутствие краткого `502` во время
отдельного backend update или недоступность всей точки входа во время frontend
recreation.
### 6. SQLite остаётся single-server storage в явно ограниченной роли
Production использует один backend process и один persistent host data
directory. Это расширяет прежнее локальное решение ADR 001: docs получают новый
ADR, который разрешает SQLite только для текущего single-server deployment и
фиксирует условия пересмотра — несколько replicas, иной availability target,
рост write contention или потребность в отдельном database service.
Перед migration backend останавливается и создаётся согласованная копия всего
data directory. Остановка исключает конкурентные writes и позволяет сохранить
database вместе с возможными WAL companion files без нового backup service.
Encryption key хранится отдельно от data backup; восстановление требует оба
артефакта.
Local development использует другой named volume/data directory и другие
secrets. Production data никогда не монтируется в development environment.
### 7. Обновления меняют только выбранный service
Frontend update меняет только frontend image reference, загружает image и
recreates frontend без зависимостей. Backend и data остаются запущенными, но
единая внешняя точка входа кратко недоступна во время recreation.
Backend update сначала сохраняет backup при остановленном backend, затем меняет
backend image reference. Migration service и backend используют один reference; backend
стартует только после успешной migration и становится доступен после healthcheck.
Frontend не пересоздаётся. nginx повторно разрешает service DNS после смены
backend container address.
Release compatibility остаётся обязанностью API evolution: новая сторона
ставится первой только когда прежняя сторона совместима с ней. Compose-файл
фиксирует пару image references, но не вводит второй version negotiation mechanism.
## UI/UX Applicability and Package
**Applicability and scope:** `N/A` — change меняет development, packaging и
server operations, но не экраны, состояния, interactions или responsive
поведение WealthLedger. Новый визуальный пакет не требуется.
## Delivery Map
| Area | Status | Architectural outcome or N/A reason |
| --- | --- | --- |
| frontend | `affected` | Multi-stage development/production image, same-origin client configuration, Vite API proxy, nginx SPA/API routing и image publication workflow. |
| backend | `affected` | Multi-stage development/production image, runtime Prisma migration assets, container readiness и image publication workflow. |
| specs | `affected` | Новая capability `application-delivery` и утверждаемый SDD package. |
| docs | `affected` | Deployment/runbook, обновлённая container topology и ADR ограниченного single-server SQLite deployment. |
Root workspace дополнительно владеет Compose orchestration и кратким entrypoint
README как cross-area coordination; это не переносит component build ownership
из frontend/backend и не добавляет root CI помимо commitlint.
## Solution Assessment
| Вариант | Fit и зрелость | Security / operations | Ограничения и migration cost | Решение |
| --- | --- | --- | --- | --- |
| Docker Compose + два component images | Нативно покрывает один server, service DNS, healthchecks, volumes и targeted recreation; широко поддерживается | Малый operating surface; registry credentials и host Docker требуют обычной защиты | Краткий простой, один host, нет automatic rollback | Выбран как минимальное полное решение |
| Один объединённый application image | Простая установка одного artifact | Один lifecycle и один process boundary проще оператору | Связывает независимые релизы, пересобирает неизменённый component, усложняет watch и нарушает repository ownership | Отклонён |
| Kubernetes или Swarm | Зрелая orchestration для replicas и rolling updates | Добавляет control plane, политики и постоянную эксплуатацию | Не нужен одному server и не решает текущие SQLite/multi-instance ограничения | Отклонён до появления HA требований |
| Caddy маршрутизирует отдельно frontend и backend | Использует существующий server proxy | Убирает internal gateway layer | Server должен знать внутреннюю topology, появляются два application upstream и deployment перестаёт быть self-contained | Отклонён по выбранной внешней границе |
| Отдельный постоянный gateway service внутри Compose | Стабильный port при frontend recreation | Может уменьшить внешний простой frontend update | Третий runtime service и конфигурация без требования zero-downtime | Отклонён; пересмотреть при SLO без краткого простоя |
| SQLite на persistent directory | Уже реализован, зрел для single-process embedded workload, не требует database service | Минимальная сеть и credentials; backup требует остановки | Один writer/process topology, нет HA; новая ADR boundary обязательна | Выбран в ограниченной single-server роли |
| PostgreSQL service | Зрел для concurrency, replicas и независимого DB lifecycle | Требует отдельные credentials, backup, upgrades и monitoring | Миграция Prisma provider и существующих данных вне scope; текущая нагрузка не требует | Отклонён до появления multi-instance/availability требования |
| Собственная packaging/orchestration layer | Может точно повторить частные команды | Создаёт собственный security и maintenance surface | Дублирует Compose, registry и standard image lifecycle | Отклонён |
Gitea Container Registry и Actions используются как уже доступные mature
platform capabilities. Предварительное условие Apply — runner с Docker build/push
capability и package permissions; при отсутствии этого условия Design не
заменяется самописным registry uploader, а publish task остаётся blocked.
## Risks / Trade-offs
- Frontend recreation кратко отключает и SPA, и API ingress; это принято
текущим non-goal zero-downtime.
- Backend update может кратко возвращать `502`; healthcheck ограничивает окно,
но не устраняет его.
- Старые browser tabs могут запрашивать chunks прежнего frontend. nginx задаёт
revalidation для HTML и immutable cache для hashed assets, но прежние assets
после recreation не сохраняются. Пользователю может потребоваться reload;
автоматическое восстановление старых вкладок остаётся вне текущего scope.
- SQLite на server расширяет прежний local-only baseline. Новый ADR делает это
осознанным ограничением, а не молчаливым изменением production guarantee.
- Воспроизводимость version tags зависит от запрета их перезаписи: workflow
отклоняет существующий tag. Переназначать опубликованные tags нельзя.
- Development работает через Vite, production — через nginx. Production Compose
smoke проверяет различающийся web-server boundary до поставки.
- Полная runtime image воспроизводимость зависит от base-image availability;
digests и сохранённые release images уменьшают, но не устраняют registry risk.
## Migration Plan
1. Утвердить SDD package и новый ADR о границе SQLite на одном сервере.
2. Подготовить component images и проверить их отдельно с каноническими gates.
3. Добавить root Compose topology и development override; подтвердить watch,
same-origin API, отсутствие внешнего backend port и изоляцию local data.
4. Добавить release-tag publication в component repositories и получить version tags
проверенных images из Gitea Registry.
5. На server создать restricted environment/secrets и data directories,
сохранить исходную базу и encryption key, затем выполнить первый migration
service и production smoke через loopback port.
6. Настроить Caddy на единственный loopback port и проверить login, refresh,
чтение существующих данных и один внешний financial request.
7. Зафиксировать установленную пару image references и удалить прежний runtime
только после успешной проверки нового.
### Rollback Contract
| Field | Decision |
| --- | --- |
| Status | `required` |
| Reason | Change вводит server persistence, migrations, secrets и новую delivery boundary. |
| Trigger | Migration failure, unhealthy component, невозможность login/refresh, нечитаемое encrypted connection, повреждение данных или критическая регрессия smoke scenario. |
| Actions | Не направлять Caddy на первый deployment либо вернуть предыдущий component image reference. Старую версию backend запускать только после проверки совместимости с текущей schema. Если schema несовместима, остановить приложение и отдельно предложить восстановление согласованного backup; не выполнять restore автоматически. |
| Data safety | Backup создаётся при остановленном backend из всего data directory; encryption key сохраняется отдельно. Данные, созданные после backup, считаются более новыми и не заменяются без явного решения оператора о потере этого интервала. |
| Verification | Проверить component health, schema migration status, login/refresh, чтение существующих user/card data, decrypt/use сохранённого T-Bank connection и отсутствие повторной migration ошибки; затем записать фактически установленную пару image references. |
@@ -0,0 +1,46 @@
## Why
WealthLedger сейчас запускается как набор локальных процессов, а поставка на
сервер не определена. Разработка не воспроизводит границы runtime-среды, а
оператор не имеет согласованного способа установить, обновить, сохранить или
восстановить приложение.
Нужен единый поддерживаемый путь запуска для локальной разработки и одного
сервера, чтобы поставка каждой версии была воспроизводимой и не требовала
ручного пересоздания окружения.
## What Changes
Будет определён поддерживаемый способ запускать WealthLedger локально и на
одном сервере. Внешняя серверная маршрутизация передаёт весь трафик
приложения на одну точку входа WealthLedger; приложение само обслуживает
интерфейс и API.
Оператор сможет независимо обновлять интерфейс или API согласованными
версиями поставки. Пользовательские данные, сессии и секреты сохраняются
между штатными перезапусками и обновлениями. Для обновления и восстановления
будут определены необходимые безопасные действия и их границы.
Не входят непрерывное обновление без краткого простоя, несколько экземпляров
API, высокая доступность, смена хранилища данных, изменение внешнего reverse
proxy и управление серверной инфраструктурой вне WealthLedger.
## Capabilities
### New Capabilities
- `application-delivery`: поддерживаемый локальный и серверный запуск,
обновление компонентов и сохранность состояния WealthLedger.
### Modified Capabilities
Нет.
## Impact
Пользователь получает одинаково работающую среду разработки и поставленное
приложение. Оператор получает воспроизводимую установку, независимое
обновление частей приложения и понятные границы восстановления данных.
Изменение затронет frontend, backend, specs, docs и общую orchestration
workspace; детали решений и delivery будут определены в последующих
утверждаемых артефактах.
@@ -0,0 +1,105 @@
## Purpose
Определяет поддерживаемые эксплуатационные свойства локального и серверного
запуска WealthLedger, включая поставку компонентов и сохранность состояния.
## ADDED Requirements
### Requirement: Локальная среда воспроизводит границы поставленного приложения
Поддерживаемый локальный запуск SHALL предоставлять один browser origin для
интерфейса и API WealthLedger. Разработка интерфейса MUST сохранять быстрый
цикл обратной связи при изменении исходного кода, а разработка API MUST
перезапускать затронутый runtime без ручной пересборки всей среды. Локальные
данные MUST быть изолированы от server data.
#### Scenario: Разработчик открывает локальное приложение
- **WHEN** поддерживаемая локальная среда успешно запущена
- **THEN** браузер получает интерфейс и обращается к API через тот же origin,
не зная внутреннего адреса API
#### Scenario: Разработчик меняет интерфейс
- **WHEN** разработчик изменяет исходный код интерфейса в запущенной локальной
среде
- **THEN** браузер получает изменение без ручной полной пересборки среды
### Requirement: Серверное приложение имеет одну внешнюю точку входа
Поддерживаемый server deployment SHALL принимать HTTP-трафик WealthLedger на
одной внешней точке входа. Через неё MUST быть доступны интерфейс и API с
сохранением их действующих путей и session semantics. API и хранилище данных
MUST NOT требовать отдельного внешнего сетевого доступа.
#### Scenario: Внешний reverse proxy направляет запрос приложению
- **WHEN** server reverse proxy передаёт запрос на единственную точку входа
WealthLedger
- **THEN** пользователь получает интерфейс, а запросы к действующему API
обрабатываются приложением без отдельного маршрута reverse proxy к API
### Requirement: Поставка допускает независимое обновление компонентов
Система SHALL предоставлять versioned delivery artifacts для интерфейса и API
как независимых компонентов. Оператор MUST иметь возможность установить
выбранную версию только одного компонента, сохранив другой компонент и
постоянное пользовательское состояние работающими. Поддерживаемая поставка
MUST явно фиксировать выбранные версии обоих компонентов.
#### Scenario: Обновляется только интерфейс
- **WHEN** оператор устанавливает новую версию интерфейса при неизменной
совместимой версии API
- **THEN** API и его постоянное состояние не пересоздаются, а приложение
начинает отдавать новую версию интерфейса
#### Scenario: Обновляется только API
- **WHEN** оператор устанавливает новую версию API при неизменной совместимой
версии интерфейса
- **THEN** интерфейс не пересоздаётся, а приложение начинает обрабатывать
запросы новой версией API после её готовности
### Requirement: Постоянное состояние и секреты переживают штатный lifecycle
Пользовательские данные, session state и ключи, необходимые для чтения
зашифрованных подключений, MUST сохраняться при штатных перезапусках и
обновлениях компонентов. Секреты MUST задаваться отдельно от versioned delivery
artifacts и MUST NOT попадать в интерфейс, журналы или публичный HTTP-ответ.
#### Scenario: Приложение штатно перезапускается
- **WHEN** оператор штатно перезапускает приложение без восстановления данных
- **THEN** после запуска доступны прежние пользовательские данные и действующие
session semantics, а сохранённые зашифрованные подключения остаются читаемыми
тем же ключом
### Requirement: Обновление API сохраняет целостность данных
Перед тем как новая версия API начинает принимать запросы, необходимые
изменения схемы постоянных данных MUST быть применены успешно. При ошибке
изменения данных новая версия API MUST NOT объявляться готовой. Краткий простой
обновляемого компонента допустим; непрерывность обслуживания и несколько
одновременных экземпляров API не являются обещанием этой capability.
#### Scenario: Подготовка данных новой версии не прошла
- **WHEN** обязательное изменение постоянных данных завершается ошибкой
- **THEN** новая версия API не начинает обслуживать запросы, а оператор получает
диагностируемый неуспешный результат обновления
### Requirement: Восстановление не выдаёт откат приложения за откат данных
Поддерживаемая процедура обновления и восстановления MUST различать возврат
версии компонентов и восстановление пользовательских данных. Она MUST требовать
согласованную резервную копию перед изменением постоянных данных и MUST NOT
заменять более новые пользовательские данные прежней копией как побочный эффект
возврата версии приложения.
#### Scenario: Оператор возвращает предыдущую версию API
- **WHEN** оператор возвращает прежнюю версию API после неуспешного обновления
- **THEN** процедура явно проверяет совместимость этой версии с текущими
данными и не выполняет восстановление старой копии данных без отдельного
решения оператора
@@ -0,0 +1,219 @@
# Контейнерная разработка и поставка WealthLedger
Основание — утверждённые [Proposal](proposal.md),
[Delta Specs](specs/application-delivery/spec.md) и [Design revision 1](design.md).
UI/UX package неприменим: пользовательские экраны и interactions не меняются.
Checkbox закрывается только после выполнения действия и его Verification.
Перед Node-командами активируется `.nvmrc` соответствующего независимого
репозитория. Git-команды выполняются из репозитория-владельца; production data,
secrets и опубликованные release tags не используются как test fixtures.
Задачи выполняются по порядку разделов. Frontend 2.1–2.2 и backend 3.1–3.2
могут идти параллельно после 1.2. Workspace orchestration начинается после
готовности обоих component images. Реальный server rollout выполняется только
после локального production smoke и документированного backup.
## 1. SDD и внешние prerequisites
- [x] 1.1 [specs] Проверить утверждённый SDD package закреплённым OpenSpec CLI и сохранить canonical change структурно валидным до Apply.
- **Covers:** `N/A` — обязательный gate canonical SDD authority; он не добавляет runtime behavior, но разрешает реализацию утверждённых требований и Design.
- **Verification:** с версией из `openspec/.cli-version` выполнить `openspec schema validate wealth-ledger-sdd --verbose`, `openspec validate containerized-application-delivery --type change --strict --store wealth-ledger-specs`, `openspec validate --specs --strict --store wealth-ledger-specs`, затем `npm run verify`.
- **Result (2026-09-16):** по явному уточнению владельца использован установленный OpenSpec 1.13.0 вместо 1.10.0. Все четыре проверки прошли; 25 specs passed, 0 failed. Repository-wide pin не изменён.
- [ ] 1.2 [workspace] Подтвердить prerequisites выбранного Solution Assessment: Docker Compose на development/server hosts, Docker-capable Gitea runner, Container Registry и раздельные package write/read credentials.
- **Covers:** `N/A` — техническое предусловие утверждённой Compose/Registry delivery; оно не вводит самостоятельное поведение приложения и блокирует custom fallback при отсутствии platform capability.
- **Verification:** зафиксировать поддерживаемые Docker/Compose и Gitea versions; `docker version`, `docker compose version`, успешные authenticated registry login для CI publisher и server pull-only identity без вывода credentials.
- **Partial result (2026-09-16):** локально Docker Desktop 4.91.0, Engine 29.8.0 (Linux arm64), Compose 5.5.1; Gitea API сообщает 1.27.3. Server Docker/Compose и Docker-capable runner пока не подтверждены. MCP identity не имеет доступа к Actions secrets (`user should be the owner of the repo`); раздельные publisher/pull-only login не проверены. Publication и server rollout остаются внешними gates; независимая локальная реализация продолжается.
## 2. Frontend image и ingress
- [x] 2.1 [frontend] Добавить development и production image targets: Dockerized Vite watch с same-origin `/api/` proxy и nginx runtime со SPA fallback, API proxy, runtime DNS re-resolution, cache policy и non-root-compatible filesystem.
- **Covers:** `application-delivery` — `Локальная среда воспроизводит границы поставленного приложения`; `application-delivery` — `Серверное приложение имеет одну внешнюю точку входа`.
- **Verification:** собрать оба targets; development container через container network отдаёт HMR и сохраняет `/api/v1/...`; production container отдаёт SPA/deep link, проксирует API без rewrite, не раскрывает backend port и после backend recreation подключается к новому container address.
- [ ] 2.2 [frontend] Добавить release-tag workflow публикации frontend image с проверкой версии, canonical frontend gate, OCI revision metadata и immutable semver tags без `latest`.
- **Covers:** `application-delivery` — `Поставка допускает независимое обновление компонентов`.
- **Verification:** workflow syntax принимается Gitea; controlled candidate run после `npm run verify` публикует image, registry возвращает digest, clean pull по digest запускает тот же static artifact; credential отсутствует в logs и image history.
- [x] 2.3 [frontend] Выполнить frontend repository completion gate после packaging и routing changes.
- **Covers:** `N/A` — repository-owned проверка реализации Design и отсутствие regression в существующем frontend; отдельного observable requirement не создаёт.
- **Verification:** `npm ci`, `npm run verify`, `git diff --check`; вручную просмотреть новые Docker/nginx/workflow files и отсутствие secrets.
### Результаты frontend (2026-09-16)
- 2.1: оба targets собраны на Linux arm64; Vite HMR WebSocket connected,
`/api/v1/probe?check=path` сохранён. nginx read-only + tmpfs отдаёт deep link
с `no-cache`, proxy сохраняет path/query/host/proto/request ID. Backend без
host port пересоздан с другим IP (.2 → .5); nginx восстановил proxy без restart.
- 2.2: release-tag workflow реализован с `ubuntu-latest` (label подтверждён
владельцем). Реальный runner Docker access, registry write/read credentials,
Gitea workflow acceptance и controlled publication пока не проверены.
- 2.3: `npm ci`, `npm run verify`, `git diff --check` прошли. Docker context
ограничен allowlist; `.env`, credentials и host dependencies не копируются.
Новые Docker/nginx/workflow files просмотрены; publication secrets идут
только в отдельный Docker login, не в image build arguments.
## 3. Backend image, migration и readiness
- [x] 3.1 [backend] Добавить development и production image targets с Linux native dependencies, compiled application, generated Prisma client, runtime Prisma CLI/migrations, T-Bank proto assets, non-root process и выделенным data path.
- **Covers:** `application-delivery` — `Локальная среда воспроизводит границы поставленного приложения`; `application-delivery` — `Постоянное состояние и секреты переживают штатный lifecycle`; `application-delivery` — `Обновление API сохраняет целостность данных`.
- **Verification:** development target выполняет Nest watch; production target стартует из чистого image, находит proto contracts, принимает config только из environment, пишет только в data path; одноразовый `prisma migrate deploy` проходит на fresh и representative existing SQLite, а приложение не стартует после контролируемой migration failure.
- [ ] 3.2 [backend] Добавить release-tag workflow публикации backend image с проверкой версии, полными backend gates, OCI revision metadata и immutable semver tags без `latest`.
- **Covers:** `application-delivery` — `Поставка допускает независимое обновление компонентов`.
- **Verification:** workflow syntax принимается Gitea; controlled candidate run выполняет `npm run verify` и `npm run test:e2e`, публикует image и получает digest; clean pull по digest выполняет migration и production start, credential отсутствует в logs и image history.
- [x] 3.3 [backend] Выполнить backend repository completion gates и проверить container readiness без нового публичного endpoint.
- **Covers:** `N/A` — repository-owned проверка packaging, existing auth boundary и SQLite integration из Design; самостоятельного нового API behavior нет.
- **Verification:** `npm ci`, `npm run verify`, `npm run test:e2e`, `git diff --check`; healthcheck принимает завершённый HTTP-ответ existing auth route, а stopped/uninitialized process остаётся unhealthy.
## 4. Workspace Compose orchestration
- [x] 4.1 [workspace] Добавить production Compose topology с version-tagged frontend/backend references, migration service, healthchecks, persistent host data directory, internal-only backend и единственным configurable frontend loopback port.
- **Covers:** `application-delivery` — `Серверное приложение имеет одну внешнюю точку входа`; `application-delivery` — `Поставка допускает независимое обновление компонентов`; `application-delivery` — `Постоянное состояние и секреты переживают штатный lifecycle`; `application-delivery` — `Обновление API сохраняет целостность данных`.
- **Verification:** `docker compose config` с non-secret fixture environment; rendered config содержит один host port, один backend replica, одинаковый backend image reference у migration/runtime и отдельный data mount; missing required image/config/secret values дают blocking configuration/start failure.
- [x] 4.2 [workspace] Добавить development Compose override с local image targets, source watch/mounts, container-owned dependencies, отдельным local data volume и одним frontend browser origin.
- **Covers:** `application-delivery` — `Локальная среда воспроизводит границы поставленного приложения`; `application-delivery` — `Постоянное состояние и секреты переживают штатный lifecycle`.
- **Verification:** с чистыми local volumes запустить Compose; frontend edit появляется через HMR, backend edit перезапускает Nest, `/api/v1/...` идёт через frontend origin, host `node_modules` не используется, restart сохраняет local data, production data path отсутствует в rendered config.
- [x] 4.3 [workspace] Обновить root tracking allowlist и README entrypoints без custom scripts или нового root CI, затем проверить orchestration configuration.
- **Covers:** `N/A` — root только связывает независимые component repositories и объясняет стандартные Compose entrypoints согласно Design; runtime behavior реализуется component images и Compose tasks 4.1–4.2.
- **Verification:** `docker compose config` для production и development variants, `git diff --check`, просмотр `git status --short` и staged paths подтверждают только явно разрешённые root files; root workflow остаётся только commitlint.
## 5. Эксплуатационная документация
- [x] 5.1 [docs] Добавить ADR об ограниченном single-server SQLite deployment и обновить container architecture фактической Docker/Caddy topology, secret и persistence boundaries.
- **Covers:** `application-delivery` — `Серверное приложение имеет одну внешнюю точку входа`; `application-delivery` — `Постоянное состояние и секреты переживают штатный lifecycle`.
- **Verification:** ADR явно связывает решение с прежним local-only baseline, одним backend instance и triggers пересмотра; architecture diagram показывает только frontend port наружу, internal backend и persistent SQLite; ссылки и термины согласованы.
- [x] 5.2 [docs] Описать development, first deployment, independent component update, stopped-backend backup, compatibility check, rollback и explicit restore runbooks.
- **Covers:** `application-delivery` — `Локальная среда воспроизводит границы поставленного приложения`; `application-delivery` — `Поставка допускает независимое обновление компонентов`; `application-delivery` — `Обновление API сохраняет целостность данных`; `application-delivery` — `Восстановление не выдаёт откат приложения за откат данных`.
- **Verification:** dry-run runbook на disposable data проходит без неописанного шага; frontend-only и backend-only paths не пересоздают другой component; restore требует отдельного решения и сообщает о потере данных после backup.
- [x] 5.3 [docs] Выполнить docs repository completion gate.
- **Covers:** `N/A` — repository-owned проверка durable as-built documentation и ссылок без нового runtime behavior.
- **Verification:** `npm ci`, `npm run verify`, `git diff --check`; проверить Mermaid rendering, взаимные ADR/architecture/runbook links и отсутствие credentials или host-private values.
## 6. Сквозная проверка поставки
- [x] 6.1 [workspace] Проверить development Compose на чистом checkout и production Compose на локально собранных release-equivalent images.
- **Covers:** `application-delivery` — `Локальная среда воспроизводит границы поставленного приложения`; `application-delivery` — `Серверное приложение имеет одну внешнюю точку входа`.
- **Verification:** development подтверждает HMR/watch и same-origin login; production smoke подтверждает SPA deep link, registration/login/refresh, API proxy, отсутствие host backend port и clean startup через migration service.
- [x] 6.2 [workspace] На disposable representative SQLite проверить persistence, frontend-only update, backend-only update, migration failure и rollback boundaries.
- **Covers:** `application-delivery` — `Поставка допускает независимое обновление компонентов`; `application-delivery` — `Постоянное состояние и секреты переживают штатный lifecycle`; `application-delivery` — `Обновление API сохраняет целостность данных`; `application-delivery` — `Восстановление не выдаёт откат приложения за откат данных`.
- **Verification:** restart сохраняет user/session/card data и decryptable test connection; frontend update не recreates backend/data, backend update не recreates frontend; failed migration не делает backend healthy; возврат совместимого image сохраняет current data, а restore отдельной backup copy выполняется только в изолированном scenario с явной проверкой потерянного интервала.
- [ ] 6.3 [workspace] Проверить release images из Gitea по выбранным version tags перед первым server rollout и сохранить точную установленную пару версий.
- **Covers:** `application-delivery` — `Поставка допускает независимое обновление компонентов`; `application-delivery` — `Постоянное состояние и секреты переживают штатный lifecycle`.
- **Verification:** pull-only server identity получает оба versioned images; `docker image inspect` подтверждает expected revisions; production smoke проходит на этих images с disposable config/data, после чего оператор записывает выбранные image references без secrets.
## Apply verification — 2026-09-16
### Backend (3.1–3.3)
- Development и production targets собраны на Linux arm64 / Node 24.21.0.
Node/nginx base images закреплены digest; OS packages — точными версиями.
Чистый production runtime обнаружил неверный прежний fallback proto path:
Nest копирует assets в `dist/src`; loader теперь использует `__dirname`.
Nest watch использует `ps`; `procps` добавлен только в development target.
- `npm ci`, `npm run verify` — PASS (35 suites). `npm audit` при переносе
существующего Prisma 7.10.0 в runtime dependencies — 0 vulnerabilities.
- `npm run test:e2e` на macOS дважды завершился native Node SIGABRT
(`RemoveEnvironmentCleanupHook`, `env != nullptr`). Тот же штатный gate
выполнен в Linux development image: **8 suites / 98 tests PASS**. Это
Linux delivery proof; macOS native cleanup limitation не скрывается.
- `npm run test:container -- wealthledger-backend:production` — runnable
image regression: fresh/existing migration, runtime без source/.env/compiler,
non-root + read-only filesystem, существующий auth readiness, user/session
persistence. Он же вызывается publish workflow после clean pull.
- Controlled broken SQLite migration возвращает non-zero; backend остаётся
остановленным. После возвращения исправного data path migration идемпотентна.
- 3.2 остаётся открытой: YAML и shell syntax проверены локально, но Gitea
acceptance, authenticated publication и pull по registry digest не выполнены.
### Compose, docs и lifecycle (4–6)
- Production/development `docker compose config` — PASS. Проверены один
loopback host port, backend scale 1, общий backend image/data mount у
migration/runtime, readiness dependency и отсутствие production bind mount
в development services. Пустая production environment блокирует config.
- `docker compose up -d --wait` — PASS для development и production на
локально собранных release-equivalent images. Production smoke использовал
временный override local image references; canonical config остаётся
digest-pinned. Реальный Gitea digest pull этим не подменяется.
- Development: source edit вызвал HMR event и изменённый HTTP module, backend
source edit — новый Nest process и готовый API. Временные изменения
восстановлены. Same-origin registration/login/refresh и data restart PASS.
- Clean checkout proof: отдельный HEAD export + task diff, без host .env и
node_modules, новый Compose project/volume; startup и HMR/watch PASS.
- Browser proof (bundled Playwright/Chrome): clean development и production
registration/login/reload-refresh PASS; все API requests same-origin,
httpOnly/path сохранены, production cookie Secure. Caddy/TLS вне локальной проверки.
- Production: SPA deep link, registration/login/refresh, неизменённый API path,
отсутствие backend host port, non-root read-only runtime — PASS.
- Representative disposable SQLite содержит user/session/card и connection,
зашифрованный настоящим TbankCrypto на вымышленном token. Restart, migrations
и independent recreation сохраняют данные и расшифровку без внешнего T-Bank.
- Отдельные local fixture image IDs проверили frontend-only/backend-only
update без recreation другого компонента. Возврат прежнего совместимого
backend image сохраняет запись после backup. Это compatibility fixture,
не обещание совместимости произвольных будущих релизов.
- Stopped-backend backup и isolated restore с отдельным project/port/data path
— PASS: login, карта до backup и decryptable connection доступны; запись
после backup отсутствует в восстановленной копии. Текущая база не заменялась.
- Docs: ADR 015 расширяет ADR 001; topology и runbook взаимно связаны.
`npm ci`, `npm run verify` — PASS (44 Markdown files), `git diff --check`
— PASS. Mermaid source семантически просмотрен, но renderer в доступном
окружении не найден; **5.3 оставлена открытой до проверки rendering**.
- Root Boy Scout/allowlist review: только Compose, два безопасных env examples,
README и .gitignore; новых root scripts/dependencies/CI нет. Source markers
watch checks восстановлены. Чужие runtime data и checkout changes сохранены.
- Docker disk был заполнен. После адресной очистки intermediates этой задачи
и отдельно разрешённой очистки unused build cache старше суток проверки
продолжены; чужие images/containers/volumes не удалялись.
### Оставшиеся внешние gates
- 1.2: server Docker/Compose, Docker capability runner `ubuntu-latest`,
раздельные package writer и server pull-only authenticated login.
- 2.2/3.2: реальные Gitea candidate workflows и публикация frontend/backend,
OCI revisions, registry digests, clean pull и отсутствие credentials в logs.
Нужны Actions secrets `REGISTRY_USERNAME`/`REGISTRY_TOKEN` в каждом component
repo и отдельная server identity. Workflow не публикует `latest` и fail-closed
отказывается от overwrite или недоказанного отсутствия tag.
- 6.3: smoke опубликованной пары version tags на disposable server config/data,
запись installed image references/revisions до первого server rollout.
Server rollout, Caddy, TLS и реальный финансовый запрос не выполнялись.
### Упрощение deployment configuration по запросу владельца
Обязательные `FRONTEND_DIGEST`/`BACKEND_DIGEST` и отдельные repository fields
заменены на `FRONTEND_IMAGE`/`BACKEND_IMAGE` с release version tags.
Design и runbook синхронизированы. Результаты выше описывают первоначальную
проверку; обязательный digest pinning более не является deployment contract.
Migration и backend по-прежнему используют один image reference.
Verification упрощения: production/development `docker compose config` PASS;
разные version tags выбираются независимо, migration/runtime получают один
backend image, один loopback port и development volume сохранены. Пустые
image/secret values блокируют config. Docs `npm run verify`, OpenSpec strict
change validation и `git diff --check` в affected repos — PASS.
По следующему запросу владельца из `FROM` удалены base image digests:
используются `node:24.21.0-bookworm-slim` и `nginx:1.28.0-alpine`.
Version tags не гарантируют неизменность содержимого базового image.
Verification: повторная сборка production/development targets обоих компонентов
— PASS (с использованием Docker cache). Dockerfile остаются в component repos,
чтобы standalone build и release CI не зависели от root checkout.
По запросу владельца удалены дополнительные commit tags из обоих workflows;
release tag защищён от перезаписи, revision остаётся в OCI label. Production
Compose наследует healthchecks из images; development содержит собственные
checks с прежними таймингами, migration отключает check.
Verification: production/development Compose config и invariants, healthchecks
локальных production images, shell syntax обоих workflows, docs verify,
OpenSpec strict validation и diff checks — PASS. Registry publication остаётся
внешней непроверенной частью задач 2.2/3.2.
@@ -0,0 +1 @@
schema: wealth-ledger-sdd
@@ -0,0 +1,440 @@
## Context
Локальная архитектура реализована; независимая публикация отложена. Baseline исследования — frontend `3c99deb`, версия `0.3.0`,
2026-09-20. Требования — в
[proposal](proposal.md) и [delta](specs/frontend-design-system/spec.md).
В приложении уже работают React, TypeScript, Vite, Mantine, TanStack Query,
RHF/Zod, JSON-токены, общий адаптер темы, Storybook, Vitest/RTL/MSW,
dependency-cruiser и Knip. Они сохраняются. Другие production-потребители
общего UI или клиентского SDK в исследованном workspace не обнаружены.
На 2026-09-22 workspace-пакеты `@wealthledger/tokens`, `@wealthledger/ui` и
`@wealthledger/icons` используются локально как private packages. Это принятая
часть перехода. Описанные далее независимый release и Nexus остаются
предложением, отложенным до оценки пилота. Фактическое выполнение и ограничения
сохраняются в [tasks](tasks.md).
## Goals / Non-Goals
**Goals:** три небольшие границы поставки общего фундамента; единый каталог
источников; сохранение внешнего вида и предметных инвариантов; потребление
собранного пакета; ограниченные задачи, исполнимые по одной сессии.
**Non-Goals:** другой UI framework, второй transport/store/form stack,
универсальные CRUD/table/form engines, монорепозиторий всего WealthLedger,
mobile adapter, тёмная тема, массовый rewrite CSS и тестирование внутренностей
Mantine. Календарь платежей требует отдельного продуктового запроса.
## Decisions
### 1. Репозиторий и направления зависимостей
Рекомендация: **npm workspaces внутри существующего frontend**. Корневое
приложение сохраняет `src`, Vite entry, собственные providers и `private: true`.
Новые пакеты располагаются в `packages/tokens`, `packages/icons`, `packages/ui`.
Имена `@wealthledger/*` приняты для локальных packages; регистрация scope
и внешняя registry-поставка не подтверждены.
| Вариант | Польза | Цена в этом проекте | Решение |
| --- | --- | --- | --- |
| Три workspace-пакета в frontend | Один lockfile, toolchain, Storybook и атомарный review источников с потребителем | Нужно явно расширить scopes проверок; локальный link может скрыть дефект поставки | Выбран; обязательна проверка packed consumer |
| Один отдельный design-system repo с тремя пакетами | Отдельный owner, release cadence, независимые потребители | Новый CI/catalog/install, синхронизация PR и доступа к registry при одном приложении | Отложить до второго независимого production consumer либо отдельного владельца/release cadence |
| Три отдельных Git repo | Полностью независимые права/жизненные циклы | Три workflow и совместимость без подтверждённой выгоды | Не выбирать |
| Только существующий shared | Уже работает, нет стоимости публикации | Не даёт запрошенной границы версионируемой поставки | Сохраняется до готовности package foundations, не конечная цель |
Направление стрелки — «потребляет»; циклов нет:
```text
frontend app / единственный Storybook
├─ @wealthledger/ui ── @wealthledger/tokens
│ └─ Mantine + React (peers у ui)
├─ @wealthledger/icons ── React (peer)
└─ entities/features/pages → shared/api, domain models, i18n
tokens → нет runtime dependencies
ui → нет icons dependency до появления реального общего consumer
icons → нет ui/tokens/Mantine/router/API dependencies
```
Общие компоненты принимают уже подготовленные строки и slots. `EntryCard`
принимает `ReactNode` и не знает, какой пакет рисует icon/action. Domain выбирает
цвет по смыслу и форматирует деньги; UI только отображает переданное значение.
### 2. Публичная поверхность и сборка
Имена ниже в колонке «Цель» — принятые публичные exports локальных пакетов.
Внутренние `src`, `dist/*` и application alias `@/` не являются package API.
| Пакет | Цель / entrypoints | Состав поставки | Зависимости |
| --- | --- | --- | --- |
| tokens | `@wealthledger/tokens/tokens.json` | Доверенный JSON v1; JS, CSS, React-адаптер и `.d.ts` на старте не нужны | Нет runtime dependencies |
| icons | Именованные `AddIcon`, `EditIcon`, `DeleteIcon` из `@wealthledger/icons` | ESM JS + declarations; CSS только если переносится собственная анимация | React peer + external; React DOM не используется |
| ui | `@wealthledger/ui`: `StateView`, `LoadingSkeleton`, `EntryCard` и их необходимые props types; `@wealthledger/ui/theme`: `theme`, `colorRoles`, `cssVariablesResolver`; `@wealthledger/ui/styles.css` | ESM JS + declarations + собранный CSS, включая CSS Modules | React/React DOM и используемые Mantine core/hooks — peers/external; tokens — точная dependency |
`designTokens` нужен foundations story: адаптер может сохранить существующий
одноимённый export из `ui/theme`, который ссылается на JSON, а не копирует его.
JSON остаётся отдельно доступным без Mantine. Не добавлять JS-обёртку в tokens
только ради импорта JSON. Не выдавать non-web adapter за готовый.
Использовать установленный Vite для library build ui/icons и TypeScript для
declarations. Один `tsc` недостаточен для CSS Modules `EntryCard`. Формат — ESM;
CJS/SSR build не нужен текущему Vite SPA. Настройки library build ещё предстоит
проверить на зафиксированной Vite 8.3.0; готовых конфигураций здесь нет.
Публикуемые `files`/`exports` ограничивают поверхность. CSS явно включается в
side effects; JS импорт сам не устанавливает provider и не читает приложение.
Library build использует отдельную конфигурацию нужных plugins, не наследует
app Vite config целиком: PWA/manifest/service worker и dev API proxy не должны
попасть в поставку библиотеки.
Exports указывают на собранные artifacts, поэтому обычные local/CI entrypoints
сначала собирают packages, затем выполняют consumer typecheck/tests/build.
Сейчас `verify` начинает проверки до app build; этот порядок потребуется
дополнить при 2.2. Acceptance начинается на чистом checkout после `npm ci`,
без старого package dist/cache. Достаточно последовательности npm scripts,
без собственного build orchestrator.
Docker — отдельный потребитель workspace: нынешний `Dockerfile` копирует только
root manifests перед `npm ci`, а `.dockerignore` не допускает packages.
В 2.1–2.2 нужны точечные manifests/source/build-config allowlists, установка
с workspace manifests и package build перед consumer. Host node_modules,
готовый локальный dist и реальные `.env` в контекст не попадают. Development
и production targets проверяются отдельно. В текущем Compose монтируются
только app src/public: изменения packages сначала требуют rebuild dev image.
Отдельный package bind-mount/watch в Compose отложен до фактической потребности;
host development может использовать package watch без изменения root repo.
Сначала peer ranges узкие, по проверенной комбинации React 19.3.0 и Mantine
9.6.1; расширять их только после проверки потребителя. React/React DOM
devDependencies для разработки не должны попадать в bundle. Проверять
`npm ls react react-dom @mantine/core` и содержимое bundle/packed consumer;
`dedupe` не заменяет корректную упаковку. Две несовместимые версии Mantine
также нельзя скрыть локальным symlink.
### 3. Токены: использовать уже существующий JSON
В [tokens.json](../../../../frontend/packages/tokens/tokens.json) уже есть
переносимые значения и описания. **Не менять формат v1 ради упаковки.**
`primitive → semantic → component` — модель владения и ссылок, а не повод
немедленно переименовать все ключи в новый стандарт.
| Уровень | Текущая основа и целевое правило |
| --- | --- |
| Primitive | `color.base`, `color.palettes`, `typography`, `spacing.values`, `radius.values`, `shadows.values`, `breakpoints.values`; исходные эффективные значения сохраняются |
| Semantic | Существующие `color.roles.*` с `color` + `shade` ссылаются на primitive; `primary` с `shade: null` сохраняет разрешение через primaryShade |
| Component | Пока новые не нужны. Добавлять только именованный совместно управляемый контракт реального общего компонента, если semantic role недостаточно; ссылаться на semantic, затем primitive при отсутствии смысловой роли |
Имена описывают роль (`surface`, `border`, `positive`), а не экран или случайный
цвет. Ссылки однонаправленные и разрешимые; обратных ссылок из primitive на
semantic/component нет. Новый color role включает назначение и ограничение
применения. Семантический положительный цвет не вычисляет знак финансовой суммы.
Сохранить единицы: размеры/тени в px адаптируются существующими `rem`/`rgba`,
breakpoints в em не масштабируются, line-height без единицы. Имеющиеся
[theme tests](../../../../frontend/packages/ui/src/theme.test.ts) защищают
именно преобразования и ссылки, не каждое значение JSON.
Общие CSS-роли `--wl-color-*` производит прежний `CSSVariablesResolver`.
Spacing/radius/type scales продолжают поступать в Mantine theme и его CSS
variables. Статическая генерация второго `tokens.css` не нужна: появился бы
ещё один адаптер и риск расхождения scale. JSON экспортируется без сборочного
token engine; Style Dictionary, DTCG-converter и runtime schema отложены до
конкретного внешнего потребителя/формата. Источник остаётся доверенным файлом,
не внешним пользовательским документом.
Светлая схема сохраняется. `primaryShade.dark` не означает готовую dark theme.
Бизнес-коды, суммы/валюты, московские даты, pagination limits, brand gradients
`BankCardFace` и случайная геометрия SVG не превращаются в глобальные токены.
Первое применение semantic roles — точные эквиваленты gray-3/gray-0/green-6/
red-6 в `EntryCard`; перенос и эта правка идут отдельно.
### 4. Mantine, тема и providers
`Stack`, `Group`, `Button`, `Paper`, `ActionIcon`, поля, Modal, Dates/Charts
остаются прямыми импортами Mantine. Изменение defaults решается темой.
Wrapper нужен только собственному контракту: busy region у `LoadingSkeleton`,
empty/error composition у `StateView`, layout/image fallback/actions у `EntryCard`.
App владеет MantineProvider, DatesProvider, QueryClientProvider, AuthProvider,
Router и i18n. Storybook имеет свой Mantine/Dates/i18n runtime с той же темой.
UI не создаёт `WealthLedgerProvider`, не импортирует локализацию продукта и
не включает app providers. `ui/styles.css` содержит существующую визуальную
основу и стили общих компонентов; app/preview подключают его явно после
Mantine CSS. Продуктовые route transitions и bank-card styles остаются в app.
Theme Modal transition и глобальная CSS animation сейчас сосуществуют.
Их визуальный конфликт не доказан. После выделения выбрать один механизм
открытия по browser comparison, предпочтительно уже настроенный Mantine
transition; нельзя удалить второй механизм на основании одного сходства кода.
### 5. Иконки и движение
Начальный набор — реально повторяющиеся plus/pencil/trash из Card/Cards views.
Имена `AddIcon`, `EditIcon`, `DeleteIcon` — предлагаемая поверхность, не найденные
exports. Размер задаёт `size` (default 24), геометрия сохраняет `viewBox`, цвет
получается через `currentColor`. Существующие размеры и strokeWidth 1.5/2
сохраняются у callers; выравнивание рисунка не совмещается с переносом.
Канонический исходник SVG для небольшого набора — **SVG-разметка в именованных
React-компонентах** `packages/icons/src/svg/<Name>.tsx`. Это один редактируемый
источник, без параллельных ручных `.svg` и `.tsx`. Отдельные raw SVG-файлы в
первой поставке не публикуются: non-React consumer пока отсутствует. Если
потребуются исходные `.svg` как самостоятельный продуктовый артефакт, перенести
канон в `src/svg/*.svg` и сравнить штатный SVGR CLI с ручным поддержанием;
не писать собственный генератор. Сейчас ни dependency, ни pipeline для этого
не нужны. JS/types несут SVG-компоненты, это явно отражено в составе поставки.
По умолчанию рисунок декоративный (`aria-hidden`, без фокуса). У standalone
meaningful icon потребитель задаёт доступное имя и image semantics. У
`ActionIcon` имя остаётся на действии; SVG не дублирует его. Общий пакет не
выбирает подпись «редактировать операцию» и не получает DTO.
`BankOperationIcon` и `operation-appearance.ts` остаются в entity: mapping
category/kind → рисунок/цвет/анимация — бизнес-логика. Собственная геометрия и
движение рисунка могут перейти в icons позже при реальном reuse. Hover/focus
trigger по сценарию, route transitions, bank/card state остаются приложению.
Существующий `bind-icon-animation` переиспользуется, если переносится тот же
animation contract; он не копируется в пакет ради трёх статичных иконок.
Все переносимые движения обязаны сохранять reduced-motion поведение.
### 6. Каталог источников компонентов
Порядок поиска: **существующие компоненты приложения → собственная библиотека
→ Mantine → уже разрешённые dependencies → обоснованное новое решение**.
Новый компонент появляется у владельца реальной ответственности, а не потому,
что текущий компонент отсутствует в Storybook. Таблица — компактный маршрут,
не каталог каждого JSX-узла. Номера обозначают этапы в [tasks](tasks.md).
| Назначение | Текущий источник | Целевой источник | Правило применения | Этап |
| --- | --- | --- | --- | --- |
| Layout, buttons, inputs, dialogs | `@mantine/core`, `@mantine/dates` | Те же пакеты | Прямые imports; variant/default через theme, не WL-копия каждого primitive | 1, 2 |
| Цветовые роли, шкалы | `shared/theme/tokens.json` | `tokens/tokens.json` + адаптер `ui/theme` | JSON — значения, адаптер — Mantine, бизнес-смысл — consumer | 2 |
| Тема/общие стили | `shared/theme` | `ui/theme`, `ui/styles.css` | Одна тема App/Storybook; providers у runtimes | 2 |
| Busy skeleton region | `shared/ui/LoadingSkeleton` | `ui` | Label и region общие, skeleton shapes принадлежат странице | 3.1 |
| Empty/error | `shared/ui/StateView` | `ui` | Строки/action props; query/error classification вне UI | 3.1 |
| Строка с изображением и значением | `shared/ui/EntryCard` | `ui` | Готовые strings/slots, fallback и responsive actions; без money parsing | 3.2–3.3 |
| Plus/edit/delete | Inline SVG Card/Cards views | `icons` | Named imports; label и disabled у ActionIcon | 4 |
| Навигация и active route | `shared/ui/AppShell` | `app/ui` | Product shell; не публичный компонент ui-пакета | 1.1 |
| Карта и её операции | `entities/bank-card` | Там же | `BankCardFace`, `BankOperationIcon`, `BankSummaryView`, `BankCardFields`, `BankFeedback` — domain | Без переноса |
| Период банковских данных | `entities/bank-card/BankPeriodPicker`, model/period | Там же | Draft/URL/query semantics сохраняются; не универсальный date service | 5, без extraction |
| Период дохода | `pages/account-income/.../IncomePeriodForm.view` | Та же page; pure helpers в её model | Изолировать story от VM, сохранить validation/submit | 5.3 |
| Позиции/структура портфеля | `pages/account/ui/components/PositionCard.view`, `PortfolioStructure.view` | Там же | Page-local композиция допустима; не переделывать под EntryCard | Без переноса |
| Недавние события | `entities/brokerage-event` + Account page | Там же; общий EntryCard из ui | Первый продуктовый пилот, финансовое представление у entity | 3.2 |
| Общие instrument details | `features/instrument-details` | Там же | Реальный reuse stocks/bonds; typed presentation contract отдельно от общего UI | Без переноса |
Последние непакетные источники выбираются через существующие `index.ts`.
В таблице обозначения `shared/ui/...` описывают файлы до миграции; это не разрешение
на deep import. Текущие владельцы указаны в колонке цели.
### 7. Предметный UI, API и конечные словари
Рекомендация: оставить domain UI в `entities/features/pages`. Второй
производственный consumer, которому нужен тот же стабильный domain contract,
и отдельный release owner — проверяемое условие рассмотрения business package.
Storybook не считается таким потребителем. Повторная карточка может потребовать
переноса page → entity внутри приложения, но не нового npm-пакета.
`shared/api` сохраняет snapshot, generated types, typed transport и auth-aware
boundary. Генерируются **типы**, а не весь handwritten client. Query keys,
normalization, freshness и invalidation остаются в entity/feature owner;
не входят ни в generated artifact, ни в ui. Отдельный `api-client` сейчас
потребовал бы отделить Vite environment, auth/retry lifecycle и query policy без
второго потребителя. Вернуться к вопросу при втором независимом приложении.
Backend остаётся единственным editable HTTP source. Frontend получает
предоставленный snapshot, локально генерирует и сравнивает declarations;
соседний backend checkout не становится входом CI. DTO aliases из generated
`paths` сохраняются. UI form models допустимы, когда отличаются от transport.
Для frontend собственных конечных словарей — принятый `as const` + union:
`enum` несовместим с текущим `erasableSyntaxOnly`. Набор operation kinds можно
свести у bank entity и проверить через generated `BankOperation['kind']`;
не объявлять вторую DTO enum. Wire literals сохраняются. `ProblemDetailsDto.code`
остаётся `string`, unknown code получает fallback; нельзя объявлять закрытый
список ошибок. Общий `apiErrorCode` извлекает code, одинаковое recovery
классифицируется ближайшим допустимым owner без обратных FSD imports.
### 8. Политика библиотек
Версии закреплены в frontend package manifests и lockfile. Решения ниже
не означают новой установки или обновления.
| Область | Сейчас | Решение и причина | Этап |
| --- | --- | --- | --- |
| Build/React/Router | Vite, TS, React Compiler, React Router | Сохранить; расширить имеющийся build для library artifacts | 2 |
| Server/local state | TanStack Query / React state / URL | Сохранить owners; Redux из transitive deps не делает его app store | Все |
| Forms/validation | RHF, resolvers, Zod | Сохранить; vocabulary у entity, controller/view банковской формы отдельно | 5.2, 5.4 |
| UI/charts | Mantine; Recharts/react-is обеспечивают peer graph charts | Сохранить, не удалять peer deps из-за отсутствия direct imports | Все |
| Styles/условные классы | CSS Modules, template/ternary, data-* | Сохранить один этот подход; clsx/classnames не вводить без сложности, которой здесь нет | 3–5 |
| Icons | Inline SVG | Унифицировать повторяющиеся общие рисунки в малом собственном наборе | 4 |
| Motion | motion для routes, CSS/native для рисунков | Сохранить по разным владельцам; проверить дублирование Modal отдельно | 5.1 |
| Dates | Day.js + Mantine Dates + native Date/Intl | Сохранить роль каждого; Moscow/ISO policy остаётся domain/page | 5.3 |
| Money | String + BigInt cents, display formatters | Сохранить точность и unknown≠zero; decimal library не добавлять | Все |
| API/errors | openapi-fetch/react-query/typescript | Сохранить; не добавлять axios, второй generator или schema registry | Все |
| Tests | Vitest/RTL/MSW | Сохранить; `fireEvent` принят, user-event только transitively | 2–5 |
| Catalog | Storybook 10.6.0 | Один runtime, расширить glob и границы на packages | 2–4 |
| Quality | Biome, Knip, dependency-cruiser | Дополнить действующие scopes, не ставить Steiger/ещё один analyzer | 1–2 |
| Browser/visual CI | Принятого E2E runner/visual diff нет | Ручной browser proof пилота; Playwright — при повторяемой browser-only регрессии и владельце CI | Отложено |
| Release | release-it + native npm + Gitea | Использовать текущий подход, адаптировать workspace releases; Changesets/Lerna не нужны для трёх пакетов | 6 |
### 9. Предлагаемое уточнение conventions
Это предложения для будущих задач; действующие инструкции сейчас не меняются.
AGENTS остаётся коротким router, постоянный каталог переносится в подходящий
раздел existing UI convention/README после подтверждённого перехода.
| Владелец правила | Конкретное уточнение | Когда |
| --- | --- | --- |
| `.ai/architecture.md` | Package graph вне FSD; src public APIs; app shell; никакого package → app или same-layer обхода | 1–2 |
| `.ai/conventions/ui.md` | Каталог источников и search order; тема против wrapper; props-only общие компоненты | 2–3 |
| UI + Storybook | JSON/semantic roles, explicit CSS import, светлая схема, иконки/currentColor/reduced-motion | 2–4 |
| `.ai/conventions/data.md`, TypeScript/React | Generated vocabulary vs local const unions; errors открыты; package не владеет query state | 5.2 |
| Engineering/dependencies | Workspace direct deps, peers, exact lock, packed consumer, ограничения exports | 2, 6 |
| Storybook | Один runtime, colocated package/product stories, новые reachability scopes | 2–3 |
| Testing | Поведение → риск → минимальная boundary; package artifact contract; browser proof не равен build | 2–3, 6 |
### 10. Storybook и проверки
Один существующий frontend Storybook показывает `Design System` и `Product`.
Общие stories переезжают вместе с ui/icons; foundations читают опубликованный
JSON/адаптер. Product stories остаются рядом с entities/pages. Обновляются
только globs/aliases/check scopes, не создаётся отдельное приложение каталога.
Mantine/theme/Dates/i18n уже есть в preview; Query/Auth/API providers не нужны.
Synthetic props и callbacks сохраняются; MSW остаётся в Vitest, не в worker.
| Наблюдаемый контракт | Минимальный способ доказать |
| --- | --- |
| JSON → размеры/тени/semantic CSS, unscaled breakpoints | Перенести существующие theme unit tests; не проверять каждый literal |
| JS/types/CSS/JSON доступны вне workspace | Pack + чистый consumer с type/build/render и без source aliases |
| EntryCard image fallback, actions при focus/touch | Один небольшой RTL case на собственный fallback/действие, synthetic story и browser review |
| Деньги/version/idempotency/invalid input | Существующие targeted RTL/MSW tests формы; новые cases только если меняется соответствующая boundary |
| Query/auth/recovery | Имеющиеся MSW tests; no real backend; не тестировать это через stories |
| Keyboard/name/reduced motion/narrow layout | Выбранные собственные interactions в browser; примитивные гарантии Mantine не переписывать |
`build:catalog` не исполняет `play`; существующий `storybook.test.tsx` проверяет
лишь выбранные группы. Не обещать screenshot regression по факту сборки.
Snapshot всех компонентов и полный новый E2E stack не планируются.
Новые workspace-папки не охвачены автоматически: нынешние TS/Knip/deps/story
настройки главным образом ориентированы на `src`. Package type/build/lint,
dependencies и запрет app imports вводятся вместе с соответствующим пакетом.
Нельзя временно исключить packages целиком и назвать gate полным. Same-layer
rule сначала проверяется на текущем graph, затем включается для чистого scope;
legacy exception имеет имя, причину и задачу удаления. Existing coverage floors
не понижаются; перенос tests/coverage ownership не должен скрыть потерю сценария.
### 11. Локальная работа и потребление
Приняты приватные npm workspaces внутри frontend: один install/lockfile,
сборка пакетов и потребление через exports. Проверки охватывают приложение,
пакеты и Storybook. Собранные tarballs проверены вне workspace без source aliases;
эта проверка выявляет отсутствующие exports, типы, CSS и несовместимые peers.
Публикация, независимый SemVer, package release automation и настройка Nexus
не входят в текущий переход. Вернуться к ним можно при подтверждённой пользе
для независимого потребителя; тогда отдельно определить registry, права,
неизменность версий, совместимость и откат. Текущий app release не меняется.
## UI/UX Applicability and Package
**Applicability: N/A для нового визуального дизайна.** Цель — сохранение уже
существующего приложения, без новых экранов и рисунков. Новый макет/visuals и
hash registry не создаются. Для проверки переноса выбран следующий scope.
Пилот: `AccountPage → useAccountPageViewModel → useBrokerageEvents →
BrokerageEventsPreview → BrokerageEventCard → EntryCard`. Это read-only
preview выплат; `PositionCard` не использует EntryCard и не подходит для
переноса без изменения представления. Generic icons в пилоте отсутствуют и
мигрируют отдельно в Card/Cards.
Сохраняются loaded/loading/error/empty, missing amount как unknown, а не ноль,
registry-date/account metadata, валюта, logo fallback, длинные строки,
preview limit и ссылка полного списка. Сверяются desktop и viewport 375px,
доступный focus и отсутствие горизонтального overflow. У generic EntryCard
actions дополнительно проверяются fine/coarse pointer и reduced motion на
synthetic story. Текущий RecentEvents error-priority не меняется молча.
## Delivery Map
| Area | Status | Architectural outcome or N/A reason |
| --- | --- | --- |
| frontend | affected | Workspaces tokens/icons/ui, ограниченная миграция, checks и package release integration |
| backend | N/A | Пакеты не меняют HTTP/auth/storage; замечание об errors вынесено в отложенный scope |
| specs | affected | Этот change и новые требования к поставке/иконкам; main specs обновятся только после принятой реализации |
| docs | affected | После поставки уточняются design overview и описание frontend container/release boundary |
## Solution Assessment
| Решение | Fit / зрелость / ограничения / стоимость | Выбор |
| --- | --- | --- |
| Существующие npm workspaces + Vite/TS | Стандартная упаковка; не требует нового Git repo; CSS/peers и published surface нуждаются в проверке | Да |
| Отдельный design-system repo | Ясная независимость ownership, но удваивает окружение каталога/CI при одном app | Позже по явному consumer/owner trigger |
| Style Dictionary или собственный token compiler | Полезны нескольким форматам/платформам; сейчас JSON + Mantine adapter уже решают задачу | Нет; runtime trust boundary не меняется |
| Ручные React SVG / SVGR pipeline | Несколько рисунков дешевле поддерживать в одном TSX source; SVGR нужен каноническим raw SVG exports | Ручные SVG-компоненты сейчас, без генератора |
| release-it/native npm / Changesets | Первый стек уже принят; второй полезен большому потоку независимых release contributions | Сохранить существующий app release; независимый package release отложен |
Новые внешние runtime dependencies не требуются. Риск supply chain сохраняется
под существующим security gate; peer/version checks и registry ACL нельзя
заменить предположением о готовом Nexus.
## Risks / Trade-offs
- Workspace links скрывают отсутствующие exports/assets и конфликт React:
независимый packed consumer обязателен.
- Package graph расширяет проверяемую область: старый зелёный `deps src` не
подтверждает новые packages. Scopes расширяются при появлении владельца.
- CSS order/global reset влияют на всё приложение. Сначала перенос без
изменения значений, затем отдельно semantic roles и Modal consolidation.
- Общие исходники в frontend — осознанный выбор для одного потребителя;
публикация не означает уже существующий независимый каталог/команду.
- Изменение view/controller рядом с денежной формой рискованнее UI extraction:
это отдельная поздняя задача с сохранением tests version/idempotency.
## Migration Plan
Границы owners → package foundations → общие компоненты и read-only пилот →
иконки и точечное выравнивание → packed consumer → удаление совместимости.
Единственный подробный backlog и зависимости находятся в [tasks](tasks.md).
Во время перехода `shared/theme` и три старых UI entrypoints могут быть тонкими
re-exports package API. Они не содержат второй JSON, CSS или реализации.
Новые consumers используют package paths; добавлять exports/логику в facade
запрещено. Удаление — когда поиск оставшихся runtime/test/story imports даёт
ноль и app/catalog/packed consumer прошли gates. Старые inline icons живут до
своего consumer batch, но новые копии извлечённых рисунков не добавляются.
### Rollback Contract
| Field | Decision |
| --- | --- |
| Status | required: меняются локальные imports, сборка и владельцы компонентов |
| Trigger | Нет exports/CSS/types, peer conflict, несовместимое представление либо failed pilot/recovery |
| Actions | Вернуть согласованный diff приложения, packages, imports и lockfile; при выпуске приложения использовать прежний app image |
| Data safety | API/storage/money wire format не меняются; не откатывать чужие продуктовые изменения и не сбрасывать пользовательские данные |
| Verification | Повторить соответствующий consumer gate и пилот на прежней версии; проверить восстановление согласованного состояния приложения и packages |
### Осознанно отложенное
| Находка/возможность | Причина и условие возврата |
| --- | --- |
| F8: RecentEvents error приоритетнее cached data | Реальное отличие от общего recovery convention. Не менять при extraction; отдельная product recovery задача после согласования expected stale/error UX, с refetch-error regression test |
| F9: ProblemDetails errors schema/example и validation mapping | Отдельная backend contract задача: metadata + HTTP behavior test, затем предоставленный snapshot; не основание вручную исправлять frontend DTO |
| F12: Основная frontend-quality-guardrails spec требует initial 500 kB и baseline lazy chunks, а действующий verify не содержит такого gate | Отдельно согласовать актуальность сохранённого требования и способ enforcement. Не считать Vite warning блокирующим budget gate; package migration сравнивает фактический размер сборки, не заявляя выполнения отсутствующей проверки |
| Единый recovery classifier всех investment errors | Одинаковые codes ещё не доказывают одинаковый UX; объединять только подтверждённый общий recovery без нарушения FSD |
| Business/API packages, dark/mobile, raw SVG pipeline, Playwright/visual service | Нет принятого второго consumer или значимого browser-only regression scope |
## Решения для подтверждения перед зависимыми действиями
1. Публичные имена `@wealthledger/tokens`, `@wealthledger/icons`, `@wealthledger/ui`,
scope ownership и видимость. Рекомендуется приватная поставка WealthLedger;
другое решение меняет доступ и release setup, не текущие данные.
2. Nexus npm endpoint, владелец publish/read credentials, политика неизменности
и хранения прошлых версий. Эти решения нужны только при отдельном запросе внешней публикации.
3. Расхождение F12 между сохранённой spec и текущими quality-командами:
рекомендуемый default — сохранять требование до отдельного решения владельца,
не ослаблять его и не объявлять выполненным. Автоматизация либо корректировка
требования оформляются отдельным ограниченным scope после уточнения причины.
Остальное — аргументированные defaults данного предложения, а не список
неразрешённых вопросов. Перед реализацией нужен отдельный запрос пользователя;
workspaces внутри frontend уже реализованы, независимые версии остаются отложенными.
@@ -0,0 +1,69 @@
## Why
У WealthLedger уже есть общий визуальный фундамент, каталог компонентов и
типизированное взаимодействие с сервером. Следующий шаг — сделать границы
переиспользования и поставки этого фундамента явными: участник должен быстро
находить подходящее решение, а приложение — получать проверенную совместимую
версию без копирования реализаций.
Исследование текущего frontend выявило конкретные незавершённые границы:
повторяющиеся рисунки иконок, обход существующих цветовых ролей, продуктовую
навигацию в общем UI и неполную автоматизацию архитектурных ограничений.
## What Changes
**Статус: локальная реализация и итоговая проверка завершены.**
Локальные границы приложения и private workspace-пакеты реализованы;
проверенные пункты и оставшиеся ограничения перечислены в [tasks](tasks.md).
Независимое версионирование и публикация через Nexus отложены до оценки пользы
пилота и не входят в требования этого перехода.
Первоначальный комплект плановых Markdown-файлов был подготовлен по запросу
пользователя в одной сессии. Последующие разрешения касались ограниченных
этапов; они не разрешают публикацию, deployment или archive.
- Выделить используемый общий визуальный фундамент в приватные workspace-пакеты,
сохранив внешний вид и работоспособность приложения между небольшими этапами.
- Установить единый порядок выбора компонентов и понятную границу между
общим представлением, предметным UI и сценариями приложения.
- Сделать повторно используемые собственные иконки доступными с единым
визуальным и доступным контрактом.
- Подтвердить переход существующим небольшим продуктовым срезом и проверкой
потребления собранных артефактов, а не только локальных исходников.
- Уточнить применимые инженерные правила после соответствующих изменений,
сохранить существующие проверки и поэтапно закрыть найденные пробелы.
Сохраняются денежная точность, обработка неизвестных значений, поведение
авторизации, запросов, форм, восстановления после ошибок и существующих страниц.
Предметные расчёты и серверные контракты не становятся частью общего UI.
Не входят редизайн, тёмная тема, мобильный адаптер, календарь обязательных
платежей, смена работающего стека, переустройство всех репозиториев и массовое
перемещение компонентов. Публикация, deployment, commit, push, PR и archive не разрешены.
Локальная реализация ограничена отдельно согласованными этапами.
## Capabilities
### New Capabilities
Нет: новая capability на каждый пакет или каталог не требуется.
### Modified Capabilities
- `specs/frontend-design-system/spec.md`: добавить требования к потреблению
локальных пакетов общего фундамента и доступному контракту собственных
общих иконок. Существующие требования к единой теме и предметной независимости
сохраняются. [Delta](specs/frontend-design-system/spec.md).
## Impact
- Frontend: поставка общей основы, выбор источников компонентов, ограниченная
миграция потребителей и развитие существующих проверок.
- Backend: производственный контракт и поведение не меняются; отдельное
замечание к описанию ошибок остаётся отложенным исследованием владельца API.
- Specs: этот change и delta; основные as-built требования пока не изменяются.
- Docs: описание текущего локального устройства обновлено в 7.2;
независимая registry-поставка не заявляется.
Решения, альтернативы и каталог источников — в [design](design.md), единственная
очередь работ и текущее состояние — в [tasks](tasks.md).
@@ -0,0 +1,60 @@
## ADDED Requirements
### Requirement: Общий фундамент доступен через проверяемые локальные пакеты
Общий визуальный фундамент SHALL принадлежать приватным npm workspaces внутри
frontend и иметь явную публичную поверхность. Приложение SHALL использовать
пакеты через объявленные exports. Собранные артефакты MUST содержать модули,
типы, стили и данные согласно контракту каждого пакета и быть пригодными для
проверки вне рабочего дерева frontend. Общие пакеты MUST NOT требовать серверных
данных, авторизации, маршрутов или состояния WealthLedger. Совместимость пакетов
между собой и с приложением SHALL проверяться; workspace links MUST NOT быть
единственным подтверждением пригодности артефактов. Независимые релизы и
публикация в registry не входят в этот переход.
#### Scenario: Приложение потребляет собранную поставку
- **WHEN** потребитель устанавливает собранные артефакты в чистом окружении без
ссылок на рабочее дерево поставщика
- **THEN** публичные модули, типы, стили и данные доступны, общий компонент
отображается на заявленной теме без зависимостей от приложения поставщика
#### Scenario: Локальная разработка скрыла отсутствующий артефакт
- **WHEN** локальная ссылка разрешает импорт, отсутствующий в собранной поставке
- **THEN** поставка не признаётся готовой по результату одной локальной сборки;
потребление именно собранных артефактов должно быть подтверждено отдельно
#### Scenario: Приложение использует локальные пакеты
- **WHEN** frontend устанавливает зависимости из своего lockfile и собирается
- **THEN** приватные workspaces доступны через публичные exports без публикации
в registry; предметные компоненты и API-клиент остаются в приложении
### Requirement: Собственные общие иконки имеют доступный контракт
Общие иконки SHALL иметь предсказуемые размеры и возможность получать цвет
от потребителя, не определяя бизнес-смысл операции или состояние приложения.
Декоративная иконка MUST NOT создавать отдельную точку фокуса или повторять
доступное имя соседнего действия. Смысловая иконка без сопровождающего текста
MUST иметь доступное имя. Собственное движение иконки SHALL учитывать
предпочтение уменьшенного движения; выбор момента запуска по пользовательскому
сценарию SHALL оставаться у потребителя.
#### Scenario: Иконка находится в именованном действии
- **WHEN** собственная иконка дополняет кнопку с доступным именем
- **THEN** кнопка сохраняет имя и клавиатурное поведение, а рисунок декоративен
и не создаёт дополнительную точку фокуса
#### Scenario: Рисунок самостоятельно передаёт смысл
- **WHEN** иконка используется без текста и без именованного родительского
элемента как самостоятельное изображение
- **THEN** потребитель задаёт доступное имя, передающее её назначение
#### Scenario: Пользователь уменьшил движение
- **WHEN** включено предпочтение уменьшенного движения
- **THEN** необязательная анимация общей иконки не выполняется, а значение и
доступность действия сохраняются
@@ -0,0 +1,239 @@
# План перехода frontend
**На 2026-09-22 выполнены и проверены 1.1–1.2, 2.1–2.2, 3.1–3.3, 4.1–4.2, 5.1–5.4 и 7.1–7.3** по отдельному
разрешению пользователя. Остальные задачи — предложения, не разрешение
на реализацию или публикацию. Результаты и ограничения приведены ниже.
Прочитать [proposal](proposal.md), затем нужное решение [design](design.md).
Здесь единственный backlog инициативы, отдельного roadmap нет.
## Дорожная карта и зависимости
```text
1.1 AppShell owner ────────┐
1.2 import enforcement ────┴─ 2.1 tokens/workspaces ─ 2.2 ui theme
│
3.1 common states
│
3.2 EntryCard + pilot ─ 3.3 roles
2.1 ─ 4.1 icons ─ 4.2 Card/Cards consumers
2.2 ─ 5.1 Modal review
1.2 ─ 5.2 operation vocabulary ─ 5.4 form boundary
1.2 ─ 5.3 income period helper boundary
3.3 + 4.2 + packed consumer proof ─ 7.1 retire facades
Выполненные локальные 1–5 ─ 7.2 текущее as-built описание
3.2–3.3 + 5.1–5.4 + 7.1–7.2 ─ 7.3 acceptance всей инициативы
```
1.1 и 1.2 независимы по смыслу, но могут менять общие конфиги: параллельные PR
согласуют diff. 5.1–5.4 не блокируют выпуск основы и не входят скрыто в пилот.
Иконки не нужны read-only пилоту RecentEvents. Пункты внешней публикации удалены из текущего плана по решению пользователя:
пакеты остаются приватными workspaces frontend. Остальные
задачи используют предложенные defaults после отдельного запроса реализации.
### Общие условия каждой будущей сессии
- Работать в repo владельца, сначала его AGENTS и только triggered conventions;
проверить Git status, текущий HEAD и чужие изменения. План привязан к
исследованным ревизиям, не к обещанию актуального remote develop.
- Перед Node-командой активировать местную `.nvmrc`: `nvm use --silent`.
Для code/dependency PR frontend требует `npm ci` и итоговый `npm run verify`;
targeted команды ниже дополняют этот gate, не заменяют его.
- Scope PR — одна задача. Не смешивать перемещение с новым поведением,
обновлением версий библиотек и исправлением соседнего продукта.
- Значимый тест начинается с наблюдаемого результата/риска; сохранять имеющиеся
regression tests. Не добавлять тест wrapper existence, Mantine internals
или snapshots всех вариантов ради переноса.
- По завершении повторить routing, проверить diff/новые файлы, Boy Scout review,
`git diff --check`, записать реально выполненные команды и ограничения.
Непройденная проверка не отмечается как выполненная задача.
### Реальные команды и будущие проверки
Сейчас доступны: `npm run deps`, `npm run deps:storybook`, `npm run knip`,
`npm run lint`, `npm run typecheck`, `npm run openapi:check`,
`npm run test:run -- <существующий path>`, `npm run test:coverage`,
`npm run build`, `npm run build:catalog`, `npm run verify`,
`npm run release:preview`. Их поведение исследовано, но они **не запускались**
в текущей Markdown-сессии. `typecheck` может записать tsbuildinfo;
tests/builds пишут cache/coverage/dist/catalog. `openapi:generate` меняет
tracked declarations и не нужен этой миграции.
Package build/declarations/check scopes и packed consumer проверены.
Независимый package release не входит в текущий план. Не
считать вымышленный `test:packages` или `release:packages` действующей командой.
Native `npm pack --dry-run`, `npm pack`, `npm ls` использованы для проверки пакетов. `release:prepare` приложения
создаёт commit; не использовать для пробного package release.
## 1. Уточнить владельцев и существующие ограничения
- [x] 1.1 [frontend] Перенести route-aware AppShell к владельцу приложения.
- **Covers:** `N/A` — техническое исправление F3; product shell не должен стать публичной частью ui, согласно design §1/4.
- **Verification:** `npm run deps`, `npm run typecheck`, `npm run knip`, существующие route/navigation tests, итоговый `npm run verify`; browser navigation/focus и active route.
- **Читать / область:** `.ai/architecture.md`, UI, `src/app/ui/AppShell.tsx` и CSS (до переноса — shared/ui), его callers в `app/routes`, связанные auth/route tests. Найти все imports до переноса.
- **Изменить / готово:** AppShell и принадлежащие ему navigation styles находятся в `app/ui` с допустимым public import. Labels, links, Outlet, transitions, responsive shell прежние; shared/ui не знает product routes. Коротко уточнён legacy пункт architecture.
- **Не входит / сохранить:** Новая навигация, новый wrapper shell, миграция всех SVG, auth policy. Никаких изменений URL и bootstrap states.
- **Откат:** Вернуть перенос и imports одним PR; данных/внешних artifacts нет. Зависимость: нет.
- [x] 1.2 [frontend] Закрыть same-layer imports действующим dependency-cruiser.
- **Covers:** `N/A` — enforcement уже принятого FSD profile, F4; не новая пользовательская возможность.
- **Verification:** Зафиксировать вывод существующего `npm run deps` до правки; после неё тот же gate отклоняет временный контролируемый forbidden import между двумя slices, чистый graph проходит; удалить пробный import, затем `npm run verify`.
- **Читать / область:** Architecture/imports, `dependency-cruiser.cjs`, реальные alias/relative imports в pages/features/entities; public API примеры account-operations и bank-card.
- **Изменить / готово:** Точные запреты соседних slices pages/features/entities, без запрета imports внутри одного slice и без ошибочного применения к shared segments. Если baseline выявит legacy, правило включается для чистой области с адресным исключением и условием удаления, без broad ignore.
- **Не входит / сохранить:** Новый analyzer, автоматический fixer, массовая раскладка FSD, запрет page-local composition. Не писать постоянный анализатор проверки анализатора.
- **Откат:** Вернуть только rule change, обязательный semantic review остаётся. Зависимость: нет.
## 2. Подготовить пакеты без изменения представления
- [x] 2.1 [frontend] Ввести workspace tokens и охват его контрактов.
- **Covers:** `frontend-design-system` — `Общий фундамент доступен через проверяемые локальные пакеты`
- **Verification:** Будущая package-конфигурация: `npm pack --dry-run` показывает JSON; чистый временный consumer вне workspace читает JSON обычным parser; `npm ci`, расширенные существующие type/lint/Knip/deps scopes, `npm run verify`; чистые Docker builds development/production с новым workspace manifest и JSON.
- **Читать / область:** Engineering/dependencies, package/lock, TS/Knip/deps configs, `Dockerfile`, `.dockerignore`, `packages/tokens/tokens.json`, `shared/theme/theme.ts`, `theme.test.ts`; design §1–3/10. После 1.1–1.2; имена остаются предлагаемыми до принятия design.
- **Изменить / готово:** `packages/tokens` владеет тем же JSON v1; root app private; один npm workspace lock, прямые зависимости, ограниченные files/exports. Адаптер читает package JSON. Проверки явно видят packages и запрещают package → app. Docker получает workspace manifests перед npm ci и только нужные package inputs через узкую allowlist, без host node_modules/dist/.env.
- **Не входит / сохранить:** Новая schema, Style Dictionary, mobile/dark, преобразование всех токенов и CSS. Не поддерживать две редактируемые копии JSON; app appearance и все units прежние.
- **Откат:** Вернуть JSON/imports/manifest/lock как связанный diff; package ещё не опубликован.
- [x] 2.2 [frontend] Упаковать Mantine adapter и общие стили в ui.
- **Covers:** `frontend-design-system` — `Общий фундамент доступен через проверяемые локальные пакеты`
- **Verification:** Существующие theme tests на новом owner; будущие Vite library build + declaration emit и packed consumer type/build/render без app alias; на чистом checkout после `npm ci`, без прежнего package dist/cache, `npm run verify` сначала собирает packages, затем проверяет consumer. `npm ls react react-dom @mantine/core`; отдельные чистые Docker development/production builds и render темы.
- **Читать / область:** `packages/ui/src/{theme.ts,styles.css,theme.test.ts}`, facade `shared/theme/index.ts`, `app/ui/App.tsx`, `.storybook/{main.ts,preview.tsx,dependency-cruiser.cjs}`, `vite.config.ts`, `vitest.config.ts`, package scripts, `Dockerfile`, `.dockerignore`, design §2–4. Зависимость: 2.1.
- **Изменить / готово:** `ui/theme`/`ui/styles.css`, dependency tokens, peers/external React/Mantine, CSS side effects, JS/d.ts/CSS в package. App/preview явно подключают styles и прежние providers. Package build предшествует всем проверкам потребителя в local/CI/Docker; scope tests/coverage следует owner. `shared/theme` только re-export. Host dev допускает package watch; в Docker изменение packages требует документированного rebuild dev image, без нового root orchestration.
- **Не входит / сохранить:** Новый global provider, новый Storybook app, component wrappers, Modal animation cleanup. Library build не наследует app PWA/service-worker/proxy plugins. Не менять light scheme, source values, CSS order и effective theme; сравнить фактический bundle size с baseline, не заявляя отсутствующий budget gate F12.
- **Откат:** Вернуть adapter/styles/imports и manifest/lock единым PR. Временный facade удаляется в 7.1, новые consumers его не расширяют.
## 3. Общие компоненты и продуктовый пилот
- [x] 3.1 [frontend] Перенести StateView и LoadingSkeleton с их stories.
- **Covers:** `frontend-design-system` — `Общий фундамент доступен через проверяемые локальные пакеты`
- **Verification:** `npm run deps:storybook`, `npm run build:catalog`, существующие проверки StateView/LoadingSkeleton/composeStories в новом scope, будущий packed component render, `npm run verify`.
- **Читать / область:** Три UI-conventions темы, два `src/shared/ui` компонента/stories и callers, `src/test/storybook.test.tsx`, package ui. Зависимость: 2.2.
- **Изменить / готово:** Реализации и CSF stories у ui, один catalog glob включает package stories. Props без query/i18n/router; busy label/status и error alert/action сохраняются. Проверка Storybook reachability включает новый каталог. Если нужен facade, он только переэкспортирует.
- **Не входит / сохранить:** Перемещение page skeleton shapes, новый loading framework, полный набор stories для Mantine. No backend и реальные финансовые данные.
- **Откат:** Вернуть перенос; содержимое/поведение исходных components не менялось. Удаление facade — 7.1.
- [x] 3.2 [frontend] Перенести EntryCard и подтвердить пилот RecentEvents.
- **Статус 2026-09-22:** Реализация, 278 tests/full verify, Docker, packed consumer и browser desktop/375px прошли. Пользователь подтвердил успешную ручную проверку coarse pointer/reduced motion по предложенному чек-листу; browser-ограничение закрыто. CSS перенесён побайтово.
- **Covers:** `frontend-design-system` — `Общий фундамент доступен через проверяемые локальные пакеты`
- **Verification:** `npm run test:run -- src/pages/account/ui/AccountPage.test.tsx`, существующие ledger/CardPage cases использующих EntryCard; минимальный UI-test собственного image fallback при необходимости; `npm run deps:storybook`, `npm run build:catalog`, packed consumer render, `npm run verify`; browser сравнение 375px/desktop.
- **Читать / область:** `EntryCard.tsx`/CSS и все callers; `AccountPage.vm.ts::toRecentEvents`, `useBrokerageEvents`, `BrokerageEventsPreview`, `BrokerageEventCard` и их stories, design UI/UX scope. Зависимость: 3.1.
- **Изменить / готово:** Один EntryCard в ui с тем же CSS/props; entity пилота импортирует публичный ui package. В Storybook — существующие RecentEvents states и малая generic story длинного контента/image fallback/actions. Пилот реально проходит через API owner до прежнего presentation, без смены data flow. Остальные callers могут пока использовать re-export.
- **Не входит / сохранить:** Generic icons, PositionCard rewrite, исправление F8. Сохраняются unknown≠zero, currency/date/account, logo fallback, preview limit/full-list link; у других callers — sign и disabled placeholder/archive guards.
- **Откат:** Вернуть consumer import и extraction PR. Не вводить вторую реализацию EntryCard для пилота. Facade removal — 7.1.
- [x] 3.3 [frontend] Подключить существующие semantic roles внутри EntryCard.
- **Статус 2026-09-22:** Реализованы border/subtle/surface. Positive/negative уже были semantic (shade 9); neutral gray-7 оставлен без неточного сопоставления. npm ci/full verify (278 tests) и browser computed colors/narrow/focus прошли. Пользователь подтвердил ручную проверку coarse pointer/reduced motion по чек-листу; пункт закрыт.
- **Covers:** `N/A` — F1: применение уже существующей темы после структурного переноса, без нового UI behavior.
- **Verification:** Сравнить computed colors и выбранные EntryCard/RecentEvents states до/после; gray-3→border, gray-0→subtle эквивалентны; текущие positive/negative (shade 9) сохранены; background tint/white→surface проверен; `npm run verify` и browser narrow/focus/coarse-pointer/reduced-motion.
- **Читать / область:** ui EntryCard/CSS, tokens color.roles, ui/theme resolver, generic и product stories. Зависимость: 3.2.
- **Изменить / готово:** Прямые эквиваленты заменены semantic CSS variables; текущий вид и actions не меняются. Если эквивалентность конкретной пары не доказана, оставить её локальной и пояснить, не создавать токен автоматически.
- **Не входит / сохранить:** Новые palettes, card brand gradients, компонентный токен на каждое свойство, массовый CSS sweep.
- **Откат:** Вернуть только CSS/inline-role diff; package ownership остаётся.
## 4. Собственные общие иконки
- [x] 4.1 [frontend] Подготовить минимальный icons package.
- **Covers:** `frontend-design-system` — `Собственные общие иконки имеют доступный контракт`; `frontend-design-system` — `Общий фундамент доступен через проверяемые локальные пакеты`
- **Verification:** Будущие package build/declarations/pack consumer checks; synthetic decorative и meaningful icon examples с именем/размером/currentColor; `npm ls react`, `npm run deps:storybook`, `npm run verify`.
- **Читать / область:** Card/Cards inline plus/pencil/trash, `BankOperationIcon` как пример accessibility, `bind-icon-animation` только для границ ответственности, design §5. Зависимость: 2.1; после 2.2 можно переиспользовать package build pattern без общего самописного генератора.
- **Изменить / готово:** Именованные Add/Edit/Delete SVG-компоненты, один TSX source на рисунок, React peer/external, public types, ограниченные exports. Static package не тянет motion/UI/domain; декоративность по умолчанию и доступное meaningful использование документированы.
- **Не входит / сохранить:** SVG registry по строке, raw SVG+TSX дубли, SVGR/dependency, перенос категорий/навигационных анимаций. Сохранить geometry/stroke/size существующих рисунков, без редизайна.
- **Откат:** Удалить ещё не используемый package diff; до публикации внешнего контракта нет. Если 4.2 уже интегрирован, откатывать согласованно.
- [x] 4.2 [frontend] Перевести только повторяющиеся Card/Cards actions на icons.
- **Covers:** `frontend-design-system` — `Собственные общие иконки имеют доступный контракт`
- **Verification:** `npm run test:run -- src/pages/card/ui/CardPage.test.tsx`, `npm run test:run -- src/pages/cards/ui/CardsPage.test.tsx`, `npm run verify`; browser accessible names/keyboard и appearance выбранных Add/Edit/Delete actions.
- **Читать / область:** `pages/card/ui/CardPage.view.tsx`, `pages/cards/ui/CardsPage.view.tsx`, ActionIcon consumers и imports. Зависимость: 4.1.
- **Изменить / готово:** Повторяющиеся SVG заменены named imports; размеры/strokeWidth задаются явно, если раньше различались. Имя и disabled остаются на action; внутри SVG нет дополнительного focus. В изменённом scope больше нет старых копий этих рисунков.
- **Не входит / сохранить:** BankOperationIcon mappings, формы/mutations, поведение edit/delete и confirm dialogs, иной визуальный стиль.
- **Откат:** Вернуть caller diff; пакет может оставаться неиспользуемым до отдельного решения, опубликованную версию не удалять.
## 5. Адресные улучшения после основы
Эти задачи самостоятельны. Их незавершённость не оправдывает расширение
пакетного PR или утверждение, что изменено поведение формы в пилоте.
- [x] 5.1 [frontend] Проверить и при необходимости свести Modal animation к одному владельцу.
- **Статус 2026-09-22:** BankCardForm browser A/B показал различный lifecycle: CSS даёт mount entrance, Mantine — exit/callback. Production-код сохранён; причины документированы в UI convention. Пользователь подтвердил ручную проверку reduced motion, Escape/Отмена и возврата фокуса; пункт закрыт.
- **Covers:** `N/A` — F5, устранение подтверждённого дублирования механизмов без нового продукта.
- **Verification:** Browser comparison открытия/закрытия существующей банковской Modal: overlay, motion, focus return, reduced motion; relevant Modal/form tests и `npm run verify` только при code diff.
- **Читать / область:** ui theme Modal defaults, ui global styles, BankCardForm/OperationForm consumers. Зависимость: 2.2.
- **Изменить / готово:** Выбран один механизм либо документирован конкретный различный смысл обоих. При эквивалентности предпочтён Mantine transition и убран только лишний CSS. Наблюдение «видимых проблем нет» само по себе не требует нового кода.
- **Не входит / сохранить:** Новые timings, анимационная система, замена motion маршрутов. Keyboard/focus/closing rules неизменны.
- **Откат:** Вернуть animation-only diff; component API/data не меняются.
- [x] 5.2 [frontend] Свести runtime vocabulary банковских operation kinds у entity.
- **Covers:** `N/A` — часть F6, одна типизированная коллекция для существующих schema/choices без изменения wire contract.
- **Verification:** `npm run typecheck`, `npm run test:run -- src/pages/card/ui/OperationForm.test.tsx`, `npm run deps`, `npm run verify`; все четыре прежних вида и defaults сохранены.
- **Читать / область:** `entities/bank-card/api/queries.ts` и index, `OperationForm.tsx`/`.view.tsx`/`.types.ts`, TypeScript/data conventions. Зависимость: 1.2.
- **Изменить / готово:** Schema и choices используют один `as const` набор, проверенный относительно generated `BankOperation['kind']`. Не добавлен enum, DTO copy или cast неизвестного code; строки UI остаются i18n.
- **Не входит / сохранить:** Generic errors registry, backend enum generation, механическое именование каждого literal, controller refactor.
- **Откат:** Вернуть маленький vocabulary/import diff; значения wire прежние.
- [x] 5.3 [frontend] Отделить pure period helpers от AccountIncomePage VM.
- **Covers:** `N/A` — F7, устранение транзитивной зависимости presentation story от VM по design §6/10.
- **Verification:** Existing account-income targeted tests, `npm run deps:storybook`, `npm run build:catalog`, `npm run verify`; synthetic form submit/reset/error states без API/query providers.
- **Читать / область:** `AccountIncomePage.vm.ts`, `components/IncomePeriodForm.view.tsx`, их imports/tests, соседний page model и Storybook convention. Зависимость: 1.2.
- **Изменить / готово:** Только реально используемые pure date helpers у page model и оба callers используют их; полезная isolated presentation story проходит reachability. Нового global date helper/service нет.
- **Не входит / сохранить:** Общий DateRangePicker, изменение диапазонов/часового пояса и перенос логики в UI package. Moscow dates, validation и submit/reset прежние.
- **Откат:** Вернуть helper move/story diff; URL/API поведение не менялось.
- [x] 5.4 [frontend] Отделить controller банковской OperationForm от presentation.
- **Covers:** `N/A` — часть F6, применение существующей container/VM/view границы без изменения денежных сценариев.
- **Verification:** До и после `npm run test:run -- src/pages/card/ui/OperationForm.test.tsx`; особенно conflict/draft/explicit version adoption, retained UUID, full refund и max money; `npm run deps:storybook`, затем `npm run verify`.
- **Читать / область:** Полные OperationForm/OperationForm.view/types/tests, entity form-validation/mutations и UI/data/testing conventions. Зависимость: 5.2.
- **Изменить / готово:** Query/mutation/recovery orchestration принадлежит page VM/controller; существующий fields view расширен только нужными presentation props/callbacks/slots. Runtime helpers сейчас приходят из banking barrel, который переэкспортирует API/hooks: эту транзитивную связь убрать через props/callbacks, type-only DTO imports допустимы. Не вводить deep-import exception и не ослаблять story gate. RHF context в fields остаётся; `CardPage` целиком не переписывается.
- **Не входит / сохранить:** Generic form engine, extraction в feature/package без второго consumer, новые requests/optimistic updates. Не менять amount string/BigInt, intent lifecycle, version adoption, source constraints, pending close guards и вложенное создание категории.
- **Откат:** Вернуть этот behavior-preserving PR. Любое необходимое изменение поведения остановить и согласовать отдельно, не прятать в refactor.
## 7. Закончить переход и описать поставленное состояние
- [x] 7.1 [frontend] Удалить только исчерпанные compatibility facades.
- **Уточнение scope:** Пользователь разрешил удалить локальные facades с проверкой packed consumer вместо registry consumer. Nexus и независимый release остаются отложенными.
- **Covers:** `N/A` — прекращение временного двойного import path после подтверждённого потребления, design Migration Plan.
- **Verification:** `rg` по runtime/test/story imports старых theme/UI paths даёт ноль; `npm run knip`, `npm run deps`, `npm run deps:storybook`, app/catalog/packed consumer checks и `npm run verify`.
- **Читать / область:** Facades `shared/theme` и EntryCard/StateView/LoadingSkeleton, все оставшиеся callers, component source catalog, frontend README и conventions. Зависимости: 3.3, 4.2 и подтверждённый packed consumer без workspace/source aliases.
- **Изменить / готово:** Оставшиеся consumers используют package API, ненужные re-exports удалены, нет дубля JSON/CSS/implementation. В existing conventions закреплены каталог и import rules, AGENTS только маршрутизирует темы.
- **Не входит / сохранить:** Массовая чистка unrelated shared/lib, запрет прямого Mantine, перенос domain UI. Старые подходы вне migrated scope не блокируются новым широким правилом.
- **Откат:** Вернуть thin facades/imports; они не восстанавливают копии реализаций. Условия удаления снова проверяются перед повтором.
- [x] 7.2 [docs] Обновить только подтверждённое as-built описание.
- **Уточнение scope 2026-09-22:** По запросу следующего пункта без browser verification описано текущее локальное состояние после 5.4. Завершение 7.1 не требуется для этого описания: переходное состояние и отсутствие registry-поставки указаны явно; после 7.1 описание facades актуализировано. После будущей поставки документация обновляется вместе с ней.
- **Covers:** `N/A` — описание фактически принятой реализации и поставки; не новый behavior contract.
- **Verification:** Docs `npm run verify` по местному CONTRIBUTING, проверка локальных ссылок/диаграмм и semantic review по поставленному frontend; `git diff --check`.
- **Читать / область:** Docs AGENTS/README/CONTRIBUTING, `design/overview.md`, `architecture/containers.md`, при необходимости release boundary в `architecture/deployment.md`; frontend принятый package contract. Основание текущего описания: выполненные 1–5; удаление facades по 7.1 отражено в документации.
- **Изменить / готово:** Тематические страницы говорят о реально используемой теме/пакетах/границах frontend и каталога. Указывается, что raw SVG/mobile/dark/API package не поставлены. При неполной 5.x не обещать завершение этих пунктов.
- **Не входит / сохранить:** Анализ/план в permanent docs, новый roadmap, переописание backend, ADR на каждый маленький выбор. Один устойчивый ADR возможен лишь если принято значимое repository/release решение и нужен его rationale.
- **Откат:** Вернуть описание вместе с отменённой поставкой; не менять исторические archives.
- [x] 7.3 [specs] Сверить реализацию с delta и закрыть плановые расхождения.
- **Статус 2026-09-22:** Semantic сверка завершена для согласованного локального объёма: proposal/design/delta согласованы с private workspaces и packed consumer; внешняя публикация исключена. Код и ручные browser checks проверены ранее. Specs verify, локальные ссылки и whitespace проверены после правок. После установки OpenSpec выполнена команда `openspec validate evolve-frontend-stack --type change --strict --store wealth-ledger-specs`: change valid. Пункт завершён.
- **Covers:** `frontend-design-system` — `Общий фундамент доступен через проверяемые локальные пакеты`; `frontend-design-system` — `Собственные общие иконки имеют доступный контракт`
- **Verification:** Semantic проверка всех delta scenarios, `npm run verify` specs, при доступном CLI `openspec validate evolve-frontend-stack --type change --strict --store wealth-ledger-specs`; локальные ссылки и `git diff --check`.
- **Читать / область:** Specs AGENTS/README, proposal/delta/design/tasks и фактические результаты consumer/rollback/accessibility checks. Зависимости полного acceptance: 3.2–3.3, 5.1–5.4 и 7.1–7.2; требования delta должны соответствовать согласованному локальному объёму.
- **Изменить / готово:** Tasks отражают только фактически проверенные результаты; согласованные изменения scope возвращены в planning artifacts, не скрыты. Main specs обновляются только в отдельно разрешённом штатном завершении после реального внедрения; untouched deferred findings остаются явно отложенными.
- **Не входит / сохранить:** Автоматический archive/approval, Evidence/state/hash registry, декларация успеха на основании одного build. Промежуточная сверка не означает завершения change.
- **Откат:** Исправить несоответствующее предложение/отметку; main specs не описывают ещё не реализованный переход.
## Переходный период и отложенные вопросы
Точный перечень отложенного — в design: F8 recovery RecentEvents, F9 backend
errors contract, F12 расхождение bundle-budget spec/gate, общий investment recovery classifier и новые потребители.
Они не входят скрыто в tasks выше. Если требуется менять HTTP/observable
recovery, нужен отдельный согласованный scope и соответствующий spec contract.
Facades допустимы только от 2.2/3.x до 7.1; новых возможностей они не получают.
Новые components/imports следуют целевому каталогу. Старые inline рисунки
допустимы лишь до миграции своего caller batch, а новые дубли уже вынесенных
рисунков не создаются. Совместимость снимается по нулю imports и успешному
consumer proof, а не по истечении произвольного срока.
Проверены задачи **1.1–1.2, 2.1–2.2, 3.1–3.3, 4.1–4.2, 5.1–5.4 и 7.1–7.3**.
Недостающие browser checks 3.2/3.3/5.1 выполнены вручную пользователем:
он подтвердил корректность всех пунктов переданного чек-листа.
Независимые версии и Nexus отложены до оценки пользы после пилота;
checkbox не разрешает дальнейшую реализацию или публикацию.
Последний полный frontend `npm run verify`: 49 test files / 282 tests,
проверки зависимостей, типов, lint, Knip, OpenAPI и сборки app/catalog прошли.
Свежий UI tarball проверен вне workspace: install, typecheck, build и browser render.
Docs `npm run verify`: 45 Markdown-файлов, 0 issues.
@@ -0,0 +1,2 @@
schema: wealth-ledger-sdd
created: 2026-10-07
@@ -0,0 +1,96 @@
## Context
Backend — один NestJS process и SQLite. Сейчас screen-driven синхронизация раскладывает запросы экранов на source work; владельцы membership, portfolio, operations, forecast, catalog и candles используют reservations, attempt/revision fences и короткие транзакции без RPC внутри них. Текущий portfolio snapshot перезаписывается и каскадно удаляется при замене подключения или исчезновении счёта. `TbankSyncService` принимает экранный POST; frontend `screen-sync` запускает его и наблюдает status. Справочные данные, результаты поиска и отрицательные lookup живут в сохранённых таблицах, а detail является зависимостью событий и рыночных данных. Новый режим требует смены инициатора, не второй конкурирующей системы записи.
## Goals / Non-Goals
**Goals:** сохранить единственного владельца публикации каждого источника; сделать расписание устойчивым к рестарту в рамках одного процесса; связать точки истории с успешной публикацией портфеля и подтверждённым доступом; отделить сетевое чтение интерфейса от брокерского обновления; перевести подробные сведения инструментов на онлайн-источник без потери сохранённых рыночных и личных данных.
**Non-Goals:** график и публичная история точек, новый механизм синхронизации между процессами, повторное сканирование полной рыночной истории и уже полных операций, изменение банковских карт и auth-refresh.
## Decisions
### Один durable clock поверх существующих source owners
В `tbank-sync` вводится одиночный backend scheduler с persisted последней попыткой для текущего подключения: начало, завершение/состояние и диагностические разделы без токенов. При успешном PUT подключения фоновой работе передаётся новая граница; при старте process scheduler сверяет её и последнее начало, не опираясь на frontend GET. Проверка часовым таймером только предлагает работу; атомарное резервирование сверяет user/revision, время начала предыдущей попытки и отсутствие ещё активной попытки. Во время работы новое резервирование не допускается; пересечённый срок пропускается, а не исполняется «вдогонку». После рестарта незавершённая попытка старого process становится прерванной, но её время начала остаётся основанием следующего запуска; новый process не публикует её поздние результаты. Успешное подключение начинает новую первую попытку, даже если прежний токен был заменён незадолго до срока. Штатный процесс инициализирует таймер и освобождает его при shutdown.
Scheduler делает один обход подтверждённого membership, затем даёт portfolio owner обновить каждый доступный счёт, operations owner — только изменяемый хвост, forecast/source owners — события бумаг состава. Передаёт доступ и ревизию владельцам, не пишет их личные наборы сам и не держит транзакцию при сетевых вызовах. Результаты отдельных источников дают общий итог `succeeded/partial/failed` и безопасные идентификаторы счёта/раздела. При недействительном токене применяется существующая блокировка личных ответов. Альтернатива — reuse screen intent с поддельным экраном `accounts`: он не покрывает операции и события для каждого счёта и случайно включает ненужные зависимости; выделение фонового orchestration над теми же owners точнее.
### История отдельно от revision, но внутри атомарной публикации
Состояние истории принадлежит `(userId, accountId)` и переживает смену token/revision; текущий snapshot и source work по-прежнему принадлежат `(userId, revision, accountId)`. Точки используют точную десятичную строку, ISO-валюту и фактическое время успешной публикации, без состава позиций. Проверка полноты оценки всех позиций предшествует вставке любых валютных точек; пустой безвалютный портфель не даёт строк. Для однозначности повторной публикации точка привязана к идентификатору успешной попытки portfolio, а её запись и замена snapshot происходят в одной короткой транзакции после conditional attempt claim. При потере revision/доступа вся транзакция отменяется. Неоценённый состав сохраняется текущим без исторических точек.
Видимость истории определяется текущим подтверждённым membership и признаком удаления счёта, а не одной лишь идентичностью исторических строк. Успешное подтверждение списка, успешный PUT нового token и DELETE обновляют пометки доступа вместе со сменой текущей границы; неуспешный PUT/GET membership не меняет их. Чтение возможной истории в будущем обязано проверять session, текущее неотклонённое подключение и активное подтверждение конкретного accountId: `deletedAt = null` само по себе не является полномочием. Это позволяет сохранить прошлые точки, не воскресить приватный снимок прежней revision и не раскрыть историю между отключением и повторным подтверждением. Поскольку endpoint истории сейчас не предусмотрен, API для её чтения не проектируется.
### Сначала разделить источники по смыслу, затем удалить screen import
Обычные GET инвестиционных областей становятся локальными для membership/portfolio и изменяемых операций/прогнозов; их freshness отражает час, а ошибки попыток живут отдельно. Исключения — первичная загрузка отсутствующего исторического интервала операций и впервые запрошенного диапазона свечей; покрытие остаётся владельцем операций/candles. Текущие quotes/незавершённые свечи обновляются по запросу не чаще часа. Удаление ручного screen POST и его наблюдения следует после перехода всех consumers на чистые reads и статус последнего обхода. Иначе нынешние `sync=observe` GET и retry-кнопка могут обходить новый график; транспортный retry сохраняется, но не вызывает брокерский импорт. Публичный DTO статуса описывает только последнюю попытку и читается под той же auth/access boundary. Backend публикует OpenAPI как контракт, frontend обновляет свой snapshot из него отдельно.
### Онлайн-справочник без разрушения зависимостей
Публичный instrument detail, company и catalog search идут через существующие T-Bank/MOEX/NCR clients по запросу с проверкой UID/ticker и сохранением различия «не найдено / неоднозначно / источник недоступен». Общая активная работа может коалесцировать одинаковые сетевые вызовы в процессе, но отдельная постоянная копия справочника и отрицательного lookup не создаётся. Для прогноза необходимые атрибуты запрашиваются внутри его работы; ошибка не заменяет последний успешный forecast. Для сохранённых quotes, candle coverage, событий и portfolio labels остаётся только минимальная стабильная identity, нужная для ключей и чтения; ссылки и пользовательские данные не зависят от удаляемого detail payload. Старые справочные записи выводятся из использования до рассмотрения их удаления; каскады не должны уничтожать свечи. При сбое онлайн-источника блок detail показывает отдельную ошибку, остальные сохранённые данные доступны в своих границах.
## UI/UX Applicability and Package
**Applicability and scope:** `new/changed` — на инвестиционных экранах убираются ручные команды синхронизации, добавляются 30-минутное перечитывание и статус последнего обхода на кабинете. Макет графика не нужен. Пакет полностью inline; локальные PNG/SVG не требуются.
**Package revision:** `1` (этот Design).
**Screens, states and interactions:**
| Экран / блок | Без результата | Данные сохранены | Сбой / частичный успех | Действие |
| --- | --- | --- | --- | --- |
| Кабинет/счета | штатная первая загрузка и отсутствие доступных счетов | прежние карточки и время обновления | сообщение о последнем обходе: состояние, время, названия проблемных счетов/разделов без token/error payload | переход к счёту или настройкам; кнопки синхронизации нет |
| Портфель/операции/доход/события | локальная загрузка либо недостающий исторический период | сохранённые значения и исходное время остаются при перечитывании | server-side stale отдельно от transport error; прежние значения не становятся новым snapshot | повтор только HTTP-чтения при сетевой ошибке |
| Карточка инструмента и поиск | справочный блок ожидает онлайн-ответ; свечи могут впервые загружаться отдельно | сохранённые позиции, операции, подписи, котировки/свечи и события сохраняют самостоятельные состояния | ошибка справочного блока не скрывает независимые данные; подпись snapshot не обещает свежие подробности | открытие и повтор HTTP-чтения сведений без команды синхронизации личных счетов |
30-минутное перечитывание действует только пока инвестиционный экран открыт; для онлайн-справочного блока оно также означает новый запрос сведений. Время последней попытки не подменяет `lastSuccessfulAt` отдельного счёта. Текст частичного успеха включает доступный список проблемных разделов; не показываются сырые upstream сообщения.
**Responsive behavior:** использовать существующие адаптивные контейнеры и ширины списков/карточек; строка статуса переносит названия счетов и разделов на узком экране, не закрывает содержимое и имеет доступное текстовое обозначение помимо цвета. Новых плотных таблиц, графиков или мобильной навигации нет.
**Reused source and differences:** существующие as-built страницы кабинета, портфеля, операций и карточек инструмента сохраняют компоновку; новый пакет не зависит от изменяемой внешней ссылки либо архивного макета. Отличия перечислены выше: статус обхода вместо ручного refresh и локальная ошибка online detail. Идентификатор immutable archived reuse не применяется: новый статус и новые переходы состояний описаны здесь inline, не объявляются точной копией прошлой версии.
## Delivery Map
| Area | Status | Architectural outcome or N/A reason |
| --- | --- | --- |
| frontend | `affected` | Убрать screen-triggered mutation и инвестиционные sync-кнопки, сохранить transport retry, добавить периодическое чтение и статус на кабинете, независимо показывать ошибку online detail. |
| backend | `affected` | Durable scheduler, учёт частичного результата и доступа, точки истории, локальные read boundaries, онлайн-справочник и опубликованный API статуса. |
| specs | `affected` | Нормативные delta requirements для нового расписания, истории и обновлённых инвестиционных/клиентских сценариев. |
| docs | `affected` | Обновить объяснение свежести, областей и глоссарий, зафиксировать новое решение относительно ADR 017 без переписывания его исторического контекста. |
## Solution Assessment
| Вариант | Fit и maturity | Security / data integrity | Operating cost и ограничения | Migration cost |
| --- | --- | --- | --- | --- |
| Внешний cron/worker с отдельным экземпляром | Зрелый для fleet, но дублирует приложение при single-process SQLite | Нужны распределённые lock/lease и граница токенов между процессами | Отдельный deployment и координация | Высокая; против текущей single-process модели |
| Nest scheduling dependency | Зрелые cron decorators/timers, но сама библиотека не хранит результат попыток | В любом случае нужны persisted CAS/fences для restart и подключения | Ещё один runtime package и жизненный цикл без выигрыша от cron expression | Небольшая, но ненужная зависимость |
| Native Node timer + существующие SQLite/Prisma reservations | Подходит для одного процесса; точность минутных тиков не подменяет persisted время начала | Conditional reservation + revision/attempt fence, история в DB transaction | Нет нового сервиса или пакета; при нескольких процессах требуется отдельная координация | Умеренная, повторное использование владельцев источников |
Выбран native timer с durable CAS: уже существующий `TbankSyncWork` и SQLite дают необходимые primitives, но сам таймер не является гарантом уникальности. Для истории использовать уже установленный Prisma/SQLite вместо time-series базы; данные малы, rollback и backup живут в существующем deployment. Для справочных сведений использовать действующих integration clients и online reads; новая cache/service boundary не оправдана, а долговременное кэширование прямо противоречит принятым требованиям. Отказ от него повышает зависимость detail от T-Bank, поэтому ошибки изолируются от сохранённых финансовых наборов. Перед настройкой фактического числа upstream вызовов требуется сверить действующие первичные ограничения провайдера и измерить размер/прирост локальной базы на реальном объёме; произвольные внешние числовые лимиты не фиксируются как допущение.
## Risks / Trade-offs
- При простое backend точки пропускаются; нет искусственной интерполяции и накопленной очереди RPC.
- Один timer без отдельной межпроцессной аренды небезопасен для нескольких экземпляров: deployment сохраняет единственный process.
- Переход на онлайн-справочник увеличивает latency и чувствительность detail/search к upstream; нельзя распространять ошибку на сохранённый портфель или его события.
- Исчезновение/возврат счёта и одновременная смена token требуют fence внутри коротких транзакций; `deletedAt` без актуального подтверждения membership не даёт права чтения.
- Удаление таблиц справочника до развязки FK/dependencies может стереть историю свечей; destructive cleanup откладывается до проверки сохранности рыночных данных.
- Старый frontend и новый backend должны сосуществовать в переходе: публикация DTO статуса предшествует удалению screen POST; потребителю передаётся явный OpenAPI artifact.
## Migration Plan
1. Добавить историю и metadata последней попытки совместимым расширением SQLite; не переписывать уже применённые migrations и не выполнять reset на рабочей базе. Привязать запись истории к существующему portfolio publisher и видимость к подтверждённому membership/connection lifecycle.
2. Ввести backend-only расписание поверх существующих source owners и read-only статус; включить первичную загрузку и восстановление после рестарта при сохранённом времени попытки. Только после проверки idempotency и отказов перевести GET на read-only для текущих личных данных.
3. Развязать online reference от долговременных detail/search/lookup записей, сохранив минимальные identity и таблицы quotes/candles/forecast; сначала перевести readers, затем совместимой миграцией удалять лишь действительно неиспользуемые справочные payload, не каскадируя рыночную историю.
4. Получить опубликованный backend OpenAPI и отдельно обновить frontend query/статус/кнопки в рамках согласованного contract. Затем убрать неиспользуемый screen-triggered endpoint; зафиксировать изменение ADR и as-built страниц. Измерить рост SQLite и provider call volume до изменения частоты/лимитера.
### Rollback Contract
| Field | Decision |
| --- | --- |
| Status | `required` |
| Trigger | Нарушение доступа к истории после замены token, повторная публикация точек, порча сохранённых свечей при переходе справочника либо несовместимость нового frontend с API статуса. |
| Actions | Остановить scheduler, вернуть предыдущие совместимые сборки из проверенного комплекта и восстановить DB из согласованного backup перед destructive переходом; при additive стадии допустимо вернуть старый runtime с сохранением новых неиспользуемых таблиц. Не возобновлять прежний screen-triggered режим одновременно с включённым hourly scheduler. |
| Data safety | Сохранить исходные snapshots, coverage, forecast и точки истории; никогда не удалять или заново создавать production SQLite ради rollback. Перед очисткой справочника обеспечить проверенный backup/restore и доказать отсутствие каскадов в candles. |
| Verification | Проверить на копии с представительными данными доступность прежних личных данных только у текущего подключения, идемпотентность точек, сохранность candles/coverage и совместимость опубликованного API до возврата трафика. |
@@ -0,0 +1,32 @@
## Why
Сейчас актуализация инвестиционных данных зависит от открытия экранов и ручного действия. Неактивный кабинет не получает новые данные, а долгосрочную динамику стоимости счёта восстановить по последнему снимку невозможно. Нужны предсказуемое часовое обновление независимо от frontend и накопление пригодных для будущего графика оценок. Согласованный объём — [issue #22](https://git.personal.ksv741.ru/WealthLedger/main/issues/22).
## What Changes
- Обновлять доступные счета, состав и стоимость портфелей, актуальные операции и будущие события по бумагам портфеля раз в час силами backend; первое обновление выполнять после подключения. Сохранённые данные отдавать сразу, сохраняя последнее успешное состояние при ошибке и показывая результат последней попытки, включая частичный успех.
- Накапливать оценку стоимости каждого счёта по валютам на фактическое время успешного получения полной оценки, без конвертации и подмены отсутствующей цены нулём. Сохранять историю при смене токена и отключении, скрывать её без подтверждённого доступа и продолжать при восстановлении доступа к тому же счёту.
- Убрать ручную синхронизацию с T-Bank и обновление личных данных при открытии инвестиционных экранов. Открытые экраны перечитывают сохранённые данные каждые 30 минут. Сохранить первичную загрузку отсутствующих исторических интервалов и диапазонов свечей, но не перечитывать полностью загруженную старую историю автоматически.
- Получать справочные данные инструмента и результаты поиска онлайн без отдельного долговременного хранения; сохранить подписи последнего снимка портфеля, события, котировки и свечи согласно их разным правилам актуализации.
- Вне объёма: сам график и публичное чтение его точек, валютная конвертация, многопроцессное расписание, автоматическая повторная сверка уже полных старых интервалов операций и свечей, а также изменение действий для ручных банковских карт и повторного чтения после сетевой ошибки.
## Capabilities
### New Capabilities
- `specs/hourly-investment-sync/spec.md`: независимый от открытых экранов часовой обход и наблюдаемый результат последней попытки.
- `specs/portfolio-value-history/spec.md`: точная история оценки по счёту и валюте с учётом полноты и границы доступа.
- `specs/online-instrument-information/spec.md`: получение справочных сведений и поиска онлайн без сохранённой самостоятельной копии, отдельно от котировок, свечей и событий.
### Modified Capabilities
- `specs/portfolio-view/spec.md`: чтение последнего снимка без запуска синхронизации, часовая свежесть, отсутствие ручной синхронизации и сохранение доступных подписей позиций.
- `specs/brokerage-accounts/spec.md`: быстрое чтение сохранённых счетов и сводок, видимость частичных ошибок и периодическое перечитывание экрана.
- `specs/tbank-connection/spec.md`: первая загрузка после подключения и сохранение скрытой истории стоимости при отключении/замене токена.
- `specs/brokerage-account-upcoming-events/spec.md`: обновление прогноза по расписанию с сохранением последнего полного результата при сбое.
- `specs/frontend-data-resilience/spec.md`: разделение повтора HTTP-чтения и запрещённой ручной синхронизации при часовом обновлении.
- `specs/brokerage-account-ledger/spec.md`: первичная загрузка отсутствующего периода без повторной сверки уже полного исторического периода при открытии страницы.
## Impact
Пользователь быстрее открывает инвестиционные экраны и видит время и возможную неполноту последней синхронизации; открытый экран получает новые сохранённые данные без команды обновления. Затронуты инвестиционные экраны, фоновое обновление и хранение личных данных, правила доступа после смены подключения, отображение ошибок и онлайн-получение информации об инструментах. Переход требует согласовать действующие требования, которые пока предписывают короткие сроки свежести и ручные повторы.
@@ -0,0 +1,20 @@
## ADDED Requirements
### Requirement: Отсутствующий исторический интервал загружается только впервые
Система MUST позволять авторизованному User при первом открытии ещё не загруженного периода операций доступного Brokerage Account начать получение недостающего интервала без ожидания T-Bank в локальном ответе. Полностью загруженный прошлый интервал MUST NOT повторно сверяться из-за чтения страницы, часового обхода или удалённой команды ручной синхронизации. Изменяемый хвост операций MUST обновляться часовым обходом. Незавершённый интервал и ошибка загрузки MUST NOT обозначаться полными, а последний подтверждённый результат MUST оставаться доступным.
#### Scenario: Первый просмотр отсутствующего прошлого периода
- **WHEN** User открывает доступный исторический период без подтверждённой полноты
- **THEN** система возвращает локальный результат с явным состоянием неполноты и начинает загрузку недостающих данных
#### Scenario: Повторное открытие полного периода
- **WHEN** User повторно открывает уже полностью загруженный прошлый период
- **THEN** система читает сохранённые операции без повторной сверки с T-Bank
#### Scenario: Ошибка страницы загрузки
- **WHEN** загрузка части отсутствующего периода завершилась ошибкой
- **THEN** ранее подтверждённые операции сохраняются, а неполная часть не помечается как полная
@@ -0,0 +1,20 @@
## MODIFIED Requirements
### Requirement: Upcoming events use complete source results
Система MUST возвращать только последний полностью успешно сформированный прогноз по доступному счёту. Если необходимые данные брокера или MOEX недоступны во время часового обновления, система MUST NOT заменять последний успешный прогноз частичным списком или изменять время его успеха. Инструменты, для которых источник не предоставляет поддерживаемые события, MUST пропускаться без синтетических строк «нет данных». Получение сохранённого прогноза MUST NOT запускать синхронизацию при открытии экрана; ошибку последней попытки MUST показывать отдельно.
#### Scenario: Недоступность источника
- **WHEN** необходимый источник данных брокера или MOEX временно недоступен при часовом обновлении
- **THEN** последний полный прогноз остаётся доступен с исходным временем, частичный список не публикуется, а ошибка отражается в статусе обхода
#### Scenario: Инструмент без поддерживаемых данных
- **WHEN** Portfolio содержит инструмент, для которого источник не предоставляет поддерживаемые будущие события
- **THEN** система не добавляет для него синтетическую строку в результат
#### Scenario: MOEX отвечает без запрошенного блока
- **WHEN** MOEX отвечает успешно, но не содержит запрошенного блока данных (например, дивидендов)
- **THEN** система трактует ответ как отсутствие данных по инструменту и не завершает обновление ошибкой
#### Scenario: Чтение при временном сбое обновления
- **WHEN** User открывает события доступного счёта после неудачного часового обновления и для счёта существует прежний полный прогноз
- **THEN** система возвращает прежний прогноз без ожидания источника и обозначает ошибку обновления отдельно
@@ -0,0 +1,84 @@
## MODIFIED Requirements
### Requirement: Состояния недоступности списка счетов
Система MUST читать подтверждённый доступный список счетов текущего подключения из последнего сохранённого результата и MUST NOT ожидать T-Bank или запускать проверку только из-за GET. При отсутствии T-Bank connection система MUST отвечать `409` Problem Details с code `TBANK_NOT_CONNECTED`. Если T-Bank уже отклонил сохранённый token как неаутентифицированный или без прав, система MUST отвечать `400` Problem Details с code `INVALID_TBANK_TOKEN` и сохранять connection, не выдавая личных данных. Временная ошибка часового обновления MUST оставлять последний подтверждённый список доступным с исходным временем и отдельным состоянием ошибки; при ещё неизвестном списке система MUST обозначать ожидание первой загрузки, не выдавая частичный список за полный.
#### Scenario: Подключение отсутствует
- **WHEN** User без сохранённого T-Bank connection запрашивает список
- **THEN** система не обращается к T-Bank и отвечает `409` Problem Details с code `TBANK_NOT_CONNECTED`
#### Scenario: Сохранённый token больше не действителен
- **WHEN** T-Bank отклоняет сохранённый token при часовом обновлении списка как неаутентифицированный или без прав
- **THEN** последующее чтение получает `400` Problem Details с code `INVALID_TBANK_TOKEN` и не раскрывает личные данные, но connection сохраняется
#### Scenario: T-Bank временно недоступен
- **WHEN** часовая проверка списка завершается временной либо иной upstream-ошибкой после ранее успешной проверки
- **THEN** обычное чтение возвращает последний подтверждённый список без ожидания T-Bank, сохраняя его исходное время и отдельную ошибку обновления
#### Scenario: Первый обход ещё не завершён
- **WHEN** User открывает список после подключения до получения первого подтверждённого списка
- **THEN** система не выдаёт частичный или неподтверждённый список за полный и сообщает о продолжающейся загрузке
### Requirement: Экран списка брокерских счетов
Клиент SHALL загружать список при открытии защищённого маршрута `/accounts` без ручной синхронизации и без автоматического refetch при возврате фокуса. Клиент MUST использовать успешный frontend-кэш списка в течение 60 секунд. После истечения этого срока клиент MAY перечитать локальный результат; пока экран открыт, он MUST перечитывать результат backend каждые 30 минут без запуска импорта. При уже имеющемся успешном списке он MUST сохранять последний успешный Client Cached Snapshot видимым во время фонового перечитывания и после его ошибки, ясно отделяя это состояние от успешного нового ответа. При успешном непустом ответе клиент MUST отобразить адаптивный одноколоночный список широких интерактивных карточек счетов с локализованными типом и, если доступна, датой открытия без времени; каждая карточка MUST ясно показывать тип, имя, финансовую сводку, вторичные метаданные и заметное действие открытия счёта, вести на `/accounts/:accountId` по всей области и сохранять доступное клавиатурное состояние. После успешной замены или удаления T-Bank connection клиент MUST инвалидировать список счетов.
Для доступной `portfolioSummary` карточка MUST показывать не более двух Money-итогов в исходных валютах и обозначать наличие остальных валют, если они есть; она MUST NOT показывать или подразумевать FX-конвертацию, межвалютный агрегированный итог либо Cash Balance как отдельный второй итог. При `portfolioSummary: null` карточка MUST показывать локальное безопасное состояние временной недоступности сводки, сохраняя метаданные счёта и возможность открыть его. При доступной пустой сводке карточка MUST не выдавать отсутствие итогов за временную ошибку.
#### Scenario: Пользователь открывает счёт из списка
- **WHEN** клиент успешно загрузил непустой список и User выбирает карточку счёта
- **THEN** клиент переходит на `/accounts/:accountId` с `id` выбранного счёта
#### Scenario: Список пуст
- **WHEN** клиент получает успешный пустой массив
- **THEN** клиент показывает штатное сообщение об отсутствии инвестиционных счетов T-Bank
#### Scenario: Список берётся из свежего frontend-кэша
- **WHEN** User повторно открывает список в течение 60 секунд после успешной загрузки
- **THEN** клиент показывает сохранённые счета без нового API-запроса
#### Scenario: Устаревший список загружается повторно
- **WHEN** User открывает список после истечения 60 секунд frontend-кэша и уже существует последний успешный список
- **THEN** клиент оставляет этот список видимым во время нового чтения сохранённого результата и ясно обозначает фоновое перечитывание без команды импорта
#### Scenario: Ошибка фонового обновления не скрывает snapshot
- **WHEN** перечитывание устаревшего списка завершается ошибкой при наличии последнего успешного Client Cached Snapshot
- **THEN** клиент сохраняет snapshot видимым, безопасно сообщает о неуспешном чтении и предоставляет повтор именно чтения
#### Scenario: Карточка ограничивает валютные итоги
- **WHEN** доступная сводка счёта содержит итоги в трёх или более валютах
- **THEN** карточка показывает не более двух исходных валютных итогов и обозначает наличие остальных без общего сконвертированного значения
#### Scenario: Недоступность сводки локальна карточке
- **WHEN** клиент получает список, в котором `portfolioSummary` одного счёта равна `null`
- **THEN** он показывает временную недоступность только на этой карточке, не скрывает остальные карточки и сохраняет переход на детали недоступной сводки счёта
#### Scenario: Пустая сводка не является ошибкой
- **WHEN** клиент получает доступную `portfolioSummary` с пустым `totalsByCurrency`
- **THEN** он показывает нейтральное состояние отсутствия финансовых итогов, а не состояние временной недоступности
### Requirement: Клиентские состояния ошибки списка счетов
Клиент MUST NOT автоматически повторять неуспешный запрос списка вне согласованного периодического перечитывания открытого экрана. При `TBANK_NOT_CONNECTED` клиент SHALL показать объяснение отсутствия подключения и действие «Открыть настройки T-Bank». При `INVALID_TBANK_TOKEN` клиент SHALL показать объяснение недействительного token и то же действие. При `TBANK_UNAVAILABLE` клиент SHALL показать безопасное объяснение временной недоступности, действие повторного HTTP-чтения и действие открытия настроек T-Bank. При иной ошибке клиент MUST показать общий безопасный error-state только с действием повторного чтения. Если последний успешный Client Cached Snapshot отсутствует, эти состояния MUST NOT показывать список; если snapshot существует во время фонового перечитывания, клиент MUST сохранить его видимым и показать соответствующее безопасное предупреждение и только применимые действия. Эти действия MUST NOT инициировать синхронизацию с T-Bank.
#### Scenario: Подключение требуется
- **WHEN** клиент получает `TBANK_NOT_CONNECTED`
- **THEN** клиент не показывает список и предлагает открыть настройки T-Bank
#### Scenario: Известная ошибка не повторяется автоматически
- **WHEN** запрос списка завершается известной ошибкой
- **THEN** клиент не выполняет немедленный повтор, пока User не выберет действие повторного чтения либо не наступит очередной 30-минутный срок открытого экрана
#### Scenario: Token требует внимания
- **WHEN** клиент получает `INVALID_TBANK_TOKEN`
- **THEN** клиент не показывает список и предлагает открыть настройки T-Bank
#### Scenario: Временная ошибка T-Bank
- **WHEN** клиент получает `TBANK_UNAVAILABLE`
- **THEN** клиент не показывает список и предлагает повторить локальное чтение либо открыть настройки T-Bank без команды импорта
#### Scenario: Неизвестная ошибка загрузки
- **WHEN** клиент получает ошибку без известного кода списка счетов
- **THEN** клиент не показывает список и предлагает только повторить чтение
#### Scenario: Известная ошибка при сохранённом snapshot
- **WHEN** фоновое перечитывание списка с последним успешным Client Cached Snapshot получает `TBANK_UNAVAILABLE`
- **THEN** клиент сохраняет snapshot видимым, сообщает о временной недоступности и предлагает повторить чтение либо открыть настройки T-Bank
@@ -0,0 +1,16 @@
## MODIFIED Requirements
### Requirement: Client Cached Snapshot остаётся доступным при обновлении Accounts и Portfolio
Frontend MUST сохранять Client Cached Snapshot списка Brokerage Accounts или Portfolio на экране, если для этого же набора данных уже был успешно получен ответ. Начальная загрузка без такого ответа может показывать блокирующее состояние загрузки. Client Cached Snapshot MUST оставаться видимым во время перечитывания сохранённых данных и после его ошибки; он не является server-side fallback и MUST NOT изменять или интерпретировать поле `Portfolio.isStale`. Повтор после транспортной ошибки MUST повторять чтение, а не инициировать синхронизацию с T-Bank.
#### Scenario: Список Brokerage Accounts обновляется в фоне
- **WHEN** экран Accounts уже показывает Client Cached Snapshot и начинается фоновое перечитывание того же списка
- **THEN** frontend оставляет список видимым и показывает только неблокирующий индикатор обновления
#### Scenario: Обновление Portfolio завершается ошибкой после успешного ответа
- **WHEN** экран Brokerage Account уже показывает Client Cached Snapshot Portfolio, а последующее чтение этого же Portfolio завершается ошибкой
- **THEN** frontend сохраняет Portfolio видимым, показывает локализованное неблокирующее предупреждение о показе последних полученных данных и предоставляет повторную попытку чтения без команды синхронизации
#### Scenario: Server-side fallback и client cached snapshot различаются
- **WHEN** backend возвращает Portfolio с `isStale` либо перечитывание Client Cached Snapshot завершается ошибкой
- **THEN** frontend отображает соответствующее server-side либо client-side предупреждение независимо, не представляя один вид состояния другим
@@ -0,0 +1,50 @@
## Purpose
Задаёт независимое от открытых экранов часовое обновление инвестиционных данных пользователя и наблюдаемый результат последней попытки при частичных сбоях.
## ADDED Requirements
### Requirement: Часовой обход личных инвестиционных данных
Backend SHALL начинать первый обход после успешного подключения T-Bank и последующие обходы не ранее чем через 60 минут от начала предыдущей попытки для подключения. При старте после простоя он MUST учитывать время последней попытки, выполнять не более одного актуального обхода и MUST NOT воспроизводить пропущенные часы. Если попытки ещё не было и данных нет либо они старше часа, обход MUST начинаться при старте. Неуспешная попытка MUST NOT повторяться ранее следующего часового срока даже после перезапуска. Перекрывающиеся обходы MUST NOT выполняться; если срок настал во время ещё идущего обхода, соответствующий запуск пропускается.
Обход MUST проверять доступные счета и обновлять портфель каждого доступного счёта, изменяемый хвост операций и будущие события по бумагам текущего портфеля. Он MUST NOT повторно загружать уже полные прошлые интервалы операций, завершённые свечи или полную рыночную историю. Он MUST NOT зависеть от открытого frontend. Смена подключения MUST NOT позволять завершившейся поздно попытке старого подключения изменять личные данные или время их успеха.
#### Scenario: Первое подключение между часовыми запусками
- **WHEN** пользователь успешно подключает T-Bank после запуска backend
- **THEN** первый обход начинается без ожидания следующего календарного часа, а следующий допускается через час от начала первого
#### Scenario: Рестарт после неуспешной попытки
- **WHEN** backend перезапускается до истечения часа после неуспешного обхода с отсутствующими данными
- **THEN** обход не повторяется при рестарте, а последнее успешное состояние остаётся доступным согласно действующей границе доступа
#### Scenario: Перекрытый часовой срок
- **WHEN** начатый час назад обход всё ещё выполняется
- **THEN** второй обход не запускается, а пропущенная попытка не ставится в очередь сразу после завершения первой
#### Scenario: Возобновление после простоя
- **WHEN** backend не работал несколько часов и после старта обнаружил, что последняя попытка старше часа
- **THEN** он запускает только один текущий обход без синтетических прошлых обновлений
### Requirement: Сохранность и статус последней попытки
Успех одного счёта или источника MUST сохраняться независимо от ошибки другого; неполный либо неуспешный результат источника MUST NOT заменять его последнее успешное состояние или время. Backend SHALL предоставлять аутентифицированному пользователю статус только его последнего обхода: время начала, состояние (выполняется, полностью успешен, частично успешен или неуспешен) и проблемные счета и разделы без чувствительных сведений. Кабинет MUST показывать результат, время и проблемные счета/разделы при частичном успехе; отсутствие состоявшейся попытки MUST быть отличимо от успеха. Статус не предоставляет возможности начать синхронизацию с frontend.
#### Scenario: Ошибка одного счёта
- **WHEN** портфели двух счетов обновились, а третий не обновился
- **THEN** новые данные первых двух сохраняются, у третьего остаётся последнее успешное состояние, а статус обхода обозначает частичный успех и проблемный счёт
#### Scenario: Ошибка событий при успешных портфелях
- **WHEN** все портфели обновлены, но событие одной бумаги не обновилось
- **THEN** последний успешный прогноз сохраняется и статус обхода обозначает частичный успех с проблемным разделом
#### Scenario: Пользователь другого подключения запрашивает статус
- **WHEN** авторизованный пользователь запрашивает статус чужого обхода либо попытки старого подключения
- **THEN** система не раскрывает детали чужой или прежней границы доступа
@@ -0,0 +1,33 @@
## Purpose
Определяет получение справочных сведений и результатов поиска инструментов непосредственно от источника без самостоятельной долговременной копии, отдельно от личных данных и рыночной истории.
## ADDED Requirements
### Requirement: Справочные сведения и поиск запрашиваются онлайн
При чтении карточки акции или облигации и выполнении поиска система MUST получать справочные сведения инструмента, включая доступные название, тикер, ISIN, описание и показатели компании, онлайн, а не из отдельной долговременной сохранённой копии. Повторное чтение открытой страницы MUST запрашивать эти сведения снова. Недоступность источника MUST отображаться как ошибка соответствующего справочного блока, не скрывая доступные сохранённые позиции, операции, котировки, свечи и события. Подписи в последнем успешном снимке портфеля MAY использоваться как fallback для идентификации инструмента; они MUST NOT представляться как успешно полученные сейчас подробные сведения. Для расчёта будущих событий MAY запрашиваться необходимые справочные данные без их отдельного хранения.
#### Scenario: Повторное открытие карточки акции
- **WHEN** пользователь повторно открывает страницу акции после прежнего успешного чтения
- **THEN** справочные сведения запрашиваются онлайн заново, а сохранённая копия карточки не выдаётся за актуальный ответ
#### Scenario: Источник справочника недоступен
- **WHEN** подробные сведения акции получить не удалось, но доступен последний сохранённый снимок портфеля
- **THEN** справочный блок показывает локальную ошибку, а сохранённые позиции и их доступные подписи остаются видимы без обещания актуальности подробных сведений
### Requirement: Рыночные и событийные данные имеют отдельную политику
Система MUST продолжать сохранять котировки и запрошенные диапазоны свечей; при открытии страницы обновление текущей котировки и незавершённой свечи MUST выполняться не чаще одного раза в час. Будущие дивиденды, купоны и другие поддерживаемые события MUST сохраняться и обновляться в составе часового обхода бумаг портфеля. Полностью загруженные прошлые диапазоны свечей MUST NOT повторно загружаться автоматически. Получение этих данных MUST NOT запускать синхронизацию личных счетов при открытии страницы.
#### Scenario: Свежая котировка
- **WHEN** пользователь повторно открывает страницу инструмента менее чем через час после получения котировки
- **THEN** система использует сохранённую котировку без преждевременного повторного запроса цены
#### Scenario: Исторический диапазон свечей открыт впервые
- **WHEN** пользователь открывает диапазон свечей, которого ещё нет локально
- **THEN** система загружает его для просмотра, не начиная синхронизацию личного портфеля
@@ -0,0 +1,48 @@
## Purpose
Определяет накопление достоверных точек стоимости брокерского счёта по исходным валютам с сохранением истории между подключениями без раскрытия её вне подтверждённого доступа.
## ADDED Requirements
### Requirement: Полная оценка становится точкой истории
Система MUST добавлять точки на фактическое время каждого успешно полученного полного финансового снимка отдельного Brokerage Account, включая первую загрузку; стоимость MUST храниться раздельно по валютам без конвертации, полным JSON состава или подстановки искусственных точек за часы простоя. Если хотя бы у одной ненулевой позиции текущая стоимость не определена, точки MUST NOT записываться ни для одной валюты этого обновления, хотя новый состав счёта MAY оставаться текущим снимком. Пустой счёт без известной валюты MUST NOT создавать нулевую денежную точку. Ошибка получения обязательных данных MUST NOT менять предыдущие точки или последний успешный снимок. История MUST NOT иметь заранее установленного срока удаления.
#### Scenario: Полная оценка двух валют
- **WHEN** успешно получен полный снимок счёта со стоимостью в RUB и USD
- **THEN** появляются отдельные точки RUB и USD с фактическим временем получения, без общего конвертированного значения
#### Scenario: Неизвестная цена одной позиции
- **WHEN** получен новый состав счёта с оценёнными RUB-активами и позицией без известной цены
- **THEN** состав доступен как текущий снимок с неполной оценкой, но новые точки истории не появляются ни по одной валюте
#### Scenario: Пустой счёт без валюты
- **WHEN** успешно полученный счёт не имеет позиций, денег и известной валюты
- **THEN** история не получает точку с придуманной валютой или суммой
### Requirement: История переживает смену подключения без потери приватности
История MUST продолжаться для того же User и идентификатора Brokerage Account после замены токена или повторного подключения, если доступ к счёту подтверждён текущим подключением. При отключении либо после успешно полученного списка без ранее доступного счёта история MUST оставаться сохранённой, но помечаться удалённой и скрываться от чтения; при повторном подтверждении того же счёта пометка MUST сниматься. Ошибка получения списка MUST NOT менять видимость истории. Подтверждённый отказ токена MUST скрывать личные данные до восстановления доступа; временный сбой MUST сохранять видимым последний успешный результат при действующей границе доступа. История другого пользователя MUST NOT раскрываться.
#### Scenario: Замена токена с прежним счётом
- **WHEN** новый проверенный токен пользователя даёт доступ к прежнему идентификатору счёта
- **THEN** линия его истории продолжается, а поздняя работа старого токена не добавляет точек
#### Scenario: Счёт исчезает из подтверждённого списка
- **WHEN** успешно полученный полный список доступных счетов не содержит ранее доступный счёт
- **THEN** его история сохраняется, помечается удалённой и не раскрывается до повторного подтверждения доступа
#### Scenario: Временный сбой списка
- **WHEN** обновление списка счетов завершилось временной ошибкой
- **THEN** пометки удалённых счетов не меняются
#### Scenario: Отключение и повторное подключение
- **WHEN** пользователь отключил T-Bank, а позже подключил проверенный токен с тем же счётом
- **THEN** до подтверждения доступа история скрыта, после подтверждения она снова видима и пополняется без разрыва идентичности счёта
@@ -0,0 +1,70 @@
## MODIFIED Requirements
### Requirement: Свежесть и fallback полного Portfolio Snapshot
Система MUST хранить последний полный успешно полученный Portfolio Snapshot с принадлежностью User, экземпляру подключения и Brokerage Account. Обычный GET MUST читать сохранённые данные без ожидания T-Bank и MUST NOT запускать повторное обновление личного портфеля, даже если снимок отсутствует или старше часа; актуализацией владеет часовой обход. Portfolio source MUST считаться свежим в течение 60 минут после успешного получения. Ошибка обновления MUST сохранять прежний полный Snapshot и его время без лимита fallback. Если состав получен полностью, но отдельная позиция не имеет цены, система MUST сохранить новый состав с неполной оценкой; metadata MUST NOT определять успешность либо свежесть финансового снимка. Сохранённые подписи позиций MAY оставаться доступны при недоступности онлайн-справочника.
Система MUST делать прежние личные datasets недоступными после успешной замены или удаления подключения. Поздняя работа прежнего подключения MUST NOT публиковаться в новом. Самостоятельные справочные сведения и результаты lookup MUST запрашиваться онлайн, а не сохраняться в виде долговременного справочника; ошибка lookup не подтверждает отсутствие инструмента.
#### Scenario: Свежий Snapshot
- **WHEN** User повторно запрашивает тот же Portfolio в течение 60 минут после успешного получения
- **THEN** система возвращает свежий Snapshot без нового обращения к T-Bank
#### Scenario: Медленный lookup инструмента
- **WHEN** сведения об инструменте ещё запрашиваются онлайн
- **THEN** система не задерживает локальный ответ Portfolio ради lookup и показывает сохранённую в Snapshot доступную identity; подробные сведения остаются отдельным состоянием
#### Scenario: Fallback после временной upstream-ошибки
- **WHEN** обновление устаревшего Snapshot неуспешно из-за временной недоступности T-Bank, а в текущей границе подключения есть полный успешный Snapshot
- **THEN** система отвечает `200` этим Snapshot с `isStale: true` и исходным `lastSuccessfulAt`
#### Scenario: Недействительный token не маскируется fallback-данными
- **WHEN** T-Bank отклоняет token как неаутентифицированный или без прав при обновлении Portfolio
- **THEN** система не возвращает fallback Snapshot и отвечает `400` Problem Details с code `INVALID_TBANK_TOKEN`
#### Scenario: Неполная оценка при новом составе
- **WHEN** успешно получен полный состав позиций, но у одной позиции нет вычислимой цены
- **THEN** новый состав становится текущим Portfolio Snapshot с неизвестной стоимостью позиции, без подстановки нуля
### Requirement: Состояния и ошибки Portfolio на клиенте
Клиент SHALL загружать Portfolio при открытии защищённого маршрута `/accounts/:accountId`. Он MUST использовать успешный ответ в течение 30 секунд, без automatic retry или refetch при возврате фокуса. Открытый инвестиционный экран MUST перечитывать сохранённый результат каждые 30 минут без запуска синхронизации с T-Bank. Наблюдение известной активной работы MAY обновлять отображаемые ресурсы; чтение статуса и данных MUST NOT запускать импорт. Status сообщает состояние и ошибку без hash notifications и численных progress counters. После ухода со страницы наблюдение MUST прекращаться.
Предзагрузка всей истории свечей бумаг портфеля MUST NOT выполняться. Новый диапазон свечей загружается при его открытии; initial loading допустим. При загрузке без ранее полученного успешного ответа для этого же Brokerage Account клиент MUST показывать структурное skeleton-состояние ожидаемого содержимого, включая сводку, без вымышленных сумм. Область загрузки MUST иметь доступное обозначение занятости; skeleton MUST NOT быть интерактивным или получать клавиатурный фокус.
Истечение срока свежести MUST NOT само по себе скрывать уже доступный Client Cached Snapshot. Во время фонового обновления и после его ошибки клиент MUST сохранять ранее полученные данные того же Portfolio, включая валютные итоги и исходное `lastSuccessfulAt`. При обновлении клиент MUST показывать компактную неблокирующую индикацию без замены данных skeleton. При ошибке перечитывания он MUST показывать локализованное неблокирующее предупреждение о последних полученных данных и предоставлять действие повтора только HTTP-чтения. Сохранённые данные MUST NOT считаться новым успешным снимком или подтверждением текущей доступности Brokerage Account. Клиент MUST NOT предоставлять действие ручной синхронизации портфеля с T-Bank.
Клиент MUST показать штатное empty-состояние только при пустых `positions` и `cashBalances`, без нулевых валютных карточек. Для показываемого состава Portfolio клиент MUST сохранять время успешного получения снимка `lastSuccessfulAt`, форматируемое в UTC. При `isStale: true` клиент MUST показывать Snapshot, его исходное время и нейтральное предупреждение об устаревших данных. Это предупреждение само по себе MUST NOT добавлять действие ручного обновления. Server-side stale и ошибка перечитывания Client Cached Snapshot MUST оставаться раздельными состояниями: ошибка клиента MUST NOT менять или подменять `isStale`, а оба предупреждения и действие повтора чтения MUST оставаться доступными при одновременном наличии этих состояний.
При отсутствии ранее успешно полученного Client Cached Snapshot этого же Portfolio и ответе `404` клиент MUST показать состояние недоступного счёта с действием возврата к списку счетов без действия повтора. При отсутствии Snapshot и `TBANK_NOT_CONNECTED`, `INVALID_TBANK_TOKEN`, `TBANK_UNAVAILABLE` или иной ошибке клиент MUST показать безопасное error-состояние с доступными для ошибки действиями; действие повтора чтения разрешено только для временной и неизвестной ошибки.
#### Scenario: Показ устаревшего Portfolio
- **WHEN** клиент получает успешный ответ с `isStale: true`
- **THEN** клиент показывает позиции, остатки, валютные итоги, время последнего успешного снимка и нейтральное предупреждение; само это предупреждение не добавляет действие ручного обновления
#### Scenario: Недоступный счёт на клиенте
- **WHEN** для текущего Portfolio отсутствует ранее успешно полученный Client Cached Snapshot и клиент получает `404`
- **THEN** клиент не показывает Portfolio и предлагает вернуться к списку Brokerage Account без действия повтора
#### Scenario: Первичная загрузка Portfolio
- **WHEN** клиент ожидает Portfolio этого Brokerage Account без ранее полученного успешного ответа
- **THEN** он показывает структурный skeleton, включая место сводки, с доступным обозначением загрузки, без вымышленных или нулевых сумм и без фокусируемых placeholders
#### Scenario: Обновление после истечения срока свежести
- **WHEN** срок свежести показанного Portfolio истёк и начался часовой обход данных того же Brokerage Account
- **THEN** клиент сохраняет прежние валютные итоги и время их успешного получения, показывает неблокирующую индикацию обновления и не заменяет доступный состав и сводку skeleton-состоянием
#### Scenario: Ошибка обновления после успешного ответа
- **WHEN** клиент уже показывает Client Cached Snapshot Portfolio, а повторное HTTP-чтение этих же данных завершается ошибкой
- **THEN** клиент сохраняет данные, суммы и время прежнего снимка, показывает неблокирующее предупреждение и действие повтора чтения; ошибка не делает сохранённый снимок свежим и не изменяет `isStale`
#### Scenario: Ошибка клиентского обновления при server-side stale
- **WHEN** клиент показывает Portfolio с `isStale: true`, а фоновое перечитывание этого же Portfolio завершается ошибкой
- **THEN** прежние суммы и время остаются видимыми одновременно с нейтральным server-side предупреждением, отдельным предупреждением об ошибке клиентского перечитывания и его действием повтора чтения
#### Scenario: Повторное обновление успешно
- **WHEN** после ошибки HTTP-чтения User запускает повторное чтение и получает успешный Portfolio Snapshot
- **THEN** до ответа прежние суммы и время остаются видимыми, затем клиент показывает значения и `lastSuccessfulAt` успешного ответа, убирает предупреждение об ошибке клиентского чтения и показывает server-side предупреждение только в соответствии с новым `isStale`
#### Scenario: Штатный пустой Portfolio на клиенте
- **WHEN** успешно полученный Portfolio содержит пустые `positions` и `cashBalances`
- **THEN** клиент показывает существующее empty-состояние без валютных карточек с вымышленными нулевыми итогами
@@ -0,0 +1,31 @@
## MODIFIED Requirements
### Requirement: Создание и замена подключения T-Bank
Система SHALL позволять авторизованному User создать или заменить единственное подключение через `PUT /api/v1/tbank-connection` с token. Система MUST удалить внешние пробелы token, отклонить пустое значение как ошибку валидации и проверить token запросом T-Bank открытых счетов до сохранения. Пустой список счетов MUST считаться успешной проверкой. Успех MUST вернуть `200` с текущим статусом подключения; успешная замена MUST установить новый `connectedAt`. После успешного подключения MUST начинаться первая фоновая загрузка доступных личных данных без команды frontend. История стоимости прежних счетов MUST сохраняться, но показываться только для счетов, доступ к которым подтверждён новым подключением.
#### Scenario: Успешное создание подключения без счетов
- **WHEN** User отправляет непустой token и T-Bank успешно возвращает пустой список открытых счетов
- **THEN** система сохраняет подключение и отвечает `200` с `{ connected: true, connectedAt: <RFC 3339 UTC timestamp> }`; первая загрузка не создаёт фиктивные счета
#### Scenario: Успешная замена подключения
- **WHEN** User с подключением отправляет другой token и T-Bank успешно его проверяет
- **THEN** система заменяет token подключения, обновляет `connectedAt`, отвечает текущим статусом без token и начинает обновление подтверждённых счетов, не теряя их прежние исторические точки
#### Scenario: Невалидный token не заменяет существующее подключение
- **WHEN** T-Bank отклоняет token как неаутентифицированный или не имеющий прав
- **THEN** система отвечает `400` Problem Details с code `INVALID_TBANK_TOKEN` и сохраняет прежнее подключение без изменений
#### Scenario: T-Bank временно недоступен при проверке token
- **WHEN** проверка token не завершается из-за временной или иной upstream-ошибки
- **THEN** система отвечает `503` Problem Details с code `TBANK_UNAVAILABLE` и не создаёт либо не изменяет подключение
### Requirement: Удаление подключения T-Bank
Система SHALL позволять авторизованному User удалить своё подключение через `DELETE /api/v1/tbank-connection`. Удаление MUST быть идемпотентным и отвечать `204`; после успешного удаления backend MUST прекратить использовать удалённый token и удалить относящиеся к User кэшированные текущие данные T-Bank. История стоимости счёта MUST сохраняться с пометкой удаления и MUST NOT раскрываться до подтверждения нового доступа к тому же счёту.
#### Scenario: Удаление существующего подключения
- **WHEN** User удаляет существующее подключение
- **THEN** система удаляет token и связанные кэшированные текущие данные User, скрывает сохранённую историю стоимости и отвечает `204`
#### Scenario: Повторное удаление подключения
- **WHEN** User удаляет уже отсутствующее подключение
- **THEN** система отвечает `204`, не раскрывая сохранённую историю
@@ -0,0 +1,68 @@
## 1. Данные и граница доступа
- [x] 1.1 [backend] Добавить совместимое хранение точек оценки и видимости истории отдельно от revision; сохранять по валютам только полную оценку в той же транзакции, что и успешно заявленный снимок, с дедупликацией попытки.
- **Covers:** `portfolio-value-history` — `Полная оценка становится точкой истории`; `portfolio-view` — `Свежесть и fallback полного Portfolio Snapshot`
- **Verification:** Изолированная SQLite с применёнными прежними migrations: новая миграция сохраняет старые данные; повторная/поздняя попытка не создаёт лишних точек, неизвестная цена не записывает ни одной валюты, неуспешная публикация сохраняет снимок и историю.
- [x] 1.2 [backend] Связать видимость истории с успешными PUT/DELETE подключения и публикацией полного membership; закрыть доступ старой revision и оставить историю для повторно подтверждённого accountId.
- **Covers:** `portfolio-value-history` — `История переживает смену подключения без потери приватности`; `tbank-connection` — `Создание и замена подключения T-Bank`; `tbank-connection` — `Удаление подключения T-Bank`
- **Verification:** SQLite/HTTP проверки смены токена, отключения, отсутствующего и вернувшегося счёта, ошибки membership, чужого User и поздней попытки; прежние текущие datasets не раскрываются и история не исчезает.
## 2. Часовой обход и локальные чтения
- [x] 2.1 [backend] Применить выбранный в Design Solution Assessment: один native timer и durable conditional reservation для запуска и восстановления часовой попытки без параллельного обхода.
- **Covers:** `hourly-investment-sync` — `Часовой обход личных инвестиционных данных`
- **Verification:** SQLite/integration сценарии первого подключения, рестарта до и после 60 минут, пропущенных часов, перекрытого тика, замены revision и перезапуска незавершённой работы; ни один сценарий не создаёт конкурирующих попыток.
- [x] 2.2 [backend] Оркестрировать подтверждённый membership, портфели счетов, изменяемый хвост операций и события через существующих владельцев источников; агрегировать результат последней попытки и открыть только read-only статус авторизованному User.
- **Covers:** `hourly-investment-sync` — `Часовой обход личных инвестиционных данных`; `hourly-investment-sync` — `Сохранность и статус последней попытки`; `brokerage-account-upcoming-events` — `Upcoming events use complete source results`
- **Verification:** Integration/HTTP проверки: ошибка одного счёта или события даёт частичный статус с безопасным разделом, успешные источники публикуются, старый прогноз и время переживают сбой; чужой User/старая revision не видны, OpenAPI contract parity проходит.
- [x] 2.3 [backend] Перевести обычные GET счетов, портфеля и прогноза на локальное чтение без запуска обновления личных источников; согласовать часовую свежесть, первый незавершённый обход и отдельно отображаемый сбой.
- **Covers:** `portfolio-view` — `Свежесть и fallback полного Portfolio Snapshot`; `brokerage-accounts` — `Состояния недоступности списка счетов`; `brokerage-account-upcoming-events` — `Upcoming events use complete source results`
- **Verification:** HTTP/integration проверки локального ответа без RPC при missing/stale, неизвестного membership, временного сбоя, известного invalid token, неполной оценки и доступности последнего успешного снимка.
- [x] 2.4 [backend] Оставить для журнала запуск только первичной загрузки отсутствующих интервалов; обновление изменяемого хвоста перенести в часовой обход, не перечитывая полные прошлые интервалы.
- **Covers:** `brokerage-account-ledger` — `Отсутствующий исторический интервал загружается только впервые`; `hourly-investment-sync` — `Часовой обход личных инвестиционных данных`
- **Verification:** SQLite/integration проверка пустого полного периода, неполной страницы, повторного открытия прошлого месяца, движения конца текущего периода и неповторного чтения сохранённой истории.
## 3. Инструменты и граница публичного API
- [x] 3.1 [backend] Развязать сохранённые котировки, свечи и события от справочных payload; обновлять текущую котировку и изменяемую свечу не чаще часа, сохраняя покрытия завершённых диапазонов и полные прогнозы.
- **Covers:** `online-instrument-information` — `Рыночные и событийные данные имеют отдельную политику`; `brokerage-account-upcoming-events` — `Upcoming events use complete source results`
- **Verification:** Integration с изолированной SQLite: первая загрузка диапазона, отсутствие повторной загрузки полного прошлого диапазона, запрос свежей котировки, неудача онлайн-справочника и сохранность forecast/coverage/candles; миграция не удаляет связанные рыночные строки.
- [x] 3.2 [backend] Перевести detail, company, поиск и lookup инструментов на онлайн-ответ через существующие integration clients; отделить их ошибку от сохранённых подписей и финансовых ресурсов без самостоятельной долговременной копии.
- **Covers:** `online-instrument-information` — `Справочные сведения и поиск запрашиваются онлайн`; `portfolio-view` — `Свежесть и fallback полного Portfolio Snapshot`
- **Verification:** HTTP/service проверки повторного онлайн-запроса, совпадения UID и ticker, пустого/неоднозначного результата и upstream-сбоя при видимом сохранённом портфеле; не создаются новые долговременные detail/search/negative lookup.
- [x] 3.3 [backend] Сформировать публичный OpenAPI со статусом обхода, сохранив переходный контракт screen POST до готовности frontend.
- **Covers:** `hourly-investment-sync` — `Сохранность и статус последней попытки`; `portfolio-view` — `Состояния и ошибки Portfolio на клиенте`
- **Verification:** `npm run openapi:generate`, `npm run test:openapi-contract` и configured HTTP suite для действующего и переходного wire contract; обычный GET не инициирует screen POST.
- [x] 3.4 [backend] Проверить рост SQLite на представительном объёме и фактическую частоту upstream-вызовов; сопоставить её с действующей первичной документацией T-Bank перед настройкой частоты, выполнить штатные backend gates и durability для новой схемы.
- **Covers:** `N/A` — операционная проверка выбранного в Design хранения и scheduler, без нового пользовательского поведения.
- **Verification:** Выполнено: SQLite 617,263,104 bytes на synthetic profile (3 accounts, 300,000 operations, 200 instruments, 1,000,000 candles), persisted-read phase сделал 0 upstream calls; `npx prisma validate`, durability с pre-history baseline, `npm run test:e2e` и performance budget проходят. Ограничение `TBANK_RATE_LIMIT_PER_SECOND` (default 5 per backend instance) зафиксировано в `docs/adr/020-hourly-sync-verification-evidence.md`; production quota не расширяется.
## 4. Клиентские экраны
- [x] 4.1 [frontend] Получить опубликованный backend OpenAPI artifact и обновить локальный snapshot/generated types для статуса без импорта backend internals.
- **Covers:** `hourly-investment-sync` — `Сохранность и статус последней попытки`
- **Verification:** `npm run openapi:generate` и `npm run openapi:check` из frontend на предоставленном artifact; статус типизирован, отсутствие попытки и изоляция ошибки не заменены вымышленным успехом.
- [x] 4.2 [frontend] Удалить инвестиционные кнопки/POST ручной синхронизации и source-recovery triggers; сохранить транспортный retry только как повтор чтения, а открытые инвестиционные экраны перечитывать раз в 30 минут.
- **Covers:** `portfolio-view` — `Состояния и ошибки Portfolio на клиенте`; `brokerage-accounts` — `Экран списка брокерских счетов`; `brokerage-accounts` — `Клиентские состояния ошибки списка счетов`; `frontend-data-resilience` — `Client Cached Snapshot остаётся доступным при обновлении Accounts и Portfolio`
- **Verification:** UI/query проверки 30-минутного активного экрана, прекращения опроса после ухода, сохранения снимка при сбое и повторного HTTP GET без POST; auth-refresh и действия ручных карт не меняются.
- [x] 4.3 [frontend] Показать в кабинете время/исход последнего обхода и названия проблемных счетов и разделов; справочный блок карточки отделить от позиций, событий, свечей и локальной ошибки чтения.
- **Covers:** `hourly-investment-sync` — `Сохранность и статус последней попытки`; `online-instrument-information` — `Справочные сведения и поиск запрашиваются онлайн`; `brokerage-accounts` — `Экран списка брокерских счетов`
- **Verification:** Проверки состояний `none/running/succeeded/partial/failed`, локального сбоя detail при видимых финансовых данных, доступных текстовых подписей и переноса статуса на узком экране согласно inline UI package Design revision 1.
- [x] 4.4 [frontend] Завершить клиентский переход штатными gates и сверкой критического маршрута нового подключения → первый обход → статус → сохранённый портфель на открытом экране.
- **Covers:** `N/A` — интеграционная проверка принятого UI package и публичного контракта после изменения нескольких экранов, без отдельного нового требования.
- **Verification:** `npm run verify` в frontend и наблюдаемые состояния критического маршрута с предоставленным backend OpenAPI.
## 5. Завершение перехода
- [x] 5.1 [backend] После перехода frontend убрать неиспользуемый screen POST и наблюдение прежней работы без затрагивания нового статуса обхода.
- **Covers:** `hourly-investment-sync` — `Сохранность и статус последней попытки`; `portfolio-view` — `Состояния и ошибки Portfolio на клиенте`
- **Verification:** `npm run openapi:generate`, `npm run test:openapi-contract` и configured HTTP suite: старый trigger недоступен, обычный GET и повтор чтения не запускают импорт.
## 6. Нормативная и as-built документация
- [x] 6.1 [specs] Проверить полное соответствие девяти Delta Specs одобренному Proposal и Design, сохранить точные имена Requirements и валидировать change и основные specs.
- **Covers:** `N/A` — самостоятельная проверка нормативного артефакта и согласованной границы изменения, без добавления нового поведения.
- **Verification:** `openspec schema validate wealth-ledger-sdd --verbose`, `openspec validate hourly-portfolio-sync-history --type change --strict --store wealth-ledger-specs`, `openspec validate --specs --strict --store wealth-ledger-specs`, `npm run verify` в specs.
- [x] 6.2 [docs] После проверки реализованного поведения дополнить глоссарий и объяснение свежести/истории, обновить соответствующие страницы инвестиций и зафиксировать новый ADR, который связывает изменённое решение с ADR 017, не переписывая исторический контекст.
- **Covers:** `N/A` — as-built объяснение подтверждённых решений Design в отдельном docs owner, не параллельная норма требований.
- **Verification:** Проверить маршруты чтения, локальные ссылки и смысл финансовых значений; `npm run verify` из docs.
@@ -0,0 +1,2 @@
schema: wealth-ledger-sdd
created: 2026-09-24
@@ -0,0 +1,566 @@
## Context
Основание — принятый [ADR 017](../../../../docs/adr/017-local-tbank-reads-and-background-sync.md)
и [дельты поведения](specs/tbank-data-sync/spec.md). Это детализация согласованной
модели для реализации. Компоненты приложения сейчас не изменены.
Исследованы локальные владельцы после чтения AGENTS каждого репозитория.
Свидетельства относятся к checkout на 2026-09-24, а не к обещаниям API:
| Граница | Текущий owner и переиспользуемое поведение |
| --- | --- |
| Хранение | `backend/prisma/schema.prisma` и `src/prisma/prisma.service.ts`: SQLite/Prisma, пользователи, сессии, подключение и ручные карты; T-Bank datasets отсутствуют. Условия владения и транзакции уже применяются в `modules/bank-cards/infrastructure/bank-cards.repository.ts`. |
| Доступ | `backend/src/integrations/tbank/tbank-access.service.ts`: `withSupportedAccounts` и `withAccountForConnection` вызывают `getOpenAccounts`; generation вычисляется из encryptedToken. `tbank-account.policy.ts` владеет допустимыми типами счетов. |
| Интеграция | `backend/src/integrations/tbank/tbank.client.ts`: общий FIFO, лимит 5 RPC/с по умолчанию, максимум 50 ожидающих, deadline и ограниченные повторы; повторный limiter не нужен. |
| Личный источник | `portfolio/application/portfolio.service.ts` хранит RAM-снимок 30 с / fallback 5 мин; `operations/application/brokerage-operations.service.ts` — страницы 30 с и обход курсоров. `income/income-analytics.service.ts` строит selected/previous/yearAgo через общий сборщик операций. Пути внутри `backend/src/modules/investments/`. |
| Общий источник | `backend/src/integrations/tbank/instrument-metadata.service.ts` уже разделяет общие metadata и pending lookup. Каталоги stocks/bonds смешивают общую информацию и личную позицию в user+generation RAM-кэше. Market services режут свечи по 365 суток. |
| Выплаты и смежные данные | `upcoming-events/upcoming-events.service.ts` отдельно получает portfolio; stock dividends приходят из T-Bank, bond events — из MOEX. Stock company use case получает asset description/fundamentals; bond metrics включают MOEX. Источники не подменяются. |
| Frontend | `frontend/src/entities/*/api` владеют generated queries; page VM собирает зависимости. `features/instrument-details/model/section.ts` знает только HTTP/query lifecycle. `features/tbank-connection/model/connection-boundary.ts` уже сбрасывает финансовую границу после успешной mutation. |
| Контракт | Backend DTO/controllers/Swagger — editable source. `frontend/src/shared/api/spec/openapi.json` — явно передаваемый snapshot. Accounts и search сейчас массивы; общего sync/coverage API нет. |
Исходный proto допускает изменение operation ID
(`backend/src/integrations/tbank/proto/contracts/operations.proto:496`),
но проект явно принимает стабильность. `is_complete` свечи задаёт источник
(`marketdata.proto:438`). В `operations.proto:472–489` не определены
inclusive/exclusive границы, срок жизни cursor и значение пропущенного `from`.
Эти пробелы не заменяются предположениями о гарантиях брокера.
## Goals / Non-Goals
**Goals:** один постоянный источник чтения в существующей БД; независимые
полнота, свежесть и состояние работы; атомарная публикация согласованного
набора; безопасный перезапуск; отсутствие RPC на пути ответа заполненной БД;
контракт всех экранных зависимостей и минимальный наблюдаемый worker.
**Non-Goals:** Redis, новый сервер БД, отдельный сервис очередей, cron,
распределённое выполнение, версии каждого портфельного снимка, live
подтверждение доступа при каждом чтении, архив отключённых счетов,
новый frontend store, изменение денежных правил или источников MOEX.
## Decisions
### 1. Существующие domain owners читают БД; RPC принадлежит синхронизации
Сохраняются controllers, application services, нормализация Money и domain
политики. Их read path получает сохранённые источники и согласованное
metadata, а запрос работы передаёт небольшому T-Bank sync coordinator.
PortfolioService становится общим владельцем последнего полного portfolio
для accounts summary, личных позиций инструментов и будущих событий.
Сборщик операций становится одним локальным источником ledger/income;
filtered HTTP page больше не является единицей источниковой полноты.
Рассчитанные доходы не требуют отдельной постоянно дублируемой таблицы.
Синхронизация использует существующие TbankClient, metadata mapping,
market-window splitting, точную денежную арифметику и MOEX adapters.
Нормализованные числа сохраняются десятичными строками, не float.
Presentation metadata не блокирует выдачу финансового снимка; fallback
UID/FIGI/name остаётся доступен до её фоновой загрузки.
Синхронная проверка token при PUT подключения сохраняется. Успешно полученный
в этой проверке список можно сразу опубликовать как membership нового
подключения в той же транзакции, без второго GetAccounts.
### 2. Личные и общие модели в SQLite
Это логические группы таблиц, не универсальный JSON-кэш всех HTTP-ответов:
| Группа | Ключ, содержимое и инвариант |
| --- | --- |
| Подключение и membership | Существующий `TbankConnection` получает opaque `revision` и состояние известного отказа token. Список открытых поддерживаемых счетов принадлежит `(userId, revision)`; хранит accountId, type, name, openedOn, upstream order, fetchedAt, membershipRevision. Список заменяется целиком атомарно, включая успешный пустой. |
| Portfolio | `(userId, revision, accountId)` → последний полный нормализованный snapshot, fetchedAt и dataRevision. Допустимо хранить состав snapshot как versioned JSON: он читается целиком, не нужен отдельный исторический агрегат. |
| Operations | Уникальность `(userId, revision, accountId, brokerOperationId)`; индекс `(userId, revision, accountId, occurredAt, brokerOperationId)`, дополнительные instrument/category поля для фильтров. Полный исходный нормализуемый payload сохраняет точные поля, executed state и version маппинга. Cursor/пустая строка не заменяют отсутствующий ID. |
| Инструменты | Общий ключ provider + instrument UID; FIGI/ticker — aliases с существующими правилами неоднозначности. Metadata, company/fundamentals и catalog discovery имеют собственные успешные timestamps и coverage; отрицательный успешный lookup отличается от ошибки. |
| Рыночные данные | Общие quote/trading-status и необходимые price dependencies; дневная свеча уникальна по `(provider, instrumentUid, interval, time)`, содержит `isComplete`. Календари имеют source/type/instrument/range и исходные события. MOEX enrichment сохраняет собственный source и существующую семантику недоступности. |
| Последний полный прогноз | Private snapshot account events и instrument personal payout estimates по `(userId, revision, account scope, instrument при наличии, applied range)`, с dependency revisions, source timestamps и calculatedAt. Заменяется только после готовности всех обязательных источников; это fallback результата, не история portfolio. |
| Покрытие и работа | Завершённые диапазоны, fetchedThrough/fetchedAt, dataRevision, известная нижняя граница и её происхождение; pending intent, attemptId, processRunId, диапазон, стадия и прогресс страниц. Staging страниц текущей попытки изолирован от опубликованного набора. |
Общие таблицы не содержат user/account/quantity/estimatedAmount или token.
Личные payout estimates, holdings и фильтрованная личная история вычисляются
из разрешённых accounts и portfolio/operations. Статус пользовательской
работы хранится отдельно от общих instrument records; ошибка credentials
не становится общим отрицательным lookup.
FK и unique constraints обеспечивают идентичность; транзакции проверяют
владение и revision. Не создаются generic repository framework, event bus
или универсальный job engine. Точные SQL-имена выбираются по локальным
Prisma conventions без изменения этой модели.
### 3. Доступ без синхронного GetAccounts
Read path: локальная сессия → существующее подключение и его revision →
известная ошибка token → membership текущей revision → данные разрешённых
счетов. Token для этого не расшифровывается.
- Membership отсутствует: `200` с `data: null`, `availability: missing` и
состоянием discovery, без данных неизвестного accountId. После discovery
следующий read возвращает данные либо прежний `404`.
- Membership свежий: разрешённые данные читаются локально.
- Membership старше пяти минут: используется последнее успешное наблюдение,
обычный запрос ставит его фоновое обновление. Это ограниченная задержка
обнаружения отзыва доступа, принятая локальной моделью, не live authority.
- Успешный новый список немедленно ограничивает все последующие одиночные
и aggregate reads; добавленный ещё не загруженный счёт делает aggregate
неполным. Ответ «Все счета» использует одну согласованную DB read transaction
с фиксированной membershipRevision.
- Известный `INVALID_TBANK_TOKEN` блокирует личные reads и возвращает прежний
`400`; `409 TBANK_NOT_CONNECTED`, `404`, session/validation errors сохраняются.
Старые записи не удаляются вследствие transient failure, но не выдаются
вопреки известному отказу доступа. Исправление token выполняется существующим PUT.
При замене успешная транзакция назначает новую случайную revision, сохраняет
новый token и очищает/делает недоступными прежние private datasets/jobs
по существующей семантике invalidation. Delete остаётся идемпотентным и
каскадно убирает личные данные подключения; специальный архив не вводится.
Общие неперсональные данные могут оставаться.
Каждая публикация данных, coverage, ошибки, timestamp и результата работы
проверяет в той же короткой транзакции текущие connection revision и
attemptId. Проверка после RPC только в памяти недостаточна. Проверка нужна
и при общей публикации, начатой credentials старого подключения.
Поздняя попытка не вправе изменить даже freshness; результаты старой revision
отбрасываются. Транзакция никогда не удерживается во время сетевого вызова.
### 4. Интервалы и атомарная сверка операций
Внутри БД диапазоны — `[from, to)` в UTC. Public semantics не меняются:
журнал принимает действующие UTC timestamps, его default начинается 1 января
UTC; даты доходов включительны в `Europe/Moscow`, и существующий
`planIncomePeriod` вычисляет selected/previous/yearAgo. Преобразование границ
выполняется один раз до планирования. Полнота доходного месяца проверяется
по объединению UTC coverage, а не по совпадению названия месяца.
Источник по операциям загружается ограниченными календарными UTC-месяцами
и фрагментами нужных диапазонов. Верхняя граница текущей попытки фиксируется
в момент её создания. Для неизвестной inclusivity upstream применяется
перекрытие соседних окон с защитной секундой; записи локально отбираются
по точному `[from, to)` и дедуплицируются по исходному ID. Это защищает
стык, но не доказывает отсутствие будущих исправлений брокера.
Обычная полнота счёта строится только по всем EXECUTED операциям интервала
без category/instrument фильтров. UI filters и pagination применяются после
чтения. Ручной refresh личной истории инструмента может перечитать только
`instrumentUid + shown range`; его reconciliation удаляет отсутствующие
записи только этого predicate и не присваивает полноту всему счёту.
Уже полное общее покрытие может обслуживать такой subset; обратное неверно.
Каждая успешная страница записывается в staging attempt с page count и
прогрессом. При наличии полного прежнего набора staging никогда не виден
как его замена. Новые большие периоды становятся доступными по завершении
малых интервалов при первичном заполнении, поэтому экран может иметь `partial` coverage без показа
непроверенных страниц. Сбой, repeated/missing next cursor, malformed ID или
противоречащие версии одного ID внутри попытки оставляют интервал незавершённым.
Идентичные дубликаты страницы безопасно схлопываются.
Только успешное исчерпание всех страниц фиксированного интервала разрешает
транзакцию публикации: upsert новых/изменённых полей, удаление отсутствующих записей
в перечитанном predicate, публикация coverage и времени успешной сверки.
Account dataRevision увеличивается только при изменении rowset;
coverage/timestamp/status без изменения строк меняют только stateVersion.
Читатель видит старый либо новый набор,
не их смесь. Между интервалами частичная готовность допустима и явно видима;
сумма всего периода окончательна только при полном покрытии его зависимостей.
Для повторной сверки уже полного показанного периода единица публикации —
весь запрошенный scope, а не отдельный календарный chunk. Staging объединяет
его интервалы, сравнения и счета (`all`) в одну publication group; успешные
страницы/окна сохраняют прогресс, но commit/upsert/delete/coverage всех
участников выполняются только после успеха группы. Любая ошибка оставляет
весь прежний результат. Такое же правило действует для ручного refresh
показанного диапазона свечей. Первичная загрузка ранее отсутствующих данных
по малым интервалам остаётся инкрементальной. Пересекающиеся работы
присоединяются к группе и не публикуют её часть независимо. DB transaction
короткая и локальная; RPC выполняются до неё, а staging не держится в RAM.
Для изменяемого конца `to=now` полнота означает «все операции по состоянию
на `asOf` последней успешной сверки». Новый ещё не наблюдённый хвост времени
в пределах того же текущего месяца и TTL не считается пропуском, запускающим
новую загрузку; ответ сохраняет исходный `asOf`, не утверждая покрытие будущих
операций. Настоящие пропуски до `asOf`, новый явно запрошенный исторический
диапазон и переход месяца этому исключению не подчиняются.
Текущий месяц обновляется целиком до зафиксированного now при истечении
пяти минут, чтобы учесть изменения уже виденных операций. После смены месяца
его покрытие до прежнего now не распространяется до конца месяца:
следующий подходящий запрос завершает хвост. Полные прошлые интервалы
не сверяются автоматически. Успешная наблюдённая смена полей того же ID
обновляет запись и производные результаты; поиск сменившихся ID отсутствует.
### 5. Планирование, приоритет и восстановление
Поддерживаемая поставка — один backend process с существующей SQLite.
Запрос сохраняет intent, возвращает локальные данные и передаёт выполнение
in-process coordinator. На startup нет обхода очереди или запуска импорта.
Process получает случайный runId; следующий подходящий запрос перехватывает
незавершённую работу другого runId новой attemptId. Persisted provider cursor
не переиспользуется после перезапуска: незавершённый интервал начинается
с первой страницы, staging прежней попытки очищается. Завершённые интервалы
и опубликованные данные сохраняются.
Приоритеты: discovery доступа → нужные экрану зависимости и интервалы,
включая comparisons → остальная история счёта и рыночная история его бумаг.
Между окнами/страницами coordinator отдаёт приоритет новой экранной работе;
уже отправленный RPC не прерывается ради перестановки. Один producer
догрузки не ставит тысячи RPC заранее: страницы идут последовательно,
новые окна планируются постепенно, через прежний bounded limiter.
Очередь RPC и bounded retry не дублируются на уровне worker.
Ключ personal work: user + connectionRevision + account + source kind +
normalized interval/predicate. Координатор сериализует публикации одного
account source и для обычной загрузки вычитает покрытые/выполняемые
пересечения; одновременно запрошенные остатки объединяет. Force refresh
не вычитает полное покрытие: он обязан перечитать запрошенный target,
но присоединяется к уже выполняемой подходящей загрузке. Повторный force refresh присоединяется к
подходящей активной попытке, уже получающей полный запрошенный диапазон;
TTL при этом не отменяет запрос. Новая область расширяет pending intent.
Shared work использует provider/instrument/source/range identity.
Общий успешный результат переиспользуется независимо от пользователя.
У каждого наблюдателя свой авторизованный handle, без выдачи чужих job IDs
и credentials errors. Ошибка token инициатора завершает его попытку, не
портит общий источник; новый допустимый запрос другого пользователя может
запустить попытку со своими credentials. Старый job не перехватывает token
другого пользователя сам.
Остальная история начинается с известной даты открытия; если её нет,
используется существующая техническая граница `1970-01-01T00:00:00Z`,
явно помеченная как requested floor, а не доказанная дата открытия/минимум
источника. Сохраняются confirmed intervals и `historyBoundary: unknown`;
«вся история загружена» не показывается до подтверждения нижней границы.
Отказ источника за пределами доступного диапазона сохраняется как ограничение,
не как пустой полный интервал. Отсутствие `from` не используется как
недокументированное обещание all-time. Это не мешает полной готовности
подтверждённых периодов экрана.
После transient failure попытка заканчивается с error; новый обычный запрос
может возобновить недостающее/устаревшее, ручной — повторить независимо от TTL.
Наблюдение результата этого не делает. Logout/уход останавливает наблюдение
frontend, но не отменяет уже запрошенную допустимую серверную работу.
Замена/delete подключения останавливает публикацию по revision fence.
### 6. Общие market data и производные выплаты
Quotes и portfolio обновляются через минуту; metadata/catalog/company и
календари — через сутки. Freshness принадлежит конкретному source/range,
поэтому успешный lookup одного инструмента не делает свежим весь каталог.
Catalog discovery сохраняет успешный набор UID/порядок отдельно от
enrichment по нормализованному поисковому query и типу инструмента, сохраняя
существующий предел результатов. `findShares/findBonds` не загружают весь
каталог: успешный поиск SBER не подтверждает результат GAZP. Новый query
без подтверждённого покрытия запускает discovery, а не возвращает ложный
пустой результат. Metadata уже известных UID переиспользуется; повтор того
же свежего полного query не вызывает discovery RPC. Отсутствие enrichment
не превращает найденный набор в пустой поиск.
Свечи сохраняют исходную `isComplete`, диапазоны и `historyFrom`.
Переиспользуются окна до 365 суток. Завершённый диапазон без свечей хранится
как проверенное пустое покрытие. Текущий UTC-день всегда изменяемый,
включая пустое окно до торгов; его TTL — минута. Смена UTC-дня сама не
закрывает ни пустое открытое окно, ни незавершённую свечу: следующий запрос
повторяет прошедшее окно; успешный закрытый пустой диапазон подтверждает
отсутствие, для имеющейся свечи нужно `isComplete=true`.
Известный `first_1dayCandleDate` определяет нижнюю границу all-range.
Если его нет, сохраняется существующее `*_HISTORY_START_UNAVAILABLE`,
не создаётся ложная полная история. Предзагрузка касается бумаг сохранённого
текущего портфеля и использует ту же общую coverage, что открытый график.
События инструментов сохраняются без пользовательской ожидаемой суммы.
Календари купонов/дивидендов и company data охватываются общим read path;
future events используют сохранённый portfolio и исходный календарь,
сохраняя неопределённость прогноза, cancelled-dividend и offer semantics.
MOEX bond events/metrics остаются MOEX: необходимые для экрана результаты
также сохраняются и обновляются в этой работе, без нового MOEX scheduler.
Недоступность optional enrichment не делает финансовые данные ошибочными;
неполнота обязательного календаря не выдаётся за полный пустой прогноз.
Последний полный прогноз/личная оценка сохраняется отдельным private
snapshot по показанному scope/range. Если новый portfolio добавил бумагу,
а её календарь не загрузился, следующий read, в том числе после restart,
возвращает прежний полный forecast с его прежними dependency timestamps и
ошибкой обновления. При отсутствии такого snapshot возвращается missing/partial
без окончательного списка. Готовый новый результат атомарно заменяет snapshot;
снимок с иным периодом или membership не выдаётся под новым scope, а доступ
всегда повторно ограничивается текущим membership.
### 7. Публичный контракт и наблюдение работы
**Contract Gate Decision: required.** Меняется wire shape всех T-Bank data
GET, включая bare-array accounts и stock/bond search. Backend сохраняет
маршруты и тип внутреннего payload, добавляя единый envelope:
```text
{ data: T | null, meta: {
resourceKey, dataRevision, stateVersion,
availability: missing | partial | complete,
freshness: unknown | fresh | stale, lastSuccessfulAt: timestamp | null,
sync: { id: opaque | null, state: idle | queued | running | failed,
errorCode: safeCode | null },
coverage?: { requested, completeRanges, missingRanges,
asOf, historyBoundary, historyState },
dependencies?: [{ resourceKey, availability, freshness,
lastSuccessfulAt, sync, coverage?, unavailableReason? }]
} }
```
Connection generation/revision остаётся внутренней: существующая security
policy запрещает её в API/logs/errors, даже если она случайная. Frontend
использует свою существующую session/connection boundary.
`resourceKey` — нормализованная идентичность запроса без user secrets и
connection generation. `dataRevision` меняется при публикации данных;
изменение статуса работы не меняет страницу операций. Availability означает
готовность обязательных источников данного результата, а не обещание полноты
рынка или будущих выплат. `data:null` используется для missing и для
неполных финансовых агрегатов; `partial` ledger/candles может содержать
подтверждённые части. Полнота selected/comparisons задаётся отдельно:
analytics `data` доступен после полного selected, а `previous` и
`yearComparison` nullable до готовности соответствующего сравнения.
`meta.dependencies` различает missing/loading/failed comparison и
заведомо недоступное по календарю/границе истории сравнение; последнее
имеет отдельный `unavailableReason` и не планируется как пропуск.
Это явное изменение вложенного analytics DTO, а не только envelope.
Для зависимостей с разными TTL metadata содержит их состояния по
resourceKey; freshness производного результата — худшее обязательное
состояние, lastSuccessfulAt — самый ранний из использованных успешных
источников. Чтение/`calculatedAt`/`observedAt` не продлевает source freshness.
Portfolio `data.lastSuccessfulAt` и `data.isStale` сохраняются для текущих
consumers и отображают тот же portfolio source, без прежнего пятиминутного
предела хранения. Error фонового обновления не меняет успешный timestamp
и сама по себе не отменяет ещё действующий TTL.
| Операция | Контракт |
| --- | --- |
| Обычный GET существующего resource | `200` envelope, в том числе cold/partial; планирует missing/stale зависимости. До постановки проверяет auth, connection и известную принадлежность. |
| Тот же GET с `sync=observe` | Только локальное чтение; никогда не создаёт и не возобновляет work, даже при missing/stale/error. Нужен для получения изменившихся данных после status observation. |
| `POST /api/v1/tbank-sync` | Валидированный screen descriptor, force по TTL; `202` с авторизованным work handle и normalized scope. При совпадении присоединяет к work. Приём не означает успех. |
| `GET /api/v1/tbank-sync/:id` | Только статус разрешённого handle, progress, changed resource revisions и terminal outcome; никаких новых запусков/повторов. Чужой/прежней connection handle — `404`. |
Descriptor — discriminated union `screen: accounts | account | operations |
income | events | stocks | bonds | stock | bond`. Поля разрешены только
соответствующему screen: account scope (конкретный ID/`all`), applied period,
filters, instrumentUid, candle period/range. Сервер использует прежние
валидаторы дат/лимитов/UID и сам раскрывает dependency graph; произвольного
списка RPC/таблиц/чужого userId во входе нет. Зафиксированный now и нормализованный
scope возвращаются клиенту, чтобы ручная операция не переезжала на другой период.
| Экран | Зависимости force refresh |
| --- | --- |
| Accounts | Membership + portfolio summary каждого текущего доступного счёта; metadata показанных позиций по необходимости. Не вся история операций. |
| Account / all overview | Membership, portfolio, показанный recent-ledger range, current-month income, upcoming events и необходимые instrument metadata/calendars. |
| Operations | Применённые account scope и период; категории — локальная проекция. Пагинация не расширяет источниковой период. |
| Income | Применённый период + previous/yearAgo из существующего planner, membership, требуемая metadata выплат. |
| Events | Membership, portfolio и прогноз применённого включительного from/to из сохранённых календарей; default from=today UTC, to=from+30 дней. |
| Stocks / bonds search | Общий каталог, результаты/enrichment текущего поиска и показываемые prices; без истории всего каталога. |
| Stock / bond | Detail/company либо metrics, quote и price dependencies, видимый candle range, payout calendar, личная position и применённый диапазон/инструмент personal operations. |
HTTP ошибки auth/input/access сохраняют `application/problem+json`.
`TBANK_UNAVAILABLE` из принятой фоновой работы передаётся как безопасный
`meta.sync.errorCode`, включая cold failure; `503` остаётся для запроса,
который не удалось принять/обслужить. Известный отказ credentials остаётся
`400 INVALID_TBANK_TOKEN`, не успешным envelope с недоступными private data.
Frontend различает server work error и transport failure.
Работа имеет terminal `succeeded | failed | cancelled` в status endpoint;
envelope отображает successful отсутствие активной работы как `idle`.
Статус имеет `stateVersion` нужных экрану ресурсов и отдельный прогресс background
history, поэтому готовые данные можно прочитать до окончания всей догрузки.
Frontend наблюдает только известный active handle на открытом экране
с интервалом 1–5 секунд и без автоматического retry failed work. После
изменения stateVersion или terminal status выполняется `sync=observe` GET.
После ухода/terminal status наблюдение прекращается. Это наблюдение уже
запрошенной работы, не периодический запуск импорта.
`stateVersion` — opaque идентичность состояния ответа, меняется при изменении
данных, coverage (включая пустое завершённое окно), successfulAt, ошибок,
работы и доступа; это не pagination revision. Он вычисляется из версий
зависимостей и состояния без выдачи внутренних connection identifiers.
Прогресс далёкой истории, не меняющий этот resource, не меняет его
stateVersion и не заставляет повторно читать уже готовый экран.
Истечение времени не создаёт работу: freshness вычисляется из timestamp.
Entity query owner записывает observe-ответ в ту же canonical cache identity
исходного GET; не создаётся второй cache по параметру observe. Query keys
берутся из generated owner, не собираются вручную. Все новые status/mutation
ответы участвуют в существующих session/connection boundary fences.
Кроме них owner использует latest-request sequence для каждого resource:
ответ более раннего запроса не заменяет уже принятый более поздний ответ.
При status notification запоминается ожидаемый stateVersion; observe-ответ
с прежней версией не публикуется, выполняется только новое observational read.
Подтверждённый access loss отменяет/инвалидирует соответствующие personal
queries и повышает локальную access epoch; даже поздний успешный ответ той же
connection, начатый до отзыва, не восстанавливает скрытые данные.
### 8. Локальная пагинация и финансовые результаты
Порядок операций сохраняет действующую сортировку с детерминированным
tie-break по accountId/operationId. Оpaque cursor связан с user, connection,
membershipRevision для all, нормализованными filters/period/limit и
account operations dataRevision. Любое изменение relevant rowset делает
cursor недействительным. Binding хранится серверно либо проверяется keyed
digest: connection generation не находится даже в декодируемом cursor payload.
Изменение rowset возвращает существующий `INVALID_OPERATIONS_CURSOR`;
исторические копии всех rowsets не хранятся. Первая реализация использует
revision на счёт: изменение другого периода тоже может сбросить его cursor;
более узкая revision нужна только при доказанно мешающих сбросах.
Frontend сбрасывает cursor stack на первую страницу, сохраняя applied filters
и объясняя обновление списка; ни страницы, ни суммы разных revisions не
склеиваются. `hasNext=false` означает конец текущей локальной выдачи,
а не полноту истории. Coverage/status без изменения строк cursor не ломают.
Income сохраняет точные signed coupon/dividend payments и существующие
comparison rules. Stable operation ID устраняет повторный финансовый учёт;
прежняя норма о разных entries с одинаковым ID явно изменена delta spec.
Analytics читает согласованный набор операций и coverage в read transaction.
Неполнота selected скрывает его окончательные totals; неполнота comparison
скрывает только его суммы/разницы и не создаёт нулевую базу. Уже опубликованный полный результат того же
period/context сохраняется при неудачной повторной сверке; данные другого
периода никогда не подписываются новым диапазоном.
### 9. Измеримый бюджет и стоимость интеграции
500 мс — цель, не текущий результат. Начальный профиль приёмки: один backend
process, локальная SQLite на SSD, 4 CPU / 8 GiB, 3 счёта, по 100 000 операций
за 10 лет, 200 инструментов и по 5 000 дневных свечей, включая пустые
интервалы; до 1 000 операций/200 доходных выплат на счёт в активном месяце.
Измеряются account/all reads, ledger page 50, monthly income с comparisons,
instrument detail/quote/company/metrics, year/all candles и payouts/search.
Бюджет — p95 ≤ 500 мс от входа HTTP на backend до окончания ответа при
10 одновременных клиентах и не менее 100 чтений каждой группы после прогрева.
Отдельно сохраняются p50/p99/max, размер ответа, состояние БД, hardware и
время первого DB-only ответа после перезапуска. Cold import исключён из
бюджета заполненной БД, но его HTTP ответ также не ожидает RPC. Больший
реальный профиль не объявляется измеренным по этим данным.
Изоляция от брокера проверяется задержанным/отказывающим T-Bank; источник
замеряемых данных — HTTP timing и счётчики RPC существующего logger. Для
fresh полной выборки ожидается 0 RPC; для 20 simultaneous stale requests —
один логический проход каждой нужной зависимости, с явным учётом pages и
разрешённых retries. После завершения, без screen/manual requests — 0 RPC.
Повторные старые полные intervals/candles, в том числе после restart, —
0 повторных RPC этих диапазонов. Сравнение с исходной реализацией выполняется
на одинаковом navigation trace; сохраняются абсолютные числа и доля снижения
`1 - new/old`, если baseline ненулевой. Измерения не требуют real tokens
в артефактах или логах.
## UI/UX Applicability and Package
**Applicability and scope:** `new/changed` — общие состояния и действие
обновления всех T-Bank экранов, без перестройки их содержимого.
**Package revision:** `1`. Пакет полностью inline; локальные PNG/SVG не
нужны, поскольку меняются информационные состояния и существующие controls,
а не композиция экранов. Self-hash и внешние визуальные зависимости отсутствуют.
**Screens, states and interactions:**
| Состояние | Представление и действие |
| --- | --- |
| Missing без результата | Существующий структурный skeleton только отсутствующих секций, «Загружаем данные», доступное busy-состояние; без нулей/«операций нет». |
| Partial period | Подтверждённые строки видны с «Период загружен не полностью». Финансовые totals/comparisons ждут полноты. «История догружается» отдельно от состояния показанного периода. |
| Complete fresh | Контент, время источника и «Обновить» в заголовке экрана. Истечение TTL само не запускает UI timer refresh. |
| Complete stale / active | Прежний контент и timestamp, «Обновляем» либо «Данные на …»; без skeleton поверх сумм. Повторная кнопка disabled для уже запрошенного текущего scope с доступным пояснением. |
| Failed with data | Прежний контент, исходное время, безопасное предупреждение и повторное «Обновить». Transport failure и server work failure различимы; статус не сообщает об успешном обновлении. |
| Failed without data | Локальное error-состояние секции, безопасный retry либо действие настроек согласно коду. |
| Complete empty | Только подтверждённое пустое состояние; empty portfolio не создаёт нулевые карточки, пустой доход объясняет отсутствие купонов/дивидендов. |
| Access lost / connection changed | Очистка недоступного personal view, настройки для token/connection, возврат к счетам для 404. Нельзя удерживать чужую/прежнюю revision под предупреждением. |
Accounts показывают freshness списка отдельно от локального состояния
portfolio summary каждой карточки. Account/all overview сохраняет независимые
секции. Operations сохраняет применённые фильтры; income не переименовывает
старый snapshot новым period. Stock/bond показывают состояния по sections,
не смешивая общий график с личными holdings/payout estimates. Детали candle
по-прежнему обозначают незавершённость.
**Responsive behavior:** на 320–375 px timestamp, предупреждение и кнопка
переносятся без горизонтального overflow и обрезания сумм; на широком экране
используют существующий header/section layout. Focus сохраняется после
обновления; button доступен с клавиатуры, текст статуса не зависит от цвета,
`aria-live=polite` объявляет смену стадии/ошибку, но не каждую страницу.
Skeleton не фокусируется; reduced motion сохраняется.
**Reused source and differences:** runtime Mantine controls, существующие
Section/VM patterns и generated query owners переиспользуются как код.
Это новый inline пакет, не reuse изменяемого или архивного design package;
его состояние полностью определено выше.
## Delivery Map
| Area | Status | Architectural outcome or N/A reason |
| --- | --- | --- |
| frontend | `affected` | Новый snapshot/types, metadata-aware entity queries/VMs всех экранов, scoped refresh и observe-only status, connection/cursor boundaries, inline UI package. |
| backend | `affected` | Prisma storage/migration, local access/read owners, durable interval sync/fencing, shared market sources, DTO и OpenAPI; evidence корректности, latency и RPC. |
| specs | `affected` | Этот change и семь delta capabilities; traceability реализации без преждевременного изменения main specs или чужих changes. |
| docs | `affected` | As-built freshness/storage/access/flows/API/glossary и актуальный статус ADR после поставки; не второй источник требований. |
## Solution Assessment
Trigger: storage, external integration, caching, concurrency и public API.
| Вариант | Fit, maturity, security, operating cost и migration cost |
| --- | --- |
| Существующие SQLite/Prisma + транзакции, unique keys и небольшой request-driven coordinator | Выбран: зрелая используемая БД, один процесс, отсутствие нового сервиса; ownership/fencing доступны в SQL. Миграция добавляет таблицы и revision. Специфичные broker intervals/reconciliation остаются доменной логикой. |
| Продлить RAM/TanStack кэш | Зрелый дешёвый механизм показа, но несовместим с durability и полнотой после restart. Остаётся presentation cache, не решение хранения. |
| Redis + BullMQ | Зрелые retries/locks/queues, но требует нового сервиса, persistence/security/backup и двойной согласованности с SQLite; его jobs всё равно не решают domain coverage/атомарную сверку. Нет нужды в распределённых workers, поэтому цена миграции/эксплуатации не оправдана. |
| Temporal либо аналогичный workflow server | Зрелое recovery, но отдельная инфраструктура и execution model для одной последовательности RPC; domain data transactions всё равно необходимы. Не соответствует минимальному одному backend. |
| Cron/планировщик | Зрелый механизм, но несовместим с явным запретом периодических запусков. |
Собственный код ограничен доменными интервалами, dependency planning,
staging/publish и coalescing; transport queue/retries и DB lifecycle уже
существуют. Существующий runtime не предоставляет durable job framework;
добавлять его ради этих конечных стадий не требуется. При переходе к нескольким
backend processes понадобится отдельное решение о durable claims/leases;
эта поставка не выдаёт processRunId за распределённую блокировку.
## Risks / Trade-offs
- Локальный membership не замечает отзыв доступа мгновенно. Он обновляется
по пятиминутной политике; известный отказ применяется немедленно.
- Stable operation ID противоречит возможностям upstream proto, но принят
как допущение; сменившийся ID может потребовать отдельной будущей сверки.
- Broker cursor не обещает snapshot isolation: полнота означает успешный
обход доступного интервала на момент сверки, не отсутствие последующих
исправлений. Staging защищает от наших частичных публикаций.
- Долгая первичная история расходует rate-limit и диск. Ограниченные окна,
приоритет экрана и отсутствие предварительной массовой очереди сдерживают
ресурсы; объём и время первой загрузки пока не измерены.
- Запись SQLite сериализуется. Короткие транзакции и индексы должны уложиться
в бюджет; это проверяется, а не считается доказанным архитектурой.
- Envelope — несовместимое wire изменение, поэтому backend/frontend
поставляются согласованной парой. API version path сам по себе не делает
старый frontend совместимым.
## Migration Plan
1. Зафиксировать текущие DB/API invariants и measurement baseline на
изолированной базе. Добавить additive schema с revision и пустыми новыми
datasets; текущие users/sessions/connection/manual-card записи сохранить.
У существующих подключений появляется новая revision без изменения token.
2. Реализовать storage/coordinator и local owners; старые RAM snapshots
не мигрировать как доказанно полную историю. Проверить crash/rollback и
fences до включения нового read path.
3. Backend публикует OpenAPI artifact; frontend получает его явно и
регенерирует типы локально. Совместно поставляется новый UI и server
read path, включая observer semantics и terminal errors.
4. Первые пользовательские запросы заполняют данные. Startup/deploy не
запускает импорт. После подтверждённых checks обновляются as-built docs;
specs остаётся единственным SDD authority.
### Rollback Contract
| Field | Decision |
| --- | --- |
| Status | `required` |
| Trigger | Cross-user/connection leak, частичная публикация финансовых данных, повреждение миграции, несовместимый API pair либо существенная регрессия latency. |
| Actions | Остановить новый worker/backend, сохранить согласованную копию SQLite, вернуть совместимую пару backend/frontend. Не запускать старую и новую write-модель одновременно. Отдельно проверить Prisma migration state; не удалять additive tables как способ отката. |
| Data safety | Сохранить новые datasets, progress и все прежние User/Session/TbankConnection/manual-card записи. Если старая версия не поддерживает новую Prisma schema, использовать совместимый rollback build либо восстановить backup в отдельной БД; нельзя перезаписать рабочую БД старой копией, потеряв новые ручные операции. |
| Verification | Проверить integrity/FK и равенство прежних пользовательских данных, невозможность публикации остановленного worker, auth/connection boundaries, согласованность API pair и повторное безопасное чтение/возобновление после возврата исправленной версии. |
## Open Questions
Блокирующих продуктовых вопросов нет. Неизвестны измеренные latency/объём
истории и provider semantics при отсутствующем `from`/openedDate. Они не
меняют выбранный путь: явные интервалы, replay с начала, unknown lower bound
и отсутствие ложного признака полной истории. Подтверждение минимальной
даты источника может уточнить только boundary metadata; до него используется
описанный безопасный fallback. Числовые результаты будут получены задачами
проверки, а не вписаны заранее.
@@ -0,0 +1,73 @@
## Why
Повторные открытия инвестиционных экранов зависят от скорости и доступности
T-Bank, а перезапуск приложения теряет уже загруженную историю. Пользователь
согласовал [ADR 017](../../../../docs/adr/017-local-tbank-reads-and-background-sync.md):
показывать сохранённые данные сразу и обновлять их в фоне только по запросу.
## What Changes
- Все экраны T-Bank — счета, портфель, операции, доходы, инструменты,
текущие цены, свечи и выплаты — повторно используют постоянно сохранённые
данные, в том числе после перезапуска. Обычный ответ не ждёт брокера;
целевой ориентир до 500 мс ещё должен быть подтверждён измерениями.
- Запрос экрана запускает обновление отсутствующих или устаревших данных:
портфель, текущие цены и незавершённая дневная свеча — 1 минута;
текущий месяц операций и список счетов — 5 минут;
инструменты и календари выплат — сутки. Само течение времени работу не запускает.
- Полные прошлые месяцы операций и завершённые свечи сохраняются.
Пропуски догружаются при запросах; ручное обновление перечитывает показанный
период и зависимости текущего экрана, а не весь накопленный архив.
- При первой загрузке сначала доступны нужный экрану период и сравнения
доходов, затем догружается вся доступная история счёта. История бумаг
текущего портфеля загружается заранее в рамках этой работы; остальных —
при открытии инструмента. Начатая работа может завершиться после ухода.
- Пользователь различает свежесть, полноту, загрузку и ошибку обновления.
Неполный период не считается окончательным финансовым результатом или
подтверждением отсутствия операций. Ошибка сохраняет прежние данные.
- Личные данные доступны только своему пользователю в границах текущего
подключения. Старое подключение не может опубликовать поздний результат.
Существующие проверки доступа и ошибки интеграции сохраняются.
«Полнота истории счёта» используется в смысле
[глоссария](../../../../docs/glossary.md#полнота-истории-счёта): все доступные
исполненные операции периода на момент успешной сверки, включая полный пустой
период. Полнота не означает свежесть и не исключает поздние исправления.
Стабильность ID операции — принятое допущение проекта, не гарантия T-Bank;
повторно полученные поля операции могут изменяться.
**Non-goals:** периодическая синхронизация, автоматический поиск исправлений
полной старой истории, обнаружение сменившихся ID, история стоимости портфеля,
предзагрузка всего каталога, архив без подключения, новые сценарии отключения
и закрытых счетов. Эта сессия создаёт planning artifacts; реализация не выполняется.
## Capabilities
### New Capabilities
- [specs/tbank-data-sync/spec.md](specs/tbank-data-sync/spec.md): постоянное локальное чтение, запросы
синхронизации, интервальная полнота, восстановление, общие данные инструментов,
границы подключения и проверяемые критерии быстродействия.
### Modified Capabilities
- [specs/brokerage-accounts/spec.md](specs/brokerage-accounts/spec.md): локальный список, сохранённые сводки,
состояния загрузки и ошибок без синхронного ожидания брокера.
- [specs/portfolio-view/spec.md](specs/portfolio-view/spec.md): постоянный последний полный снимок,
минутная свежесть и ручное обновление.
- [specs/brokerage-account-ledger/spec.md](specs/brokerage-account-ledger/spec.md): локальная пагинация и фильтрация,
полнота периода вместо обязательного перечитывания RAM-кэша.
- [specs/brokerage-account-income-analytics/spec.md](specs/brokerage-account-income-analytics/spec.md): полнота выбранного периода
и сравнений, сохранение результата, идентичность повторно загруженных операций.
- [specs/brokerage-account-upcoming-events/spec.md](specs/brokerage-account-upcoming-events/spec.md): прогноз из сохранённых
источников с видимой полнотой и ошибкой обновления.
- [specs/frontend-data-resilience/spec.md](specs/frontend-data-resilience/spec.md): различимые состояния серверного
сохранённого результата и клиентского кэша на всех экранах T-Bank.
## Impact
Изменение затрагивает пользователей всех инвестиционных экранов, серверное
хранение и получение данных, публичный контракт состояний, интерфейс обновления
и объясняющую документацию. Финансовые правила, валютная точность, значение
прогнозов и изоляция пользователей сохраняются. Основные specs и чужие active
changes не изменяются до отдельного завершения lifecycle этого change.
@@ -0,0 +1,145 @@
## MODIFIED Requirements
### Requirement: Realized income analytics is account-scoped and period-bound
Система MUST предоставлять аутентифицированному User аналитику фактически полученного дохода выбранного открытого поддерживаемого Brokerage Account либо существующего режима «Все счета» (`all`) из счетов, доступных через текущий T-Bank connection User. В режиме `all` система MUST проверять доступ каждого входящего счёта и подтверждать полноту каждого исходного периода по всем входящим счетам; отсутствующий вклад счёта MUST NOT заменяться нулём. Применённый период MUST задаваться включительными календарными датами `from` и `to` в Europe/Moscow, MUST NOT начинаться после своего окончания, MUST NOT содержать будущую дату. Система MUST NOT ограничивать максимальную длительность периода. При отсутствии применённого периода система MUST использовать интервал от первого дня текущего месяца до сегодняшнего дня включительно.
Чтение MUST использовать сохранённые операции текущего подключения и локальное подтверждение доступа без синхронного запроса T-Bank. Ответ MUST иметь envelope `{ data, meta }` по `tbank-data-sync`. Полнота MUST определяться для выбранного периода и каждого необходимого периода сравнения отдельно. Запрос MUST приоритетно загружать их пропуски до фоновой догрузки остальной истории; известные правила календарных дат MUST сохраняться.
#### Scenario: Аналитика за период по умолчанию
- **WHEN** аутентифицированный User запрашивает аналитику доступного Brokerage Account без периода
- **THEN** система рассчитывает результат от первого дня текущего месяца по Europe/Moscow до сегодняшнего дня включительно
#### Scenario: Аналитика за допустимый пользовательский период
- **WHEN** User применяет включительный период любой длительности, в том числе более 366 дней, без будущих дат
- **THEN** система рассчитывает аналитику доступного Brokerage Account за все календарные даты этого периода по Europe/Moscow
#### Scenario: Отклонение недопустимого периода
- **WHEN** User запрашивает период с некорректной или будущей датой либо с `from` после `to`
- **THEN** система отклоняет запрос и не возвращает результат для другого или усечённого периода
#### Scenario: Отклонение недоступного счёта
- **WHEN** User запрашивает аналитику Brokerage Account, который не является открытым поддерживаемым счётом, доступным через его текущий T-Bank connection
- **THEN** система возвращает существующий ответ о недоступности счёта и не раскрывает его доход или операции
#### Scenario: Общий доход требует полноты всех счетов
- **WHEN** User запрашивает режим `all` и выбранный период одного доступного счёта ещё не загружен полностью
- **THEN** система не возвращает окончательные общие суммы, сохраняет раздельное состояние покрытия счетов и запрашивает недостающую разрешённую историю
#### Scenario: Открыт ещё не загруженный период
- **WHEN** запрошенный допустимый период ещё не имеет сохранённых операций или подтверждённого полного пустого покрытия
- **THEN** система отвечает `200` со статусом первичной загрузки или частичного покрытия, запрашивает нужные интервалы и не выдаёт нулевой окончательный доход
### Requirement: Period comparisons preserve calendar and currency meaning
Система MUST сопоставлять выбранный период с непосредственно предшествующим периодом. Для полного календарного месяца предыдущим периодом MUST быть предыдущий полный календарный месяц; для любого другого периода предыдущий период MUST иметь ту же включительную продолжительность и непосредственно предшествовать выбранному. Когда соответствующие календарные даты годом ранее доступны, система MUST также показывать year-over-year comparison; когда они недоступны, система MUST обозначать сравнение как недоступное, а не подменять его нулём. Только сравнение с подтверждённым полным покрытием обоих периодов MUST показывать абсолютные coupon, dividend и total значения и их денежные разницы отдельно по исходным валютам и MUST NOT характеризовать их как инвестиционную эффективность.
Отсутствие ещё не загруженной истории сравнения MUST отличаться от невозможности сравнения по календарю или доступной истории счёта. Система MUST сообщать загрузку, неполноту либо ошибку каждого периода сравнения и MUST NOT подменять их нулевыми суммами или денежными разницами. Полный основной период MAY отображаться при отдельно обозначенной незавершённой загрузке сравнения; его значения MUST NOT смешиваться с частичным сравнением.
#### Scenario: Сравнение полного календарного месяца
- **WHEN** User применяет период от первого до последнего дня календарного месяца
- **THEN** система сравнивает его с полным непосредственно предшествующим календарным месяцем
#### Scenario: Сравнение произвольного периода
- **WHEN** User применяет период, не являющийся полным календарным месяцем
- **THEN** система сравнивает его с непосредственно предшествующим включительным периодом той же продолжительности
#### Scenario: Доступно сравнение год к году
- **WHEN** для соответствующих календарных дат годом ранее доступна полная история подходящих операций
- **THEN** система показывает отдельные source-currency значения и денежные разницы относительно этого периода
#### Scenario: Годовая история недоступна
- **WHEN** полная история для соответствующих календарных дат годом ранее недоступна
- **THEN** система обозначает year-over-year comparison как недоступное и не показывает отсутствующие данные как нулевой доход
#### Scenario: Основной период полон, сравнение ещё загружается
- **WHEN** выбранный период имеет подтверждённое полное покрытие, а необходимый предыдущий период ещё загружается
- **THEN** система возвращает полный итог выбранного периода, отмечает загрузку сравнения и не возвращает для него окончательные суммы или разницы
#### Scenario: Сравнение догружено после основного периода
- **WHEN** все необходимые интервалы периода сравнения успешно завершены
- **THEN** следующее локальное чтение возвращает его точные валютные суммы и разницы без ожидания T-Bank
### Requirement: Realized analytics never presents an incomplete result as complete
Система MUST формировать окончательные totals, trend buckets, instrument breakdown и drill-down выбранного периода только по подтверждённому полному покрытию его исполненных операций. Каждое comparison MUST отдельно требовать полного покрытия своих исходных периодов. Повторное локальное чтение полного периода MUST NOT ожидать повторного получения cursor pages T-Bank. Ошибка любой необходимой страницы фоновой загрузки MUST сохранять подтверждённый ранее результат и покрытие, фиксировать восстанавливаемую ошибку синхронизации и MUST NOT возвращать частичные totals, comparisons, buckets или breakdown как полный результат. Если полный выбранный результат ещё не получен, envelope MUST возвращать `data: null` с отсутствующим либо частичным покрытием в `meta`, а интерфейс MUST показывать незавершённую загрузку либо её ошибку без окончательных финансовых итогов. Успешный период без подходящих выплат MUST возвращать нулевой результат и отображаться с явным объяснением пустого состояния.
#### Scenario: Все страницы операций успешно получены
- **WHEN** для покрытия расчёта требуется несколько cursor pages и каждая страница успешно сохранена с подтверждением окончания диапазона
- **THEN** система формирует результат по всем подходящим Ledger Entries со всех страниц
#### Scenario: Одна из страниц операций недоступна
- **WHEN** получение любой необходимой cursor page завершается ошибкой
- **THEN** ответ сообщает ошибку синхронизации, а страница сохраняет ранее полный результат с прежним временем либо показывает ошибку незавершённой загрузки без окончательных частичных итогов
#### Scenario: В периоде нет купонов и дивидендов
- **WHEN** все необходимые операции успешно получены, но среди них нет подходящих coupon или dividend Ledger Entries
- **THEN** страница показывает нулевой результат и объясняет, что за выбранный период купоны и дивиденды не найдены
#### Scenario: Частичная загрузка пока не нашла дохода
- **WHEN** только часть операций выбранного периода получена и в этой части нет купонов или дивидендов
- **THEN** система не возвращает окончательный нулевой результат и сообщает неполноту периода
#### Scenario: Ошибка ручной сверки полного периода
- **WHEN** ранее полный период перечитывается вручную и промежуточная страница не получена
- **THEN** прежний полный результат и время его успешной сверки остаются доступными с ошибкой обновления, а частичная новая выборка не заменяет их
### Requirement: Account overview has an independent current-month income summary
Обзор выбранного Brokerage Account MUST показывать компактный source-currency summary «Получено купонами и дивидендами» за период от первого дня текущего месяца по Europe/Moscow до сегодняшнего дня включительно и действие перехода на страницу «Пассивный доход». Summary MUST всегда использовать этот current-month период и MUST NOT наследовать последний пользовательский период страницы аналитики. Пустой или неуспешный расчёт summary MUST оставаться локальным состоянием блока и MUST NOT заменять успешно отображённый Portfolio или другие блоки обзора.
Summary MUST использовать тот же локальный источник операций и правило полноты, что и полная аналитика. До завершения требуемого текущего периода он MUST показывать состояние загрузки или неполноты без окончательного нулевого дохода. Ошибка фонового обновления MUST сохранять ранее полный summary с прежним временем и безопасным предупреждением.
#### Scenario: Обзор показывает доход текущего месяца
- **WHEN** обзор Brokerage Account загружен и current-month analytics успешно рассчитана
- **THEN** он показывает отдельные source-currency итоги с начала текущего месяца по сегодняшний день и переход к полной аналитике
#### Scenario: Пользовательский период не влияет на обзор
- **WHEN** User ранее применил на странице аналитики период, отличный от текущего месяца
- **THEN** summary обзора по-прежнему рассчитывается от первого дня текущего месяца по сегодняшний день
#### Scenario: Current-month summary недоступен
- **WHEN** Portfolio обзора успешно загружен, но расчёт current-month summary завершается ошибкой
- **THEN** Portfolio и остальные успешно загруженные блоки остаются доступными, а summary показывает собственное состояние восстановления
#### Scenario: Текущий месяц ещё не загружен полностью
- **WHEN** обзор доступен, но текущий месяц операций имеет пропуски
- **THEN** блок дохода обозначает неполноту или загрузку, не выдаёт частичный доход за окончательный и не скрывает Portfolio
### Requirement: Income payments remain traceable to exact Ledger Entries
В разделе «Выплаты» страница MUST показывать единый список карточек точных исполненных Ledger Entries применённого периода в исходном серверном порядке entries. Каждая выплата MUST иметь собственную сразу видимую карточку с описанием, датой, доступной identity Instrument и signed Money в исходной валюте. Страница MUST NOT добавлять внешние группировочные карточки или заголовки по Instrument либо валюте, subtotals инструмента или количество выплат инструмента. Список MUST сохранять отрицательные корректировки как самостоятельные Ledger Entries и MUST отражать одну запись на идентичность User, Brokerage Account, текущее подключение и operation ID; повторная загрузка того же ID MUST обновлять поля и MUST NOT удваивать выплату. Одинаковые operation IDs разных счетов MUST оставаться отдельными записями. Список MUST NOT включать записи вне аналитического итога. Отказ от группировочного UI MUST NOT отменять проверки принадлежности entries к instrumentKey, валюты и receipt count ответа или приводить к пересчёту денежных итогов на клиенте.
#### Scenario: Каждая выплата показана отдельной карточкой
- **WHEN** выбранный период содержит несколько включённых Ledger Entries одного или разных Instrument
- **THEN** раздел «Выплаты» сразу показывает отдельную карточку каждой записи в исходном порядке без внешних группировочных блоков, раскрытия, итогов инструмента и receipt count на UI
#### Scenario: Список воспроизводит точный состав дохода
- **WHEN** User просматривает загруженный раздел «Выплаты»
- **THEN** он видит все и только включённые coupon и dividend Ledger Entries применённого периода с датой, описанием, доступной identity и исходной суммой
#### Scenario: Отрицательная корректировка и повторяющиеся IDs сохраняются
- **WHEN** доход содержит отрицательную корректировку, повторно загруженный ID того же счёта либо одинаковые operation IDs разных счетов
- **THEN** корректировка отображается с исходным знаком; одна операция того же счёта имеет одну карточку с актуальными полями, а операции разных счетов остаются отдельными карточками
#### Scenario: Валюта видна в сумме каждой выплаты
- **WHEN** подходящие выплаты одного Instrument представлены в нескольких исходных валютах
- **THEN** каждая карточка показывает свою Money с исходной валютой, без отдельного заголовка валютной группы или объединённого мультивалютного итога
@@ -0,0 +1,95 @@
## MODIFIED Requirements
### Requirement: User can retrieve an account ledger
Система MUST предоставлять аутентифицированный журнал только для чтения выбранного Brokerage Account либо существующего режима «Все счета» (`all`) в границах текущего User и его T-Bank connection. В режиме `all` доступ MUST проверяться для каждого входящего счёта, операции MUST сохранять принадлежность исходному счёту, а полнота периода MUST учитывать покрытие каждого входящего счёта. Журнал MUST содержать только исполненные операции T-Bank и при отсутствии периода по умолчанию охватывать интервал от начала текущего календарного года в UTC до момента запроса.
Система MUST читать сохранённые операции и локальное подтверждение доступа текущего подключения без синхронного обращения к T-Bank, включая запрос списка счетов. Ответ MUST содержать envelope `{ data, meta }` по `tbank-data-sync`, отражающий полноту запрошенного периода независимо от текущей страницы. Отсутствующие данные MUST возвращаться с `data: null` и состоянием загрузки, а частично сохранённый период MUST обозначаться как `partial`. Только подтверждённо полный пустой период MUST обозначать отсутствие операций.
#### Scenario: Получение журнала с периодом по умолчанию
- **WHEN** аутентифицированный User запрашивает журнал принадлежащего ему Brokerage Account без периода
- **THEN** система возвращает сохранённые исполненные операции в `data` и полноту периода в `meta` за период от начала текущего календарного года в UTC до момента запроса, запрашивая отсутствующие интервалы в фоне
#### Scenario: Отклонение недоступного счёта
- **WHEN** аутентифицированный User запрашивает журнал счёта, для которого локальные данные текущего подключения подтверждают отсутствие доступа либо неподдерживаемое или закрытое состояние
- **THEN** система возвращает существующий ответ о недоступности счёта и не раскрывает операции
#### Scenario: Общий журнал содержит неполный счёт
- **WHEN** User запрашивает режим `all` и у одного из разрешённых счетов есть незагруженный интервал периода
- **THEN** система возвращает только разрешённые сохранённые записи с принадлежностью счёту и обозначает общий период неполным, не подменяя отсутствующий вклад пустым счётом
#### Scenario: Пустая загруженная часть не доказывает отсутствие операций
- **WHEN** в сохранённой части периода нет записей, но покрытие периода не завершено
- **THEN** ответ обозначает неполноту и запрошенную загрузку, а интерфейс не сообщает, что операций за период нет
### Requirement: User can browse a filtered ledger
Система MUST позволять User ограничивать журнал включительным периодом дат и любым подмножеством категорий trade, income, tax, fee, transfer, other. Система MUST фильтровать сохранённые операции локально, не перечитывая T-Bank из-за категории, instrument UID, размера страницы или перехода между страницами полного периода. Система MUST разбивать упорядоченный результат на страницы с непрозрачным cursor и возвращать признак доступности следующей страницы.
Cursor MUST быть привязан к User, текущему подключению, составу счетов, применённым фильтрам, зафиксированной верхней границе периода и версии читаемых данных. Курсор другой границы или изменившейся версии MUST отклоняться существующим `INVALID_OPERATIONS_CURSOR`, позволяя повторить первую страницу с прежними фильтрами. Доступность следующей локальной страницы MUST NOT заменять признак полноты периода. Равнозначные порядок и дубли categories MUST NOT создавать другую идентичность запроса.
#### Scenario: Фильтрация доходных операций за период
- **WHEN** User запрашивает период дат и категорию income
- **THEN** результат содержит только исполненные доходные операции с датой в пределах этого периода
#### Scenario: Продолжение истории
- **WHEN** User запрашивает cursor следующей страницы, возвращённый предыдущим ответом журнала, и версия данных не изменилась
- **THEN** система возвращает следующую страницу без повторения записей предыдущей страницы
#### Scenario: Обновление изменило данные между страницами
- **WHEN** после выдачи первой страницы успешная синхронизация изменила её набор операций и User передаёт прежний cursor
- **THEN** система возвращает `INVALID_OPERATIONS_CURSOR`, а повторная загрузка начинает первую страницу с сохранёнными применёнными фильтрами без пропуска или дублирования записей
#### Scenario: Полный период фильтруется локально
- **WHEN** User меняет категории или страницы в полностью сохранённом прошлом периоде
- **THEN** система формирует результат из сохранённых операций без нового запроса операций T-Bank
### Requirement: Ledger entries preserve broker operation meaning
Система MUST представлять одну upstream-операцию одной записью журнала и MUST передавать её timestamp, тип операции, категорию, описание, доступные идентификаторы инструмента, доступную presentation metadata инструмента, денежную выплату, количество, цену, НКД и комиссию. Денежные поля MUST сохранять исходную валюту и точное десятичное значение.
В пределах текущего подключения идентичность Ledger Entry MUST включать User, Brokerage Account и upstream operation ID. Повторное получение того же ID MUST обновлять доступные поля этой операции без создания второй записи; одинаковые ID разных счетов MUST оставаться разными записями. Стабильность ID MUST считаться допущением проекта, а не гарантией T-Bank.
#### Scenario: Отображение купонной выплаты
- **WHEN** T-Bank возвращает исполненную купонную операцию с выплатой и данными инструмента
- **THEN** запись журнала определяет её как income и передаёт исходную сумму выплаты, валюту и данные инструмента
#### Scenario: Отображение комиссии сделки без синтетической записи
- **WHEN** T-Bank возвращает исполненную сделку, содержащую выплату и комиссию
- **THEN** журнал содержит одну запись сделки с комиссией в качестве её детали
#### Scenario: Отображение движения бумаг без денег
- **WHEN** T-Bank возвращает исполненную операцию, изменяющую состав инструментов, но без денежной выплаты
- **THEN** запись журнала передаёт доступное количество и не содержит искусственной денежной выплаты с нулевым значением
#### Scenario: Доступна визуальная идентичность инструмента
- **WHEN** для инструмента исполненной операции доступна presentation metadata бренда
- **THEN** запись журнала передаёт её вместе с доступными идентификаторами инструмента, не меняя остальные сведения операции
#### Scenario: Повторная операция содержит исправленные поля
- **WHEN** успешная синхронизация получает операцию с уже сохранённым ID того же счёта и изменёнными полями
- **THEN** последующее чтение возвращает одну запись с обновлёнными полями без удвоения денежного движения
### Requirement: Account overview includes recent ledger events
Обзор счёта MUST показывать не более пяти последних сохранённых исполненных записей журнала этого счёта с состоянием полноты и загрузки требуемого периода и действие перехода к его полному журналу. Каждая показанная запись MUST иметь ту же визуальную идентичность инструмента и семантику денежного движения, что и запись полного журнала. Неуспешный или пустой запрос последних записей MUST оставаться локальным состоянием блока последних событий и MUST NOT заменять успешно отображённый обзор Portfolio.
#### Scenario: Отображение последних событий
- **WHEN** загружается обзор счёта и его журнал содержит исполненные операции
- **THEN** обзор показывает не более пяти последних записей и ссылку на полный журнал
#### Scenario: Последние записи недоступны
- **WHEN** обзор счёта успешно загружается, но запрос последних записей завершается ошибкой
- **THEN** обзор Portfolio остаётся доступным, а блок последних событий отображает собственное состояние восстановления
#### Scenario: Последняя запись не содержит visual metadata инструмента
- **WHEN** одна из последних записей не имеет доступной presentation metadata инструмента
- **THEN** обзор показывает для неё понятный текстовый fallback и сохраняет дату, тип, инструмент и денежное изменение записи доступными для чтения
#### Scenario: Первичная история обзора неполна
- **WHEN** обзор получил только часть требуемых операций или `data: null`
- **THEN** блок обозначает загрузку или неполноту и не утверждает, что показанные записи окончательны либо операции отсутствуют
## REMOVED Requirements
### Requirement: Кэш страниц журнала ограничивает локальные ресурсы
**Reason:** Постоянное хранение операций и запросная синхронизация заменяют обязательный RAM-кэш на 100 страниц с TTL 30 секунд; его запрет на выдачу старой страницы противоречит локальному чтению сохранённой истории.
**Migration:** Чтение, свежесть, объединение загрузок и сохранение прогресса переходят к `tbank-data-sync`; локальная фильтрация и привязанный к версии данных cursor регулируются `User can browse a filtered ledger`. Существующие transient cursors не являются доказательством полноты и при несовместимости возвращают `INVALID_OPERATIONS_CURSOR`.
@@ -0,0 +1,104 @@
## MODIFIED Requirements
### Requirement: User can retrieve upcoming events for an account
Система MUST предоставлять аутентифицированному User read-only список известных будущих событий выбранного открытого поддерживаемого Brokerage Account либо существующего режима «Все счета» (`all`) в границах текущего T-Bank connection User. Параметры `from` и `to` MUST сохранять существующий контракт: необязательные строгие календарные даты `YYYY-MM-DD` с включительными границами в UTC. Без `from` начало MUST соответствовать текущей календарной дате UTC; без `to` конец MUST соответствовать дате `from` плюс 30 календарных дней. При `from` позже `to` система MUST сохранять существующий отказ `INVALID_EVENTS_PERIOD`, не подменяя период. В режиме `all` доступ и полнота обязательных источников MUST проверяться для каждого входящего счёта, события MUST сохранять принадлежность счёту. Ответ MUST иметь envelope `{ data, meta }` по `tbank-data-sync`; доступный прогноз в `data` MUST содержать только события coupon, dividend, maturity и offer, отсортированные по дате по возрастанию, и момент расчёта прогноза.
Список будущих событий является прогнозом по последнему успешно сохранённому наблюдаемому состоянию счёта и MUST NOT означать, что WealthLedger хранит долговременное право владения Brokerage Account независимо от T-Bank connection.
Система MUST формировать прогноз из сохранённых позиций и календарей, не ожидая T-Bank при локальном чтении и подтверждении доступа. Свежесть и загрузка зависимостей MUST быть видимы. Отсутствующий подтверждённый результат MUST возвращаться как `data: null` с состоянием загрузки, а не пустой список. Полнота локальной загрузки поддерживаемых данных MUST NOT означать полноту информации внешнего источника обо всех возможных событиях.
#### Scenario: Получение событий доступного счёта
- **WHEN** аутентифицированный User запрашивает будущие события открытого поддерживаемого Brokerage Account, доступного через текущий T-Bank connection, без явного периода
- **THEN** система возвращает известные события от сегодняшней даты UTC до даты через 30 календарных дней включительно и момент, на который был выполнен расчёт
#### Scenario: Отсутствие событий в периоде
- **WHEN** все необходимые данные применённого периода успешно сохранены и для счёта в нём нет известных событий
- **THEN** система возвращает успешный пустой список и момент расчёта
#### Scenario: Отклонение недоступного счёта
- **WHEN** аутентифицированный User запрашивает события счёта, который не является открытым поддерживаемым счётом, доступным через его T-Bank connection
- **THEN** система возвращает существующий ответ о недоступности счёта и не раскрывает события
#### Scenario: Недопустимый период сохраняет ошибку
- **WHEN** User передаёт `from` позже `to` для выбранного счёта либо режима `all`
- **THEN** система возвращает существующую ошибку `INVALID_EVENTS_PERIOD`, не запускает загрузку другого периода и не возвращает его прогноз
#### Scenario: Прогноз ещё не может быть рассчитан
- **WHEN** позиции либо необходимый поддерживаемый календарь ещё не сохранены полностью
- **THEN** система возвращает `200` с `data: null` и состоянием запрошенной загрузки или её ошибки вместо утверждения об отсутствии событий
### Requirement: Forecasts preserve event meaning and uncertainty
Система MUST передавать для каждого события его календарную дату, тип, доступные идентификаторы и название Instrument, а также прогнозную сумму до налогов как Money, если она известна. Прогнозная сумма MUST рассчитываться по количеству бумаг полного успешно сохранённого Portfolio Snapshot, использованного при последнем успешном расчёте показываемого прогноза, с указанием времени этого наблюдения. При неудачном обновлении сохранённый прогноз MUST сохранять эту исходную зависимость и MUST NOT приписываться более новому Portfolio Snapshot. Если денежные условия события неизвестны или неприменимы, прогнозная сумма MUST быть `null`, а не нулём и не парой разрозненных полей. Дивиденд MUST обозначать переданную дату как дату реестра, а offer MUST передаваться без вымышленной суммы.
Instrument в Future Cash Event MUST иметь тот же доменный смысл, что и в Portfolio: это финансовый инструмент, к которому относится событие, даже если событие не является Portfolio Position.
#### Scenario: Прогноз купона по текущей позиции
- **WHEN** поддерживаемая облигация в текущей Portfolio Position имеет известный купон в пределах периода
- **THEN** система возвращает событие coupon с прогнозной суммой до налогов как Money в исходной валюте, рассчитанной по текущему количеству облигаций
#### Scenario: Прогноз дивиденда
- **WHEN** поддерживаемая акция в текущей Portfolio Position имеет объявленный дивиденд с датой фиксации реестра в пределах периода
- **THEN** система возвращает событие dividend с датой, обозначенной как дата реестра (`record_date`), и прогнозной суммой до налогов как Money, равной `dividend_net` на одну бумагу, умноженной на текущее количество акций
#### Scenario: Отменённый дивиденд не попадает в прогноз
- **WHEN** запись дивиденда T-Bank имеет тип выплаты `Cancelled`
- **THEN** система не добавляет такое событие в результат
#### Scenario: Отображение оферты без суммы
- **WHEN** для поддерживаемой облигации известна оферта в пределах периода, но её денежные условия недоступны
- **THEN** система возвращает событие offer с прогнозной суммой `null`
#### Scenario: Неподдерживаемое частичное погашение
- **WHEN** источник не предоставляет расписание будущих частичных погашений или амортизации облигации
- **THEN** система НЕ создаёт синтетическое событие частичного погашения
### Requirement: Upcoming events use complete source results
Система MUST использовать последний прогноз по полным успешно полученным необходимым источникам и MUST NOT возвращать частичный список как полный результат. Если обновление необходимых данных брокера или MOEX неуспешно, система MUST сохранять прежний подтверждённый прогноз и время его расчёта, возвращая `200` с ошибкой синхронизации в `meta`. Если полного прогноза ещё нет, ответ MUST содержать `data: null` и отсутствующее либо частичное покрытие с ошибкой или состоянием загрузки. Подтверждённые ошибки сессии, текущего подключения и доступа MUST сохранять существующие Problem Details и MUST NOT маскироваться прогнозом. Система MUST пропускать инструменты, для которых источник не предоставляет поддерживаемые события, не создавая строки «нет данных».
#### Scenario: Недоступность источника
- **WHEN** необходимый источник данных брокера или MOEX временно недоступен
- **THEN** система сообщает ошибку обновления, сохраняя ранее полный прогноз и время его расчёта либо `data: null`, без выдачи частичного списка за полный
#### Scenario: Инструмент без поддерживаемых данных
- **WHEN** Portfolio содержит инструмент, для которого источник не предоставляет поддерживаемые будущие события
- **THEN** система не добавляет для него синтетическую строку в результат
#### Scenario: MOEX отвечает без запрошенного блока
- **WHEN** MOEX отвечает успешно, но не содержит запрошенного блока данных (например, дивидендов)
- **THEN** система трактует ответ как отсутствие данных по инструменту и не завершает запрос ошибкой
### Requirement: Account page presents an upcoming events list
Страница Brokerage Account MUST предоставлять действие перехода на отдельную страницу «Будущие события». Эта страница MUST сохранять существующий выбор и явное применение периода через `from`/`to` и показывать все полученные события применённого периода компактным списком без пагинации и суммарного итога, с датой, типом, инструментом и прогнозной суммой в исходной валюте, если она известна. Страница MUST объяснять, что прогноз основан на текущей позиции, указан до налогов и может быть неполным из-за доступности данных источника.
Страница MUST показывать время сохранённого прогноза, состояние свежести и загрузки, а также действие «Обновить» для текущего применённого периода и его зависимостей. Неполученный результат MUST отличаться от подтверждённого пустого списка. Временная ошибка обновления MUST NOT скрывать сохранённый полный прогноз.
#### Scenario: Просмотр ближайших событий
- **WHEN** User открывает «Будущие события» из обзора Brokerage Account без явного периода
- **THEN** он видит отсортированный список событий периода по умолчанию от сегодняшней даты UTC до даты через 30 календарных дней включительно, где offer обозначен без суммы
#### Scenario: Пустое состояние страницы
- **WHEN** запрос будущих событий возвращает подтверждённый полный пустой список в `data`
- **THEN** страница сообщает, что за применённый период событий не найдено, и объясняет ограничения покрытия источника
#### Scenario: Обновление применённого периода
- **WHEN** User применил допустимый период будущих событий и нажал «Обновить»
- **THEN** обновляются необходимые позиции и календари для применённого периода, его фильтры сохраняются и обновление не подменяется периодом по умолчанию
#### Scenario: Ошибка страницы
- **WHEN** фоновое обновление будущих событий завершается временной ошибкой
- **THEN** страница сохраняет прежний полный прогноз с его временем и предупреждением либо показывает ошибку первичной загрузки при его отсутствии, не выдаёт частичный список за полный и предоставляет действие повторить обновление
### Requirement: Account overview includes upcoming events
Обзор Brokerage Account MUST показывать не более пяти ближайших Future Cash Event и действие перехода на полную страницу «Будущие события». Неуспешный или пустой запрос этого блока MUST оставаться локальным состоянием блока и не заменять успешно отображённый Portfolio.
#### Scenario: Отображение ближайших событий
- **WHEN** обзор счёта успешно загружен и календарь содержит будущие события
- **THEN** обзор показывает первые пять событий в порядке даты и действие перехода к полной странице
#### Scenario: Ближайшие события недоступны
- **WHEN** обзор счёта успешно загружен, но запрос будущих событий завершается ошибкой
- **THEN** Portfolio остаётся доступным, а блок будущих событий отображает собственную ошибку
#### Scenario: Ошибка обновления сохранённого прогноза обзора
- **WHEN** блок обзора уже показывает полный прогноз, а его обновление временно неуспешно
- **THEN** блок сохраняет показанные события и время расчёта с локальным предупреждением, не скрывая Portfolio

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