165 lines
11 KiB
Markdown
165 lines
11 KiB
Markdown
# 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 -->
|