19 Commits
Author SHA1 Message Date
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
67 changed files with 13177 additions and 162 deletions
+26
View File
@@ -0,0 +1,26 @@
name: Conventional commits
on:
pull_request:
types: [opened, synchronize, reopened, edited]
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
npm run commitlint -- --strict --from "$BASE_SHA" --to "$HEAD_SHA"
+31
View File
@@ -0,0 +1,31 @@
name: Specs security
on:
push:
branches:
- '**'
pull_request:
schedule:
- cron: '17 5 * * *'
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
+17
View File
@@ -0,0 +1,17 @@
name: Specs verify
on:
push:
branches: ['**']
pull_request:
jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: https://github.com/actions/checkout@v4
- uses: https://github.com/actions/setup-node@v4
with:
node-version-file: .nvmrc
- run: npm ci
- run: npm run verify
+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.21.0
+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"
}
}
}
}
+20 -37
View File
@@ -1,43 +1,26 @@
# Specs
## Назначение и authority
Репозиторий содержит существующие behavioral specifications и SDD history.
Для обычной разработки не создавай и не обновляй planning artifacts ради
процесса. Требование пользователя определяет работу; OpenSpec применяется
только по явному запросу.
Этот репозиторий — единственный 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 сохраняй. Для правки индекса проверь targets,
anchors и `git diff --check`; application gates принадлежат affected repos.
Если явно запрошена работа через OpenSpec, следуй
[локальным инструкциям OpenSpec](README.md#начало-работы)
и используй store `wealth-ledger-specs`. Перед Node-командами активируй
местную `.nvmrc` через `nvm use --silent`.
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.
Работа — в отдельной ветке от `develop`, затем PR в `develop`; в `main` напрямую
не пушить. Release/hotfix и синхронизация веток — по [README](README.md#релизы).
Commits и заголовки PR — Conventional Commits; для спецификаций допустим `spec:`.
Перед подготовкой релиза прочитай местный README; версии вручную не повышать.
+35
View File
@@ -0,0 +1,35 @@
# Behavioral specifications index
Эти существующие specs — источник сохранённых требований по конкретной теме.
Читай нужный requirement/scenario, если задача зависит от него; не загружай всё
дерево и не создавай OpenSpec change для обычной разработки.
Этот индекс и основные specs читаются без соседних checkout. Внешние
сведения о реализации служат справочными свидетельствами, когда предоставлены.
При конфликте с текущим поведением установи расхождение; не копируй устаревшее
правило или случайный код молча.
HTTP OpenAPI имеет единственный editable source в backend, а не здесь.
Потребитель доверяет предоставленному artifact и локально проверяет generated
types. CI каждого участника использует только его собственные входы.
| Тема / trigger | Existing spec |
| --- | --- |
| Auth/session/refresh/error contract | [User authentication](openspec/specs/user-authentication/spec.md) |
| T-Bank connection/security/cache generation | [T-Bank connection](openspec/specs/tbank-connection/spec.md) |
| Счета/сводки | [Brokerage accounts](openspec/specs/brokerage-accounts/spec.md) |
| Portfolio positions/totals/stale | [Portfolio](openspec/specs/portfolio-view/spec.md) |
| Операции/cursor/filters | [Account ledger](openspec/specs/brokerage-account-ledger/spec.md) |
| Будущие выплаты | [Upcoming events](openspec/specs/brokerage-account-upcoming-events/spec.md) |
| Доходы и периоды | [Income analytics](openspec/specs/brokerage-account-income-analytics/spec.md) |
| Shared tokens/theme | [Design system](openspec/specs/frontend-design-system/spec.md) |
| Каталог компонентов | [UI catalog](openspec/specs/frontend-ui-catalog/spec.md) |
| Loading/skeleton | [Loading presentation](openspec/specs/frontend-loading-presentation/spec.md) |
| UI error/recovery | [UI resilience](openspec/specs/frontend-ui-resilience/spec.md) |
| Query coherence/cache | [Query coherence](openspec/specs/frontend-query-coherence/spec.md), [data resilience](openspec/specs/frontend-data-resilience/spec.md) |
| OpenAPI generation | [API contract generation](openspec/specs/api-contract-generation/spec.md) |
| Logging/request context | [Structured logging](openspec/specs/backend-structured-logging/spec.md) |
| Breadcrumb/navigation | [Breadcrumbs](openspec/specs/app-breadcrumb-navigation/spec.md) |
Process specs, active changes и archives сохраняют историю прежнего SDD workflow.
Они не являются обязательной процедурой текущей задачи. Для явно запрошенной
работы с OpenSpec используй [отдельный вход](AGENTS.md).
+110
View File
@@ -0,0 +1,110 @@
# Changelog
## [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))
+42 -2
View File
@@ -4,7 +4,7 @@
## Начало работы
Перед созданием 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 применяется по явному запросу согласно [AGENTS](AGENTS.md). Proposal, Delta Specs, Design и Tasks создаются последовательно по одному после approval Change Owner. Для любой команды, читающей или изменяющей change, явно передавайте `--store wealth-ledger-specs`.
Design должен оценить `frontend`, `backend`, `specs`, `docs` в Delivery Map и при обязательном trigger включить Solution Assessment. Docs описывает as-built state и не становится параллельным store требований.
@@ -18,4 +18,44 @@ openspec validate <change> --type change --strict --store wealth-ledger-specs
openspec validate --specs --strict --store wealth-ledger-specs
```
Structural validation не заменяет semantic verification и independent read-only review перед archive. Audit-derived работа маршрутизируется через [live remediation registry](../docs/development/audit-remediation.md).
Structural validation не заменяет semantic verification и independent read-only review перед archive.
Node задаётся собственной `.nvmrc`; после `nvm use --silent` выполните `npm ci`.
`npm run verify` проверяет diff. OpenSpec validation остаётся отдельной проверкой
при изменениях требований. История релизов — [CHANGELOG](CHANGELOG.md).
## Релизы
Репозиторий выпускается независимо. `develop` — интеграция, `main` — принятые
релизы. Работа: `codex/<task>` от актуального `develop`, PR в `develop`.
Обычные PR допускают squash; release и обратная синхронизация сохраняют историю.
Commits и PR titles — Conventional Commits, включая `spec:`. Версии вручную не повышай.
1. Получи `main`, `develop` и теги: `git fetch origin --tags`. Убедись, что
`develop` содержит последний релиз. Создай `release/<name>` от проверенного
`develop`; `nvm use --silent`, `npm ci`. Hotfix начинается от `main` в
`release/hotfix-<name>` и проходит тот же процесс.
2. На чистой ветке выполни `npm run release:preview`, просмотри версию/changelog,
затем `npm run release:prepare`. Команда создаёт `chore(release): X.Y.Z`,
обновляет manifest/lockfile/CHANGELOG; тег и push не выполняет. Не обходи
guards ветки, чистого дерева, отсутствующих тегов или уже подготовленной версии.
3. Проверь diff и штатные gates этого репозитория. Отправь только release-ветку,
открой PR в `main` с заголовком `chore(release): X.Y.Z`. После зелёных
`verify`, `security`, `commits` выполни fast-forward-only merge. Если `main`
изменился, согласуй историю и повтори проверки кандидата.
4. Получи удалённый `main`, сравни его HEAD с проверенным release SHA и версией
manifest/lockfile. Создай аннотированный `vX.Y.Z` именно на этом SHA:
`git tag -a vX.Y.Z <release-sha> -m "Release X.Y.Z"`, затем отправь только
`git push origin refs/tags/vX.Y.Z`. Не запускай release-it повторно на `main`.
5. Верни release-коммит через PR `main → develop` без squash/rebase; проверь,
что `develop` содержит тег. Опубликованные теги не перемещай. При сбое после
merge создай отсутствующий тег без нового bump; до merge исправь и проверь
существующий кандидат без повторного повышения версии.
`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*` защищены.
Workflow сам не включает эти настройки. Отдельное review approval для одного
владельца не требуется. Npm publishing, deploy и Gitea Releases не включены.
+4
View File
@@ -0,0 +1,4 @@
module.exports = {
extends: ["@commitlint/config-conventional"],
rules: { "type-enum": [2, "always", ["feat", "fix", "perf", "docs", "spec", "refactor", "build", "ci", "test", "style", "chore", "revert"]] },
};
@@ -0,0 +1,2 @@
schema: wealth-ledger-sdd
created: 2026-09-12
@@ -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-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-16
@@ -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.
- [ ] 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.
+3 -3
View File
@@ -2,8 +2,8 @@ schema: wealth-ledger-sdd
context: |
WealthLedger состоит из независимых frontend, backend, specs и docs Git-репозиториев.
Canonical OpenSpec authority — зарегистрированный store wealth-ledger-specs в specs/openspec.
Docs описывает только фактическое as-built состояние; устойчивые process policies находятся в docs/development.
Локальная разработка и verification выполняются под Node 22 без CI.
Canonical OpenSpec authority — store wealth-ledger-specs в локальном openspec/.
Инструкции и validation specs находятся в этом репозитории; соседние checkout не требуются.
Локальная разработка и CI используют Node из собственной .nvmrc; порядок работы задаёт AGENTS.md.
Project artifacts объясняют решения по-русски; exact OpenSpec headings, tokens,
commands и normative keywords SHALL/MUST сохраняются на английском.
@@ -106,8 +106,6 @@ artifacts:
- **Delivery Map**: ровно `frontend`, `backend`, `specs`, `docs`; каждая
area имеет `affected` outcome или конкретный `N/A` reason. `specs` всегда
`affected`.
- **Contract Gate Decision**: `runtime-openapi` = `required`, только когда
меняются обе стороны API contract; иначе конкретный `N/A`.
- **Risks / Trade-offs** и **Migration Plan**.
- В Migration Plan — `### Rollback Contract`: `required` с Trigger,
Actions, Data safety, Verification для auth/security, data integrity/
@@ -56,12 +56,6 @@
| specs | `affected` | <!-- normative result --> |
| docs | `affected` or `N/A` | <!-- as-built documentation result or reason --> |
### Contract Gate Decision
| Gate | Status | Reason |
| --- | --- | --- |
| runtime-openapi | `required` or `N/A` | <!-- required only when both API contract sides change; otherwise concrete N/A reason --> |
## Solution Assessment
<!-- Include only when the change crosses a mandatory build-vs-buy trigger.
+18 -39
View File
@@ -2,7 +2,8 @@
## Purpose
Определяет единый, проверяемый контракт HTTP API между backend и frontend, исключающий ручное расхождение типов и маршрутов.
Определяет направление HTTP API contract между backend и frontend, исключающее
ручное расхождение frontend snapshot и generated types.
## Requirements
@@ -13,23 +14,27 @@ Backend SHALL публиковать версионированный OpenAPI-д
- **WHEN** backend запущен в локальной среде разработки
- **THEN** разработчик может получить OpenAPI-документ для генерации frontend-клиента
### Requirement: Frontend использует сгенерированный API-клиент
Frontend SHALL хранить в репозитории версионируемый snapshot опубликованного OpenAPI-документа и генерировать transport types из этого snapshot. Команда синхронизации MUST получать документ из backend, а затем обновлять snapshot и сгенерированный результат только при успешном завершении всего процесса. Сгенерированный результат MUST храниться в frontend-репозитории.
### Requirement: Frontend использует предоставленный OpenAPI snapshot
Frontend SHALL хранить в репозитории версионируемый snapshot предоставленного
актуального OpenAPI-документа и генерировать transport types из этого snapshot.
Команда генерации MUST читать frontend snapshot и обновлять сгенерированный
результат. Она MUST NOT читать файлы соседнего backend checkout, запускать
backend scripts или проверять backend runtime OpenAPI. Сгенерированный результат
MUST храниться в frontend-репозитории.
#### Scenario: API-контракт синхронизируется
- **WHEN** разработчик запускает команду синхронизации при доступном backend с валидным OpenAPI-документом
- **THEN** frontend обновляет versioned snapshot и сгенерированные transport types из опубликованного контракта
#### Scenario: Types генерируются из предоставленного snapshot
- **WHEN** разработчик запускает frontend-команду генерации
- **THEN** frontend обновляет generated transport types из versioned snapshot
#### Scenario: API-контракт изменяется
- **WHEN** backend меняет опубликованный API-контракт
- **THEN** frontend может синхронизировать snapshot и повторно сгенерировать transport types до интеграции изменения
#### Scenario: Синхронизация контракта завершается ошибкой
- **WHEN** backend недоступен или публикует невалидный OpenAPI-документ
- **THEN** команда завершается с ошибкой и существующие snapshot и сгенерированные transport types не изменяются
- **WHEN** backend меняет опубликованный API-контракт и frontend получает новый snapshot
- **THEN** frontend может повторно сгенерировать transport types до интеграции изменения
### Requirement: Frontend не зависит от OpenAPI во время работы
Работающий frontend MUST использовать ранее сгенерированные transport types и MUST NOT требовать доступность Swagger или OpenAPI endpoint во время запуска, выполнения пользовательского сценария или локальной проверки актуальности generated artifact. Доступ к backend OpenAPI endpoint разрешён только команде синхронизации snapshot и отдельной read-only runtime-contract command.
Работающий frontend MUST использовать ранее сгенерированные transport types и
MUST NOT требовать доступность Swagger или OpenAPI endpoint во время запуска,
выполнения пользовательского сценария, генерации types или локальной проверки
актуальности generated artifact.
#### Scenario: OpenAPI endpoint недоступен после сборки frontend
- **WHEN** frontend уже собран, а endpoint OpenAPI временно недоступен
@@ -39,32 +44,6 @@ Frontend SHALL хранить в репозитории версионируем
- **WHEN** разработчик запускает локальный frontend quality gate
- **THEN** проверка актуальности generated artifact использует сохранённый snapshot и не обращается к OpenAPI endpoint
#### Scenario: Разработчик проверяет runtime-контракт
- **WHEN** разработчик запускает отдельную runtime-contract command
- **THEN** command может поднять изолированный local backend только для сравнения
его полного runtime OpenAPI document с committed frontend snapshot и не
обновляет ни один из артефактов
### Requirement: Отдельная команда обнаруживает runtime drift OpenAPI
Runtime-contract command MUST получать полный OpenAPI document от изолированного
local backend по `/api/docs-json`, рекурсивно canonicalize порядок JSON object
keys, сохраняя каждое field и array order, и сравнивать его SHA-256 с canonical
committed frontend snapshot. При любом content mismatch она MUST завершаться
ошибкой и показывать diff. Verification MUST NOT вызывать snapshot
synchronization command или записывать committed OpenAPI либо generated type
artifacts.
#### Scenario: Runtime success status отличается от committed snapshot
- **WHEN** backend runtime OpenAPI описывает response status, отличный от
committed frontend snapshot
- **THEN** runtime-contract command завершается ошибкой с diff и оставляет
snapshot и generated types без изменений
#### Scenario: Runtime OpenAPI совпадает с committed snapshot
- **WHEN** каждое field canonical runtime document совпадает с canonical
committed frontend snapshot
- **THEN** runtime-contract stage завершается успешно без изменения frontend files
### Requirement: Frontend обнаруживает дрейф сгенерированного контракта
Frontend SHALL проверять, что committed generated transport types соответствуют versioned OpenAPI snapshot, и MUST завершать quality gate с ошибкой при расхождении.
@@ -7,7 +7,7 @@
### Requirement: Realized income analytics is account-scoped and period-bound
Система MUST предоставлять аутентифицированному User аналитику фактически полученного дохода только для одного открытого поддерживаемого Brokerage Account, доступного через текущий T-Bank connection User. Применённый период MUST задаваться включительными календарными датами `from` и `to` в Europe/Moscow, MUST NOT начинаться после своего окончания, MUST NOT содержать будущую дату и MUST NOT превышать 366 дней. При отсутствии применённого периода система MUST использовать интервал от первого дня текущего месяца до сегодняшнего дня включительно.
Система MUST предоставлять аутентифицированному User аналитику фактически полученного дохода только для одного открытого поддерживаемого Brokerage Account, доступного через текущий T-Bank connection User. Применённый период MUST задаваться включительными календарными датами `from` и `to` в Europe/Moscow, MUST NOT начинаться после своего окончания, MUST NOT содержать будущую дату. Система MUST NOT ограничивать максимальную длительность периода. При отсутствии применённого периода система MUST использовать интервал от первого дня текущего месяца до сегодняшнего дня включительно.
#### Scenario: Аналитика за период по умолчанию
@@ -16,12 +16,12 @@
#### Scenario: Аналитика за допустимый пользовательский период
- **WHEN** User применяет включительный период не более 366 дней без будущих дат
- **WHEN** User применяет включительный период любой длительности, в том числе более 366 дней, без будущих дат
- **THEN** система рассчитывает аналитику доступного Brokerage Account за все календарные даты этого периода по Europe/Moscow
#### Scenario: Отклонение недопустимого периода
- **WHEN** User запрашивает период длиннее 366 дней, с будущей датой либо с `from` после `to`
- **WHEN** User запрашивает период с некорректной или будущей датой либо с `from` после `to`
- **THEN** система отклоняет запрос и не возвращает результат для другого или усечённого периода
#### Scenario: Отклонение недоступного счёта
@@ -79,18 +79,33 @@
### Requirement: User explicitly applies an analytics period represented in the URL
Страница выбранного Brokerage Account «Пассивный доход» MUST позволять User редактировать черновик `from` и `to`, не изменяя видимую аналитику до явного применения. Явное применение MUST заменить применённый период, загрузить соответствующий результат и представить применённые даты в URL страницы. Сброс MUST немедленно вернуть применённый период к текущему месяцу по сегодняшний день и синхронизировать URL. Перезагрузка или открытие URL с допустимым периодом MUST восстановить тот же применённый период. Действие перехода на страницу MUST быть доступно из выбранного Brokerage Account, а не из глобальной навигации.
Страница выбранного 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** видимые итоги, сравнения, динамика и breakdown остаются основанными на последнем применённом периоде
- **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`
@@ -101,44 +116,6 @@
- **WHEN** User сбрасывает пользовательский период
- **THEN** страница немедленно применяет период от первого дня текущего месяца по Europe/Moscow до сегодняшнего дня и синхронизирует URL
### Requirement: Analytics summary and trend remain readable per currency
Страница MUST обозначать фактический результат как «Получено купонами и дивидендами» и для каждой исходной валюты показывать total, coupon subtotal и dividend subtotal выбранного периода. Она MUST объяснять, что tax operations не включены, не называя фактический результат gross, net, суммой до налогов, суммой после налогов или налоговым расчётом. Страница MUST показывать stacked column visualization с отдельными coupon и dividend значениями: период до 45 дней включительно группируется по календарным дням, период от 46 до 180 дней включительно — по календарным неделям, более длинный период — по календарным месяцам. Пустые buckets внутри периода MUST оставаться видимыми с нулевыми значениями. При нескольких валютах явный currency control MUST переключать visualization между ними, а эквивалентные значения MUST оставаться доступными как читаемый текст, не только как графика, цвет или положение.
#### Scenario: Короткий период группируется по дням
- **WHEN** включительная продолжительность выбранного периода не превышает 45 дней
- **THEN** visualization показывает отдельные календарные day buckets с coupon и dividend значениями и сохраняет пустые дни как нулевые buckets
#### Scenario: Средний период группируется по неделям
- **WHEN** включительная продолжительность выбранного периода составляет от 46 до 180 дней
- **THEN** visualization показывает calendar week buckets с раздельными coupon и dividend значениями
#### Scenario: Длинный период группируется по месяцам
- **WHEN** включительная продолжительность выбранного периода превышает 180 дней
- **THEN** visualization показывает calendar month buckets с раздельными coupon и dividend значениями
#### Scenario: User переключает валюту visualization
- **WHEN** результат содержит подходящие выплаты в нескольких валютах и User выбирает одну из них
- **THEN** visualization и его текстовый эквивалент показывают значения только выбранной исходной валюты без изменения итогов остальных валют
### Requirement: Instrument breakdown is traceable to exact Ledger Entries
Для каждой исходной валюты страница MUST показывать instrument breakdown, отсортированный по combined total по убыванию. Каждая строка MUST содержать доступную identity Instrument, coupon subtotal, dividend subtotal, combined total и количество включённых выплат. User MUST иметь возможность перейти от строки к точным исполненным Ledger Entries выбранных Instrument, валюты и периода, которые образовали её значения; drill-down MUST сохранять отрицательные корректировки и MUST NOT включать записи вне аналитического итога.
#### Scenario: Breakdown показывает состав дохода по инструментам
- **WHEN** выбранный период содержит подходящие выплаты нескольких Instrument в одной валюте
- **THEN** страница показывает строки по убыванию combined total с identity, двумя subtotals, combined total и receipt count каждого Instrument
#### Scenario: Drill-down воспроизводит строку breakdown
- **WHEN** User открывает drill-down строки Instrument
- **THEN** он видит точные coupon и dividend Ledger Entries этого Instrument, периода и валюты, сумма и количество которых соответствуют строке
### Requirement: Realized analytics never presents an incomplete result as complete
Система MUST формировать выбранный результат, его comparisons, trend buckets, instrument breakdown и drill-down только после успешного получения всех необходимых cursor pages исполненных операций. Ошибка любой необходимой страницы MUST завершать расчёт восстанавливаемой ошибкой и MUST NOT возвращать частичные totals, comparisons, buckets или breakdown как полный результат. Успешный период без подходящих выплат MUST возвращать нулевой результат и отображаться с явным объяснением пустого состояния.
@@ -177,31 +154,69 @@
- **WHEN** Portfolio обзора успешно загружен, но расчёт current-month summary завершается ошибкой
- **THEN** Portfolio и остальные успешно загруженные блоки остаются доступными, а summary показывает собственное состояние восстановления
### Requirement: Forecast is separate from realized income
### Requirement: Income payments remain traceable to exact Ledger Entries
Страница «Пассивный доход» MUST показывать семантически и визуально отдельный forecast block известных coupon и dividend Future Cash Events на следующие 30 дней от сегодняшней даты независимо от выбранного исторического периода. Forecast totals MUST включать только события coupon и dividend с известной projected amount и MUST оставаться разделёнными по исходным валютам; maturity и offer MUST NOT входить в эти totals. Блок MUST показывать три ближайших подходящих события, обозначать результат как не гарантированный прогноз и предоставлять переход к существующей полной странице «Будущие события». Forecast values MUST NOT входить в realized total, comparisons, trend или instrument breakdown. Ошибка forecast MUST оставаться локальной для блока и MUST NOT скрывать успешно загруженную realized analytics.
В разделе «Выплаты» страница 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: Forecast показывает ближайшие известные выплаты
#### Scenario: Каждая выплата показана отдельной карточкой
- **WHEN** на следующие 30 дней известны coupon или dividend Future Cash Events
- **THEN** блок показывает не более трёх ближайших подходящих событий, source-currency totals известных projected amounts, предупреждение о неопределённости и переход к полному списку будущих событий
- **WHEN** выбранный период содержит несколько включённых Ledger Entries одного или разных Instrument
- **THEN** раздел «Выплаты» сразу показывает отдельную карточку каждой записи в исходном порядке без внешних группировочных блоков, раскрытия, итогов инструмента и receipt count на UI
#### Scenario: События без суммы не искажают forecast total
#### Scenario: Список воспроизводит точный состав дохода
- **WHEN** подходящее coupon или dividend Future Cash Event не имеет известной projected amount
- **THEN** событие не добавляет нулевую или вымышленную сумму в forecast total
- **WHEN** User просматривает загруженный раздел «Выплаты»
- **THEN** он видит все и только включённые coupon и dividend Ledger Entries применённого периода с датой, описанием, доступной identity и исходной суммой
#### Scenario: Maturity и offer не входят в forecast income
#### Scenario: Отрицательная корректировка и повторяющиеся IDs сохраняются
- **WHEN** ближайшие Future Cash Events содержат maturity или offer
- **THEN** forecast block не включает их в coupon или dividend totals
- **WHEN** доход содержит отрицательную корректировку либо несколько включённых entries с одинаковым operation ID
- **THEN** каждая запись остаётся отдельной карточкой, а корректировка отображается с исходным знаком
#### Scenario: Пользовательский исторический период не влияет на forecast
#### Scenario: Валюта видна в сумме каждой выплаты
- **WHEN** User применяет любой допустимый исторический период
- **THEN** forecast block по-прежнему охватывает следующие 30 дней от сегодняшней даты
- **WHEN** подходящие выплаты одного Instrument представлены в нескольких исходных валютах
- **THEN** каждая карточка показывает свою Money с исходной валютой, без отдельного заголовка валютной группы или объединённого мультивалютного итога
#### Scenario: Forecast недоступен при успешной realized analytics
### Requirement: Analytics summary remains readable per currency
- **WHEN** realized analytics успешно загружена, а запрос forecast завершается ошибкой
- **THEN** realized totals, comparisons, trend и breakdown остаются доступными, а forecast block показывает собственную ошибку
Страница 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** эта страница остаётся доступной с прежним поведением независимо от отсутствия прогноза на странице дохода
@@ -19,7 +19,9 @@
из UI-библиотеки в токены MUST сохранять их эффективные значения; не включённые
в контракт библиотечные defaults MAY оставаться у библиотеки. Выделение фундамента MUST сохранять текущий
внешний вид, светлую схему и поведение приложения без массовой замены локальных
значений оформления.
значений оформления. Источник SHALL поставляться вместе с приложением как
доверенная конфигурация; загрузка внешних или пользовательских токенов
и отдельная runtime-валидация произвольных документов не поддерживаются.
#### Scenario: Компонент отображается в приложении и каталоге
@@ -54,10 +56,10 @@
#### Scenario: Исходные токены некорректны
- **WHEN** источник содержит неподдерживаемую единицу, некорректную шкалу или
семантическую ссылку на отсутствующий цвет
- **THEN** ошибка обнаруживается при проверке или загрузке фундамента, без
молчаливой подстановки другого визуального значения
- **WHEN** разработчик вносит некорректное значение в локальный источник
- **THEN** отдельный runtime-валидатор не применяется; корректность изменения
проверяется при review и проверке представления, автоматическое отклонение
любого некорректного документа не гарантируется
### Requirement: Общие компоненты независимы от предметной области
+240 -5
View File
@@ -115,17 +115,107 @@ Portfolio Snapshot является результатом наблюдения
- **THEN** система не возвращает fallback Snapshot и отвечает `400` Problem Details с code `INVALID_TBANK_TOKEN`
### Requirement: Состояния и ошибки Portfolio на клиенте
Клиент SHALL загружать Portfolio при открытии защищённого маршрута `/accounts/:accountId`. Он MUST использовать успешный ответ в течение 30 секунд, без automatic retry, polling или refetch при возврате фокуса. После истечения срока клиент MUST скрыть предыдущий ответ и показать loading-состояние до нового результата.
Клиент MUST показать штатное empty-состояние только при пустых `positions` и `cashBalances`. При `isStale: true` клиент MUST показывать Snapshot, `lastSuccessfulAt` и нейтральное предупреждение о временной недоступности T-Bank. При `404` клиент MUST показать состояние недоступного счёта с действием возврата к списку счетов. При отсутствии Snapshot и `TBANK_NOT_CONNECTED`, `INVALID_TBANK_TOKEN`, `TBANK_UNAVAILABLE` или иной ошибке клиент MUST показать безопасное error-состояние с доступными для ошибки действиями; действие повтора разрешено только для временной и неизвестной ошибки.
Клиент 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** клиент показывает позиции и остатки, время последнего успешного снимка и предупреждение без действия ручного обновления
- **THEN** клиент показывает позиции, остатки, валютные итоги, время
последнего успешного снимка и нейтральное предупреждение; само это
предупреждение не добавляет действие ручного обновления
#### Scenario: Недоступный счёт на клиенте
- **WHEN** клиент получает `404` для Portfolio
- **THEN** клиент не показывает Portfolio и предлагает вернуться к списку Brokerage Account без действия повтора
- **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-состояние без валютных
карточек с вымышленными нулевыми итогами
### Requirement: Представление карточек позиций на клиенте
Клиент MUST отображать `STOCK` и `BOND` отдельными секциями. Внутри каждой секции MUST сохраняться upstream-порядок позиций. Позиции иных типов MUST оставаться доступными в отдельной общей секции.
@@ -193,3 +283,148 @@ Portfolio Snapshot является результатом наблюдения
#### Scenario: Секция иных типов сохраняет прежний вид
- **WHEN** User открывает загруженный Portfolio с позициями иных типов (не `STOCK` и не `BOND`)
- **THEN** клиент отображает их компактными плитками в адаптивном grid с текстовой подписью доходности и без визуального логотипа
### 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 состоянием пустого портфеля
+1 -1
View File
@@ -170,7 +170,7 @@ Entry и Future Cash Event. Runtime/cache-термины MUST быть отде
### Requirement: README репозиториев являются компактными onboarding entrypoints
Репозитории frontend и backend SHALL предоставлять русскоязычные README
entrypoints, которые указывают Node 22, lockfile-safe bootstrap, локальные
entrypoints, которые указывают Node 24, lockfile-safe bootstrap, локальные
non-mutating verification commands, требуемую локальную configuration и ссылки
на authoritative или just-in-time documentation. README MUST различать deferred
check и passing verification gate, а detailed rationale deferred Prisma e2e и
+2 -2
View File
@@ -31,7 +31,7 @@
delivery-plan файл как источник требований, порядка поставки или фактического
поведения. Требования SHALL оставаться в OpenSpec, а delivered system overview
и долговечные ограничения SHALL находиться в `docs`. Frontend и backend MUST
требовать Node 22 и документировать lockfile-safe bootstrap через `npm ci`.
требовать Node 24 и документировать lockfile-safe bootstrap через `npm ci`.
#### Scenario: Агент определяет границы поставленной системы
- **WHEN** агент начинает работу и ищет актуальные границы WealthLedger
@@ -40,7 +40,7 @@ delivery-plan файл как источник требований, поряд
#### Scenario: Агент bootstrap-ит application repository
- **WHEN** агент начинает локальную проверку frontend или backend на чистой машине
- **THEN** он может определить Node 22 и выполнить `npm ci` без угадывания runtime policy
- **THEN** он может определить Node 24 и выполнить `npm ci` без угадывания runtime policy
### Requirement: Frontend предоставляет минимальный каркас приложения
Frontend SHALL предоставлять русскоязычный адаптивный каркас приложения с маршрутами `/login`, `/register`, `/accounts`, `/accounts/:accountId` и `/settings`, светлой финансовой темой и навигацией для счетов и настроек. Каркас MUST содержать состояния загрузки, ошибки и пустых данных без реализации доменных сценариев.
+3634
View File
File diff suppressed because it is too large Load Diff
+21
View File
@@ -0,0 +1,21 @@
{
"name": "wealth-ledger-specs",
"version": "0.1.1",
"private": true,
"engines": {
"node": "24.21.0"
},
"scripts": {
"verify": "git diff --check",
"release:preview": "release-it --dry-run --ci",
"release:prepare": "release-it --ci",
"commitlint": "commitlint"
},
"devDependencies": {
"@commitlint/cli": "21.2.2",
"@commitlint/config-conventional": "21.2.2",
"@release-it/conventional-changelog": "12.0.0",
"conventional-changelog-conventionalcommits": "10.4.0",
"release-it": "21.0.2"
}
}
+31
View File
@@ -0,0 +1,31 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended",
":semanticCommitTypeAll(chore)"
],
"timezone": "Europe/Moscow",
"labels": [
"dependencies"
],
"dependencyDashboard": true,
"rangeStrategy": "pin",
"packageRules": [
{
"matchUpdateTypes": [
"minor",
"patch"
],
"groupName": "non-major dependencies"
},
{
"matchUpdateTypes": [
"major"
],
"dependencyDashboardApproval": true
}
],
"baseBranchPatterns": [
"develop"
]
}
+10
View File
@@ -0,0 +1,10 @@
const assert = require("node:assert/strict");
const { execFileSync } = require("node:child_process");
const { version } = require("../package.json");
const git = (...args) => execFileSync("git", args, { encoding: "utf8" }).trim();
assert.equal(git("status", "--porcelain"), "", "Commit all changes before preparing a release");
const tags = git("tag", "--merged", "HEAD", "--list", "v[0-9]*", "--sort=-version:refname")
.split("\n").filter(tag => /^v\d+\.\d+\.\d+$/.test(tag));
assert.equal(version, tags[0]?.slice(1) ?? "0.0.0",
"Version is already prepared or release tags are missing. Fetch tags; finish the existing release before preparing another.");