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

47 KiB
Raw Blame History

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.

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.