# Аудит 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:** `