diff --git a/openspec/changes/archive/2026-08-25-codify-frontend-view-model-boundary/.openspec.yaml b/openspec/changes/archive/2026-08-25-codify-frontend-view-model-boundary/.openspec.yaml new file mode 100644 index 0000000..9d9054d --- /dev/null +++ b/openspec/changes/archive/2026-08-25-codify-frontend-view-model-boundary/.openspec.yaml @@ -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 diff --git a/openspec/changes/archive/2026-08-25-codify-frontend-view-model-boundary/design.md b/openspec/changes/archive/2026-08-25-codify-frontend-view-model-boundary/design.md new file mode 100644 index 0000000..1618b5d --- /dev/null +++ b/openspec/changes/archive/2026-08-25-codify-frontend-view-model-boundary/design.md @@ -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 + + diff --git a/openspec/changes/archive/2026-08-25-codify-frontend-view-model-boundary/implementation-summary.md b/openspec/changes/archive/2026-08-25-codify-frontend-view-model-boundary/implementation-summary.md new file mode 100644 index 0000000..b05f9d8 --- /dev/null +++ b/openspec/changes/archive/2026-08-25-codify-frontend-view-model-boundary/implementation-summary.md @@ -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. diff --git a/openspec/changes/archive/2026-08-25-codify-frontend-view-model-boundary/proposal.md b/openspec/changes/archive/2026-08-25-codify-frontend-view-model-boundary/proposal.md new file mode 100644 index 0000000..00246b5 --- /dev/null +++ b/openspec/changes/archive/2026-08-25-codify-frontend-view-model-boundary/proposal.md @@ -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 не должны меняться. diff --git a/openspec/changes/archive/2026-08-25-codify-frontend-view-model-boundary/tasks.md b/openspec/changes/archive/2026-08-25-codify-frontend-view-model-boundary/tasks.md new file mode 100644 index 0000000..1edb3de --- /dev/null +++ b/openspec/changes/archive/2026-08-25-codify-frontend-view-model-boundary/tasks.md @@ -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.