783 lines
47 KiB
Markdown
783 lines
47 KiB
Markdown
# Frontend Structural Maintainability Audit
|
||
|
||
**Дата:** 2026-08-31
|
||
**Session ID:** `01a055ce-cefc-75c3-860f-6ca0ec8c881c`
|
||
**Invoked skills:** `repository-audit`
|
||
**Supporting skills invoked:** `graphify` — выполнен только preflight; готового
|
||
`graphify-out/graph.json` не было, а обязательный build создаёт cache и
|
||
visualization artifacts, поэтому он был остановлен как несовместимый с
|
||
read-only scope.
|
||
**External research performed:** no
|
||
|
||
## Scope and Confidence
|
||
|
||
**Target:** `frontend/`, branch `main`, commit
|
||
`7ab043dce63a3fbacbfc9ac7d15b00dd714a541f` (`docs: remove useless`).
|
||
Исходный `git status --short --untracked-files=all` был пуст. В inventory вошли
|
||
все 145 tracked files, включая 118 файлов под `src`, 42 non-test TSX files,
|
||
22 test files, пять CSS files и 23 `index.ts` barrels.
|
||
|
||
**Reference:** `references/frontend-ai-template/`. Директория не является
|
||
Git-репозиторием, поэтому commit identity отсутствует. Для этого аудита её
|
||
identity зафиксирована SHA-256 checksum
|
||
`72834908a7effb3d57d98729a6def3996383199e3c63596707f805a6cdc64e4e`
|
||
по 220 отсортированным per-file SHA-256 records; исключены `node_modules`,
|
||
`.idea`, `.playwright-mcp`, cache, coverage, build и dist directories.
|
||
|
||
**Existing context:** `frontend-repository-audit.md` прочитан полностью только
|
||
для устранения повторов и противоречий. Его общие findings об OpenAPI drift
|
||
gate, auth journey, malformed income date, type-only dependency checks и docs
|
||
не повторяются. Этот отчёт уточняет general F-05 и F-08 и пересматривает F-07
|
||
с учётом явно заданной component-file preference.
|
||
|
||
Глубоко проверены все production TS/TSX/CSS paths, public barrels, React Query
|
||
hooks и cache mutations, form/schema setup, formatting/parsing/error helpers,
|
||
все tests и `src/test/**`, Vitest/Vite/package configuration, relevant installed
|
||
library behavior, а также эквивалентные responsibilities reference. Для
|
||
income identity и `connectedAt` contract прочитаны узкие backend owner paths,
|
||
поскольку frontend иначе не может установить semantic owner.
|
||
|
||
Проверки были статическими и read-only. `npm run verify`, tests, coverage,
|
||
build, lint, Knip, dependency-cruiser, code generation и browser checks не
|
||
запускались: они не меняли бы структурные выводы, а часть из них создаёт или
|
||
может создавать `coverage`, `dist`, caches или generated output. Install,
|
||
autofix, snapshot update, OpenSpec/SDD commands и external research не
|
||
выполнялись. Поэтому confidence high для ownership, duplication и static test
|
||
isolation evidence; runtime/visual consequences имеют medium confidence там,
|
||
где явно отмечены ограничения.
|
||
|
||
**Final preservation check:** target остался на том же commit с пустым
|
||
`git status --short --untracked-files=all`; final reference checksum совпал с
|
||
initial. Единственная запись сессии — этот Markdown report вне target и
|
||
reference.
|
||
|
||
## Executive Summary
|
||
|
||
Критических findings нет. Архитектурная основа frontend в целом здорова:
|
||
typed API boundary, FSD direction, page VM pattern, scoped CSS Modules и
|
||
risk-oriented MSW tests уже дают хорошие владельческие seams.
|
||
|
||
Главный новый риск — F-01: смена или удаление T-Bank connection не удаляет
|
||
кэшированные данные прежнего connection. Это data-ownership issue, а не
|
||
косметическая консолидация. Следом идут test isolation (F-02), дублирование
|
||
backend instrument identity внутри income view (F-03) и семь component files,
|
||
где самостоятельные semantic owners действительно скрывают основной component
|
||
(F-04). F-04 частично отменяет вывод general audit F-07, но не превращает
|
||
предпочтение в механическое one-component-per-file правило.
|
||
|
||
Остальные structural costs сосредоточены в server-state policy/key ownership
|
||
(F-05), test provider harnesses (F-06), cross-slice display/API primitives
|
||
(F-07), global styling (F-08) и одном скрытом OpenAPI type defect (F-09).
|
||
Новых библиотек не требуется: platform APIs и уже имеющиеся TanStack Query,
|
||
Mantine, Zod/RHF и CSS Modules покрывают подходящие remedies.
|
||
|
||
## Priority Findings
|
||
|
||
| Severity | Finding | Category | Action | Effort |
|
||
| --- | --- | --- | --- | --- |
|
||
| high | F-01. T-Bank connection lifecycle не удаляет прежний financial cache | Data Ownership / Cache | change | medium |
|
||
| medium | F-02. Test teardown разделён между setup и отдельными tests | Test Isolation | change | small |
|
||
| medium | F-03. Income view реконструирует server identity и collection model | Architecture / Correctness | change | medium |
|
||
| medium | F-04. Семь TSX modules скрывают основной component | Component Boundaries | change | medium |
|
||
| medium | F-05. Server-state defaults и query identity имеют разных случайных владельцев | Query Policy / Cache Keys | change / simplify | small |
|
||
| medium | F-06. Test provider и QueryClient harnesses систематически повторяются | Test Infrastructure | simplify | medium |
|
||
| medium | F-07. Cross-slice display и API primitives имеют misleading owners | Semantic Ownership | change | medium |
|
||
| medium | F-08. Global CSS владеет component и Mantine-internal styling | Styling | change / simplify | small–medium |
|
||
| medium | F-09. `connectedAt` contract скрыт через `String(...)` coercion | API Ownership / Correctness | change | small |
|
||
| low | F-10. Два presentation files владеют validation/payment invariants | Helper Ownership | change | small |
|
||
| low | F-11. Redundant position modes и test-facing proxy exports не несут invariant | Complexity | simplify | small |
|
||
|
||
## Findings
|
||
|
||
### F-01. T-Bank connection lifecycle не удаляет прежний financial cache
|
||
|
||
**Category:** Data Ownership / Cache Invalidation
|
||
**Severity:** high
|
||
**Confidence:** high
|
||
**Action:** change
|
||
**Effort:** medium
|
||
|
||
**Observed:** Успешные `PUT` и `DELETE` T-Bank connection обновляют status cache
|
||
и инвалидируют только Brokerage Accounts list. Они не удаляют уже
|
||
закэшированные данные, полученные через прежний connection.
|
||
|
||
**Evidence:** Оба mutation success path находятся в
|
||
`frontend/src/features/tbank-connection/ui/TbankConnectionSection.tsx:60-82`;
|
||
единственные dependent operations — `setQueryData` status и два вызова
|
||
`invalidateQueries({ queryKey: brokerageAccountsQueryKey })` на строках 62–63
|
||
и 73–77. Repository-wide search всех production `setQueryData`,
|
||
`invalidateQueries`, `removeQueries` и `clear` не нашёл connection cleanup.
|
||
|
||
Под connection зависят шесть query families: accounts, portfolio, operations,
|
||
events, income summary и income analytics
|
||
(`useBrokerageAccounts.ts:4-17`, `usePortfolio.ts:12-21`,
|
||
`useBrokerageOperations.ts:35-49`, `useBrokerageEvents.ts:9-19`,
|
||
`useIncome.ts:9-41`). Из них инвалидируется одна, но invalidation сохраняет её
|
||
последние data. Остальные пять не инвалидируются и не удаляются.
|
||
|
||
Scope search включал все query definitions, mutations и relevant T-Bank,
|
||
accounts и portfolio tests. Counter-evidence: logout/session loss корректно
|
||
вызывает `queryClient.clear()` в `features/auth/model/auth.tsx:57-62`, но
|
||
connection replacement/deletion не завершает Session. Backend cleanup не
|
||
может удалить browser cache.
|
||
|
||
**Semantic owner:** T-Bank connection lifecycle. Именно он меняет trust/data
|
||
boundary; отдельные entities не должны знать, когда credential был заменён.
|
||
|
||
**Why it matters:** Cached Snapshot намеренно остаётся видимым после failed
|
||
background refresh (`AccountsPage.test.tsx:46-133`,
|
||
`AccountPage.test.tsx:519-551`). После удаления connection это позволяет
|
||
продолжать показывать прежние accounts/portfolio вместе с refresh error; после
|
||
замены token данные прежнего connection могут пережить смену источника.
|
||
Canonical glossary определяет Brokerage Accounts как доступные только через
|
||
текущий T-Bank connection (`docs/architecture/domain-glossary.md:37-42`).
|
||
|
||
**Reference repository:** **not comparable.** Reference не имеет credential-
|
||
bound server-state lifecycle.
|
||
|
||
**Recommended direction:** На успешной замене и удалении выполнить одну явную
|
||
connection-boundary eviction всех T-Bank-derived queries, затем установить
|
||
новый status. Использовать текущий QueryClient/query-key shape; не создавать
|
||
entity registry или cache service без необходимости. Сфокусированное proof
|
||
должно seed accounts и один account-scoped cache и наблюдать их удаление.
|
||
|
||
### F-02. Test teardown разделён между setup и отдельными tests
|
||
|
||
**Category:** Test Infrastructure / Isolation
|
||
**Severity:** medium
|
||
**Confidence:** high
|
||
**Action:** change
|
||
**Effort:** small
|
||
|
||
**Observed:** RTL auto-cleanup фактически не активен. Из 20 React Testing
|
||
Library test files десять вызывают `cleanup` явно, а десять оставляют mounted
|
||
roots до file teardown. `configureAuthClient(null)` отдельно повторяется в
|
||
11 files.
|
||
|
||
**Evidence:** `frontend/vitest.config.ts:8-11` не включает globals; installed
|
||
Vitest default — `globals: false`
|
||
(`node_modules/vitest/dist/chunks/defaults.9aQKnqFk.js:45-50`). Installed RTL
|
||
регистрирует automatic cleanup только когда global `afterEach` существует
|
||
(`node_modules/@testing-library/react/dist/index.js:18-29`). Общий
|
||
`src/test/setup.ts:6-8` сейчас владеет только MSW lifecycle.
|
||
|
||
Без explicit cleanup остаются десять файлов: четыре entity query-hook tests,
|
||
`BrokerageAccountList.test.tsx`, `auth.test.tsx`, T-Bank state-view test,
|
||
Account Events page test, Account Page VM test и
|
||
`authenticated-query.test.tsx`. `AuthProvider` снимает module-global auth
|
||
client только при React cleanup (`features/auth/model/auth.tsx:116-123`), а
|
||
authenticated queries удаляют listeners при unmount
|
||
(`shared/api/client.ts:14-35`, `authenticated-query.ts:21-32`).
|
||
|
||
Scope — все 22 tests и test setup. Counter-evidence: Vitest file isolation
|
||
остаётся включён, MSW handlers уже reset глобально, а десять files выполняют
|
||
cleanup сами; следовательно risk ограничен главным образом соседними cases
|
||
одного file, но owner всё равно непоследователен.
|
||
|
||
**Semantic owner:** `src/test/setup.ts`.
|
||
|
||
**Why it matters:** DOM roots, query observers и auth-session listeners могут
|
||
дожить до следующего case. Следующий `configureAuthClient(...)` уведомляет
|
||
старые subscribers и способен разбудить прежний QueryClient уже против новых
|
||
handlers, создавая order dependence и лишнее удержание ресурсов.
|
||
|
||
**Reference repository:** **adopt outcome, not mechanism.** Reference включает
|
||
Vitest globals и потому получает RTL auto-cleanup. Target может сохранить
|
||
явные Vitest imports.
|
||
|
||
**Recommended direction:** Добавить общий `cleanup()` и auth-client reset в
|
||
существующий setup-level `afterEach` рядом с `server.resetHandlers()`, затем
|
||
удалить per-file copies. Не включать globals только ради cleanup.
|
||
|
||
### F-03. Income view реконструирует server identity и collection model
|
||
|
||
**Category:** Architecture / Correctness
|
||
**Severity:** medium
|
||
**Confidence:** high
|
||
**Action:** change
|
||
**Effort:** medium
|
||
|
||
**Observed:** `AccountIncomePage.view.tsx` не только рендерит props: он строит
|
||
currency buckets и chart series, группирует entries по instrument и повторяет
|
||
backend algorithm формирования instrument identity.
|
||
|
||
**Evidence:** Trend derivation занимает
|
||
`frontend/src/pages/account-income/ui/AccountIncomePage.view.tsx:179-204`;
|
||
instrument panel фильтрует полный `entries` для каждого instrument на строках
|
||
268–290; `entryKey` на строках 354–363 повторяет priority
|
||
`instrumentUid → figi → ticker/type → operation id` и добавляет currency.
|
||
Backend применяет тот же algorithm в
|
||
`backend/src/tbank/income-analytics.service.ts:187-225`, тогда как public DTO
|
||
отдаёт `instruments` и `entries` раздельно без явной membership
|
||
(`income-analytics.dto.ts:86-135`).
|
||
|
||
Подтверждены два collection-transformation clusters и одна cross-layer identity
|
||
formula. Scope search охватил все target grouping/mapping/filtering paths и
|
||
income tests. Counter-evidence: формулы сейчас совпадают; tests покрывают один
|
||
FIGI match, empty bucket и signed chart segments. Selected currency и чистая
|
||
chart geometry являются UI state и могут оставаться в trend component.
|
||
|
||
**Semantic owner:** Точное instrument-entry membership принадлежит backend/API
|
||
contract; response-to-view grouping — Income page VM/model; view владеет только
|
||
выбором currency и rendering geometry.
|
||
|
||
**Why it matters:** Изменение backend identity priority может молча дать пустые
|
||
или неверные drill-down entries при правильных totals. Текущее rendering также
|
||
выполняет `instruments × entries` filtering. Это прямо расходится с
|
||
`frontend/docs/UI-PATTERN.md:11-28`, где domain grouping и derived collection
|
||
values принадлежат VM.
|
||
|
||
**Reference repository:** **not comparable.** В reference нет analytics или
|
||
cross-layer identity contract.
|
||
|
||
**Recommended direction:** Contract должен явно передавать membership либо
|
||
сгруппированные entries; frontend display model строится один раз в VM. Если
|
||
contract пока не меняется, formula должна иметь одного изолированного owner и
|
||
focused test, а не находиться внутри JSX module.
|
||
|
||
### F-04. Семь TSX modules скрывают основной component
|
||
|
||
**Category:** Component / File Boundaries
|
||
**Severity:** medium
|
||
**Confidence:** high
|
||
**Action:** change
|
||
**Effort:** medium
|
||
|
||
**Observed:** Из 42 non-test TSX files одиннадцать содержат несколько component
|
||
declarations; семь имеют самостоятельные states/sections, props или hooks,
|
||
из-за которых главный component перестаёт быть явным owner.
|
||
|
||
**Evidence:**
|
||
|
||
| File | Confirmed declarations | Independent responsibilities |
|
||
| --- | ---: | --- |
|
||
| `pages/account-income/ui/AccountIncomePage.view.tsx` | 10 | 3 page states, 5 sections, 2 private detail components; lines 29–352 |
|
||
| `pages/account-events/ui/AccountEventsPage.tsx` | 6 | container, 4 states, event row; lines 32–128 |
|
||
| `app/routes/AppRoutes.tsx` | 5 | 3-layout skeleton, suspense wrapper, 2 auth guards, route table; lines 55–230 |
|
||
| `pages/accounts/ui/AccountsPage.view.tsx` | 5 | 4 states plus reusable refresh state; lines 42–168 |
|
||
| `pages/account/ui/AccountPage.view.tsx` | 4 | 4 exported top-level states; lines 54–231 |
|
||
| `app/ui/AppBreadcrumbs.tsx` | 3 | trail renderer, account mapping, route orchestrator; lines 20–144 |
|
||
| `features/tbank-connection/ui/TbankConnectionSection.tsx` | 2 | 7-line state router plus 116-line form/mutation/modal owner; lines 38–161 |
|
||
|
||
Scope охватил все 42 implementation TSX, 20 TSX tests и public exports.
|
||
Counter-evidence: `RouteErrorBoundary.tsx`, `BrokerageAccountList.tsx` и
|
||
`RecentEvents.view.tsx` содержат tightly-coupled small private components;
|
||
`PositionCard`, `AuthForm` и `AccountOperationsPageLoaded` имеют одного
|
||
principal component. Их splitting не окупает navigation cost.
|
||
|
||
**Semantic owner:** Каждый named page state/section, route fallback/guard и
|
||
T-Bank loaded interaction принадлежит flat sibling module в текущем UI slice.
|
||
Маленький private renderer остаётся рядом с parent.
|
||
|
||
**Why it matters:** В перечисленных семи files independently changeable owners
|
||
делят props, imports и navigation. Самый сильный случай — 388-line Income view:
|
||
chart, comparisons, breakdown и forecast меняются независимо, но любой change
|
||
требует работать в одном module.
|
||
|
||
**Reference repository:** **adopt intent, not structure.** Reference выделяет
|
||
states, но directory-per-component со stories/tests избыточен. Target-native
|
||
precedent лучше: `account-operations` использует короткий container и flat
|
||
`components/*Loading|Error|Loaded.view.tsx`; account page уже имеет восемь flat
|
||
section files.
|
||
|
||
**Recommended direction:** Выносить только independently named states/sections
|
||
в flat sibling files, сохраняя props с owner. `Comparison` можно оставить с
|
||
`IncomeComparisons`, `ForecastEvent` — с `IncomeForecast`. Не добавлять
|
||
line-count checker, directory/barrel на каждый component или обязательный
|
||
one-test-per-file.
|
||
|
||
**Reconciliation with general audit:** General F-07 был прав, что universal
|
||
splitting Must и механическое «один component — один file» создают sprawl, но
|
||
ошибочно признал конкретный Income module достаточно cohesive. С заданной
|
||
project preference семь случаев выше являются evidence-backed finding, а
|
||
маленькие исключения остаются colocated.
|
||
|
||
### F-05. Server-state defaults и query identity имеют разных случайных владельцев
|
||
|
||
**Category:** Query Policy / Cache Keys
|
||
**Severity:** medium
|
||
**Confidence:** high
|
||
**Action:** change / simplify
|
||
**Effort:** small
|
||
|
||
**Observed:** Шесть из семи protected query configurations повторяют
|
||
`retry: false`; T-Bank status использует raw `useQuery` без этого значения и
|
||
ручной `useAuth` gate. Одновременно один production consumer и два test sites
|
||
реконструируют endpoint query identity вне entity owner.
|
||
|
||
**Evidence:** `retry: false` повторён в accounts, events, operations, portfolio
|
||
и двух income hooks (`useBrokerageAccounts.ts:12-17`,
|
||
`useBrokerageEvents.ts:9-19`, `useBrokerageOperations.ts:35-49`,
|
||
`usePortfolio.ts:4-21`, `useIncome.ts:9-41`). T-Bank status outlier —
|
||
`TbankConnectionSection.vm.ts:1-15`. Global `refetchOnWindowFocus: false`
|
||
правильно задан в `app/ui/App.tsx:10-12`, но повторён в Income hook на строке
|
||
40. Installed TanStack fallback для browser queries — три retries, когда option
|
||
не задан (`node_modules/@tanstack/query-core/src/retryer.ts:170`).
|
||
|
||
`AccountOperationsPage.vm.ts:54-58` повторно строит operations `queryOptions`
|
||
только ради reset invalidation вместо получения key от entity API owner.
|
||
`AccountPage.test.tsx:535-536,570-571` дважды hard-codes generated Portfolio key.
|
||
Counter-evidence: Brokerage Accounts и T-Bank status уже экспортируют keys из
|
||
своих endpoint options; arbitrary string production keys не используются.
|
||
|
||
**Semantic owner:**
|
||
|
||
- `retry: false` — global QueryClient default с endpoint override;
|
||
- focus policy — уже global QueryClient;
|
||
- authenticated readiness — `useAuthenticatedQuery`;
|
||
- account-ID `enabled` и `staleTime` — конкретный endpoint/entity;
|
||
- query options/key factory — endpoint entity; invalidating feature/page лишь
|
||
использует его.
|
||
|
||
**Why it matters:** Status query сейчас имеет иной retry/error timing и peer-
|
||
feature dependency на auth. Новая protected query может забыть replicated
|
||
default. Operations reset и Portfolio background-refresh tests зависят от
|
||
внутреннего key shape, который может разойтись с hook normalization.
|
||
|
||
**Reference repository:** **adopt ownership, not values.** Reference задаёт
|
||
global QueryClient defaults и entity-owned Todo keys. Target уже лучше тем, что
|
||
получает keys из generated `apiQueryClient.queryOptions`; manual reference key
|
||
object копировать не нужно.
|
||
|
||
**Recommended direction:** Поднять доказанный no-automatic-retry default в
|
||
QueryClient, удалить шесть copies и redundant focus option, провести status
|
||
через `useAuthenticatedQuery`. Расширить operations/portfolio endpoint options
|
||
до единственного key owner. Сохранить caller override, разные `staleTime` и
|
||
resource-ID gates.
|
||
|
||
### F-06. Test provider и QueryClient harnesses систематически повторяются
|
||
|
||
**Category:** Test Infrastructure / Maintainability
|
||
**Severity:** medium
|
||
**Confidence:** high
|
||
**Action:** simplify
|
||
**Effort:** medium
|
||
|
||
**Observed:** 20 RTL test files содержат 36 `MantineProvider` trees в 19 files,
|
||
19 `QueryClientProvider` trees в 13 files, 25 `MemoryRouter` trees в 11 files и
|
||
22 `new QueryClient` constructions в 13 files. Только один client получает
|
||
deterministic test retry defaults.
|
||
|
||
**Evidence:** Пять pure data-hook tests повторяют QueryClient + Mantine wrapper,
|
||
хотя hooks не зависят от Mantine:
|
||
`useBrokerageAccounts.test.tsx:11-24`,
|
||
`useBrokerageEvents.test.tsx:11-24`,
|
||
`useBrokerageOperations.test.tsx:27-40`,
|
||
`usePortfolio.test.tsx:19-32` и
|
||
`authenticated-query.test.tsx:11-24`.
|
||
`AccountOperationsPage.test.tsx:195-338` четыре раза повторяет один integration
|
||
stack. Два wrappers создают QueryClient внутри render function
|
||
(`AccountIncomePage.test.tsx:94-100`, `AccountPage.vm.test.tsx:66-79`).
|
||
|
||
Scope — все tests, provider imports, render/renderHook calls и setup. Counter-
|
||
evidence: route initial entries, real AuthProvider и seeded QueryClient реально
|
||
варьируются; cache assertions правильно сохраняют прямой доступ к client.
|
||
Entity queries уже задают retry false, поэтому отсутствие test default пока не
|
||
создаёт массовых delays.
|
||
|
||
**Semantic owner:** Небольшие composable test primitives под `src/test/`;
|
||
route/auth-specific harness остаётся рядом с соответствующим test.
|
||
|
||
**Why it matters:** Изменение deterministic client policy или provider setup
|
||
требует правки множества copies. Unused Mantine/router providers скрывают
|
||
реальные dependencies, а client, созданный внутри wrapper render, делает cache
|
||
lifetime зависимым от rerender implementation detail.
|
||
|
||
**Reference repository:** **adopt factory shape only.** Reference wrapper
|
||
создаёт client снаружи, отключает retries и возвращает `{ queryClient, wrapper }`
|
||
(`test/configs/wrapper.tsx:7-27`), но всегда добавляет Mantine, Notifications и
|
||
Router. Для target это `renderWithEverything` и лишняя coupling.
|
||
|
||
**Recommended direction:** Добавить deterministic `createTestQueryClient` и
|
||
query-only wrapper/factory, возвращающий client. Mantine, Router, Auth и initial
|
||
entries подключать явными composable options только когда они нужны. Сохранить
|
||
feature-local seeded/route harnesses.
|
||
|
||
### F-07. Cross-slice display и API primitives имеют misleading owners
|
||
|
||
**Category:** Semantic Ownership / Reuse
|
||
**Severity:** medium
|
||
**Confidence:** high
|
||
**Action:** change
|
||
**Effort:** medium
|
||
|
||
**Observed:** Portfolio public API владеет display primitives, используемыми
|
||
несвязанными entities/pages, а один transport primitive продублирован в трёх
|
||
owners.
|
||
|
||
**Evidence:** `entities/portfolio/model/display.ts:1-63` типизирует
|
||
`formatMoney` через Portfolio transport. У него 10 production consumer files
|
||
в brokerage-account, broker-operation, account, income и events;
|
||
`formatSnapshotTime` и `instrumentLogoUrl` имеют по два consumers. Два peer
|
||
entities импортируют Portfolio helpers напрямую:
|
||
`BrokerageAccountList.tsx:5` и `LedgerEntryRow.tsx:5-9`. Canonical Money —
|
||
самостоятельный ubiquitous value (`docs/architecture/domain-glossary.md:20-28`).
|
||
|
||
Одинаковый unknown-object `code` extractor существует три раза:
|
||
`brokerage-account/model/display.ts:19-23`,
|
||
`portfolio/model/display.ts:43-47` и
|
||
`TbankConnectionSection.tsx:32-36`. Generated Problem Details имеет общий
|
||
`code` (`shared/api/generated/schema.d.ts:203-207`). Scope search охватил все
|
||
formatter/error definitions и consumers. Counter-evidence: monetary formatting
|
||
algorithm уже один, сохраняет decimal string и протестирован; page-specific
|
||
code-to-copy/action mappings различаются и должны остаться local.
|
||
|
||
**Semantic owner:** `formatMoney` и proven common display primitives — узко
|
||
названный neutral lower-level display owner; API error extraction —
|
||
`shared/api`; `formatShare` остаётся Portfolio; UI error mapping остаётся page/
|
||
feature. Instrument-logo URL — отдельный named presentation integration, а не
|
||
Portfolio transport helper.
|
||
|
||
**Why it matters:** Изменение Portfolio response shape затрагивает account,
|
||
ledger, events и income. Generic transport inspection загрязняет entity display
|
||
modules и требует synchronized edit в трёх местах.
|
||
|
||
**Reference repository:** **not comparable.** Reference не имеет Money,
|
||
Instrument Logo или structured Problem Details analogue.
|
||
|
||
**Recommended direction:** Переместить только подтверждённые cross-slice
|
||
primitives в narrowly named modules. Не создавать generic `utils`, Money
|
||
service/interface, новую domain layer или configurable formatter framework;
|
||
сохранить exact-string implementation.
|
||
|
||
### F-08. Global CSS владеет component и Mantine-internal styling
|
||
|
||
**Category:** Styling Boundaries / Complexity
|
||
**Severity:** medium
|
||
**Confidence:** high
|
||
**Action:** change / simplify
|
||
**Effort:** small–medium
|
||
|
||
**Observed:** `app/styles.css` смешивает reset/theme с styles четырёх component
|
||
owners и прямым selector внутренней Mantine class.
|
||
|
||
**Evidence:** AppShell rules `.app-header`, `.bottom-nav` и `.active` находятся
|
||
на строках 27–34 и 56–72, но consumers — `shared/ui/AppShell.tsx:25,43-47`.
|
||
Breadcrumb rules `[data-depth]` и mobile child hiding — строки 35–40,73–75,
|
||
consumer — `app/ui/AppBreadcrumbs.tsx:25-30`. Auth page/card/fieldset — строки
|
||
41–55, consumers — Login/Register pages. `.mantine-AppShell-main` на строках
|
||
76–78 зависит от library implementation name.
|
||
|
||
Из 11 объявленных `--ft-*` tokens используются пять; шесть dead: `bg-app`,
|
||
`bg-muted`, `text-secondary`, `danger`, `radius-card`, `shadow-card`.
|
||
`--ft-color-accent: #2563eb` повторяет Mantine primary blue из
|
||
`app/theme.ts:8-20`. Scope — все пять CSS files и все class consumers. Counter-
|
||
evidence: остальные четыре CSS files являются scoped Modules и преимущественно
|
||
используют Mantine variables; `*`, `body`, base anchor и page background имеют
|
||
реальный global owner.
|
||
|
||
**Semantic owner:** Shell/navigation — AppShell module или Mantine `classNames`;
|
||
breadcrumbs — AppBreadcrumbs module; auth layout — shared auth presentation
|
||
только если reuse остаётся реальным; reset/page theme — global app style.
|
||
|
||
**Why it matters:** Component changes требуют cross-layer lookup; generic data/
|
||
class selectors могут задеть будущую markup; library selector upgrade-sensitive;
|
||
dead/duplicated tokens создают ложную design-system surface.
|
||
|
||
**Reference repository:** **adopt localization only.** Reference широко
|
||
использует component CSS Modules, но hard-codes больше цветов и создаёт
|
||
directory-per-component styles. Target Mantine variables лучше и должны быть
|
||
сохранены.
|
||
|
||
**Recommended direction:** Оставить глобально только reset и genuine app theme.
|
||
Локализовать shell/breadcrumb/auth rules, style AppShell main через owned seam,
|
||
удалить dead tokens и использовать Mantine theme variable вместо duplicate
|
||
accent. Не добавлять CSS-in-JS или design-token dependency.
|
||
|
||
### F-09. `connectedAt` contract скрыт через `String(...)` coercion
|
||
|
||
**Category:** API Ownership / Correctness
|
||
**Severity:** medium
|
||
**Confidence:** high
|
||
**Action:** change
|
||
**Effort:** small
|
||
|
||
**Observed:** Committed OpenAPI snapshot описывает nullable date-time как
|
||
`type: object`; generated frontend type становится
|
||
`Record<string, never> | null`. Component скрывает mismatch через
|
||
`String(status.connectedAt)` перед date parsing.
|
||
|
||
**Evidence:** Snapshot —
|
||
`frontend/src/shared/api/spec/openapi.json:1271-1286`; generated type —
|
||
`shared/api/generated/schema.d.ts:232-236`; coercion —
|
||
`features/tbank-connection/ui/TbankConnectionSection.tsx:90-95`. Backend owner
|
||
объявляет `connectedAt: string | null` и возвращает ISO string
|
||
(`backend/src/tbank/tbank-connection.dto.ts:12-17`,
|
||
`tbank-connection.service.ts:18-25,47-50`). Все другие audited generated date/
|
||
date-time fields являются strings.
|
||
|
||
Scope search охватил snapshot/generated date fields и production date parsing.
|
||
Counter-evidence: runtime normal path сейчас действительно возвращает string,
|
||
поэтому visible behavior обычно корректен.
|
||
|
||
**Semantic owner:** Backend/OpenAPI contract; frontend formatter не должен
|
||
нормализовать invalid transport type.
|
||
|
||
**Why it matters:** Type boundary не доказывает parseable timestamp, а
|
||
`String(object)` превращает drift в `"[object Object]"`, скрывая contract
|
||
problem до invalid UI date. Это отличается от general audit F-01: generated
|
||
types здесь синхронны со snapshot, но сам snapshot неверен.
|
||
|
||
**Reference repository:** **not comparable.**
|
||
|
||
**Recommended direction:** Исправить source contract на nullable string с
|
||
`date-time`, синхронизировать snapshot/types и убрать coercion. Новая date
|
||
library не нужна.
|
||
|
||
### F-10. Два presentation files владеют validation/payment invariants
|
||
|
||
**Category:** Helper / Semantic Ownership
|
||
**Severity:** low
|
||
**Confidence:** high
|
||
**Action:** change
|
||
**Effort:** small
|
||
|
||
**Observed:** Два single-consumer helper clusters не являются внутренней JSX
|
||
механикой. `AuthForm.tsx:12-53` содержит email normalization/rules, UTF-8
|
||
password byte bounds и login/register schemas. `LedgerEntryRow.tsx:23-29,75-108`
|
||
содержит operation label mapping, signed payment presentation и
|
||
`isZeroDecimal`.
|
||
|
||
Scope search охватил schemas, form defaults, decimal/Money operations, mapping
|
||
и grouping functions. Counter-evidence: behavior сейчас согласован и частично
|
||
покрыт tests; количество consumers само по себе extraction не требует.
|
||
|
||
**Semantic owner:** Auth validation — соседний auth model module; cohesive
|
||
payment mapper вместе с `isZeroDecimal` — broker-operation model/display module.
|
||
`isZeroDecimal` не оправдывает generic decimal library.
|
||
|
||
**Why it matters:** Эти правила меняются вместе с auth/ledger semantics, а не с
|
||
Mantine markup, и труднее тестируются отдельно внутри component files.
|
||
|
||
**Reference repository:** Reference хранит reusable Todo schema у entity, что
|
||
подтверждает owner principle, но его folder hierarchy копировать не нужно.
|
||
|
||
**Recommended direction:** Вынести два cohesive semantic clusters к соседним
|
||
feature/entity owners. Оставить `toDateValue`, `fromDateValue` и
|
||
`fromMultiSelectValue` рядом с Mantine control adapter; `dateStart`, `dateEnd`
|
||
и `sameCategories` — рядом с operations VM; form defaults — у конкретных forms.
|
||
|
||
### F-11. Redundant position modes и test-facing proxy exports не несут invariant
|
||
|
||
**Category:** Complexity / Simplification
|
||
**Severity:** low
|
||
**Confidence:** high
|
||
**Action:** simplify
|
||
**Effort:** small
|
||
|
||
**Observed:** Две небольшие surfaces создают варианты/exports без отдельной
|
||
семантики.
|
||
|
||
Position `kind` и `wide` кодируют одну fixed mapping: stocks/bonds всегда wide,
|
||
other всегда compact (`AccountPage.vm.ts:33-36,179-214`).
|
||
`PositionSection.view.tsx:14-26` дополнительно выводит `variant`, а
|
||
`PositionCard.view.tsx:18-36` выбирает stock/bond class map. Шесть пар CSS roles
|
||
для stock и bond фактически идентичны
|
||
(`AccountPage.module.css:35-88`); реальное различие одно — identifier choice на
|
||
`PositionCard.view.tsx:48-49`.
|
||
|
||
Четыре test-oriented proxy export patterns: page alias `OperationRow` для
|
||
`LedgerEntryRow` (`AccountOperationsPage.tsx:2,22`), proxy Mantine `Button`
|
||
через `StateView.tsx:1,26`, четыре Account state re-exports
|
||
(`AccountPage.tsx:41-47`) и Accounts state public exports, используемые только
|
||
`AppRoutes.test.tsx` (`AccountsPage.tsx:32-37`, `pages/accounts/index.ts:1-6`).
|
||
|
||
Scope — all component props/classes, all barrels и consumers. Counter-evidence:
|
||
FSD slice barrels имеют реальную public-boundary роль и удаляться не должны;
|
||
stock/bond identifier semantics остаются различными.
|
||
|
||
**Semantic owner:** Один position presentation mode, выводимый из section kind;
|
||
tests импортируют actual owner либо наблюдают page behavior; public barrels
|
||
экспортируют только product surface.
|
||
|
||
**Why it matters:** Invalid prop combinations остаются representable, CSS edits
|
||
дублируются, а public APIs обещают reuse, которого нет.
|
||
|
||
**Reference repository:** **target should simplify.** Reference не proxy-export
|
||
Mantine/entity components под page-only именами.
|
||
|
||
**Recommended direction:** Удалить redundant `wide`/duplicate class mapping,
|
||
сохранив identifier distinction. Убрать proxy exports и вернуть tests к
|
||
реальным owners. Не удалять meaningful FSD barrels и не добавлять `clsx`:
|
||
единственная простая two-class interpolation этого не оправдывает.
|
||
|
||
### Evidence Ledger Reconciliation
|
||
|
||
Все обнаруженные high/medium candidates получили явный disposition:
|
||
|
||
- retained: F-01 — F-09;
|
||
- general audit F-03 (invalid income calendar date) подтверждён, но не
|
||
продублирован; он остаётся единственным полным owner этого correctness finding;
|
||
- general F-05 sharpened в F-07, general F-08 включён в F-05;
|
||
- general F-07 частично superseded в F-04 с сохранением colocated exceptions;
|
||
- объединены: oversized files/state counts/private-component size → F-04;
|
||
global selectors/dead tokens/theme duplication → F-08; Money/date/logo/error
|
||
primitives → F-07;
|
||
- discarded: объединить пять `staleTime: 30_000` — одинаковое число не
|
||
доказывает общий invariant; accounts имеет 60 seconds, а freshness ownership
|
||
endpoint-specific;
|
||
- discarded: перенести пять `enabled: Boolean(accountId)` в auth seam — это
|
||
endpoint parameter validation, не authentication;
|
||
- discarded: общий form factory/FormProvider для трёх forms — state и
|
||
validation semantics различаются;
|
||
- discarded: Day.js strict parsing, Decimal.js, Lodash/groupBy, error library,
|
||
`clsx`/`classnames`, design-token library — platform/current dependencies и
|
||
small local code достаточны;
|
||
- discarded: generic cache-key/entity registry для F-01 — один connection
|
||
boundary не оправдывает registry;
|
||
- discarded: universal one-component-per-file enforcement и reference
|
||
directory-per-component layout — navigation cost превышает пользу.
|
||
|
||
## Policy Consolidation
|
||
|
||
| Policy | Verdict | Owner / Finding |
|
||
| --- | --- | --- |
|
||
| T-Bank-derived cache eviction | consolidate as one hard boundary | connection lifecycle / F-01 |
|
||
| `retry: false` | consolidate; explicit override remains possible | global QueryClient / F-05 |
|
||
| `refetchOnWindowFocus: false` | keep global; remove Income duplicate | global QueryClient / F-05 |
|
||
| authenticated readiness | keep consolidated | `useAuthenticatedQuery` / F-05 |
|
||
| `enabled: Boolean(accountId)` | keep local | endpoint hook / F-05 |
|
||
| `staleTime` 30s/60s | keep local and documented by endpoint | entity query / F-05 |
|
||
| query options and keys | one endpoint owner; consumers reuse | entity API / F-05 |
|
||
| mutation-specific status seeding/modal reset | keep colocated | T-Bank feature / F-01 |
|
||
| form defaults/resolvers | keep form/page-specific | Auth, T-Bank, Income / F-10 |
|
||
|
||
## Helper and Semantic Ownership
|
||
|
||
| Candidate | Verdict | Finding |
|
||
| --- | --- | --- |
|
||
| `formatMoney` | move to narrowly named neutral money-display owner | F-07 |
|
||
| exact decimal-string formatting | keep implementation; do not replace with `Number`/library | F-07 |
|
||
| `formatShare` | keep Portfolio-owned | F-07 |
|
||
| `formatSnapshotTime` / truly identical calendar display | named neutral date-display owner only when semantics match | F-07 |
|
||
| `instrumentLogoUrl` | move from Portfolio to named presentation integration owner | F-07 |
|
||
| generic API `code` extraction | merge into one `shared/api` guard | F-07 |
|
||
| code-to-message/action mapping | keep page/feature-local | F-07 |
|
||
| `paymentPresentation` + `isZeroDecimal` | extract to neighboring broker-operation model | F-10 |
|
||
| auth schemas/rules | extract to auth model, not shared validation | F-10 |
|
||
| income `entryKey` | API owns membership; VM owns display model | F-03 |
|
||
| `toAccountPagePositionSections` | keep in Account page VM | F-03 counter-pattern |
|
||
| `toForecast` | keep in Income VM | F-03 counter-pattern |
|
||
| Mantine date/select adapters | keep colocated with loaded operations view | F-10 |
|
||
|
||
## Component and File Boundaries
|
||
|
||
F-04 identifies the seven evidence-backed splits and explicit exceptions. The
|
||
recommended priority is Income states/sections first, then Account/Accounts/
|
||
Events states, route fallback/guards, breadcrumbs and T-Bank loaded
|
||
interaction. Use the target's flat `components/*.view.tsx` precedent; do not
|
||
copy the reference's directory-per-component hierarchy.
|
||
|
||
## Test Infrastructure
|
||
|
||
F-02 owns global teardown and session reset. F-06 owns deterministic QueryClient
|
||
and composable provider setup. MSW lifecycle should remain where it is:
|
||
`src/test/setup.ts` already uses strict `onUnhandledRequest: "error"`, resets
|
||
handlers per test and closes the shared server once. Route/auth-specific
|
||
harnesses and direct access to seeded QueryClient remain local when the test
|
||
observes those semantics.
|
||
|
||
## Styling and Library Leverage
|
||
|
||
F-08 covers localization of component styles and removal of false token surface;
|
||
F-11 covers duplicate position selectors and rejects `clsx` for one simple
|
||
interpolation. CSS Modules, Mantine props/theme variables, platform `Intl`,
|
||
`TextEncoder`, `Map`, arrays, Zod/RHF and TanStack Query already solve the
|
||
audited non-domain concerns. F-09 is a contract correction, not a reason to add
|
||
a date library.
|
||
|
||
## Simplification Opportunities
|
||
|
||
- F-11 — collapse redundant position mode/CSS and remove test-only proxy
|
||
exports while preserving meaningful slice barrels.
|
||
- F-08 — delete six unused `--ft-*` tokens instead of introducing a design-
|
||
token layer.
|
||
- F-06 — remove unused providers from pure hook tests rather than standardizing
|
||
on a maximal wrapper.
|
||
- F-04 — split semantic owners without generating one directory, barrel, test
|
||
and stylesheet per component.
|
||
- General audit F-07's warning against mechanical splitting remains applicable
|
||
to files outside the seven evidence-backed cases.
|
||
|
||
## What Should Stay Colocated
|
||
|
||
- `RouteErrorBoundary` class, fallback and hook-aware wrapper: один React error-
|
||
boundary invariant.
|
||
- `BrokerageAccountSummary` with its list and `RecentEvent` with its section:
|
||
по одному небольшому tightly-coupled private renderer.
|
||
- Tiny T-Bank loading/error sibling views: splitting is lower value than
|
||
extracting the large loaded interaction.
|
||
- `PositionCard`, `AuthForm` и `AccountOperationsPageLoaded`: по одному principal
|
||
component; move only semantic helpers named in F-10.
|
||
- `staleTime`, resource-ID gates и query parameter normalization: endpoint
|
||
semantics, not global convenience defaults.
|
||
- Page-specific error copy/actions and T-Bank mutation-specific modal/form
|
||
behavior.
|
||
- Income trend selected-currency state and pure `positive`/`negative`/
|
||
`chartHeight` rendering geometry after the collection model moves out.
|
||
- Meaningful one-line FSD public barrels and `ProtectedLayout` route-composition
|
||
seam.
|
||
- Separate `LoginForm`/`RegisterForm` use-case adapters; merging them into a
|
||
boolean-heavy generic wrapper would reduce semantic clarity.
|
||
|
||
## What Is Already Good
|
||
|
||
- `useAuthenticatedQuery` правильно объединяет session readiness с caller's
|
||
boolean/function `enabled`; F-05 требует reuse, а не новой auth abstraction.
|
||
- API Query options создают реальные TanStack keys; arbitrary production string
|
||
keys отсутствуют, а Operations normalization сортирует categories.
|
||
- Auth transport централизует Bearer, single-flight refresh, one unauthorized
|
||
retry и session cache clearing.
|
||
- Money formatting уже централизован, exact-string based и протестирован; нужно
|
||
исправить owner, а не algorithm.
|
||
- Page container → VM → props-only view pattern хорошо реализован в Account и
|
||
Account Operations; последний даёт пропорциональный file-splitting precedent.
|
||
- Account page уже выделяет восемь semantic sections в flat components без
|
||
excessive hierarchy.
|
||
- Четыре component/page style areas используют CSS Modules и Mantine variables;
|
||
глобальная leakage ограничена одним file.
|
||
- MSW server/setup, browser shims, i18n и jest-dom имеют ясных test owners;
|
||
target использует более строгий unhandled-request policy, чем reference.
|
||
- Tests, которым нужна проверка invalidation или cached snapshot, сохраняют
|
||
явный QueryClient и наблюдают behavior, а не implementation-only mocks.
|
||
- Current platform/library usage уже library-first; ни одна новая dependency не
|
||
прошла net-complexity gate.
|
||
|
||
## Recommended Order
|
||
|
||
### Do now
|
||
|
||
1. F-01 — восстановить connection-boundary cache eviction.
|
||
2. F-02 — централизовать RTL/auth teardown и убрать order-dependent listeners.
|
||
3. F-09 — исправить `connectedAt` source contract и удалить coercion.
|
||
4. F-03 — вернуть income membership/grouping правильным API/VM owners.
|
||
5. F-05 — централизовать proven query defaults и endpoint key ownership.
|
||
|
||
### Do next
|
||
|
||
1. F-04 — разделить семь крупных semantic owner modules по target-native flat
|
||
pattern.
|
||
2. F-06 — ввести narrow composable test primitives.
|
||
3. F-07 — переместить proven cross-slice primitives к neutral/shared owners.
|
||
4. F-08 — локализовать component styling и сократить token surface.
|
||
|
||
### Consider later
|
||
|
||
1. F-10 — вынести auth/payment semantic clusters при ближайшем изменении
|
||
соответствующей feature/entity.
|
||
2. F-11 — схлопнуть position modes/CSS и test-facing proxy exports.
|
||
|
||
### Do not add
|
||
|
||
- `renderWithEverything`, global Notifications/Dates/Auth providers или
|
||
reference wrapper без явной потребности test.
|
||
- Universal one-component-per-file rule, line-count gate, directory/barrel per
|
||
component или copy reference's Storybook-oriented hierarchy.
|
||
- `clsx`/`classnames`, date/decimal/grouping/error library только ради найденных
|
||
случаев.
|
||
- Generic `utils`, Money service/interface, cache registry, second FSD checker,
|
||
CSS-in-JS или design-token framework.
|
||
- Storybook, browser MSW, Playwright, CI/CD или локальный OpenSpec store: этот
|
||
focused audit не обнаружил structural problem, которая окупила бы их
|
||
lifecycle cost.
|