Переносит архив `audits/2026-10-11/` (19 файлов) из codex/audit-remediation-2026-10-11 в develop и добавляет точечный allowlist в .gitignore.
1039 lines
100 KiB
Markdown
1039 lines
100 KiB
Markdown
# Аудит 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 |
|