Files
docs/audits/2026-08-frontend-repository-audits/frontend-structural-maintainability-audit.md
T

783 lines
47 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.
# 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.