47 KiB
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
RouteErrorBoundaryclass, fallback and hook-aware wrapper: один React error- boundary invariant.BrokerageAccountSummarywith its list andRecentEventwith 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/chartHeightrendering geometry after the collection model moves out. - Meaningful one-line FSD public barrels and
ProtectedLayoutroute-composition seam. - Separate
LoginForm/RegisterFormuse-case adapters; merging them into a boolean-heavy generic wrapper would reduce semantic clarity.
What Is Already Good
useAuthenticatedQueryправильно объединяет session readiness с caller's boolean/functionenabled; 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
- F-01 — восстановить connection-boundary cache eviction.
- F-02 — централизовать RTL/auth teardown и убрать order-dependent listeners.
- F-09 — исправить
connectedAtsource contract и удалить coercion. - F-03 — вернуть income membership/grouping правильным API/VM owners.
- F-05 — централизовать proven query defaults и endpoint key ownership.
Do next
- F-04 — разделить семь крупных semantic owner modules по target-native flat pattern.
- F-06 — ввести narrow composable test primitives.
- F-07 — переместить proven cross-slice primitives к neutral/shared owners.
- F-08 — локализовать component styling и сократить token surface.
Consider later
- F-10 — вынести auth/payment semantic clusters при ближайшем изменении соответствующей feature/entity.
- 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.