# Аудит 05 — Frontend Architecture ## 1. Audit metadata | Поле | Значение | | --- | --- | | Audit ID / name | 05 / Frontend Architecture | | Дата | 2026-10-11 | | Исполнитель | Один независимый агент; дополнительные сабагенты не запускались | | Workspace | `/Users/ksv741/Project/vibe/WealthLedger` | | Режим | Исходники/config четырёх repo и root read-only; единственная запись — этот новый отчёт | | Scope | React/TypeScript UI architecture, ownership, VM/view, Query/state, формы, presentation/recovery, routing/accessibility | | Источник задания | `audit-prompts/05-frontend-architecture.md`, прочитан целиком | | Предыдущий аудит | `audit-results/technical-audit-2026-10-11.md` прочитан как реестр гипотез; каждый использованный механизм проверен заново | ### Репозитории и ревизии В каждом checkout самостоятельно выполнены `git rev-parse HEAD` и `git status --short --untracked-files=all` до исследования. | Repo | Абсолютный путь | Точный HEAD | Начальный status | | --- | --- | --- | --- | | root | `/Users/ksv741/Project/vibe/WealthLedger` | `dc030f84c979cedb2628ef4bd97c62d6c63a9256` | Пустой | | frontend | `/Users/ksv741/Project/vibe/WealthLedger/frontend` | `c8821200ce699dab1ce65360d1968d5877a2e78b` | Пустой | | backend | `/Users/ksv741/Project/vibe/WealthLedger/backend` | `0ad84ff4f0b4712abc7e69dc299856f14af6eadd` | Пустой | | specs | `/Users/ksv741/Project/vibe/WealthLedger/specs` | `fcdc805f6c1b3be6a20a7e858e3e80cbd83a9121` | Пустой | | docs | `/Users/ksv741/Project/vibe/WealthLedger/docs` | `e6fd4f64b81dd73b33959b845a1babcfac1891c8` | Пустой | Перед чтением каждого nested repo прочитан его `AGENTS.md`. Для frontend прочитаны engineering, architecture, typescript-react, ui, data, testing; для backend — engineering и HTTP API, поскольку использовался только явно предоставленный публичный artifact. Specs выбраны через `AI_INDEX.md` и точечный поиск основных behavioral specs; docs — через README и тематические страницы. Backend application internals здесь не исследовались. **Skills:** первым загружен и применён установленный `ponytail-audit`. Его complexity-pass отделён от проверки correctness: абстракция сама по себе не является дефектом. Также применены `feature-sliced-design` с reference layer-structure, `vercel-react-best-practices`, `web-design-guidelines`, `graphify` и `verification-before-completion`. В root и frontend не найден существующий `graphify-out/graph.json`. Graphify pipeline, генерация графа и вспомогательные artifacts не выполнялись; владельцы и связи прослежены по исходникам. ### Фактически выполненные проверки | Команда / действие | Результат и граница доказательства | | --- | --- | | `git rev-parse HEAD && git status --short --untracked-files=all`, cwd каждого из пяти repo | SHA выше; все начальные деревья чистые | | `git diff --no-index --stat -- "/Users/ksv741/Project/vibe/WealthLedger/backend/openapi.json" "/Users/ksv741/Project/vibe/WealthLedger/frontend/src/shared/api/spec/openapi.json"` | Exit 0, вывода нет: два имеющихся artifact совпадают; свежесть относительно runtime не проверена | | `nvm use --silent && ./node_modules/.bin/depcruise src packages --config dependency-cruiser.cjs --output-type err-long`, cwd frontend | Первая попытка остановилась до Node: `nvm: command not found` | | `source "/Users/ksv741/.nvm/nvm.sh" && nvm use --silent && ./node_modules/.bin/depcruise src packages --config dependency-cruiser.cjs --output-type err-long`, cwd frontend | Exit 0: `no dependency violations found (420 modules, 1773 dependencies cruised)`; прямой статический analyzer, без npm wrapper, запускающего build | | Read / Glob / Grep | Source, callers, выбранные tests, schemas/config и behavioral requirements; абсолютные repo paths | | Context7 `/tanstack/query` | Проверены manual `refetch` при `enabled: false`, stale/focus semantics и встроенное polling | | Context7 `/websites/mantine_dev` | Проверена граница Fieldset disabled и Button, отображаемого ссылкой | | WebFetch официальных Web Interface Guidelines | Использованы как checklist, без автоматического превращения каждого guideline в finding | Официальные источники библиотечных фактов: [TanStack disabled queries](https://tanstack.com/query/latest/docs/framework/react/guides/disabling-queries), [TanStack window focus](https://tanstack.com/query/latest/docs/framework/react/guides/window-focus-refetching), [Mantine Fieldset](https://mantine.dev/core/fieldset/), [Mantine Button: disabled links](https://mantine.dev/core/button/). Context7 вернул документацию текущей ветки, а не гарантированный snapshot именно установленных minor versions; runtime-проверка этих versions не выполнялась. Guideline checklist получен из [официального источника](https://raw.githubusercontent.com/vercel-labs/web-interface-guidelines/main/command.md). Application tests не запускались: для выводов достаточно детерминированных source branches и прочитанных existing scenarios. Ссылки на тесты ниже означают просмотр сценария, **не свежий зелёный test run**. Install, build, codegen, migrations, full-suite/verify, запуск приложения, финансовые API, benchmark и profiler не выполнялись. Отчёты аудитов 06/07 не читались. `.gitignore`, index, commits и PR не изменялись. Финальный контроль: отчёт перечитан; `git diff --no-index --check /dev/null "audit-results/05-frontend-architecture-report.md"` и `git diff --check` выполнены; HEAD/status всех пяти repo повторно сверены. Отчёт находится в ignored `audit-results`, поэтому чистый root status сам по себе не доказывает содержимое отчёта: новый файл проверен отдельно. Ссылки на code evidence далее имеют формат `repo/relative-path:lines` и относятся строго к указанным SHA. `High` — высокая уверенность в механизме по коду, не заявление о runtime-воспроизведении. Runtime impact и частота сценариев не измерены. ## 2. Executive verdict 1. **Факт: FSD-граф работоспособен, масштабный перенос слоёв не нужен.** Проверенный analyzer не выявил циклов, запрещённых направлений и обходов public API. Реальные неудобства сосредоточены в потерях данных на границе VM/view, а не в количестве slices. Полезные owners защищены FE-008–FE-011. 2. **Факт: основной портфель теряет source metadata, analytics не включает её в loaded API.** Время и статус результата оказываются недоступны пользователю при сохранённых денежных значениях — FE-001. 3. **Факт: сохранение snapshot реализовано лучше, чем сообщение о его неуспешном перечитывании.** Investment VMs игнорируют feedback общего screen hook; section view скрывает ошибку при наличии data. Пользователь не получает предупреждение/retry — FE-002. Банковский список при cold error может вообще вернуть пустой вывод — FE-004. 4. **Подтверждённый риск исполнения: ручной timer обходит query readiness.** После его срока возможен поиск короче минимума либо запрос дочернего ресурса с пустым UID. Focus policy также противоречит сохранённому portfolio requirement. Это FE-003; частота и стоимость запросов не измерены. 5. **Факт: Fieldset не блокирует auth-ссылку.** Во время submit остаётся переход на другую auth-форму, что нарушает local pending contract и позволяет потерять контекст отправки — FE-005. Утечки identity этим наблюдением не доказаны: session guards сохраняются. 6. **Непроверенные решения отделены от дефектов.** `CURRENCY` намеренно скрывается в UI, хотя основной spec требует доступности иных позиций: FE-006 — INVESTIGATE, пока не разрешён конфликт продуктового решения. Длинные банковские списки/графики — potential bottleneck, FE-012, **UNVERIFIED**, а не установленная performance regression. **Counts:** actionable `FIX`/`SIMPLIFY` — **6**: P0 **0**, P1 **0**, P2 **5**, P3 **1**. Отдельно **KEEP 4**, **INVESTIGATE 2**. FE-006 и FE-012 не включены в подтверждённые дефекты. FE-007 — необязательное упрощение P3, не пользовательский сбой. P1 не присвоен: серьёзный эксплуатационный или дорогой системный сценарий в этом audit scope не подтверждён. ## 3. Inventory & coverage ### 3.1. Фактическое устройство и метод подсчёта - SPA: React `19.3.0`, TypeScript `5.9.3`, Vite `8.3.0`, React Router `8.3.1`. TanStack Query `5.102.8`, Mantine `9.6.1`, RHF `7.88.0`, Zod `4.6.4`, openapi-fetch `0.17.0`, openapi-react-query `0.5.4` — direct versions из `frontend/package.json:39–62`, не результат проверки runtime deployment. - React Compiler включён в `frontend/vite.config.ts:9–11`; Vitest имеет отдельный React/JSDOM profile (`frontend/vitest.config.ts:11–34`). Это не доказательство оптимальности каждого render. - По непосредственным directory entries `frontend/src`: **15 page slices, 8 entity slices, 4 feature slices, 1 widget slice**; по `frontend/packages` — **3 private packages** (`icons`, `tokens`, `ui`). Files/LOC этим методом не считались. Assets/test не интерпретировались как FSD slices. - **420 modules / 1773 dependencies** — точный вывод единственного успешного dependency-cruiser run по `src packages` и действующему config. Это размер его графа, не число production components и не семантическая оценка ownership. - **8 production callers `useScreenSync`** установлены поиском символа по `frontend/src` и чтением VM: accounts, account, operations, events, income, bonds, bond, stock. Tests/index исключены из этого счёта. - Публичные query keys образуются generated `queryOptions`; banking types выводятся из `paths` (`frontend/src/entities/bank-card/api/types.ts:1–34`). API snapshot совпал с предоставленным backend artifact, но генерация `schema.d.ts` заново не выполнялась. ### 3.2. Шесть end-to-end use cases Эти трассы включают UI route, фактических owners и shared transport. Видимые состояния установлены по source и выбранным RTL/MSW сценариям; browser render/keyboard interaction в этой сессии не воспроизводились. | Use case | Route → page → widget/feature → entity → shared API | Success / loading / error / stale / empty | | --- | --- | --- | | U1. Accounts и account/portfolio, включая `all` | `AppRoutes.tsx:194–210` → AccountsPage / AccountPage VM → PortfolioStructure, useScreenSync → brokerage-account, portfolio, broker-operation, brokerage-event, income → useResourceQuery → useAuthenticatedQuery → generated client | Structural skeleton; complete empty без вымышленных нулей; loaded группирует позиции и preview sections; access failure локален. Query snapshot сохраняется. Main source props теряются, transport feedback отсутствует: FE-001/002. Позиции `CURRENCY` скрываются: FE-006 | | U2. Instrument search | `AppRoutes.tsx:158–165` → BondsPage → instrument-search (`q`, debounce, results/card), useScreenSync → bond/useBondSearch → useResourceQuery → generated GET `/bonds` | URL хранит q, trim + debounce 300 ms, автоматический query только с двух символов. Пустой успешный массив различим от отсутствующего result; labels и ссылки семантические. Cached error тихий; initial error ведёт даже при temporary failure в settings, без retry. Timer нарушает минимальную длину: FE-002/003 | | U3. Bonds/stocks: detail и скринер | `AppRoutes.tsx:167–192` → BondPage / StockPage VM → instrument-details sections/chart/history + useScreenSync → bond/stock, broker-operation history → resource query/client. `/stocks` → StocksPage VM → generated POST search / GET search page напрямую через shared apiClient | UID/ticker canonical navigation сохраняет search; child queries ждут UID; sections независимы. Position без accounts скрыта; quote time и stale position видимы; complete empty не равен missing. Sections теряют cached-error warning/retry. Screener хранит единственный локальный snapshot результата и requestId, показывает asOf/error/empty и блокирует native controls; это не второй Query cache | | U4. Banking: journal + create/edit/refund | `AppRoutes.tsx:140–156` → CardsPage / CardPage VM → page-owned OperationForm/DeleteOperation и entity BankCardForm/CategoryManager → bank-card queries/mutations/money/date → authenticated generated client | Card/history/summary независимы; URL owns period/page/limit; placeholder предыдущей страницы только в той же card/range/limit, edit на ней запрещён. Form validation, pending, conflict draft и intent replay сохранены. Cold list error может дать blank area; retry отсутствует у части read states, cached failures тихие: FE-004 | | U5. Login/session/connection | `AppRoutes.tsx:109–138`, RouteGuards → LoginPage/RegisterPage → auth form/provider VM; settings → tbank-connection → shared client/session-aware query | Neutral bootstrap skeleton; один сериализованный refresh, один auth replay; logout очищает cache даже до ответа сети. Ошибки формы сохраняют ввод; email normalized, password untouched. Fieldset блокирует поля, но не Link: FE-005. Успешная connection mutation сбрасывает investment subtree; failed mutation сохраняет его | | U6. Income analytics как сложный URL/form/data flow | `AppRoutes.tsx:221–228` → AccountIncomePage VM → page-local IncomePeriodForm/summary/comparisons/payments + useScreenSync → income query → resource/authenticated/generated API | Applied period в URL, draft в RHF; validation по календарю/Москве; resetRevision отдельно сбрасывает calendar UI. Новый uncached period не переименовывает старые строки, показывает skeleton; partial hides totals; previous/year dependency notices независимы. Source metadata и transport warning/retry в loaded API отсутствуют: FE-001/002 | В U3 намеренная dependency `identifier → UID → child queries` нужна для точного инструмента; её нельзя назвать лишней waterfall без изменения contract. Детальные request counts и server fan-out относятся к 07. ### 3.3. UI Ownership Matrix | UI / responsibility | Фактический owner | Проверенные consumers / public API | Оценка | | --- | --- | --- | --- | | Providers, lazy routes, auth gates, route error reset | `app/ui/App.tsx`, `app/routes` | App → page slice index; ProtectedLayout → app/ui public API | KEEP: глобальная навигация не спрятана в shared | | Portfolio state selection и позиции/previews | `pages/account` | AccountPage → VM → entity APIs → loaded/empty views; `AccountPage.vm.ts:66–136` | KEEP owner; FIX потери source и recovery, не перенос fetching во view | | Структура портфеля/валюта/unknown/negative | `widgets/portfolio-structure` | Loaded и Empty account views; `PortfolioStructure.tsx:9–12` | Понятная существующая граница; перенос из-за общего FSD discouragement widgets не оправдан | | Query keys, DTO projection, access groups | `entities/*/api` + `shared/api` | portfolio key используется тестами/consumers; generated option identity | KEEP; HTTP infrastructure не дублируется per page | | Account breadcrumb scope | `app/ui/AppBreadcrumbs/AccountScopeSwitch` | `useBrokerageAccounts`, route section/search preservation; VM `:8–36` | KEEP; повторный observer использует тот же key, не собственный cache | | Instrument search input/results | `features/instrument-search` | Текущий production consumer BondsPage; `useInstrumentSearch.ts:8–22` | Небольшой coherent owner; single consumer сам по себе не основание переносить slice | | Общие chart/position/history sections | `features/instrument-details` | BondPage и StockPage; narrow structural VM types `model/types.ts:7–39` | KEEP: реальный reuse двух screens; section recovery расширяется здесь | | Stocks criteria/search snapshot | `pages/stocks` | StocksPage VM → view; public shared apiClient | KEEP local ownership; запрос не скрыт в presentation hook; URL persistence — открытый product question | | Banking queries/types/feedback/money/card form | `entities/bank-card` | CardsPage, CardPage, OperationForm, CategoryManager, BankCardForm; index `:1–30` | KEEP для согласованных money, card и conflict responsibilities | | Transfer/refund/edit orchestration | `pages/card/ui/OperationForm` | CardPage `:21–27`; RHF context fields и entity APIs | KEEP page-local feature; не дробить по числу props | | Analytics period, draft, grouping/comparisons | `pages/account-income` | VM → own form/views → income API | KEEP: бизнес-проекция отличается от wire DTO | | Periodic investment reads | `features/tbank-connection/model/screen-sync.ts` | 8 production page VMs; scope descriptors сейчас игнорируются | FE-003: readiness/policy у query-owner, а interval обходной | | Source text + busy/entry primitives | `shared/ui/SourceSyncStatus`, `packages/ui` | Accounts/Account/Operations/Events; bank/history, skeletons | KEEP semantic wrappers; ответственность шире нового имени для Mantine | | Pagination labels stocks | `StocksPagePresentation.vm.ts` | Единственный StocksPage.view | FE-007: необязательная дополнительная navigation hop | FSD checks: `frontend/dependency-cruiser.cjs:30–75` задаёт public API, same-layer и upward restrictions. Успешный graph gate подтверждает эти настроенные проверки; не доказывает правильность любого бизнес-owner. Не обнаружено actual propagation-of-change, требующего общего god slice, нового state manager, обязательного `@x` или удаления entities слоя. ### 3.4. State Ownership Matrix | State | Source of truth | Secondary copy | Consequence | | --- | --- | --- | --- | | Investment data/meta/query error/fetch status | TanStack Query envelope | `useResourceQuery` возвращает projection, VM вычисляет presentation | Второго data store нет; FE-001/002 возникают при выбрасывании полей projection | | 30-minute refresh schedule | Сейчас собственный setInterval | Query отдельно owns stale/focus/reconnect + HTTP retry | Два policy owners; readiness не соблюдается ручным refetch — FE-003 | | Accounts для breadcrumb и page | Один generated Query key | Второй observer, не копия data | KEEP; не превращать в custom global context | | Instrument `q`, period, event filter | Router search params | Debounced trim q; computed categories | Обоснованная временная debounce projection; parameters переживают canonical route | | Ledger applied filters | URL | Draft range/categories в React state | Обоснованное Apply поведение; scope change сбрасывает draft и cursors | | Brokerage cursors | `useBrokerageOperationHistory` scope-bound local pagination | Нет копии rows | Оpaque cursor остаётся API-owned; invalid cursor сбрасывается к первой странице | | Income applied period | URL normalized period | RHF draft DD.MM.YYYY, resetRevision/calendar state | Draft и committed state различаются по смыслу; form не должен refetch при каждой правке | | Screener result/page/asOf | Один page-local `page` snapshot | Нет второго Query result | Императивный search owner допустим; requestId препятствует stale completion; перенос в Query сам по себе не нужен | | Screener criteria | Local React draft | Результат от предыдущего search сохраняется отдельно | Не меняет старый список до Search; reload/back persistence не обещана отдельной spec | | Bank card/history/summary | TanStack Query | History placeholder предыдущей страницы в том же scope | Не новый cache; readonly editing guard защищает placeholder identity | | Bank operation/card draft | RHF | SubmittedValues, intent, adopted version, latest record | Это replay/conflict workflow, не ненужная копия server state; FE-010 KEEP | | Modal open, purchase page, trigger focus | Ближайший component/presentation hook | Refs для trigger/scroll, не data cache | Связный UI lifecycle; `CardPagePresentation.vm.ts:18–53` выполняет layout read в effect, не в render | | Auth identity/token | AuthProvider VM; token только ref в памяти | Shared transport session snapshot содержит readiness/handlers; userRef нужен current closure | KEEP single-flight/generation/cache clearing; не переносить token в storage | | Connection status | Query + successful mutation response | Нет локальной копии финансовых datasets | Boundary commit/reset только при успехе; error сохраняет cache | | PortfolioStructure currency | Local выбранная валюта | Selected derived из доступного structure | Fallback к первой валюте вычисляемый; неполная/negative оценка остаётся явной | ### 3.5. Query Policy Inventory Общее наследование: `frontend/src/app/ui/App.tsx:13–20` задаёт `refetchOnWindowFocus: true`, `refetchOnReconnect: true`, `retry: false`. Если строка ниже не переопределяет policy, действует это наследование. `staleTime` — время stale eligibility, **не rate limiter**. Не-auth GET дополнительно имеет transport recovery: `frontend/src/shared/api/client.ts:127–151,172–194,205–249`: deadline 15 000 ms, задержки 1000/2000 ms, permanent code exclusions и AbortSignal. Эти числа прочитаны из constants/branches; это не измеренный request budget. Auth endpoints и non-GET исключены из этого GET retry. | Use case / query-owner | enabled / key inputs | staleTime | Interval / refresh | Invalidation / recovery | | --- | --- | --- | --- | --- | | Accounts / `useBrokerageAccounts.ts:3–18` | Has session; generated GET accounts key | 1 800 000 ms | Screen timer; ещё observers в breadcrumb/account | Connection subtree reset, auth clear, accessGroup brokerage; cached transport feedback теряется | | Portfolio / `usePortfolio.ts:11–25` | Session AND accountId; path accountId | 30 000 ms | Screen timer; ручной query.refetch доступен инфраструктуре, page result hook игнорируется | Access-group cancellation/clear; сохраняется data при temporary failure | | Account preview operations / `useBrokerageOperations.ts:26–65` | Session AND accountId; normalized categories, cursor, limit/from/to/instrument | 30 000 ms | Account/ledger/detail screen timers | Connection/access reset; history scope/cursor recovery у entity | | Events / `useBrokerageEvents.ts:10–23` | Session AND accountId, from/to | 30 000 ms | Account/events screen timers | Access-group reset; partial/empty видимы, cached transport error не передан | | Summary/analytics / `useIncome.ts:14–45` | Session AND accountId; analytics from/to | 30 000 ms | Account/income screen timers | Same financial boundary; comparison dependencies из envelope, не отдельный client validation | | Bond search / `bond/api/queries.ts:97–104` | Session AND query.length >= 2; trimmed debounced query | 30 000 ms | Bonds screen timer вызывает refetch даже disabled query | Resource без brokerage accessGroup; общая connection mutation всё равно reset bonds subtree | | Bond/stock detail and child resources / соответствующие `api/queries.ts` | Session AND identifier; child session AND UID; candles also period | 30 000 ms | Detail screen timer over all resources; UID-empty child всё ещё включён в массив | Personal position/coupons/dividends и operations помечены accessGroup; connection reset охватывает весь stocks/bonds GET subtree | | Stocks screener / `StocksPage.vm.ts:29–101` | Handler Search/page; apiClient, не useQuery | Не применяется | Новый POST только по действию; GET page императивный, requestId fence | Локальный snapshot/error/pending; GET transport и session generation guards остаются; local state исчезает при unmount | | Bank card/cards/categories/expenses/operation / `bank-card/api/queries.ts:28–73` | Session; item/expenses дополнительно id; cards includeArchived в key | Не задано: Query default 0 | Focus/reconnect/mount; собственного screen timer нет | Bank mutation invalidates банковское GET subtree; session clear отдельно | | Bank journal / `bank-card/api/queries.ts:76–145` | Session AND id/from/to; key range/limit/page; all-mode отдельный suffix | Не задано: 0 | All-mode последовательное чтение страниц с одним signal; page-mode conditional placeholder | Refetch/invalidation inherited; нет UI retry у read failure; FE-004 | | Bank summary / `bank-card/api/queries.ts:147–156` | Session; cardId/from/to | Не задано: 0 | Query select сортирует exact cents; busy передан в view | Mutation invalidation и session clear; error только без data, без retry | | Connection / `TbankConnectionSection.vm.ts:5–22` | Session, generated status key | Не задано: 0 | Cached progress/error/retry явно переданы | Успех PUT/DELETE коммитит status и reset investment subtree; неуспех не reset | | Auth / `auth.vm.ts:81–151` | Cold-start refresh; serial auth requests; memory session | Не Query cache | Single-flight refresh + serialized cookie-writing operations | clearSession → generation increment/queryClient.clear; смена identity reset active/remove inactive | Тестовый QueryClient выключает Query retries (`frontend/src/test/query.tsx:15–20`), но не отражает автоматически все production focus/timer/transport policies. `observe` не вызывается production UI по найденным callers; вопрос удаления совместимости передан 04, не раскрывался здесь как самостоятельный dead-code audit. ### 3.6. Render, forms, accessibility и contracts: результат покрытия - **Rendering:** позиции вычисляются в page model, chart data/table — в presentation VM. Child VM types принимают narrow structural contracts, даже если caller передаёт весь vm object. Оснований заменить их universal component или добавить `memo` каждому компоненту не установлено. Большие all-mode/candle arrays отмечены только FE-012. - **Keys:** проверены account id, instrument UID, operation id/account pair, candle time, category id, group key. Есть occurrence keys у income entries (`model/display.ts:101–117`), что защищает повторяющиеся identifiers; logoName входит в некоторые keys. Remount cost/регрессия не измерены, массовое изменение keys не рекомендовано. - **Effects:** screen timer имеет cleanup, resourcesRef обновляется effect; его `_scope` и resource queryKey не управляют timer. Initial availability имеет budget 10 000 ms и identity (`shared/lib/initial-availability.ts:18–46`), но `isFetching` input не используется. Это не основание удалять bounded initial-wait UX: hook не является polling source. URL/form reset effects и cursor recovery выполняют внешнюю синхронизацию, а не второй data cache. - **Forms:** RHF + resolver; factories учитывают актуальные cards/categories, refund limit и explicit adopted version. Нормализация amount/email/date имеет разные владельцы и разные инварианты; общий universal normalizer не нужен. Серверная validation/authorization остаётся обязательной. - **Accessibility:** семантические Link/Button, именованные action icons, labels/field errors, busy skeleton, pagination labels, modal focus return, keyboard-focusable price table, reduced motion theme проверены по source. Подтверждённый pending-link gap — FE-005. Наличие main landmark не доказывает route focus/skip-navigation experience: screen reader, contrast, responsive overflow и реальный keyboard path **UNVERIFIED** без browser session. - **Contract:** типы data/request выводятся из generated paths; unknown money отличается от zero, formatMoney сохраняет decimal строку (`shared/lib/display.ts:21–61`). Chart `Number` используется для визуальной шкалы, точная таблица сохраняет original close. ApiErrorCode — subset открытого wire code с fallback (`shared/api/problem-details.ts:19–28`), не runtime validator всей schema. Malformed runtime payload не обещает гарантированной локальной recovery: protected route ErrorBoundary — последний safe fallback. **Ограничения репрезентативности:** изучены все route definitions и выбранные end-to-end owners, не каждый JSX/CSS/test во frontend. Невыполненные browser и measurement checks не скрыты за успешным dependency gate. Backend execution, DRY cross-repo, systemic dead code, качество всех assertions, CI/tooling и load efficiency находятся вне scope; их сигналы перечислены в разделе 8. ## 4. Findings ### FE-001 — Source metadata теряется в API финансового view - **Decision:** FIX. - **Severity:** P2. - **Confidence:** High. - **Evidence:** `frontend/src/pages/account/ui/AccountPage/AccountPage.vm.ts:46–50,125–135`; `frontend/src/pages/account/ui/AccountPage/AccountPage.tsx:13–34`; `frontend/src/pages/account/ui/AccountPage/components/AccountPageLoaded.view.tsx:28–38,55–59`; `frontend/src/pages/account-income/ui/AccountIncomePage/AccountIncomePage.vm.ts:24–39,110–119`; `frontend/src/pages/account-income/ui/AccountIncomePage/components/AccountIncomePageLoaded.view.tsx:36–70`. - **Claim:** у account container уже вычисленный `source` не передаётся view; у analytics loaded model source отсутствует целиком. Source warning/time нельзя восстановить из сохранённых денежных rows без envelope. - **Current mechanism:** route вызывает page VM → resource query отдаёт data/meta → VM account возвращает source → container явно перечисляет props без source. Empty account branch также не несёт metadata. Income VM использует meta для partial/comparison notices, но выбрасывает freshness/time/attempt/availability. `Portfolio.isStale` на основном экране не читается отдельным consumer. - **Impact / actual cost:** old complete snapshot после failed hourly attempt выглядит как обычный результат; отсутствует контекст происхождения сумм. **Impact unmeasured**: частота старых snapshots и пользовательские ошибки интерпретации не измерены. Прямой propagation defect, а не новый API requirement. - **Why now:** информация уже опубликована и частично отображается на соседних screens. Specs `frontend-data-resilience/spec.md:11–28` и `portfolio-view/spec.md:121–158` сохраняют явный source context; полезность правила здесь конкретна — не выдавать старые суммы за новое наблюдение. - **Minimum change:** передать account source; включить источник в loaded/empty финансовое presentation API; income использует тот же существующий status renderer с page-owned completeness/availability wording. Не вводить client timestamps вместо server `lastSuccessfulAt`. - **Before / after API:** ```tsx // До: source вычислен, но потерян. // После: остальные props сохраняются, source проходит boundary. // Income loaded VM: добавить source: analytics.meta; // view получает source, а не useIncomeAnalytics внутри view. ``` - **Guarantees at risk:** нельзя путать server stale, failed hourly attempt и client transport failure; data/null/complete empty, деньги/валюты и исходное время сохраняются. Для существующих responses meta гарантирована contract, optional compatibility props можно оставить там, где нужны fixtures. - **Counterarguments / trade-offs:** тихий экран может быть намеренной UX политикой: `AccountPage.test.tsx:803–855` явно ожидает отсутствие source time. Это доказательство действующего поведения, но не его полезности. Соседний Accounts view уже показывает SourceSyncStatus; время не требует нового слоя. - **Verification or rollback:** RTL/MSW current account route с full data и failed/partial attempt; source timestamp неизменен, sums остаются; empty data не становится missing. Изменить silent expectations в существующем test, добавить income source presentation case. Проверять view props и route, не только helper. Rollback только presentation diff, Query policies не трогать. - **Effort:** M (account wiring S; полный scope включает income/empty states). - **Related finding IDs:** FE-002, FE-008, FE-009, FE-011. ### FE-002 — Investment view contract теряет transport recovery - **Decision:** FIX. - **Severity:** P2. - **Confidence:** High. - **Evidence:** `frontend/src/features/tbank-connection/model/screen-sync.ts:37–61`; `frontend/src/pages/account/ui/AccountPage/AccountPage.vm.ts:78–84`; `frontend/src/pages/accounts/ui/AccountsPage/AccountsPage.vm.ts:21–58`; `frontend/src/pages/account-income/ui/AccountIncomePage/AccountIncomePage.vm.ts:66–69,110–119`; `frontend/src/features/instrument-details/model/section.ts:14–27`; `frontend/src/features/instrument-details/ui/InstrumentSection.view.tsx:22–28,76–84`; `frontend/src/pages/bonds/ui/BondsPage/BondsPage.vm.ts:17–20,27–39`. - **Claim:** data сохранена, но cached transport error/progress/retry не доходит до пользовательской boundary. InstrumentSection даже полученный error скрывает при `hasData`; нет локального retry API. - **Current mechanism:** page VMs вызывают `useScreenSync`, не сохраняя его результат. Loaded unions не содержат client feedback. `section` сводит HTTP failure и source failure в boolean; view Alert имеет guard `!hasData`. Search boolean error тоже требует отсутствия data. Operations/events передают source meta, но не client failed-read state. Initial generic Account/Accounts error (`AccountPageError.view.tsx:46–50`, `AccountsPageError.view.tsx:41–45`) не имеет retry; income error view также только StateView. - **Impact / actual cost:** неуспешное перечитывание неразличимо от штатного snapshot; восстановление требует внешней navigation/focus/timer вместо действия затронутой section. **Impact unmeasured**. Наличие data предотвращает разрушение экрана, но не выполняет recovery contract. - **Why now:** существующий status UI и Query.refetch уже достаточны. `frontend-data-resilience/spec.md:18–24`, UI convention `:133–146` и docs `architecture/cross-cutting/ui-resilience.md:19–35` требуют двух различимых состояний именно для сохранённых финансовых результатов. - **Minimum change:** page VM отдаёт client feedback (`refreshing`, `refreshError`, `retry`), view показывает неблокирующее предупреждение и read-only retry. SectionState раздельно несёт source и transport status; retry относится к failed query/section. Access failures сохраняют отдельный navigation-only путь. Использовать Mantine Alert/Button и существующую presentation семантику, не universal global error store. - **Before / after API:** ```ts // До: type SectionState = { error: boolean; hasData: boolean; loading: boolean; partial?: boolean }; // После: оставшиеся поля сохраняются. type SectionFeedback = { refreshError: boolean; refreshing: boolean; sourceFailed: boolean; retry?: () => void; }; // Page/section VM собирает feedback из query; view получает named props. // section error с hasData => warning + data, без data => local error + retry. ``` - **Guarantees at risk:** не скрывать data при transient error; не показывать чужой session/connection snapshot; retry — только HTTP read, не manual import. Не перемещать query hook во view. Invalid cursor сначала восстанавливает scope через existing history owner, а не повторяет бесконечно тот же cursor. - **Counterarguments / trade-offs:** отсутствие кнопки manual source sync оправданно; это не запрет повторить HTTP. Quiet behavior действительно закреплён `AccountPage.test.tsx:751–855`, `AccountsPage` quiet tests и `StockPage.test.tsx:392–407,449–471`; tests не заменяют behavioral решение. Source warning из FE-001 не объясняет transport error, поскольку HTTP мог не доставить новое meta вовсе. Не следует показывать дублирующиеся alerts. - **Verification or rollback:** existing route RTL/MSW: initial temporary fail, cached fail, slow refetch, successful retry, independent failed company section, invalid cursor и confirmed access loss. Проверять завершение failed response, сохранённые суммы/draft и доступную кнопку, а не private callback count. Изменяются ожидаемые visible outcomes, не весь test stack. - **Effort:** M. - **Related finding IDs:** FE-001, FE-003, FE-004, FE-008, FE-011. ### FE-003 — Screen timer обходит `enabled`, policy разбита между owners - **Decision:** FIX (с native simplification после behavior proof). - **Severity:** P2. - **Confidence:** High. - **Evidence:** `frontend/src/features/tbank-connection/model/screen-sync.ts:7–35,39–50`; `frontend/src/pages/bonds/ui/BondsPage/BondsPage.vm.ts:9–13`; `frontend/src/entities/bond/api/queries.ts:97–104`; `frontend/src/pages/stock/ui/StockPage/StockPage.vm.ts:23–55`; `frontend/src/pages/bond/ui/BondPage/BondPage.vm.ts:23–55`; `frontend/src/shared/api/authenticated-query.ts:42–50`; `frontend/src/app/ui/App.tsx:13–20`. - **Claim:** timer вызывает manual refetch каждого resource, не проверяя `isEnabled`/`isFetching`. Disabled Query защищает автоматическое fetching, но не ручной `refetch`. Это подтверждено текущей TanStack documentation. - **Current mechanism:** 30-minute setInterval, scope descriptor игнорируется, latest resources лежат в ref. `ScreenResource` не содержит isEnabled, хотя useResourceQuery возвращает его (`resource-query.ts:132–146`). При пустом bond query endpoint всё равно получает request; при неразрешённом UID все dependent observers существуют с `uid: ""`, timer их принудительно читает. Отдельно stale portfolio после 30 секунд допускает focus refetch из App, хотя `portfolio-view/spec.md:121–124` запрещает focus-triggered refetch. - **Impact / actual cost:** invalid-input GET, ложные local errors при ожидании prerequisite, чтение в другой частоте, чем ожидает policy owner. **Impact unmeasured**: traffic/latency/API load не считались. StaleTime не превращает ручной timer в readiness-aware schedule. - **Why now:** root-cause fix в одном общем владельце защищает все восемь callers. Потерянные scope inputs создают необходимость поддерживать descriptors, не влияющие на execution. - **Minimum change:** сначала фильтровать enabled и уже fetching resources во всех manual refresh/retry/timer ветках. Focus policy выбрать явно у investment query owner, не менять banking только ради одного правила. Следующий компактный вариант — встроенный `refetchInterval` у query owner, с polling только у нужного screen observer; убрать собственный timer и неиспользуемый scope. Проверить observers Accounts/breadcrumb, прежде чем включать interval во всех экземплярах одного hook. - **Before / after API:** ```ts // Минимальная коррекция existing API: type ScreenResource = { isEnabled: boolean; isFetching: boolean; refetch: () => Promise /* existing feedback fields */ }; // resources.filter(r => r.isEnabled && !r.isFetching) перед manual refetch. // Native destination, policy находится в entity query options: useResourceQuery({ ...options, enabled: ready, refetchInterval: READ_INTERVAL_MS, refetchIntervalInBackground: false, refetchOnWindowFocus: false }); // Вместо useScreenSync({ unused scope fields }, [resources]). ``` - **Guarantees at risk:** session-aware enabled, AbortSignal, canonical keys, cached snapshot, interval stop on unmount и read-only backend semantics. Native background/focus defaults меняют нынешнюю работу hidden tab; нельзя выдать такое изменение за чисто механический refactor. - **Counterarguments / trade-offs:** текущий timer объединяет ресурсы одного экрана и может быть осознанной политикой hidden tab. Query polling без observer ownership способен размножить timers для одного key. Поэтому immediate readiness guard подтверждён, а полное native replacement требует узкого proof; новый scheduler framework не нужен. Reconnect policy не объявляется дефектом автоматически: product meaning minimum interval надо согласовать. - **Verification or rollback:** fake timers + existing screen-sync test boundary: пустой/однобуквенный поиск, unresolved ticker, позднее получение UID, unmount, cached refetch failure, два observers одного key, focus/reconnect. MSW должен подтвердить отсутствие invalid request, а не только call helper. Existing `screen-sync.test.tsx:21–80` проверяет ручные reads, но не disabled interval. Rollback native phase к исправленному enabled-aware timer. - **Effort:** M (immediate shared guard S). - **Related finding IDs:** FE-002, FE-008, FE-011, FE-012. ### FE-004 — Banking read-state API не обеспечивает локальный recovery - **Decision:** FIX. - **Severity:** P2. - **Confidence:** High. - **Evidence:** `frontend/src/pages/cards/ui/CardsPage/CardsPage.vm.ts:15–45`; `frontend/src/pages/cards/ui/CardsPage/CardsPage.view.tsx:31–77`; `frontend/src/pages/card/ui/CardPage/CardPage.vm.ts:95–118,140–145`; `frontend/src/pages/card/ui/CardPage/CardPage.view.tsx:52–83,133–142,177–186`; `frontend/src/pages/card/ui/CardPage/CardHistory.view.tsx:38–123`; `frontend/src/entities/bank-card/ui/BankSummary/BankSummary.view.tsx:17–23,84–125`. - **Claim:** initial cards failure без data/loading возвращает `null`, не error state. Card/history/summary не дают retry для соответствующего read; cached card failure fields computed в VM не используются view, history и summary warnings подавлены при наличии data. - **Current mechanism:** CardsPageViewCards обрабатывает только items и isLoading, затем null. CardPage initial error предлагает только `/cards`. History возвращает rows раньше анализа failure; outer message имеет `history.error && !rows`. Summary error также требует `!data` и имеет только текст. VM summary type не передаёт error object/retry. Existing BankFeedback уже принимает `cached`, `loadingError`, `retry`. - **Impact / actual cost:** blank cards area после ошибки похожа на отсутствие результата UI; пользователь не может адресно восстановить journal/summary. Сохранённый баланс не становится нулём — это полезная гарантия, но ошибка чтения остаётся скрытой. **Impact unmeasured**. - **Why now:** `specs/openspec/specs/manual-bank-cards/spec.md:631–665` прямо различает empty/error и требует repeat. Это необходимо для ручного учёта, не generic preference к количеству Alert components. - **Minimum change:** query owner остаётся bank-card; page VM отдаёт section feedback и named retries. CardsPage добавляет error branch и cached warning; CardPage/BankSummaryView используют BankFeedback либо простой локальный Alert/Button. Ошибка одной section не убирает успешные card/history/summary. - **Before / after API:** ```ts // До: summary: { data, error: boolean, fetching, loading }. // После, без смены query key/transport: summary: { data: summary.data, error: summary.error, fetching: summary.isFetching, loading: summary.isLoading, retry: () => void summary.refetch() } // Cards branch: !data && isError => BankFeedback(loadingError, retry). // Cached branch: data + error => rows и BankFeedback(cached, retry). ``` - **Guarantees at risk:** точный balance из backend, запрет optimistic success, independence от T-Bank, pagination scope, disabled edit на placeholder rows, drafts/version/intent сохраняются. NotFound не должен предлагать повтор записи/создание дубля; recovery повторяет только GET. - **Counterarguments / trade-offs:** summary failure уже отображает текст без data, и prerequisites cardsFeedback существует. Finding не утверждает, что все banking errors потеряны. Отсутствие alert при cached failure закреплено `CardPage.test.tsx:315–341`; после исправления нужны новые ожидаемые outcomes. Invalidation всего банковского subtree пока сохраняется. - **Verification or rollback:** RTL/MSW по существующим CardsPage/CardPage и BankSummary tests: cold cards failure, failure only summary/history, cached failures, GET retry success, сохранение balance, draft и current URL. Для placeholder rows оставить текущий edit guard. Rollback presentation wiring, не write protections. Не создавать общий banking error manager. - **Effort:** M. - **Related finding IDs:** FE-002, FE-009, FE-010, FE-011. ### FE-005 — Auth Link остаётся активным во время отправки - **Decision:** FIX. - **Severity:** P2. - **Confidence:** High. - **Evidence:** `frontend/src/features/auth/ui/AuthForm/AuthForm.tsx:26–45,51–84`; `frontend/src/features/auth/ui/LoginForm/LoginForm.tsx:6–9`; `frontend/src/features/auth/ui/RegisterForm/RegisterForm.tsx:6–9`; `frontend/src/features/auth/model/auth.vm.ts:121–133`; `specs/openspec/specs/user-authentication/spec.md:109–122`. - **Claim:** `
` не делает дочерний anchor disabled. Mantine Button с `component={Link}` здесь не получает отдельного interaction guard, поэтому переключение login/register остаётся доступно. - **Current mechanism:** native fieldset блокирует form controls; React Router Link отображается ссылкой. На `Button component={Link}` нет disabled data, aria-disabled, tab exclusion или guarded click. Mantine docs явно требуют для link separate `data-disabled` и предотвращения navigation. - **Impact / actual cost:** пользователь меняет auth route во время request, теряет старую форму/draft/error context; authentication всё ещё завершается в общем provider. Нарушение pending UX подтверждено механизмом; account compromise/утечка не доказаны. **Impact unmeasured**. - **Why now:** малая локальная правка удовлетворяет уже принятому правилу блокировки всех auth actions и не требует новой auth orchestration. - **Minimum change:** guard navigation while pending, обозначить link недоступным и исключить его из tab order; сохранить настоящую ссылку в обычном состоянии. Handler находится у AuthForm owner. - **Before / after API:** ```tsx // До: