Files
specs/openspec/changes/archive/2026-08-25-codify-frontend-view-model-boundary/design.md
T

165 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 -->