Files
main/audits/2026-10-11/reports/05-frontend-architecture-report.md
codex-bot d07fbfabb1 docs: archive technical audit 2026-10-11 in develop (#36)
Переносит архив `audits/2026-10-11/` (19 файлов) из codex/audit-remediation-2026-10-11 в develop и добавляет точечный allowlist в .gitignore.
2026-10-17 19:38:03 +03:00

1039 lines
100 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Аудит 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 вычислен, но потерян.
<AccountPageLoaded portfolio={vm.portfolio} positionSections={vm.positionSections} />
// После: остальные props сохраняются, source проходит boundary.
<AccountPageLoaded portfolio={vm.portfolio} positionSections={vm.positionSections} source={vm.source} />
// 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<unknown> /* 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:** `<Fieldset disabled={isSubmitting}>` не делает дочерний 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
// До:
<Button component={Link} to={to} variant="subtle" />
// После: onSwitchAuthPage предотвращает переход только при isSubmitting.
<Button component={Link} to={to} variant="subtle"
aria-disabled={isSubmitting || undefined}
data-disabled={isSubmitting || undefined}
tabIndex={isSubmitting ? -1 : undefined} onClick={onSwitchAuthPage} />
```
- **Guarantees at risk:** keyboard и pointer behavior должны совпасть; не
заменять navigation div/click; submission error сохраняет ввод; auth queue,
generation guard и raw password остаются. Одного CSS pointer-events guard
недостаточно для keyboard navigation.
- **Counterarguments / trade-offs:** маршрут после успешного auth всё равно
redirected, поэтому это локальный UX дефект, а не P1 auth vulnerability.
Browser back не обязан блокироваться этим локальным исправлением. Existing
AuthForm test (`:8–32`) проверяет mismatch, не pending navigation.
- **Verification or rollback:** deferred onSubmit в existing RTL/MemoryRouter:
заполнить valid inputs, submit, попытаться переключить route click/Enter,
дождаться failure и проверить сохранение ввода + восстановление ссылки.
Browser keyboard check нужен для реальной focusability. Rollback только
interaction props/handler.
- **Effort:** S.
- **Related finding IDs:** FE-010, FE-011.
### FE-006 — Видимость CURRENCY: подтверждённое скрытие, спорное намерение
- **Decision:** INVESTIGATE.
- **Severity:** P2 (потенциальная потеря видимости позиции, не подтверждённый дефект продукта).
- **Confidence:** Medium; механизм исключения High, нужность изменения UNVERIFIED.
- **Evidence:** `frontend/src/pages/account/model/positions.ts:70–94,136–160`;
`frontend/src/pages/account/ui/AccountPage/AccountPage.vm.ts:109–135`;
`frontend/src/pages/account/ui/AccountPage/components/PositionSection.view.tsx:14–39`;
`frontend/src/pages/account/model/positions.test.ts:15–69`;
`frontend/src/pages/account/ui/AccountPage/AccountPage.test.tsx:287–325,349–394`;
`specs/openspec/specs/portfolio-view/spec.md:22–33,164–169`.
- **Claim:** CURRENCY не проходит ни stocks, ни bonds, ни other filter.
Currency-only portfolio остаётся loaded, но не показывает карточку позиции.
Это намеренно закреплено текущими tests, тогда как saved spec различает
торгуемый валютный инструмент и Cash Balance и требует доступности иных типов.
- **Current mechanism:** единственный production caller модели — account VM,
также используемый route `/accounts/all`. Denominator включает CURRENCY,
но sections исключают её; cash view показывает только backend cashBalances.
- **Impact / actual cost:** возможна невидимая quantity/valuation позиции,
участвующей в sums/shares; не доказано, что реальный upstream в рабочем
наборе возвращает самостоятельные currency contracts, которые требуется
показывать. **Impact unmeasured; product defect UNVERIFIED**.
- **Why now:** конфликт делает изменение этого model опасным для следующего
разработчика: tests и spec дают противоположные answers. Это не повод
автоматически принять spec или удалить tests.
- **Minimum change:** один product/domain ответ: является ли CURRENCY в
публичном Portfolio самостоятельной строкой, которую UI должен показывать?
Сверить безопасный synthetic payload с contract, не ходить в real API.
При положительном ответе удалить только exclusion из other predicate;
при отрицательном — явно согласовать behavioral spec с UI policy.
- **Before / after API:** API формы sections не меняется. Возможное изменение
после решения: `other = !STOCK && !BOND && !CURRENCY` →
`other = !STOCK && !BOND`. `cashBalances` и `totalsByCurrency` не пересчитывать.
- **Guarantees at risk:** currency instrument ≠ cash balance, никакой FX
конвертации и двойного суммирования; server totals authoritative; order,
unknown values и all-account source identity сохраняются.
- **Counterarguments / trade-offs:** tests прямо защищают отдельный cash block
и скрытие валютных instruments; это сильный признак принятого product choice.
Нельзя назвать это случайной опечаткой. Но cash block не показывает quantity
торгуемой позиции и сам по себе не объясняет скрытие contract row.
- **Verification or rollback:** после решения изменить существующий
positions test и route test: CURRENCY-only, CURRENCY + cash same currency,
all-account rows, отсутствие double-added totals. До решения production
predicate и tests сохранять. Rollback локального filter, без API migration.
- **Effort:** S после ответа; до ответа правка не рекомендована.
- **Related finding IDs:** FE-008, FE-010.
### FE-007 — Малый presentation VM добавляет hop без самостоятельной orchestration
- **Decision:** SIMPLIFY.
- **Severity:** P3.
- **Confidence:** High для scope/caller, не измеренная экономия.
- **Evidence:** `frontend/src/pages/stocks/ui/StocksPage/StocksPagePresentation.vm.ts:1–20`;
`frontend/src/pages/stocks/ui/StocksPage/StocksPage.view.tsx:205–208,259–268`.
- **Claim:** hook только получает t и возвращает два Pagination label callback;
sole consumer находится рядом и уже получает t. Это optional `yagni:`
кандидат, не основание отменять MVVM или presentation hooks вообще.
- **Current mechanism:** view → extra hook → useTranslation → callbacks;
нет query, form context, local state/effect или независимого lifecycle.
- **Impact / actual cost:** дополнительный файл/переход при изменении двух
доступных labels. **Impact unmeasured**: agent tokens/maintenance time и
render overhead не измерены; runtime benefit не заявлен.
- **Why now:** только при ближайшем изменении Pagination. Самостоятельная
cleanup задача имеет низкую ценность относительно FE-001–FE-005.
- **Minimum change:** callbacks рядом с Pagination/view либо небольшой
page-local presentation component, если этого требует текущий lint profile.
Не создавать shared pagination label abstraction ради одного caller.
- **Before / after API:** `const { getControlProps, getItemProps } =
useStocksPagePresentationVm()` → local named callbacks с existing `t`;
public `<StocksPageView vm={vm}>` и Pagination props не меняются.
- **Guarantees at risk:** сохранить translated control/page names, typing
`ComponentProps<typeof Pagination>`, keyboard semantics и отсутствие data hook
во view. Локальный lint может потребовать узкого согласованного exception.
- **Counterarguments / trade-offs:** hook даёт единый файл-layout по convention,
и это небольшая цена. `CardPagePresentation.vm.ts:18–73` имеет реальный
scroll/focus/layout lifecycle и не является аналогом для удаления.
- **Verification or rollback:** existing StocksPage page-control RTL scenario
`StocksPage.test.tsx:127–172`; имена/переходы одинаковы. Отдельный новый
passthrough test не нужен. Rollback локального placement, без Query changes.
- **Effort:** S.
- **Related finding IDs:** FE-008, FE-009.
### FE-008 — KEEP полезные page VMs и coherent presentation owners
- **Decision:** KEEP.
- **Severity:** P3 (защищаемый механизм; не дефект).
- **Confidence:** High.
- **Evidence:** `frontend/src/pages/account/ui/AccountPage/AccountPage.vm.ts:66–136`;
`frontend/src/pages/account-income/ui/AccountIncomePage/AccountIncomePage.vm.ts:41–119`;
`frontend/src/pages/card/ui/CardPage/CardPage.vm.ts:21–158`;
`frontend/src/pages/stock/ui/StockPage/StockPage.vm.ts:23–98`;
`frontend/src/pages/card/ui/CardPage/CardPagePresentation.vm.ts:18–73`.
- **Claim:** эти VMs реально owns route/data/form/draft/grouping/recovery
orchestration. Удаление файла перенесёт связную сложность в view, а не уберёт её.
- **Current mechanism:** page container выбирает state; VM объединяет
resources и derived sections; views получают props. Income separates URL
commit и RHF draft. Card presentation VM сохраняет scroll position при paging.
- **Impact / actual cost:** исправлять недостающие projection fields локально
проще, чем менять все view consumers. Экономия от удаления **not measured**,
оправданная ответственность подтверждена конкретными callers.
- **Why now:** complexity audit легко ошибочно отождествляет VM с boilerplate;
FE-007 не должен превратиться в массовое объединение файлов.
- **Minimum change:** сохранить boundaries; расширить их только source/client
feedback из FE-001/002/004. Не вводить дополнительный generic VM framework.
- **Before / after API:** `AccountPage → useAccountPageViewModel → state view`
остаётся тем же; после исправлений добавляются explicit source/feedback props,
query hooks остаются внутри owner VM.
- **Guarantees at risk:** hook ordering, current closures, grouping by source
account, independent sections, URL/draft separation, scroll preservation.
- **Counterarguments / trade-offs:** props/файлов больше, чем в монолитном
компоненте. Это приемлемо для перечисленных обязанностей; single caller и
размер view не доказывают необходимость ни split, ни merge.
- **Verification or rollback:** существующие route/form/pagination RTL scenarios
остаются boundary проверки; browser scroll proof требуется для изменения
Card presentation lifecycle. Никакой новой проверки «существует VM».
- **Effort:** S (сохранить owner при локальных правках).
- **Related finding IDs:** FE-001, FE-002, FE-003, FE-006, FE-007.
### FE-009 — KEEP semantic Mantine wrappers и package foundation
- **Decision:** KEEP.
- **Severity:** P3 (не дефект).
- **Confidence:** High.
- **Evidence:** `frontend/packages/ui/src/EntryCard/EntryCard.tsx:7–80`;
`frontend/packages/ui/src/LoadingSkeleton/LoadingSkeleton.tsx:5–16`;
`frontend/packages/ui/src/StateView/StateView.tsx:5–25`;
`frontend/src/shared/ui/SourceSyncStatus/SourceSyncStatus.tsx:13–47`;
`frontend/packages/ui/src/theme.ts:27–75`;
`frontend/src/pages/card/ui/CardPage/CardHistory.view.tsx:58–102`.
- **Claim:** wrappers имеют project presentation responsibility: entry
semantics/actions/tone, accessible busy region, safe state shell и исходное
время/attempt text. Это не повторная реализация Mantine Button/Modal.
- **Current mechanism:** Mantine controls и theme используются напрямую;
shared package предоставляет low-level presentation, переводы и retry
принадлежат callers. Form bodies используют Fieldset/Modal, не custom widgets.
- **Impact / actual cost:** удаление wrappers размножит проверенные status/entry
структуры и пропустит busy/unknown/tone semantics. **Benefit not measured**.
- **Why now:** minimal code не должен сокращаться за счёт accessibility и
единого смысла financial display. FE-001/004 можно решить существующими parts.
- **Minimum change:** сохранить wrappers, расширять только недостающий feedback
в существующем owner. Не строить новую design system поверх Mantine.
- **Before / after API:** `<LoadingSkeleton label={t(key)}>{shapes}</LoadingSkeleton>`
и `<EntryCard title metadata value valueLabel tone actions />` остаются без
изменений; page source/retry props добавляются у callers по необходимости.
- **Guarantees at risk:** semantic busy state, decorative media, explicit money
currency/sign/unknown, reduced motion и owned modal exit/focus lifecycle.
- **Counterarguments / trade-offs:** SourceSyncStatus знает generated resource
metadata в shared; это reuse инфраструктурного envelope, а не financial
calculation. Перенос только ради буквального запрета domain names не имеет
подтверждённой пользы. Малый wrapper не автоматически лишний.
- **Verification or rollback:** existing wrapper stories/RTL и page states;
contrast/focus/reduced motion — browser proof при visual изменении.
Не добавлять tests, которые лишь повторяют Mantine props.
- **Effort:** S.
- **Related finding IDs:** FE-001, FE-004, FE-007, FE-010.
### FE-010 — KEEP form boundaries, exact money, replay и version adoption
- **Decision:** KEEP.
- **Severity:** P3 (не дефект; снятие защит создаст другой риск).
- **Confidence:** High.
- **Evidence:** `frontend/src/pages/card/ui/OperationForm/OperationForm.vm.ts:29–81,90–151,192–225`;
`frontend/src/pages/card/ui/OperationForm/OperationForm.schema.ts:46–104,107–131`;
`frontend/src/entities/bank-card/model/money.ts:4–23`;
`frontend/src/entities/bank-card/model/form.schema.ts:8–42`;
`frontend/src/entities/bank-card/ui/BankCardForm/BankCardForm.vm.ts:18–45,50–115`;
`frontend/src/features/auth/model/auth.schema.ts:23–57`.
- **Claim:** local version/intent/submittedValues/latest/refundLimit не являются
лишним Query cache. Это отдельный подтверждённый workflow, который нельзя
заменить «всегда бери свежую версию из query» или Number money input.
- **Current mechanism:** RHF draft сохраняется при error; stable UUID intention
повторяется после потерянного create response; adopted version меняется
только явным действием; same submitted values могут дойти до server replay
даже после изменения source. Amount/date и refund validation имеют boundary
schemas, а query invalidates related bank subtree.
- **Impact / actual cost:** защищает от дублирования намерения и молчаливого
принятия чужой версии. Pure money parsing выполняется bigint, transport amount
остаётся decimal string. **Benefit not measured**, инварианты не заменяют
доказательство backend transaction/replay — это scope 06/13.
- **Why now:** именно эта сложность легко выглядит как state duplication при
mechanical simplification. Existing tests `OperationForm.test.tsx:154–227,
228–324,404–479` описывают lost response/refund/version behavior.
- **Minimum change:** KEEP; исправления read feedback не меняют form intent,
explicit adoption, positive/range checks, raw password или server validation.
- **Before / after API:** `useOperationFormVm({ card, cards, operation, onClose })`
→ тот же API; `POST { id: intent, amount: { currency: "RUB", value } }` и
`PATCH { ...body, version }` неизменны. Currency/date normalization не
объединяется с email/password policy.
- **Guarantees at risk:** exact cents/ranges, no rounding, changed-intent
conflict, refund bounds, server-side ownership/atomicity, password untouched,
draft retention, disabled controls и focus return.
- **Counterarguments / trade-offs:** frontend checks могут устареть между
validation и mutation. Это причина сохранить серверную проверку, не убрать
frontend UX validation. Explicit version adoption намеренно не подменяет draft
свежим record, хотя требует state и дополнительных действий.
- **Verification or rollback:** current financial form scenarios — нужная
regression boundary; extra numeric edge cases у existing pure tests,
configured backend tests относятся владельцу 06/13. Не оценивать atomicity
mock form тестом. Rollback simplification при потере любого перечисленного
invariant; сейчас simplification не предлагается.
- **Effort:** S.
- **Related finding IDs:** FE-004, FE-005, FE-006, FE-009, FE-011.
### FE-011 — KEEP generated API и session/connection reuse boundaries
- **Decision:** KEEP.
- **Severity:** P3 (не дефект).
- **Confidence:** High для source mechanisms; fresh runtime proof не выполнялся.
- **Evidence:** `frontend/src/shared/api/authenticated-query.ts:34–58`;
`frontend/src/shared/api/client.ts:53–119,284–290`;
`frontend/src/features/auth/model/auth.vm.ts:41–115,121–151`;
`frontend/src/features/tbank-connection/model/connection-boundary.ts:11–39`;
`frontend/src/features/tbank-connection/ui/TbankConnectionSection/TbankConnectionSectionLoaded.vm.ts:32–58`;
`frontend/src/shared/api/resource-query.ts:37–67,99–102,129–149`.
- **Claim:** authenticated/resource wrappers не являются бессмысленными
passthrough: session enabling, access loss cleanup, metadata projection и
confirmed connection reset защищают reuse финансовых данных.
- **Current mechanism:** generated queryFn/options/key; protected enabled
содержит session snapshot; auth requests serialized/single-flight, client
проверяет generation после HTTP/refresh. Logout clears cache; connection
mutation reset GET brokerage/stocks/bonds only on success. Access-group
failure cancel + clear data, а ordinary temporary failure сохраняет snapshot.
- **Impact / actual cost:** снижает риск late-response/identity reuse и
конкурирующих auth loops. **Benefit not measured**; crypto/server auth safety
отдельно не проверялась. Один shared transport вместо отдельных clients
подтверждён прослеженными consumers.
- **Why now:** native simplification FE-003 должна использовать эти boundaries,
не обходить их прямым fetch. Совпадение artifacts не оправдывает ручные DTO.
- **Minimum change:** KEEP; никаких localStorage token stores, URL-wide request
registries, hash validators или дополнительных epochs. `observe` compatibility
оценивать отдельно по callers, не удаляя access projection вместе с ним.
- **Before / after API:** `useResourceQuery(generatedOptions)` → тот же API;
`commitTbankConnectionBoundary(client, confirmedStatus)` → тот же success-only
commit. GET/refetch returns envelope, `.data` projection сохраняет его
отличие от payload.
- **Guarantees at risk:** canonical keys, AbortSignal, no previous session data,
cold-start neutral UI, one auth retry, failed connection mutation preserves
cache, confirmed access loss clears private resource, unknown code fallback.
- **Counterarguments / trade-offs:** access loss uses lower-level query.setState;
это чувствительная библиотечная точка. Упрощать только после равносильного
proof. Existing tests `resource-query.test.tsx:174–209`,
`auth.test.tsx:146–187,279–354` и connection boundary scenarios дают полезную
boundary, но не объявлены свежим runtime подтверждением в этом аудите.
- **Verification or rollback:** existing auth/client/resource/connection MSW
suites при любом touched boundary; late response + identity/connection switch,
failure without reset, logout network failure. Не требовать нового global
state manager или cross-tab locking: spec допускает re-login другой вкладки.
- **Effort:** S.
- **Related finding IDs:** FE-001, FE-002, FE-003, FE-005, FE-010.
### FE-012 — Длинные all-mode lists и candle tables: potential bottleneck
- **Decision:** INVESTIGATE.
- **Severity:** P3.
- **Confidence:** Low для практической значимости; структура проходов подтверждена.
- **Evidence:** `frontend/src/entities/bank-card/api/queries.ts:120–143`;
`frontend/src/pages/card/ui/CardPage/CardPage.vm.ts:56–93`;
`frontend/src/pages/card/ui/CardPage/CardHistory.view.tsx:38–105`;
`frontend/src/features/instrument-details/ui/InstrumentChart/InstrumentChart.vm.ts:13–37`;
`frontend/src/features/instrument-details/ui/InstrumentChart/InstrumentChart.view.tsx:94–153`;
`frontend/vite.config.ts:9–11`.
- **Claim:** all journal mode materializes all returned rows, then maps them
в presentation, chart/table model проходит все candles. Реального slow render,
memory pressure или INP regression **UNVERIFIED**.
- **Current mechanism:** sequential pages + accumulated items in one Query;
view строит каждый EntryCard. Chart creates numeric series and exact reversed
priceRows; ScrollArea ограничивает viewport, но не количество rows.
- **Impact / actual cost:** potential CPU/DOM/memory cost на длинной истории;
**Impact unmeasured**. Ни число обычных операций, ни length candles, ни commit
time не измерены. Перенос на virtualization пока не оправдан.
- **Why now:** только если пользователь/безопасный profiler подтвердит лаг
на выбранном all-mode или all-history. Не считать guideline threshold
универсальным финансовым требованием.
- **Minimum change:** сначала один synthetic representative profile. При
установленной render стоимости ограничить presentation/использовать native
content visibility там, где подходит; более сложную virtualization добавлять
только при недостаточности native решения и сохранённой accessibility.
- **Before / after API:** сейчас и до измерения `useBankOperations(...limit:
"all")` / `<InstrumentChart vm={vm}>` неизменны. Возможный after меняет
internal row renderer/window, не DTO, query identity и точную таблицу.
- **Guarantees at risk:** полнота выбранного списка, stable IDs, keyboard search/
navigation/scroll, доступность точных money values и целостность source scope.
- **Counterarguments / trade-offs:** наборы данных могут быть небольшими; browser/React Compiler
могут сделать текущую реализацию достаточной. Paging/default page limit уже
ограничивает обычный journal. Chart/table служит доступной альтернативой
изображению: удалять таблицу ради ускорения нельзя.
- **Verification or rollback:** safe synthetic browser profile, зафиксировать
fixture size, viewport и commit/interaction evidence; после изменения сравнить
тот же scenario и keyboard/table availability. Request timing/counts — 07.
При отсутствии измеримого выигрыша оставить нынешний API/renderer.
- **Effort:** M для measurement и возможного narrow fix.
- **Related finding IDs:** FE-003, FE-008, FE-009.
## 5. DELETE / SIMPLIFY / KEEP / INVESTIGATE
| Decision | IDs / область | Решение и сохраняемый смысл |
| --- | --- | --- |
| DELETE | Нет подтверждённых standalone кандидатов в scope 05 | Не удалять entity/feature/widget из-за одного caller или имени. `observe` — сигнал 04 |
| SIMPLIFY | FE-007 | `yagni:` убрать отдельный label-only presentation hook при ближайшем touched Pagination; labels сохранить |
| FIX + native destination | FE-003 | `native:` сначала enabled-aware refetch, затем рассмотреть Query interval у owner; сохранить screen observer policy |
| FIX | FE-001, FE-002, FE-004, FE-005 | Source/transport/read recovery и pending link; исправления в existing boundaries |
| KEEP | FE-008 | Полезные page VMs, page-local composition, portfolio widget, Card scroll presentation hook |
| KEEP | FE-009 | Semantic Mantine wrappers, theme, skeleton/entry/state parts |
| KEEP | FE-010 | RHF/Zod, decimal/bigint, stable intent, submitted replay, explicit version adoption, серверная validation |
| KEEP | FE-011 | Generated DTO/key identity, auth single-flight/queue/generation, connection/access cache fences |
| INVESTIGATE | FE-006 | Явное product решение видимости валютной позиции; source behavior установлен, defect status UNVERIFIED |
| INVESTIGATE | FE-012 | Render hot spots только после measurement |
Complexity-pass `ponytail-audit`: **net: not measured; dependency removals:
none established.** Оценка строк/процентов не выдумывалась; prepared application
diff отсутствует. Correctness fixes могут добавить несколько presentation props,
и это необходимая информация, а не нарушение принципа minimal complexity.
## 6. Cost & benefit
| Область | Ожидаемый класс benefit | Фактическое измерение | Цена / обязательные regression boundaries |
| --- | --- | --- | --- |
| FE-001 source wiring | Reliability и понятность financial result; меньше diagnostic ambiguity | not measured | Account/analytics/empty source presentation; не менять timestamp/money/availability semantics |
| FE-002/004 recovery | Maintenance: один явный section feedback contract; UX восстановления | not measured | Temporary vs access/source errors, cached/empty/missing, draft preservation, GET-only retry |
| FE-003 readiness/native polling | Runtime: исключить неподготовленные GET; cognitive load: один policy owner | not measured; сокращение запросов не оценивалось | Disabled/UID/focus/reconnect/background/unmount и несколько observers; тестовая сложность M |
| FE-005 pending link | Reliability/keyboard UX, малая стоимость local handler | not measured | Pointer + keyboard navigation, failure retains draft, restored link |
| FE-007 local labels | Agent navigation/cognitive load | not measured | Сохранённые a11y names; возможное lint convention решение; выполнять opportunistically |
| FE-008–011 KEEP | Сохранение доказуемых responsibilities и reuse guards | not measured | Не обещать экономию CI minutes или security без исполнения соответствующих checks |
| FE-006 visibility | Potential financial explainability | not measured / intended behavior UNVERIFIED | Сначала contract/product answer; валютный инструмент не считать cash, totals не пересчитывать |
| FE-012 rendering | Potential runtime CPU/DOM/memory | not measured / bottleneck UNVERIFIED | Same fixture profiling; exact values, stable keys и keyboard table availability |
Нет основания добавлять новую dependency, store, transport client, global VM
framework или набор генераторов. Текущий installed stack покрывает предлагаемые
изменения. Экономия runtime/CI/agent tokens численно не оценивалась.
## 7. Top actions
Только подтверждённые изменения; **6 действий**, не обязательство реализовать
всё за один PR. INVESTIGATE вынесены в verification plan.
### Quick wins
1. **FE-001:** передать уже вычисленный `source` в AccountPageLoaded и сохранить
metadata в empty branch. Самый маленький production wiring fix; existing
route assertions должны начать проверять source time/attempt вместе с sums.
2. **FE-003:** добавить enabled/fetching guard в общий manual refetch owner;
проверить disabled search и unresolved UID через fake timers/MSW.
3. **FE-005:** блокировать auth Link отдельно от native Fieldset; deferred
submit + keyboard recovery proof.
### Несколько owners / тестовое подтверждение
4. **FE-002 + остальная часть FE-001:** explicit source/client feedback в
investment loaded models и section recovery. Первым representative account,
затем analytics и detail sections; не переносить fetching во view.
5. **FE-004:** локальные cold/cached error/retry branches для cards/history/
summary с existing BankFeedback; сохранить независимость успешных sections.
6. **FE-003:** согласовать investment focus/reconnect/background policy и
перейти к native polling, если narrow proof нескольких observers подтвердит
равносильность. Краткое изменение существующего решения/ADR оправдано только
при изменении semantics hidden tab/minimum interval, не для readiness guard.
FE-007 не входит в обязательные top actions: малый cleanup только при следующем
изменении Pagination. KEEP не превращаются в новые work items.
## 8. Cross-audit signals
| Направление | Короткий сигнал, files и рекомендуемый audit ID |
| --- | --- |
| 06 Backend Architecture | Frontend принимает password minimum в UTF-8 bytes (`frontend/src/features/auth/model/auth.schema.ts:36–42`), artifact содержит `CredentialsDto.minLength/maxLength` (`backend/openapi.json:5239–5250`, совпадает с frontend snapshot). Сверить runtime validators с `specs/.../user-authentication/spec.md:10`; runtime mismatch здесь UNVERIFIED |
| 06 Backend Architecture | UI recovery требует HTTP-only read/retry guarantee; сверить источники `/brokerage-accounts/.../income-*` и historical operations/candles с принятой границей `docs/adr/019-hourly-reads-and-online-instrument-reference.md:9–29`. Backend execution в этом аудите не проверялось |
| 07 API/Data Flow Efficiency | `screen-sync.ts:28–32`, `App.tsx:13–20`, bond/stock query policies: измерить реальный request budget focus/reconnect/timer; FE-003 доказывает readiness bypass, а не нагрузочную метрику |
| 07 API/Data Flow Efficiency | `bank-card/api/queries.ts:120–139` all-mode последовательно собирает страницы; `InstrumentChart.vm.ts:13–37` работает с полным candle result. Оценить payload/GET budget отдельно от render hypothesis FE-012 |
| 07 API/Data Flow Efficiency | `BondsPage.view.tsx:27–109` показывает nominal/couponFrequency/flags/quote/classCode/ISIN; использовать эти actual UI consumers при оценке стоимости online search enrichment, не объявлять detail поля ненужными по внешнему виду карточки |
| 04 Dead Code / Legacy | `resource-query.ts:69–127` observe/observationKey найден только в resource-query tests, production consumers не найдены; `InvestmentRefreshState` найден в stories/test catalog, не page composition. Проверить compatibility/export scope прежде удаления |
| 08 Test Quality | Quiet/missing-warning expectations: `AccountPage.test.tsx:751–855`, `CardPage.test.tsx:315–341`, `AccountIncomePage.test.tsx:618–649`; поправки visible contract требуют пересмотра assertions. `StocksPage.test.tsx:175–237` late response completion barrier — вопрос 08, без самостоятельного test audit |
| 15 Specs / Documentation | FE-006: explicit tests скрывают CURRENCY, `portfolio-view/spec.md:22–33,164–169` говорит другое. Stocks/bonds docs содержат старые source/manual-refresh описания вместе с добавленным Online reference (`stocks.md:149–169,194–199`; `bonds.md:105–134,147–151`) |
| 13 Reliability / Security | Form intent/version и session/connection fences FE-010/011 должны входить в security/atomicity regression boundaries; подтверждение backend CAS/replay/transactions здесь не выполнялось |
Эти сигналы не являются findings аудитов 06/07 и не входят в severity counts.
## 9. Open questions / verification plan
### Вопросы, действительно меняющие решение
1. **CURRENCY visibility (FE-006):** скрывается обычный баланс брокера или
самостоятельная contract position? Разрешить один конфликт tests/spec на
synthetic Portfolio с currency-only и currency+cash. До ответа KEEP filter.
2. **Refresh semantics (FE-003):** «30 минут» — только периодический read либо
minimum для focus/reconnect/mount? Для portfolio focus уже запрещён spec;
остаётся выбор policy других resources и hidden tab. Не ставить 30-minute
staleTime всем queries автоматически.
3. **Multiple observers:** Accounts hook используется page и breadcrumb.
Размещая native interval, какой observer owns periodic screen read?
Самый дешёвый proof — isolated Query wrapper с двумя observers и fake timers.
4. **Screener deep links:** нужен ли возврат к searchId/criteria/page после
browser back/reload? Сейчас local snapshot связен и соответствует button
Search UX. Без этого требования миграция в URL/Query — лишняя работа.
5. **Real render scale (FE-012):** достаточно ли обычной pagination и текущих
chart/table passes? Только synthetic profile с фиксированным размером
fixture/viewport меняет решение о memo/virtualization.
### Самый дешёвый последовательный verification plan
| Шаг | Boundary и ожидаемое доказательство | Команда / ограничение |
| --- | --- | --- |
| V1 | Source metadata проходит реальную VM/container/view цепочку; complete empty, failed attempt и original time различимы | После будущей реализации существующие AccountPage/AccountIncomePage RTL/MSW cases; не только helper unit |
| V2 | Failed background GET оставляет суммы/draft, показывает warning, retry доставляет новый success; source stale остаётся отдельно | Existing page tests с deferred response и ожиданием actual settled failure; permanent access failure очищает data |
| V3 | Disabled/min-length/UID-empty observers не выполняют manual periodic GET; interval stops after unmount | Existing screen-sync boundary расширить fake timers + MSW; focus/reconnect отдельно; production app не нужен |
| V4 | Cold banking error не blank/empty; cards/history/summary independently retry; balance exact | Existing CardsPage/CardPage/BankSummary RTL/MSW tests; same card/range/limit placeholder edit prohibition сохраняется |
| V5 | Auth route не меняется click/Enter во время submit, после failure сохраняется ввод и ссылка активна | Existing AuthForm MemoryRouter scenario с deferred promise; narrow browser keyboard proof для focusability |
| V6 | Form/identity protections остаются при touched changes | Targeted existing OperationForm, client, auth, resource-query, TbankConnectionSection tests; mutation/access/source boundaries не подменять mocks реального backend |
| V7 | Accessibility/visual states | Synthetic narrow/wide browser check без real API: focus return, labels/live status, contrast, table scrolling, reduced motion. Сейчас UNVERIFIED |
| V8 | Performance hypothesis меняет решение только после evidence | Один synthetic profile FE-012, не production benchmark и не full network audit; при отсутствии лагов не добавлять memo/virtualization |
Перед будущими Node-командами — local `nvm use --silent`. Стандартный
`npm run test:run -- <path>` в текущем manifest запускает package build;
его нельзя считать разрешённой read-only командой этой сессии. Поэтому
test commands выше — план для implementation phase, не якобы выполненная
verification. Для application change владелец обязан выбрать local gates;
этот отчёт не заменяет их.
### Итог read-only и semantic review
Все actionable conclusions привязаны к source branch и actual caller.
Защиты money, form replay/version, generated API и session/access reuse
сохранены в KEEP. Проверено, что рекомендации не требуют нового shared
framework или обязательной cross-repo build dependency. Новые artifacts,
кроме этого отчёта, агентом не создавались. Итоговые HEAD совпали с таблицей
раздела 1; tracked/staged/unstaged/untracked status всех пяти repo пустой;
`git diff --check` без замечаний. Root ignored report проверен отдельно
через no-index whitespace check и полный Read.
## Машинно-считываемая таблица для консолидации
| ID | Decision | Severity | Confidence | Repo | Files | One-line impact | Effort | Related IDs |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| FE-001 | FIX | P2 | High | frontend | src/pages/account/ui/AccountPage/AccountPage.tsx; src/pages/account/ui/AccountPage/AccountPage.vm.ts; src/pages/account-income/ui/AccountIncomePage/AccountIncomePage.vm.ts | Source metadata теряется перед financial view | M | FE-002, FE-008, FE-009, FE-011 |
| FE-002 | FIX | P2 | High | frontend | src/features/instrument-details/model/section.ts; src/features/instrument-details/ui/InstrumentSection.view.tsx; src/pages/account/ui/AccountPage/AccountPage.vm.ts; src/pages/accounts/ui/AccountsPage/AccountsPage.vm.ts | Cached investment read failure не имеет warning/retry | M | FE-001, FE-003, FE-004, FE-008, FE-011 |
| FE-003 | FIX | P2 | High | frontend | src/features/tbank-connection/model/screen-sync.ts; src/pages/bonds/ui/BondsPage/BondsPage.vm.ts; src/entities/bond/api/queries.ts; src/app/ui/App.tsx | Timer обходит enabled, focus расходится с portfolio policy | M | FE-002, FE-008, FE-011, FE-012 |
| FE-004 | FIX | P2 | High | frontend | src/pages/cards/ui/CardsPage/CardsPage.view.tsx; src/pages/card/ui/CardPage/CardPage.vm.ts; src/pages/card/ui/CardPage/CardPage.view.tsx; src/entities/bank-card/ui/BankSummary/BankSummary.view.tsx | Banking cold list error может быть blank, read retry отсутствует | M | FE-002, FE-009, FE-010, FE-011 |
| FE-005 | FIX | P2 | High | frontend | src/features/auth/ui/AuthForm/AuthForm.tsx | Auth Link остаётся активным во время submit | S | FE-010, FE-011 |
| FE-006 | INVESTIGATE | P2 | Medium | frontend | src/pages/account/model/positions.ts; src/pages/account/model/positions.test.ts; src/pages/account/ui/AccountPage/AccountPage.test.tsx | CURRENCY скрывается; intended visibility UNVERIFIED | S | FE-008, FE-010 |
| FE-007 | SIMPLIFY | P3 | High | frontend | src/pages/stocks/ui/StocksPage/StocksPagePresentation.vm.ts; src/pages/stocks/ui/StocksPage/StocksPage.view.tsx | Label-only VM добавляет необязательный navigation hop | S | FE-008, FE-009 |
| FE-008 | KEEP | P3 | High | frontend | src/pages/account/ui/AccountPage/AccountPage.vm.ts; src/pages/account-income/ui/AccountIncomePage/AccountIncomePage.vm.ts; src/pages/card/ui/CardPage/CardPagePresentation.vm.ts | Page VMs и UI lifecycle имеют реальную orchestration | S | FE-001, FE-002, FE-003, FE-006, FE-007 |
| FE-009 | KEEP | P3 | High | frontend | packages/ui/src/EntryCard/EntryCard.tsx; packages/ui/src/LoadingSkeleton/LoadingSkeleton.tsx; packages/ui/src/StateView/StateView.tsx; src/shared/ui/SourceSyncStatus/SourceSyncStatus.tsx | Mantine wrappers сохраняют semantic display и busy states | S | FE-001, FE-004, FE-007, FE-010 |
| FE-010 | KEEP | P3 | High | frontend | src/pages/card/ui/OperationForm/OperationForm.vm.ts; src/pages/card/ui/OperationForm/OperationForm.schema.ts; src/entities/bank-card/model/money.ts; src/entities/bank-card/ui/BankCardForm/BankCardForm.vm.ts | Form state защищает exact money, replay и version adoption | S | FE-004, FE-005, FE-006, FE-009, FE-011 |
| FE-011 | KEEP | P3 | High | frontend | src/shared/api/client.ts; src/shared/api/authenticated-query.ts; src/shared/api/resource-query.ts; src/features/auth/model/auth.vm.ts; src/features/tbank-connection/model/connection-boundary.ts | Generated/session/connection boundaries защищают reuse данных | S | FE-001, FE-002, FE-003, FE-005, FE-010 |
| FE-012 | INVESTIGATE | P3 | Low | frontend | src/entities/bank-card/api/queries.ts; src/pages/card/ui/CardPage/CardHistory.view.tsx; src/features/instrument-details/ui/InstrumentChart/InstrumentChart.vm.ts; src/features/instrument-details/ui/InstrumentChart/InstrumentChart.view.tsx | Long-list/candle render cost UNVERIFIED без profile | M | FE-003, FE-008, FE-009 |