docs(specs): archive frontend view model boundary change

This commit is contained in:
2026-08-25 13:17:45 +03:00
parent 736885fe65
commit 5cc42f30ca
5 changed files with 323 additions and 0 deletions
@@ -0,0 +1,7 @@
schema: wealth-ledger-sdd
created: 2026-08-25
goal: Codify and restore the frontend ViewModel/View boundary without changing user-visible behavior.
skip_specs: true
affected_areas:
- frontend
- specs
@@ -0,0 +1,164 @@
# codify-frontend-view-model-boundary — Design
## Context
Frontend уже использует FSD-направление импортов, typed API hooks и page pattern
`container + ViewModel + view` на части finance pages. Однако текущий
`AccountPage` показывает противоречивый пример: container вызывает
`useAccountPageViewModel`, но `AccountPage.view.tsx` дополнительно вызывает
server-state hooks для recent Account Ledger и Future Cash Events, группирует
Portfolio Positions по типам, считает display-only доли и содержит много
самостоятельных presentation sections в одном файле.
Reference template фиксирует другую семантику: `*.tsx` без суффикса — container
или public component, `*.vm.ts` — hook, который инкапсулирует данные, действия и
state mapping, а `*.view.tsx` — чистое представление. В текущем frontend docs
эта граница описана короче и не проговаривает отрицательные правила, поэтому
AI-agent может принять локальный god-view за допустимый pattern.
Ограничения frontend: React/Vite, TanStack Query, Mantine, React Router,
i18n через `useTranslation`, FSD public API checks, canonical gate
`npm ci && npm run verify` под Node 22. Change не вводит новый automatic
guardrail: enforcement остаётся docs/review guidance плюс исправленный локальный
пример и tests.
## Goals / Non-Goals
**Goals:**
- Восстановить `AccountPage` как корректный пример page container,
ViewModel и маленьких presentation views.
- Перенести recent data loading, state selection, Portfolio Position grouping,
derived display flags/numbers и callbacks из view layer в ViewModel/container
layer.
- Раздробить account page presentation на именованные соседние components,
когда section/state имеет самостоятельный смысл.
- Уточнить frontend-local docs и `frontend/AGENTS.md`, чтобы будущие AI agents
видели явную границу container/ViewModel/view.
- Покрыть VM-derived presentation data focused tests и сохранить existing
render tests как regression net пользовательского поведения.
**Non-Goals:**
- Automatic lint, dependency-cruiser или AST guardrail для VM/view boundary.
- Нормализация `account-operations`, `account-events`, login, register или
settings pages.
- Изменение API hooks signatures, auth-gating pattern или централизованного
access-token handling.
- Изменение backend, HTTP routes, OpenAPI snapshot, generated types, domain
docs или product behavior.
- Визуальный redesign account page.
## Decisions
**D1. Сохранить template-семантику ролей файлов.** `AccountPage.tsx` остаётся
container: он вызывает `useAccountPageViewModel()` и выбирает top-level state
view. `AccountPage.vm.ts` становится owner данных и derived presentation model:
portfolio state, recent ledger/events state, refresh/retry callbacks, position
sections и display-only calculations. `*.view.tsx` и nested view components
получают props и рендерят JSX.
*Alternative:* сделать `AccountPage.view.tsx` единственным component, который
вызывает VM hook. Отклонено: это переворачивает роль `*.view.tsx` относительно
reference template и текущих conventions.
**D2. View modules остаются render-only, но не hook-free.** Presentation views
могут использовать `useTranslation`, form-context hooks для form views и
локальный UI-only state, например image fallback state. Они не вызывают
server-state/API hooks, auth hooks, route/navigation hooks или VM hooks, не
делают domain grouping и не считают derived presentation values из collection
data. Простое форматирование одного переданного значения через существующие
formatters остаётся presentation concern.
*Alternative:* запретить все hooks во view. Отклонено: это вынудит переносить
i18n labels и локальный UI fallback state в VM, делая VM слишком dependent от
деталей rendering.
**D3. VM отдаёт presentation model, а не только raw domain data.** ViewModel
возвращает данные, уже разделённые на account page sections, вместе с flags,
share denominator/result values и recent sections states. View остаётся
ответственной за layout, labels, Mantine components, links и final rendering.
Это оставляет вычисления тестируемыми без DOM, но не превращает VM в generator
готовой разметки или локализованных strings.
**D4. Дробить view по смысловым границам.** Account page presentation делится на
nearby components для heading/refresh state, totals/cash, position sections,
position card/logo и recent sections. Критерий split — самостоятельная
ответственность и имя, а не только число строк. Если section может быть названа
и протестирована/прочитана независимо, она не должна накапливаться внутри
одного большого view file.
**D5. Docs first, automated enforcement later.** Этот change уточняет
`frontend/docs/UI-PATTERN.md`, `frontend/docs/CONVENTIONS.md` и короткий routing
pointer в `frontend/AGENTS.md`. Hard checker не добавляется, потому что сейчас
нет утверждённого design для надёжного AST rule, исключений и maintenance cost.
Rule остаётся review guidance; corrected AccountPage и tests уменьшают риск
повтора.
**D6. Tests фиксируют новую поверхность ответственности.** Новые focused tests
проверяют ViewModel-derived presentation data: секции позиций, display-only
доли, recent ledger/events state и retry/loading/error propagation. Existing
render tests сохраняют observable account page behavior, включая stale snapshot,
nullable values, logo fallback, recent failure isolation и cached portfolio
visibility.
## Delivery Map
| Area | Status | Architectural outcome or N/A reason |
| --- | --- | --- |
| frontend | `affected` | `AccountPage` возвращается к container/ViewModel/view boundary; account page views дробятся по смысловым presentation components; frontend-local docs и `AGENTS.md` фиксируют guidance; focused VM tests добавляются к existing render regressions. |
| backend | `N/A` | Backend behavior, routes, DTOs, T-Bank integration и OpenAPI runtime не меняются. |
| specs | `affected` | Этот SDD change хранит approved Proposal, skipped Delta Specs, Design и Tasks для behavior-preserving frontend architecture hardening. |
| docs | `N/A` | Отдельный as-built docs repository не меняется: product capability и documented system behavior не меняются; frontend-local conventions живут в frontend repo. |
### Contract Gate Decision
| Gate | Status | Reason |
| --- | --- | --- |
| runtime-openapi | `N/A` | Change не затрагивает backend и не меняет frontend/backend API contract, generated client или OpenAPI snapshot. |
### Solution Assessment
Формальная build-versus-buy assessment не triggered. Change не вводит и не
заменяет auth, security, storage, external integration, observability, caching,
concurrency или infrastructure/tooling. Automatic checker явно оставлен
non-goal; docs/review guidance не требует выбора стороннего решения.
## Risks / Trade-offs
- **Regression при переносе вычислений.** Перемещение grouping/share/recent
state из view в VM может изменить детали отображения. Mitigation: focused VM
tests фиксируют derived data, existing render tests фиксируют пользовательски
видимые states.
- **VM становится слишком presentation-specific.** VM получает display flags и
grouped sections, но не локализует text и не собирает JSX. Это удерживает
границу между testable preparation и rendering.
- **Docs-only guardrail может быть пропущен.** Без automatic checker нарушение
возможно. Mitigation: `frontend/AGENTS.md` ведёт AI agents к явной boundary
guidance, а largest local account page перестаёт быть плохим примером.
- **Слишком мелкое дробление компонентов.** Split по каждому JSX fragment
ухудшит навигацию. Mitigation: дробление основано на semantic responsibility
и самостоятельном имени section/state, а не на механическом лимите строк.
## Migration Plan
1. Добавить focused tests для ViewModel-derived account page presentation data,
чтобы зафиксировать переносимую логику до refactor.
2. Расширить `AccountPage.vm.ts`: перенести recent queries, position grouping,
share calculations, refresh/retry data и recent section states в возвращаемую
presentation model.
3. Раздробить account page views на nearby presentation components и обновить
`AccountPage.view.tsx` так, чтобы view modules получали props и не вызывали
data/auth/router/navigation/VM hooks.
4. Обновить frontend-local docs и `frontend/AGENTS.md` с explicit
container/ViewModel/view boundary и split guidance.
5. Запустить targeted account page tests, затем frontend canonical gate
`npm ci && npm run verify`; после implementation выполнить semantic
verification против Proposal и Design и workspace dispatcher для change.
Rollback — обычный revert frontend/specs commits; schema, data migration,
runtime contract migration и feature flag не вводятся.
## Open Questions
<!-- none -->
@@ -0,0 +1,48 @@
# codify-frontend-view-model-boundary — Implementation Summary
## Semantic Verification
Date: 2026-08-25
Checked areas:
- User-visible account page behavior: existing render regressions still pass for
loading, error, empty, loaded, stale Portfolio Snapshot, nullable values,
signed returns, logo fallback, recent failure isolation and cached portfolio
visibility.
- VM/View boundary: `AccountPage.vm.ts` now owns Portfolio Position grouping,
share/return/logo derived presentation data, recent Account Ledger and Future
Cash Events query state, retry props and route targets. `AccountPage.view.tsx`
and `components/*.view.tsx` render props only.
- Forbidden view dependencies: grep found no `useBrokerageOperations`,
`useBrokerageEvents`, `useAuth`, `useParams`, `useNavigate`,
`useAccountPageViewModel`, `denominators` or `positions.filter` occurrences
in account page view files or presentation components.
- Access token boundary: `accessToken` remains only in `AccountPage.vm.ts` and
test auth mocks under `src/pages/account/ui`.
- API/backend contract: backend files, OpenAPI snapshot, generated client and
entity API hook signatures were not changed.
- Frontend guidance: `frontend/docs/UI-PATTERN.md`,
`frontend/docs/CONVENTIONS.md` and `frontend/AGENTS.md` now describe the
container/ViewModel/view boundary and semantic split guidance.
- Bundle guardrail: the account lazy route baseline was updated from 21,872 to
22,366 bytes after the semantic presentation split. Initial JavaScript payload
remains 509,165 bytes and passed the existing budget.
Observed verification:
- Initial baseline before edits: `npm run test:run -- src/pages/account/ui/AccountPage.test.tsx` — passed, 1 file / 12 tests.
- RED VM tests before implementation: `npm run test:run -- src/pages/account/ui/AccountPage.vm.test.tsx` — failed on missing `positionSections`, `recentOperations` and `recentEvents` presentation state.
- Targeted account page verification after implementation: `npm run test:run -- src/pages/account/ui/AccountPage.test.tsx src/pages/account/ui/AccountPage.vm.test.tsx` — passed, 2 files / 15 tests.
- Focused frontend checks: `npm run typecheck`, `npm run lint`, `npm run knip` and `npm run deps` — passed.
- Frontend canonical gate under Node 22.21.0 with Node 22 first in `PATH`: `npm ci && npm run verify` — passed after bundle baseline update; 18 test files / 64 tests, coverage 83.84% statements / 78.41% branches / 78.36% functions / 84.5% lines, production build and bundle check passed.
- OpenSpec structural check: `openspec validate codify-frontend-view-model-boundary --type change --strict --store wealth-ledger-specs` — passed.
- Workspace dispatcher under Node 22.21.0 with Node 22 first in `PATH`: `bash specs/scripts/verify-workspace-change.sh codify-frontend-view-model-boundary` — passed; Delivery Map frontend/specs affected and backend/docs N/A verified, main specs 20/20 passed, frontend canonical verification passed.
Conclusion:
The implementation matches the approved Proposal and Design as a
behavior-preserving frontend architecture hardening change. The account page now
provides a corrected local example of the container/ViewModel/view boundary,
frontend-local guidance documents that boundary, and no user-visible capability,
backend behavior, route, API contract, durable data behavior or non-goal changed.
@@ -0,0 +1,69 @@
## Why
Frontend conventions уже называют разделение ViewModel/View, а project template
описывает эту границу подробнее. Однако поставленный finance UI сейчас содержит
page view, который смешивает рендеринг с server-state hooks, соседней загрузкой
данных, доменной группировкой и derived presentation calculations. В результате
получается misleading example для будущих AI-assisted changes: агент может
следовать крупнейшей локальной странице и всё равно нарушить intended boundary.
Это нужно исправить сейчас, потому что проблема не сводится к локальной
сложности одного файла. Текущий пример ослабляет guidance, которое должно
удерживать будущую frontend-работу маленькой, reviewable и testable. Если
граница остаётся implicit, та же ошибка может повториться на следующих finance
pages даже после ручной очистки текущего файла.
## What Changes
- Восстановить frontend page architecture так, чтобы data fetching, state
selection, domain grouping, derived display flags и callbacks жили в
ViewModel или container layer, а view modules оставались сосредоточены на
rendering props.
- Уточнить frontend-local guidance для AI agents и maintainers, чтобы роли
container, ViewModel и view modules были explicit и согласованы с reference
template.
- Держать view modules маленькими по ответственности: когда section или state
имеет отдельный смысл и может быть самостоятельно назван, его нужно выносить
в соседний presentation component, а не накапливать внутри одного большого
view.
- Сохранить текущее user-visible поведение account page, routing, localized
text, loading/error/empty/loaded states, cached-snapshot behavior и API
contract.
- Добавить focused regression coverage для derived presentation data,
перенесённых во ViewModel, сохранив existing rendering coverage для видимых
состояний страницы.
- Non-goal: вводить automatic lint или dependency-cruiser rule для этой границы
в данном change.
- Non-goal: нормализовать другие account-related pages, которые сейчас
используют другие page architecture patterns.
- Non-goal: менять HTTP routes, generated OpenAPI types, backend behavior,
finance domain terminology, visual design или portfolio calculations за
пределами переноса existing display-only derivation в правильный frontend
layer.
## Capabilities
### New Capabilities
_Нет._
### Modified Capabilities
_Нет._
Это behavior-preserving frontend architecture и documentation hardening change.
Он отказывается от Delta Specs через `skip_specs: true`, потому что не меняются
user-visible capability, API contract, durable data behavior или product
requirement. Любое обнаруженное observable behavior change должно вернуться к
Proposal и Delta Specs до Design.
## Impact
Users должны видеть одно и то же поведение account page до и после этого
change. Frontend maintainers и AI agents получают более ясное local rule о том,
где должна жить page logic, и исправленный account page example, которому можно
следовать.
Affected implementation area — frontend page structure и frontend-local
architecture documentation. Specs отслеживает этот SDD lifecycle. Backend и
отдельный as-built docs repository не должны меняться.
@@ -0,0 +1,35 @@
# codify-frontend-view-model-boundary — Tasks
## 1. Frontend: зафиксировать VM-derived behavior тестами
- [x] 1.1 Добавить focused tests для `useAccountPageViewModel` в `frontend/src/pages/account/ui/AccountPage.vm.test.tsx` или расширить существующий colocated account page test, если текущая test setup уже надёжно покрывает hook под `QueryClientProvider`, `MantineProvider` и `MemoryRouter`. Tests должны зафиксировать, что ViewModel возвращает grouped position sections для stocks/bonds/other, display-only share values, refresh/retry props и loaded/empty/error state selection без обращения к DOM-rendering details. Verification: `npm run test:run -- src/pages/account/ui/AccountPage.vm.test.tsx` в `frontend` либо точный account page test file, если tests останутся в существующем файле.
- [x] 1.2 Добавить focused tests для recent Account Ledger и Future Cash Events presentation state в ViewModel: loading, error, empty, loaded first five items/events и isolated retry/open-link data должны приходить из VM как props/state для presentation views. Verification: `npm run test:run -- src/pages/account/ui/AccountPage.vm.test.tsx` в `frontend` либо точный account page test file, если tests останутся в существующем файле.
- [x] 1.3 Сохранить existing account page render regressions для stale Portfolio Snapshot, nullable values, logo fallback, signed returns, recent section failure isolation и cached portfolio visibility. Если после refactor меняются props state views, обновить tests только на новую presentation interface без изменения expected visible behavior. Verification: `npm run test:run -- src/pages/account/ui/AccountPage.test.tsx` в `frontend`.
## 2. Frontend: перенести account page logic во ViewModel
- [x] 2.1 Расширить `frontend/src/pages/account/ui/AccountPage.vm.ts`, чтобы он возвращал discriminated presentation model для top-level states и loaded/empty content: account heading data, portfolio refresh state, totals, cash balances, position sections и retry callbacks. Verification: focused VM tests из 1.1 проходят.
- [x] 2.2 Перенести `useBrokerageOperations` и `useBrokerageEvents` из view layer в `AccountPage.vm.ts`. ViewModel должен ограничивать recent operations/events до данных, необходимых presentation components, включая loading/error/empty/loaded flags, first five events, operation payment fallback data и route target для full ledger/events pages. Verification: focused VM tests из 1.2 проходят.
- [x] 2.3 Перенести из view layer grouping Portfolio Positions по `STOCK`, `BOND` и other, вычисление currency denominators/share display data, wide-section flags и signed/unlabeled return display flags. ViewModel не должен локализовать text или собирать JSX. Verification: focused VM tests из 1.1 проходят, `npm run typecheck` в `frontend`.
- [x] 2.4 Убрать `accessToken` из props state views account page, если после переноса recent hooks он больше нужен только ViewModel. Не менять signatures entity API hooks и не выполнять auth-gating refactor вне scope. Verification: `rg -n "accessToken" src/pages/account/ui` в `frontend` показывает token только там, где он остаётся responsibility VM/container.
## 3. Frontend: раздробить account page presentation
- [x] 3.1 Создать nearby presentation components под `frontend/src/pages/account/ui/components/` для самостоятельных sections account page: heading, portfolio refresh state, portfolio totals, cash balances, position section/card/logo, recent operations и recent events. Каждый component должен получать props и рендерить JSX без data/auth/router/navigation/VM hooks. Verification: `npm run typecheck` в `frontend`.
- [x] 3.2 Обновить `frontend/src/pages/account/ui/AccountPage.view.tsx`, чтобы exported Loading/Error/Empty/Loaded views только компоновали presentation props и nearby components. Файл не должен импортировать `useBrokerageOperations`, `useBrokerageEvents`, `useAuth`, `useParams`, `useNavigate` или `useAccountPageViewModel`, и не должен содержать collection grouping или share denominator calculations. Verification: `rg -n "useBrokerageOperations|useBrokerageEvents|useAuth|useParams|useNavigate|useAccountPageViewModel|denominators|positions\\.filter" src/pages/account/ui/*.view.tsx src/pages/account/ui/components` в `frontend` не находит запрещённые occurrences в view files, кроме false positives, явно проверенных вручную.
- [x] 3.3 Сохранить допустимые presentation hooks во view components: `useTranslation` для labels и локальный UI-only `useState` для image fallback. Не переносить локализованные strings или `Avatar` fallback state в ViewModel. Verification: account page render tests из 1.3 проходят.
- [x] 3.4 Убедиться, что split не создаёт cross-slice deep imports и не расширяет public API страницы сверх существующих exports `AccountPage*` из `AccountPage.tsx`. Verification: `npm run deps` и `npm run knip` в `frontend`.
## 4. Frontend: уточнить AI guidance и conventions
- [x] 4.1 Обновить `frontend/docs/UI-PATTERN.md`: явно описать роли `*.tsx` container, `*.vm.ts` ViewModel и `*.view.tsx` presentation views; перечислить запрет для view modules на data/auth/router/navigation/VM hooks, domain grouping и derived collection calculations; зафиксировать допустимые presentation hooks `useTranslation`, form-context hooks и локальный UI-only state. Verification: manual read-through против Design D1-D3.
- [x] 4.2 Обновить `frontend/docs/CONVENTIONS.md`: расширить `File roles` так, чтобы AI agent видел не только suffix names, но и boundary responsibilities, включая правило semantic split для маленьких files по ответственности. Verification: manual read-through против Design D4-D5.
- [x] 4.3 Обновить `frontend/AGENTS.md` коротким pointer на VM/view boundary в `docs/UI-PATTERN.md` и semantic split guidance. Не дублировать полный документ в AGENTS. Verification: `rg -n "ViewModel|view|UI-PATTERN|semantic|responsibility" AGENTS.md docs/UI-PATTERN.md docs/CONVENTIONS.md` в `frontend`.
## 5. Verification, semantic review и workspace gate
- [x] 5.1 Запустить targeted account page tests после refactor. Verification: `npm run test:run -- src/pages/account/ui/AccountPage.test.tsx src/pages/account/ui/AccountPage.vm.test.tsx` в `frontend`; если VM tests размещены в существующем файле, команда должна указывать фактические test files.
- [x] 5.2 Запустить frontend canonical gate под Node 22. Verification: `npm ci && npm run verify` в `frontend`.
- [x] 5.3 Проверить OpenSpec change структурно. Verification: `openspec validate codify-frontend-view-model-boundary --type change --strict --store wealth-ledger-specs` из `specs`.
- [x] 5.4 Выполнить semantic verification против Proposal и Design: подтвердить, что user-visible account page behavior, routes, localized text keys, loading/error/empty/loaded states, cached-snapshot behavior, API contract, backend behavior и non-goals не изменились. Verification: записать checked areas и conclusion в implementation summary перед archive.
- [x] 5.5 Запустить workspace change dispatcher под Node 22 после завершения affected tasks. Verification: `bash specs/scripts/verify-workspace-change.sh codify-frontend-view-model-boundary` из workspace root.