11 KiB
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
- Добавить focused tests для ViewModel-derived account page presentation data, чтобы зафиксировать переносимую логику до refactor.
- Расширить
AccountPage.vm.ts: перенести recent queries, position grouping, share calculations, refresh/retry data и recent section states в возвращаемую presentation model. - Раздробить account page views на nearby presentation components и обновить
AccountPage.view.tsxтак, чтобы view modules получали props и не вызывали data/auth/router/navigation/VM hooks. - Обновить frontend-local docs и
frontend/AGENTS.mdс explicit container/ViewModel/view boundary и split guidance. - Запустить 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 не вводятся.