971 lines
87 KiB
Markdown
971 lines
87 KiB
Markdown
# Аудит 07 — API & Data Flow Efficiency
|
||
|
||
## 1. Audit metadata
|
||
|
||
- **Дата:** 2026-10-11. **Результат:** независимый статический аудит полного пути
|
||
React Query → HTTP → Nest → embedded SQLite/Prisma либо T-Bank/MOEX → UI.
|
||
- Workspace: `/Users/ksv741/Project/vibe/WealthLedger`.
|
||
- Единственное изменение: новый
|
||
`audit-results/07-api-and-data-flow-efficiency-report.md` в root workspace.
|
||
Исходники, конфигурации, тесты, public artifacts и история четырёх repo read-only.
|
||
- Начальные `git rev-parse HEAD && git status --short`:
|
||
|
||
| Repo / путь относительно указанного workspace | HEAD | Начальный status |
|
||
| --- | --- | --- |
|
||
| root `/Users/ksv741/Project/vibe/WealthLedger` | `dc030f84c979cedb2628ef4bd97c62d6c63a9256` | clean |
|
||
| `frontend` | `c8821200ce699dab1ce65360d1968d5877a2e78b` | clean |
|
||
| `backend` | `0ad84ff4f0b4712abc7e69dc299856f14af6eadd` | clean |
|
||
| `specs` | `fcdc805f6c1b3be6a20a7e858e3e80cbd83a9121` | clean |
|
||
| `docs` | `e6fd4f64b81dd73b33959b845a1babcfac1891c8` | clean |
|
||
|
||
**Инструкции и skills.** В самом начале загружен установленный `ponytail-audit`:
|
||
complexity-pass ищет минимальное сокращение coordination, а performance/correctness
|
||
исследуются отдельно по заданию 07. Применены `performance-optimization`,
|
||
`graphify`, `verification-before-completion`. Прочитаны собственные AGENTS всех
|
||
repo; frontend engineering/architecture/data/auth/testing/TS/UI, backend
|
||
engineering/architecture/data/http/integrations/security/testing/errors/observability.
|
||
Root conventions применены только к итоговому Markdown.
|
||
|
||
**Graphify:** существующего `graphify-out/graph.json` не найдено в root и в четырёх
|
||
repo. Проверены directory listings root/frontend/backend и отдельные repo-local
|
||
Glob. Попытки Glob по отсутствующим frontend/backend `graphify-out` дали ошибку
|
||
поиска; последующий поиск от существующих repo roots — `No files found`.
|
||
Граф, extraction, scripts и дополнительные artifacts не создавались. Все edges
|
||
ниже подтверждены source trace, а не предполагаемым графом.
|
||
|
||
**Источники гипотез:** `technical-audit-2026-10-11.md` прочитан исключительно как
|
||
реестр направлений проверки. Все выводы ниже заново сверены с callers, source,
|
||
конфигурацией и intended semantics. Отчёты аудитов 05/06 и другие результаты
|
||
консолидации не читались.
|
||
|
||
**Метод и исполненные проверки:** Read/Glob/Grep с абсолютными repo paths;
|
||
Git metadata в пяти cwd; byte-for-byte сравнение имеющихся public artifacts:
|
||
|
||
```text
|
||
cwd = /Users/ksv741/Project/vibe/WealthLedger
|
||
git diff --no-index --exit-code "backend/openapi.json" "frontend/src/shared/api/spec/openapi.json"
|
||
result = exit 0, output empty
|
||
```
|
||
|
||
Это подтверждает равенство файлов на диске, **не** свежую генерацию runtime schema.
|
||
Проверены пути operations, bond search и screener в snapshot:
|
||
`frontend/src/shared/api/spec/openapi.json:2356,3336,4194,6066`.
|
||
|
||
**Library semantics:** Context7 `/tanstack/query` подтвердил ручной `refetch` при
|
||
`enabled: false`, stale mount/focus/reconnect и независимость interval от staleTime;
|
||
документация также различает автоматический dedup и императивный cancel/refetch.
|
||
Источники: [disabling queries](https://tanstack.com/query/latest/docs/framework/react/guides/disabling-queries),
|
||
[important defaults](https://tanstack.com/query/latest/docs/framework/react/guides/important-defaults),
|
||
[useQuery](https://tanstack.com/query/latest/docs/framework/react/reference/useQuery).
|
||
Установлен Query `5.102.8`, `openapi-react-query 0.5.4`:
|
||
`frontend/package.json:46,54`. Context7 lookup openapi-react-query не дал точного
|
||
пакета; вместо подмены другим generator прочитан установленный source
|
||
`frontend/node_modules/openapi-react-query/src/index.ts:194–219`:
|
||
key = method/path/init, queryFn передаёт AbortSignal.
|
||
Context7 `/prisma/web` и официальный transactions URL сейчас выдают Prisma 8,
|
||
тогда как проект использует `7.10.0` (`backend/package.json:50–51,64`).
|
||
Version-specific SQL expansion/parallelism **UNVERIFIED**; бюджет ниже считает
|
||
явные Prisma model calls, не обещает «include = один SQL» и не предлагает API v8.
|
||
Nest guard/controller/DTO поведение установлено по исходникам, спорной
|
||
library-specific Nest семантики в recommendations нет.
|
||
|
||
**Не запускалось:** приложение/порты, live bank/broker/MOEX/NCR API, install,
|
||
build, full-suite, codegen, migrations, production benchmark, staging/commit/PR.
|
||
Тесты не запускались: достаточен static proof механизмов; штатные frontend test
|
||
scripts запускают build packages, а SQLite suites создают/изменяют БД. Чтение
|
||
тестов — evidence их intent, не утверждение об их текущем прохождении.
|
||
|
||
## 2. Executive verdict
|
||
|
||
1. **Факт:** Accounts и Portfolio читаются локально; Events не делает upstream RPC,
|
||
но может вычислять и публиковать forecast в SQLite на GET. Основной read-model
|
||
оправдан и должен сохраниться.
|
||
2. **Подтверждённая граница лишней работы:** обычные Income GET инициируют
|
||
membership/operations, а Ledger дополнительно планирует отсутствующую историю
|
||
вне выбранного периода. Это второй владелец работы рядом с hourly walk,
|
||
FLOW-001. Даже HTTP `sync=observe` у Ledger не является passive переключателем.
|
||
3. **Подтверждённый fan-out, не измеренная задержка:** online bond search =
|
||
`1 + N + I(N>0)` logical RPC, `0≤N≤30`; details параллельны между собой,
|
||
prices ждёт их завершения. Detail-поля действительно нужны карточке;
|
||
безопаснее сначала убрать независимую waterfall, FLOW-003.
|
||
4. **Подтверждённые усилители работы:** timer вручную refetch-ит disabled queries
|
||
(FLOW-002); неизвестный заранее invalid broker token превращается в HTTP 500
|
||
и повторяется frontend transport до трёх раз (FLOW-004).
|
||
5. **Факт структуры DB cost:** banking history выполняет `3 + Σ(1+t+s+c+r)`
|
||
model reads на страницу; embedded SQLite устраняет сетевую DB latency, но не
|
||
стоимость повторного materialization/queries, FLOW-005.
|
||
6. **Fresh/consistent risk по коду:** hourly reuse стабильного bond calendar и
|
||
пересекающийся fallback forecast способны удерживать старый результат после
|
||
новой попытки, FLOW-006/007. Это не аргумент удалять CAS или complete publication.
|
||
7. **Непроверенные гипотезы:** общий выбор 30 секунд / 30 минут, полная hydration
|
||
всех portfolio snapshots и объём income DTO требуют policy/scale evidence,
|
||
FLOW-008/009. Их нет в confirmed actionable severity.
|
||
|
||
**Counts:** actionable **7**: P0=0, P1=0, P2=7, P3=0.
|
||
Отдельно **KEEP=1**, **INVESTIGATE=2**. Runtime latency, throughput, bytes,
|
||
production rate exhaustion и ускорения **UNVERIFIED**; для всех findings
|
||
**Impact unmeasured**. Severity основана на подтверждённом механизме и локальном
|
||
риске, не на выдуманных p95 или процентах.
|
||
|
||
## 3. Inventory & coverage
|
||
|
||
### 3.1. Метод подсчёта и область применимости
|
||
|
||
Изучены 8 сценариев A–H, включая все пять обязательных. Выборка risk-based:
|
||
личные reads, online fan-out, cursor/history, аналитика, scheduler/calendar,
|
||
financial read/write projection и catalog pagination. Это не endpoint census
|
||
всего приложения и не аудит frontend composition/backend domain style.
|
||
|
||
- **HTTP:** один GET/POST/PUT к приложению — один attempt. Query cache hit = 0;
|
||
shared in-flight Query key = один автоматический fetch, а не число observers.
|
||
- **RPC:** `TbankClient` method invocation — logical RPC. Transport attempts
|
||
отдельно: 1–2 при допущенном RPC; queue rejection = 0 сетевых attempts.
|
||
MOEX HTTP отдельно от gRPC, NCR HTTP отдельно от обоих.
|
||
- **DB:** считаются явно вызванные Prisma model reads. `$transaction`, BEGIN,
|
||
COMMIT, relation SQL expansion, disk I/O, hydration time не приравниваются к
|
||
отдельному удалённому round-trip. Нужен SQL trace для actual SQL count.
|
||
- Сценарии счёта имеют действующую сессию/connection/membership. JWT validate не
|
||
читает DB (`backend/src/modules/auth/infrastructure/jwt.strategy.ts:21–25`).
|
||
Auth bootstrap/refresh, logos CDN и действия пользователя вне одного сценария
|
||
вынесены за основной бюджет. Ошибки access могут оборвать граф раньше.
|
||
- «Типовой» ниже означает **явно заданную условную ветку**, не measured median.
|
||
Произвольная сессия с focus/reconnect/таймерами не имеет конечного общего max.
|
||
|
||
Обозначения:
|
||
|
||
| Symbol | Значение |
|
||
| --- | --- |
|
||
| `A` | число доступных счетов текущего подключения |
|
||
| `N` | выбранные search results после дедуп ISIN/UID и limit, 0…30 |
|
||
| `R_T` | недостающие account/month intervals выбранного Ledger/Income scope |
|
||
| `R_H` | недостающие history intervals вне выбранного scope |
|
||
| `p_j` | cursor pages interval j, ≥1 при завершённой загрузке даже пустого interval; upper unknown |
|
||
| `m` | membership logical RPC, 0 или 1 при новом source attempt |
|
||
| `K` | actual distinct metadata producer calls, 1–2 на producer (UID/FIGI fallback) |
|
||
| `M(X)` | metadata DB reads: `ceil(U/400)+ceil(F/400)+ceil(V/400)` для distinct UID/FIGI и найденных canonical UID |
|
||
| `b` | непустые calendar batches: `ceil(S/100)+ceil(B/100)` для unique stock/bond UID в scope |
|
||
| `J` | дополнительные DB reads awaited source reservation branches; зависит от missing coverage/active work |
|
||
| `Q_F` | DB reads forecast.resolve на выбранной ветке; формула ниже |
|
||
|
||
M не равен количеству positions: identifiers дедуплицируются, matched UID может
|
||
отсутствовать или быть неоднозначным. Источник: metadata repository `44–112`.
|
||
|
||
### 3.2. Статический request graph с actual callers
|
||
|
||
```text
|
||
App QueryClient (focus/reconnect on; Query retries off)
|
||
→ session-aware query → generated [method,path,init] / AbortSignal
|
||
→ authenticatedFetch → GET transport: deadline + ≤2 transient retries
|
||
→ Nest JWT guard / DTO validation / user.sub
|
||
Accounts → Portfolio.saved → access.local + SQLite snapshots + hourly
|
||
Portfolio → saved + saved metadata → точная projection → UI
|
||
Operations → local + readPlan → local page + metadata → UI cursor page
|
||
↘ missing chosen scope AND history backfill → worker RPC
|
||
Income → collectRanges → local period union → exact totals/entries → UI
|
||
↘ membership.request → readPlan(history:true) → worker RPC
|
||
Events → local Portfolio + saved calendar → forecast.resolve (conditional write)
|
||
Bonds search → onlineToken → FindInstrument → N BondBy → GetLastPrices
|
||
→ public projection → card fields → UI
|
||
Hourly native tick → durable reservation → membership → portfolios → operations
|
||
→ calendar sources → local forecast calculation/publication
|
||
```
|
||
|
||
Query/transport owners: `frontend/src/app/ui/App.tsx:13–21`,
|
||
`frontend/src/shared/api/authenticated-query.ts:15–50`,
|
||
`frontend/src/shared/api/client.ts:53–119,127–151,205–249`.
|
||
Backend composition inspected in portfolio/operations/income/instruments/
|
||
instrument-sources/tbank-sync module files; the source providers are shared,
|
||
not redeclared per consuming module.
|
||
|
||
### 3.3. Flow Request Ledger
|
||
|
||
Числа относятся к одной успешной попытке queryFn без transport retries;
|
||
retry multipliers и background work раскрыты отдельно. Cache min=0 требует
|
||
fresh cache для **всех** relevant keys и отсутствия imperative refetch.
|
||
|
||
| Flow / scenario + source | HTTP min / conditional typical / max одной загрузки | Foreground DB reads | Upstream logical / critical path | Conditions / cache / enabled |
|
||
| --- | --- | --- | --- | --- |
|
||
| **A Accounts list**: AccountsPage.vm `21–24` + AccountsController `19–22` | 0 / 1 cold / 1 | 6 при valid connection | 0 / local DB + DTO | accounts staleTime 1 800 000 ms; shell observers share same key; session required |
|
||
| **B Account overview/Portfolio**: AccountPage.vm `66–84` | 0 / 4 если accounts fresh, иначе 5 / 5 distinct GET | `6 + (4+M(P)) + (10+M(L)+J) + (7+M(P)+M(S)+3b+Q_F) + income branch` | local Portfolio=0 RPC; total может включать D/E background; foreground GET параллельны | portfolio/operations/events/income enabled accountId; operations limit5; same account key title/breadcrumb dedup |
|
||
| **C Online bond search**: BondsPage.vm `9–13`, BondController `55–65`, discovery `295–337` | 0 disabled/fresh / 1 valid cold / 1 | 1 onlineToken read | empty=1; nonempty=N+2 (3…32); path Search → slowest Detail → Prices | debounce300ms; trimmed q≥2; Query staleTime30s; backend uppercase normalizes; frontend case-different keys не дедупятся |
|
||
| **D Brokerage Ledger**: AccountOperationsPage.vm `77–99`, history hook `12–30`, OperationsController `22–38` | 0 / 1 page GET + shared accounts GET если cold / 2 включая shell | `10 + M(page) + J`, cursor ещё +5; SQL expansion unknown | background `Σ p_j`, j∈dedup(R_T∪R_H); foreground не ждёт RPC | page100; backend max1000, take limit+1; filters/categories/cursor server-side; full coverage chosen period не отменяет missing history |
|
||
| **E Income analytics**: AccountIncomePage.vm `64–69`, IncomeController `66–79` | 0 / 1 analytics + accounts GET если cold / 2 включая shell | observe=6; ordinary fully covered/fresh membership=20 + one write fence; extra `J` при missing work | `m + Σ p_j` over missing period union + outside history; foreground обычно local, membership.done schedule detached | selected/previous/yearAgo ≤3 calendar periods, unlimited duration; frontend НЕ передаёт observe; summary на overview тот же owner с 1 периодом |
|
||
| **F Hourly refresh + forecast**: scheduler `56–175` | 0 browser requests; 0 Nest HTTP инициаторов | variable reservations/staging/publication; `q` poll iterations → q work reads; finite max unknown | `1+2A+Σp_i+K+D_c+S_c` T-Bank плюс MOEX; sequential owner phases | one process native tick60s, due60min, no overlap reservation; missing/invalid connection gates; detail/dividend/bond-event stable cache |
|
||
| **G Manual bank card history**: CardPage.vm `35–45`, bank-card queries `76–144` | 0 session disabled / 4 cold or stale mount GET (card/cards/history/summary) / 4; all mode=3+P_h | history `3+Σ(1+t+s+c+r)`; other endpoints separately | 0 RPC; all mode sequential HTTP pages | default staleTime0 revalidates on mount; UI page10; choices5/10/20/50/100/all; backend limit100; all loop100/page until hasNext=false |
|
||
| **H Stock screener**: StocksPage.vm `29–55,65–101`, StockScreenerService `65–109` | idle0 / explicit search1 POST /1; next page1 GET | search1 current read; next page1 local read | search1 Shares RPC; page0 RPC | outside Query: no Query cache/dedup; snapshot TTL5min/max100/page30/user+revision; GET retry applies only to page, POST not transient retried |
|
||
|
||
Paths в строках с сокращёнными именами раскрываются в findings и owner tables.
|
||
Полные frontend sources flows A–H:
|
||
`frontend/src/pages/accounts/ui/AccountsPage/AccountsPage.vm.ts:21–24`,
|
||
`frontend/src/pages/account/ui/AccountPage/AccountPage.vm.ts:66–84`,
|
||
`frontend/src/pages/bonds/ui/BondsPage/BondsPage.vm.ts:9–13`,
|
||
`frontend/src/pages/account-operations/ui/AccountOperationsPage/AccountOperationsPage.vm.ts:77–99`,
|
||
`frontend/src/pages/account-income/ui/AccountIncomePage/AccountIncomePage.vm.ts:64–69`,
|
||
`frontend/src/pages/card/ui/CardPage/CardPage.vm.ts:35–45`,
|
||
`frontend/src/pages/stocks/ui/StocksPage/StocksPage.vm.ts:29–55,65–101`.
|
||
Flow F инициируется только
|
||
`backend/src/modules/investments/tbank-sync/tbank-hourly.scheduler.ts:56–175`.
|
||
Для B сумма DB — foreground endpoints одного холодного overview, а не transaction
|
||
всего экрана; J/background не измерены. Для D/E общий session max — unknown;
|
||
invalid cursor может дать дополнительный fetch первой страницы
|
||
(`frontend/src/entities/broker-operation/model/useBrokerageOperationHistory.ts:32–59`).
|
||
Для G `P_h=max(1,ceil(T/100))` только при стабильном наборе T операций; при
|
||
конкурентных изменениях offset dataset точный total requests unknown.
|
||
|
||
**Дополнительный detail flow.** BondPage.vm `23–55`: detail → после получения UID
|
||
quote/metrics/candles/position/coupons/operations (6 dependent queries). Title
|
||
`frontend/src/app/routes/DocumentTitle/DocumentTitle.tsx:62–66` использует тот же
|
||
detail key, не дополнительный GET. Для успешной холодной загрузки без redirect
|
||
7 distinct GET; UID→ticker canonical navigation (`BondPage.tsx:10–12`, VM `62–65`)
|
||
может добавить новый detail key/GET, итого 8. Detail by UID =1 RPC, by ticker =
|
||
Search→Detail=2 RPC (`discovery:227–249`). Остальные endpoints могут планировать
|
||
source workers; их общий count зависит от coverage, calendar/source cache и
|
||
пагинации, **unknown**, не «7 HTTP = 7 RPC». Quote/metrics разные keys, но
|
||
overlapping source work coalesces в одном enrichment owner. Online detail не
|
||
публикуется в shared source cache, поэтому не заменяет cold worker detail.
|
||
|
||
### 3.4. Cache & Refresh Ownership Matrix
|
||
|
||
| Data/cache | Identity / lifetime | Owner / trigger | Dedup / isolation / замечание |
|
||
| --- | --- | --- | --- |
|
||
| frontend accounts | generated method/path; stale30min | account entity; mount/focus/reconnect + screen timer | page/title/breadcrumb/switch разделяют Query; timer только page |
|
||
| frontend portfolio/ledger/income/events | generated path+account+period/categories/cursor/limit; stale30s | entity + page useScreenSync interval30min | нет дедуп по похожему payload или разному limit; manual refetch игнорирует enabled |
|
||
| frontend bond/stock detail/search/sections | identifier/period/q в generated key; stale30s | entity, detail-dependent enabled; page timer | case q differs while backend normalizes uppercase; online requests не coalesce на backend |
|
||
| frontend bank queries | generated key, all mode добавляет `all`; default stale0 | entity; mutations invalidate related subtree | placeholder only same card/period/limit; all mode включает собственный pagination loop |
|
||
| session/connection cache | Query cache reset/clear, token in memory | auth + connection commit | generated keys без user/revision допустимы только с этой boundary; KEEP |
|
||
| saved membership/portfolio | user/revision/account; freshness60min | hourly membership force + portfolio force | pending/reservation coalesces; GET local; missing differs from confirmed empty |
|
||
| operations/coverage | user/revision/account/time/instrument; exact intervals | hourly mutable current-month tail; first missing reads + legacy history | page cursor binds owner/filter/revisions; background missing range dedup; FLOW-001 |
|
||
| instrument metadata | provider+UID/FIGI lookups; `fresh=complete` | portfolio metadata worker; savedMany reads local | batching400, producer identity shared; no credential/holding in public records |
|
||
| online search/detail/company | no persistent online result | discovery/section read request | synchronous upstream; no server single-flight shown; online-only requirement, не автоматически defect |
|
||
| quote/comparison/trading status/bond-current/yield | instrument/source/kind/range; TTL60min | enrichment requestMany quote policy | prices batch up to30, shared source producers; ordinary quote/metrics GET can create work |
|
||
| dividend/coupon/bond-events/detail/rating source | stable after successful publish; dividends/coupons range key | enrichment + hourly event caller | bonds no calendar range key; dividends hourly exact window changes daily; FLOW-006 |
|
||
| candles | instrument/range coverage, complete past immutable | first uncovered read; queued gaps, no all-history prefetch | window split, same-scope producer; today tail/quote comparison отдельны; don't remove completeness |
|
||
| forecast | user/revision/scopeKey incl window; dependency CAS | ForecastService.result from events/coupons GET and hourly get | GET can write; no RPC in events get; full fallback only; FLOW-007 |
|
||
| screener snapshot | searchId + user/revision, TTL5min, max100 | explicit POST; GET pages readonly | coherent page30; separate from Query; KEEP |
|
||
| NCR registry | one Promise entry, TTL6h; failure TTL60s | NcrRatingsClient.rating | public registry single-flight + negative failure cache; not personal data |
|
||
|
||
Owners/evidence: frontend query files перечислены в ledger;
|
||
`frontend/src/features/tbank-connection/model/screen-sync.ts:20–51`;
|
||
`backend/src/integrations/tbank/tbank-source.policy.ts:3–21`;
|
||
`instrument-metadata.repository.ts:295–329`;
|
||
`catalog-enrichment.service.ts:178–227,496–579,596–625,679–697`;
|
||
`candle.service.ts:101–124,264–325`;
|
||
`stock-screener.service.ts:19–26,84–109,131–138`;
|
||
`backend/src/integrations/ncr/ncr-ratings.client.ts:12–55`.
|
||
|
||
**Duplicate-query вывод:** несколько useBrokerageAccounts consumers не означают
|
||
несколько HTTP. Title `DocumentTitle.tsx:24–32`, breadcrumbs
|
||
`AccountBreadcrumbs.tsx:16–25`, switch `AccountScopeSwitch.vm.ts:8–20`, page
|
||
используют один entity hook. Query promise/cache работает на same key.
|
||
Императивные timer.refetch, direct screener API, разные limit/period/q и backend
|
||
source work — за пределами этого автоматического dedup. Нет production
|
||
hydrate/dehydrate/persistQueryClient пути в просмотренном `frontend/src`;
|
||
`useResourceQuery:129–148` только проецирует envelope, не создаёт второй cache.
|
||
|
||
### 3.5. Upstream call budget table
|
||
|
||
| Source / условный сценарий | Logical calls без retry | Network attempts / границы | Total vs critical path |
|
||
| --- | --- | --- | --- |
|
||
| Search пустой | 1 FindInstrument | 1…2 если admitted | один RPC |
|
||
| Search N>0 | `1+N+1`, max32 | до `2(N+2)` при recoverable retry каждого RPC | Search → details cohort → Prices; не N последовательных деталей |
|
||
| Search transient response/body timeout, browser ≤3 attempts | до `3(N+2)` logical если каждый server attempt дошёл до всех stages | до `6(N+2)`; при N30 условная граница192 | разные server attempts могут перекрываться; это не measured traffic |
|
||
| Search invalid token на FindInstrument | 1 на backend invocation; browser3 →3 | auth gRPC НЕ retryable | FLOW-004, mapping убирает повтор, не исходный auth check |
|
||
| Invalid token только на detail cohort | `1+N`, prices не начинается; ≤3(1+N) при трёх HTTP | queue admission/failure может уменьшить count | Promise.all отвергает весь request, уже запущенные sibling details не отменяет |
|
||
| UID detail / unambiguous ticker detail | 1 / 2 | ≤2 / ≤4 | ticker сначала search, затем detail; consumer действительно требует identity |
|
||
| Company with assetUid | 1 detail +2 asset calls | ≤6 | detail → parallel(description,fundamentals); absent assetUid=1 |
|
||
| Hourly successful baseline без new sources | `1 + 2A + Σp_i` | до удвоения T-Bank calls при transient recovery | membership → Σ account portfolio pair(max of2) → Σ operations pages → events |
|
||
| Hourly new metadata/calendar | добавка `K + D_c + S_c`; K=1…2 на producer; D_c=new instrument detail; S_c=new stock dividend window | ≤2× добавка T-Bank | metadata queue может работать в фоне, enrichment drain сериализует source producers |
|
||
| Hourly new bond calendar | MOEX `Σ_b(c_b+o_b+1)` HTTP | retry не добавляется; каждая request timeout10s | parallel(coupons pages,offers pages,description), page цепочки последовательны |
|
||
| Ledger first missing + outside history | `Σ_{j∈dedup(R_T∪R_H)} p_j` | ≤2Σp_j на завершённых admitted pages | worker one page/turn, cursor chain sequential; count не зависит от UI limit5/100 |
|
||
| Income обычный GET | `m + Σ_{j∈dedup(R_T∪R_H)} p_j` | ≤2(m+Σp_j) за один successful worker pass | membership.done → schedule; response не ждёт worker completion |
|
||
| Local Accounts/Portfolio/Events GET | 0 T-Bank, 0 MOEX | 0 | local read/DTO; events forecast CAS write возможен |
|
||
| Screener POST / page GET | 1 Shares /0 | ≤2 /0 | весь online catalog на явное действие; pagination local snapshot |
|
||
| NCR rating | cache miss1 HTTP; same pending/cache0 | timeout10s, failure cooldown60s | registry shared across ISIN, отдельный от hourly events источник |
|
||
|
||
`D_c/S_c` считаются по effective producers после shared cache/coalescing, не по
|
||
сумме позиций счетов. `c_b/o_b≥1` для нормальных успешных MOEX page walks;
|
||
без runtime payload верх неизвестен. T-Bank metadata/service details:
|
||
`tbank.client.ts:261–315,358–448,504–585`;
|
||
MOEX `moex.client.ts:83–93,119–177`.
|
||
|
||
**Retry/deadline budget.** Frontend GET: ≤3 attempts, 15s на fetch **и чтение body**,
|
||
1s+2s паузы (`client.ts:15–18,127–151,205–249`). Если срабатывают все три
|
||
deadline, programmed component ≈48s, не guaranteed wall-clock SLA.
|
||
Query retries globally false. При UNAUTHORIZED возможен один auth refresh и
|
||
protected replay; консервативная compositional граница ≤7 HTTP attempts
|
||
(два read phases до3 плюс один refresh), но сочетание зависит от того, какая
|
||
attempt вернула 401. Сетевые server-side effects не начинаются при auth reject.
|
||
Known InvalidTbankToken/Forbidden/NotFound не transport retried.
|
||
|
||
T-Bank queue starts `r`/second (default5), waiting capacity50, RPC deadline15s
|
||
назначается **после** queue wait, transient retry один через1s. Queue wait не
|
||
входит в RPC deadline; default не означает фактический production config.
|
||
Search32 занимает `ceil(32/r)` rate windows при чистом бюджете; при default r5
|
||
это7 окон, не обещание7 секунд. Уже есть limiter, добавлять pool/retry вокруг
|
||
него не нужно. HTTP abort не передаётся controller→TbankClient; client request
|
||
возвращает Promise, handle не сохраняется для cancel (`tbank.client.ts:571–585`).
|
||
Поэтому новые HTTP attempts могут выполнять тот же online fan-out, пока старый
|
||
ещё жив. Actual timeout incidence **UNVERIFIED**. Durable first-load work
|
||
coalesces; **нельзя** безусловно умножать весь history import на3.
|
||
|
||
### 3.6. DB Read Pattern table
|
||
|
||
| Path | Source-inferred model-read count | Pattern / limit / interpretation |
|
||
| --- | --- | --- |
|
||
| access.local | 1 model call with accounts relation | `tbank-access.service.ts:262–288`; access gate, SQL child queries unknown |
|
||
| hourly metadata | 2 when valid connection | `tbank-sync.repository.ts:245–259`; current revision + work |
|
||
| Portfolio.saved | 1 access +1 snapshot relation read +2 hourly =4 | `portfolio.service.ts:55–59`, `portfolio.repository.ts:25–47`; loads ALL account payloads before service filter |
|
||
| Accounts.list | saved4 + repeated hourly2 =6 | `brokerage-accounts.service.ts:17,39`; projects summaries, no upstream; repeated lookup real, significance unmeasured |
|
||
| Portfolio.read | `4+M(positions)` | saved metadata batch400; no per-position remote/detail call |
|
||
| Ledger readPage | 5, plus relation SQL unknown | connection,coverage,work,membershipWork,rows `operations.repository.ts:82–218`; `take limit+1`, stable three-field order |
|
||
| Ledger foreground | `10+M(page)+J`; cursor extra5 | `operations.service.ts:74,119,145–153,214`; first readPage validates binding before scheduling, preserve validation-before-side-effect |
|
||
| Income observe | 1 access+5 readPage=6 | exact period union, categories income, no `take`; not a row N+1 |
|
||
| Income ordinary full warm path | 20 reads +1 updateMany fence, no worker launch | initial access1+page5; membership reserve read1; schedule access1+page5+readPlan(access1+coverage1); final page5; source scheduling J if missing |
|
||
| Events foreground | `7+M(portfolio positions)+M(supported positions)+3b+Q_F` | get: access1+portfolio4+hourly2; saved metadata again; observe readMany(access/source/work)=3 per batch |
|
||
| forecast.resolve | no candidate:2+I(fallback lookup); candidate early fallback:1+I(fallback lookup); candidate validation:2+I(fallback lookup)+ceil(sourceVersions/100) reads | write fence/upsert counted separately; `forecast.repository.ts:35–174`; preserve parameter-limit batches and atomic source validation |
|
||
| Bank history | `3+Σ(1+t+s+c+r)` | card ownership + count +page, then per-row loaders; LIMIT≤100 + offset. Valid income1 / transfer2 / expense3 / refund4 extra reads per row |
|
||
| Bank all mode | sum history cost over P_h HTTP pages | full hydration deliberately user-selected; no runtime scale bound in frontend all loop |
|
||
| Screener search/page | 1 /1 model read | selected result stored in process Map, no DB per instrument |
|
||
|
||
Ledger base10 assumes selected nonempty intervals: access1 + readPlan access1/
|
||
coverage1 + readPage5 +hourly2. Empty selected scope skips readPlan and subtracts2.
|
||
`J` excludes detached worker staging/publish reads. Income ordinary20 assumes
|
||
membership fresh, targets nonempty, all historical coverage already complete,
|
||
no connection switch; refresh-launched membership detaches schedule and changes
|
||
foreground count, so20 is not global minimum/maximum. Metadata formula includes
|
||
empty and missing identifiers, не максимум3 для произвольного all portfolio.
|
||
|
||
No DB latency/SQL query count measured. Embedded database подтверждена
|
||
`backend/prisma/schema.prisma:7–9`,
|
||
`backend/src/prisma/prisma.service.ts:16–21` и `env.validation.ts:35–36`.
|
||
Новые indexes/remote DB/cache tier без EXPLAIN/profile не обоснованы.
|
||
|
||
### 3.7. DTO / filtering / privacy / observed metrics
|
||
|
||
- Operations фильтрует time/category/instrument на сервере, page bounded;
|
||
payload содержит financial details сверх compact row UI, но contract ledger
|
||
требует точную запись (`specs/.../brokerage-account-ledger/spec.md:31–44`).
|
||
Удалять поля только по одному presentation consumer нельзя.
|
||
- Income читает union2–3 periods одним page loader без limit и формирует totals,
|
||
comparisons, buckets, instruments, entries (`income-analytics.service.ts:81–93,138–162`).
|
||
UI display использует entries/comparisons, не buckets/instruments
|
||
(`frontend/src/pages/account-income/model/display.ts:97–129`). Однако все выплаты
|
||
сразу и unlimited period — явное требование spec `10,157–179`; pagination или
|
||
усечение сумм не являются механическим performance fix, FLOW-009.
|
||
- Account preview получает весь events dataset и отображает первые5
|
||
(`frontend/src/pages/account/model/previews.ts:75–96`), тогда как operations
|
||
уже запрашивает limit5. Полная standalone events page — реальный consumer;
|
||
новый preview endpoint без bytes/scale evidence не предлагается.
|
||
- Public DTO projections — trust boundary, а не лишняя копия Prisma rows:
|
||
`publicInstrument`, `toBond`, `mapBrokerageOperation`, safe error envelope.
|
||
Shared instrument/calendar cache не содержит token/user holdings; private
|
||
forecasts, coverage, snapshots и cursors ограничены user/revision/account.
|
||
- Доступен **исторический synthetic baseline**, не текущий measurement:
|
||
`backend/test/fixtures/tbank-read-baseline.json:2–18` (immediate upstream,
|
||
limiter/retries excluded, прежнее состояние persistence). Его numbers не
|
||
переносились на HEAD. Current benchmark source `test/tbank-read-baseline.e2e-spec.ts:502–541`
|
||
тоже исключает transport limiter и создаёт artifacts; он не запускался.
|
||
Текущие production logs/trace и RUM в evidence отсутствуют.
|
||
|
||
## 4. Findings
|
||
|
||
### FLOW-001 — Personal GET сохраняет незапрошенный import/backfill
|
||
|
||
- **Decision:** FIX. **Severity:** P2. **Confidence:** High. **Effort:** M.
|
||
- **Evidence:** `backend/src/modules/investments/operations/application/brokerage-operations.service.ts:143–151,391–435`;
|
||
`backend/src/modules/investments/operations/application/brokerage-operations-sync.service.ts:46–125`;
|
||
`backend/src/modules/investments/operations/presentation/brokerage-operations.controller.ts:22–38`;
|
||
`backend/src/modules/investments/operations/presentation/brokerage-operation.dto.ts:36–82`;
|
||
`backend/src/modules/investments/income/income-analytics.controller.ts:24–79`;
|
||
`backend/src/modules/investments/investment-sync-plan.ts:287–298`.
|
||
- **Claim / consumers:** Account overview recent operations+income summary,
|
||
AccountOperationsPage and AccountIncomePage ordinary GETs can launch work beyond
|
||
the permitted first missing Ledger interval. Income invokes membership;
|
||
Ledger also invokes history backfill even with `sync=observe` because its DTO
|
||
has no sync field and controller не выбирает observe.
|
||
- **Current mechanism:** both production callers pass `history:true` to
|
||
`readPlan`; helper computes lifetime history minus shown targets and requests
|
||
remaining missing intervals at history priority. Unknown opening falls back
|
||
to1970. Income membership TTL miss can fetch GetAccounts, then detached schedule
|
||
plans periods and history; no TTL miss still enters reservations/schedule.
|
||
- **Impact / actual cost:** `Ledger=Σp(R_T∪R_H)` вместо `Σp(R_T)`;
|
||
`Income=m+Σp(R_T∪R_H)` вместо0 personal RPC. Page limit5/100 не ограничивает
|
||
backfill месяцы/pages. Полный выбранный период + отсутствующий другой месяц
|
||
достаточно для лишнего RPC даже при пустом upstream результате. При failures
|
||
повторные reads могут снова резервировать failed work; active work coalesces,
|
||
поэтому amplification не равна автоматически3×. **Impact unmeasured.**
|
||
- **Why now:** сохранённая intended boundary явно ограничивает исключение
|
||
(`specs/openspec/specs/tbank-data-sync/spec.md:9–22`,
|
||
`brokerage-account-ledger/spec.md:131–144`); hidden work расходует общий limiter
|
||
и source очередь в конкуренции с hourly/online search.
|
||
- **Minimum change:** у Ledger выключить outside-scope history branch; у Income
|
||
сделать collectRanges чтением независимо от совместимого observe flag.
|
||
Сохранить Ledger first-missing plan и complete-result/readiness contract.
|
||
Новый endpoint или общий refresh coordinator не нужен.
|
||
- **Guarantees at risk:** current revision/account membership, exact interval,
|
||
cursor prevalidation, coverage полнота, atomic reconciliation и сохранённые
|
||
суммы. Нельзя выдавать отсутствие истории за нулевой income.
|
||
- **Counterarguments / trade-offs:** прежний UX preloads всю историю и облегчает
|
||
comparisons. Current specs это запрещает; cold history Income может оставаться
|
||
incomplete до разрешённой загрузки Ledger. Этот UX нужно согласовать явно,
|
||
не восстанавливать covert import. Не удалять loading/history metadata вслепую.
|
||
- **Verification or rollback:** isolated seeded real SQLite + mocked T-Bank:
|
||
выбранный полный January и отсутствующий December; Ledger GET/no observe/observe
|
||
должен дать0 RPC; missing January должен получить толькоJanuary; Income GET
|
||
должен не создавать membership/work. Assert requested intervals, not only count.
|
||
Existing tests `backend/test/tbank-ledger-local.e2e-spec.ts:368–409` фактически
|
||
допускают RPC у observe; income helper `tbank-income-local.e2e-spec.ts:33–43`
|
||
по умолчанию тестирует observe. Regression proof нужно поменять, не считать
|
||
эти тесты доказательством ordinary GET. Rollback targeted owner diff при нарушении
|
||
completeness, без удаления сохранённой БД.
|
||
- **Related finding IDs:** FLOW-002, FLOW-008, FLOW-010.
|
||
|
||
### FLOW-002 — Screen timer вручную обходит enabled и readiness
|
||
|
||
- **Decision:** FIX. **Severity:** P2. **Confidence:** High. **Effort:** S.
|
||
- **Evidence:** `frontend/src/features/tbank-connection/model/screen-sync.ts:7–35,39–51`;
|
||
`frontend/src/shared/api/authenticated-query.ts:45–50`;
|
||
`frontend/src/entities/bond/api/queries.ts:45–104`;
|
||
`frontend/src/pages/bonds/ui/BondsPage/BondsPage.vm.ts:9–13`;
|
||
`frontend/src/pages/bond/ui/BondPage/BondPage.vm.ts:23–55`.
|
||
- **Claim / consumers:** пустой/короткий search и detail-dependent resources
|
||
остаются в resources array даже когда enabled=false; 30-minute interval всё
|
||
равно вызывает refetch. Возможна попытка без session или UID. Восемь production
|
||
page callers найдено в account/accounts/operations/income/events/bond/bonds/stock.
|
||
- **Current mechanism:** interval управляется lifetime page mount, а не Query
|
||
key/enable state; ScreenResource даже не требует isEnabled. Refresh/retry
|
||
тоже не фильтруют disabled. TanStack ручной refetch исполняется с enabled=false.
|
||
- **Impact / actual cost:** BondsPage idle: intended0 → один logical GET на tick;
|
||
backend MinLength2 отклоняет его400 без RPC. BondPage без resolved UID на tick:
|
||
до6 dependent logical GET attempts вместо0; invalid UUID paths отклоняются до
|
||
source, но operations пустой account identifier формирует неправильный URL.
|
||
Число successful HTTP attempts зависит от session/abort. **Impact unmeasured.**
|
||
- **Why now:** disabled is explicit prerequisite, не спорная freshness policy.
|
||
- **Minimum change:** учитывать isEnabled и не отменять уже выполняющийся read
|
||
на timer path; либо native Query refetchInterval у owner. Первый минимальный
|
||
guard достаточен для дефекта; полная policy migration зависит от FLOW-008.
|
||
- **Guarantees at risk:** auth gating, dependent-query ordering, cancellation,
|
||
HTTP-only retry и connection reset; не добавлять skip guards к каждому page.
|
||
- **Counterarguments / trade-offs:** timer появляется лишь через30мин; backend
|
||
validation снижает impact. Native interval может перестать polling background
|
||
tab, что надо решить явно. Imperative user retry доступных ресурсов сохранить.
|
||
- **Verification or rollback:** fake time+MSW: короткий q, unresolved UID, signed-out
|
||
session →0 GET после tick; затем enabled→один GET для same key; timer during
|
||
active read не запускает replacement. Existing
|
||
`screen-sync.test.tsx:21–80` проверяет manual refresh, не disabled timer.
|
||
Использовать App policy, а не retry-disabled test-client как её замену.
|
||
- **Related finding IDs:** FLOW-001, FLOW-008, FLOW-010.
|
||
|
||
### FLOW-003 — Prices ждёт все details без data dependency
|
||
|
||
- **Decision:** SIMPLIFY. **Severity:** P2. **Confidence:** High. **Effort:** S.
|
||
- **Evidence:** `backend/src/modules/investments/instruments/application/catalog-discovery.service.ts:295–325`;
|
||
`catalog-selection.ts:5–40`;
|
||
`backend/src/integrations/tbank/tbank.client.ts:358–379,441–447`;
|
||
`frontend/src/pages/bonds/ui/BondsPage/BondsPage.view.tsx:27–109`;
|
||
`backend/src/modules/investments/instruments/bonds/application/bond-catalog.service.ts:25–55,92–118`.
|
||
- **Claim / consumers:** текущая compact bond card требует nominal/maturity,
|
||
frequency/floating/perpetual и logo/exchange; цены требуют только selected UID,
|
||
уже известный сразу после FindInstrument. Зависимость prices→all details лишняя.
|
||
- **Current mechanism:** search → selected≤30 → await Promise.all(N BondBy) →
|
||
await batchGetLastPrices(N UID) → DTO. Сами details уже parallel.
|
||
- **Impact / actual cost:** total=`N+2` остаётся; critical path сейчас
|
||
`T_search + max(T_detail_i) + T_prices`, включая очереди/retries. Можно сделать
|
||
`T_search + max(max(T_detail_i),T_prices)` только при доступном limiter budget.
|
||
**Impact unmeasured**, milliseconds неизвестны.
|
||
- **Why now:** это независимая зависимость доказана аргументами API, без изменения
|
||
полноты карточки/контракта и без нового endpoint.
|
||
- **Minimum change:** после selection начать batch prices вместе с detail cohort
|
||
через existing promises/Promise.all и existing TbankClient limiter.
|
||
- **Guarantees at risk:** same selected UID/prices join, selection/order/ISIN
|
||
dedup, bounded result count, valid token handling, отсутствие partial success
|
||
вместо принятой whole-request error.
|
||
- **Counterarguments / trade-offs:** если detail fails, ранее prices не расходовался,
|
||
после parallel может добавиться1 logical RPC. Queue contention может съесть
|
||
latency benefit. Сократить N до0 нельзя: InstrumentShort proto `1398–1416` не
|
||
содержит используемые bond conditions, тогда как Bond `513–516,535` содержит.
|
||
Bulk full catalog другой trade-off и не внедряется без provider/UX proof.
|
||
- **Verification or rollback:** controlled mock detail deferred, prices must start
|
||
before release; assert exact same DTO/count=`N+2`, empty search=1; detail failure
|
||
не возвращает partial success. Synthetic limiter-aware timing отдельно. Если
|
||
время не улучшается вне noise или failure-rate cost растёт — вернуть порядок.
|
||
- **Related finding IDs:** FLOW-004, FLOW-008, FLOW-010.
|
||
|
||
### FLOW-004 — Permanent upstream auth failure ошибочно усиливается GET retry
|
||
|
||
- **Decision:** FIX. **Severity:** P2. **Confidence:** High. **Effort:** S.
|
||
- **Evidence:** `backend/src/integrations/tbank/tbank.client.ts:527–565`;
|
||
`backend/src/modules/investments/instruments/application/catalog-discovery.service.ts:149–165,227–249,295–317`;
|
||
`catalog-enrichment.service.ts:241–267`;
|
||
`backend/src/modules/investments/instruments/stocks/application/stock-screener.service.ts:65–68`;
|
||
`backend/src/common/filters/http-exception.filter.ts:19–31,50–64`;
|
||
`frontend/src/shared/api/client.ts:127–193`.
|
||
- **Claim / consumers:** online search/detail/ticker/company не используют
|
||
существующий translator. Remote PERMISSION_DENIED/UNAUTHENTICATED после
|
||
onlineToken локальной проверки даёт Error →500 InternalServerError → GET retries.
|
||
Screener POST тоже теряет public code, но transient transport не повторяет POST.
|
||
- **Current mechanism:** invalidToken=true в local row корректно даёт400 до RPC;
|
||
finding относится к токену, ещё не известному backend как invalid. gRPC auth
|
||
не retries, но application mapping пропущен. Durable enrichment catch — другой
|
||
путь с rejectMembershipToken, он не доказывает защиту online path.
|
||
- **Impact / actual cost:** Search auth fail на первом FindInstrument:
|
||
один queryFn делает3 HTTP/3 upstream RPC вместо1/1. Failure в detail cohort:
|
||
до3(1+N) logical RPC, цены не вызываются. Consumer получает InternalServerError
|
||
вместо ожидаемого публичного InvalidTbankToken для recovery.
|
||
**Impact unmeasured.**
|
||
- **Why now:** permanent/transient codes уже разделены frontend и integration;
|
||
ложный500 превращает штатную смену credentials в повторную работу.
|
||
- **Minimum change:** reuse
|
||
`backend/src/integrations/tbank/tbank-token.service.ts:26–35` на online boundary,
|
||
сохраняя pending durable attempt policy и safe error envelope.
|
||
- **Guarantees at risk:** без raw credential/error disclosure; не маркировать новую
|
||
connection invalid по late response старого token; если нужно persistence
|
||
invalidation, только revision-aware CAS. Для прекращения retries достаточно
|
||
translation, не нужно нового token state machine.
|
||
- **Counterarguments / trade-offs:** auth error может означать недостаточные scope,
|
||
но existing TbankClient уже сводит её к InvalidToken. Offline known invalid
|
||
connection уже защищена; проблема только нового upstream refusal. При transient
|
||
unavailable retry сохраняется.
|
||
- **Verification or rollback:** configured Nest HTTP с mocked RPC auth status →
|
||
400/INVALID_TBANK_TOKEN; MSW через actual client →1 GET, no transient delays.
|
||
Separate Search failure after N queued details, company и POST screener.
|
||
Existing `tbank.client.spec.ts:230–241` проверяет RPC, а не online HTTP mapping.
|
||
- **Related finding IDs:** FLOW-003, FLOW-010.
|
||
|
||
### FLOW-005 — Bank history N+1 внутри embedded read transaction
|
||
|
||
- **Decision:** SIMPLIFY. **Severity:** P2. **Confidence:** High. **Effort:** M.
|
||
- **Evidence:** `backend/src/modules/banking/bank-cards/application/bank-operations.service.ts:41–91,122–165,333–369`;
|
||
`backend/src/modules/banking/bank-cards/infrastructure/bank-cards.repository.ts:62–72`;
|
||
`frontend/src/entities/bank-card/api/queries.ts:120–144`;
|
||
`frontend/src/pages/card/ui/CardPage/CardPage.vm.ts:35–45`.
|
||
- **Claim / consumers:** list and refund-expense picker используют mapper-loader
|
||
одиночной операции; одинаковая карта/category/refund family перечитывается для
|
||
каждой строки, хотя page уже загружена.
|
||
- **Current mechanism:** base ownership+count+page=3 reads;
|
||
row =card1 +target(t) +source(s) +category(c) +refunds(r).
|
||
Для корректных видов: income1, transfer2, expense3, refund4. Limit≤100.
|
||
Default UI10 expenses →33 reads; page100 refunds →403 reads. Это условные
|
||
examples, не статистика production. Corrupt mixed shape может иметь5 row reads.
|
||
- **Impact / actual cost:** per-page cost линейна числу rows; refund sums читают
|
||
полные sibling lists многократно. `all` frontend складывает эту работу по HTTP
|
||
pages. SQLite embedded: **нет** remote DB RTT; materialization/queries остаются.
|
||
Promise.all вокруг rows не устраняет model call count. **Impact unmeasured.**
|
||
- **Why now:** count структура доказана; list hot path и all-mode существуют.
|
||
Runtime ускорение надо доказать перед включением более сложного loader.
|
||
- **Minimum change:** сохранить transaction; загрузить distinct cards, source
|
||
expenses, categories, refund rows для page одним bounded набором запросов,
|
||
затем применить существующую projection. При page empty не грузить relations.
|
||
Не менять single-operation/write path ради list optimization.
|
||
- **Guarantees at risk:** owner in every batch query, exact bigint sums,
|
||
archived/deleted/reference semantics; refund row availableRefund исключает
|
||
именно текущую row, expense — нет. Нельзя удалять financial transaction/CAS/
|
||
idempotency. `bank-operations.service.ts:181–195` содержит write invariant,
|
||
а здесь меняется только read projection.
|
||
- **Counterarguments / trade-offs:** default page10 и SQLite могут быть достаточно
|
||
быстры; отдельный page loader увеличит maintenance. Existing bounded batch
|
||
ориентир3+≤4 relation reads, а не обещание одного SQL. Встроенный include должен
|
||
проверяться на фактический SQL/bytes; full unbounded refunds family может всё
|
||
ещё доминировать.
|
||
- **Verification or rollback:** synthetic page mixed income/transfer/expense/
|
||
несколько refunds одной expense, cross-user/deleted cases; DTO равен before,
|
||
count не растёт на1 loader на row. Затем query/duration trace на isolated SQLite.
|
||
При нейтральной latency и неоправданной сложности — сохранить current loader.
|
||
Existing `backend/test/bank-cards.e2e-spec.ts:424–462,550–580,779–791` сохраняет
|
||
observable semantics; execution в этом аудите отсутствует.
|
||
- **Related finding IDs:** FLOW-009, FLOW-010.
|
||
|
||
### FLOW-006 — Hourly calendar refresh не обновляет сохранённые calendars
|
||
|
||
- **Decision:** FIX. **Severity:** P2. **Confidence:** High. **Effort:** M.
|
||
- **Evidence:** `backend/src/modules/investments/tbank-sync/tbank-hourly.scheduler.ts:166–175`;
|
||
`backend/src/modules/investments/upcoming-events/upcoming-events.service.ts:182–239`;
|
||
`backend/src/modules/investments/instruments/application/catalog-enrichment.service.ts:113–125,189–193,596–625,679–692`;
|
||
`specs/openspec/specs/hourly-investment-sync/spec.md:8–9`.
|
||
- **Claim / consumers:** hourly должен обновлять события текущих holdings, но
|
||
requestMany получает force=false; stable успешный calendar не обновляется.
|
||
Bond-events не имеет rangeKey, поэтому suppress сохраняется после смены дня;
|
||
dividend key changes daily, но внутри дня calendar reuses previous window.
|
||
- **Current mechanism:** только quote policy проверяет TTL; dividends/bond-events
|
||
stable. New positions/calendars действительно грузятся, а сохранённые нет.
|
||
Forecast может выглядеть полным с прежними событиями при новом Portfolio.
|
||
- **Impact / actual cost:** warm calendar upstream budget0 не является
|
||
корректной экономией, если provider изменил объявленные/отменённые выплаты.
|
||
Fix добавляет calls, не уменьшает их. При hourly refresh effective unique
|
||
S ранее сохранённых stock windows adds до S GetDividends; B ранее сохранённых
|
||
bond calendars adds Σ(c_b+o_b+1) MOEX HTTP при полном refresh, с учётом shared
|
||
producers/cache scope. Уже отсутствующие источники и сейчас загружаются.
|
||
**Impact unmeasured**, изменённые real calendars не измерялись.
|
||
- **Why now:** требование hourly explicitly covers future events; permanent stable
|
||
reuse не удовлетворяет его даже без выбора точного TTL общего online справочника.
|
||
- **Minimum change:** hourly caller принудительно обновляет именно calendar kinds
|
||
или equivalent existing policy; stable detail metadata reuse сохраняется.
|
||
Обновление нужного календаря не должно включать quotes/full market history.
|
||
- **Guarantees at risk:** shared producer isolation, complete multi-page MOEX,
|
||
same portfolio quantities, source version checks и fallback при error. Force
|
||
whole source group вслепую может ненужно повторять detail каждого часа.
|
||
- **Counterarguments / trade-offs:** calendars меняются редко, но не неизменяемы;
|
||
обязательный hourly refresh увеличивает rate budget. Нужно измерить масштабы и
|
||
coalescing одного UID между счетами; осторожно повторно не запускать same source
|
||
после уже завершённого первого account refresh в этом же walk.
|
||
- **Verification or rollback:** two successful controlled hourly source passes
|
||
same window, второй upstream calendar изменён/отменён → новое saved version
|
||
и forecast; second pass failure сохраняет first complete timestamps. Проверить
|
||
2 accounts same UID, no duplicate calendar call внутри одного намеренного pass.
|
||
Existing future-events suite запускает manual source refresh, scheduler stub
|
||
(`backend/test/upcoming-events.e2e-spec.ts:140–147`), не полный hourly tick.
|
||
- **Related finding IDs:** FLOW-007, FLOW-010.
|
||
|
||
### FLOW-007 — Forecast fallback блокирует новый complete candidate
|
||
|
||
- **Decision:** FIX. **Severity:** P2. **Confidence:** High. **Effort:** M.
|
||
- **Evidence:** `backend/src/modules/investments/upcoming-events/upcoming-events.service.ts:152–173,271–275`;
|
||
`backend/src/modules/investments/portfolio/infrastructure/forecast.repository.ts:50–80,99–174`;
|
||
`backend/src/modules/investments/portfolio/application/forecast.service.ts:35–65,77–93,98–120`;
|
||
`specs/openspec/specs/brokerage-account-upcoming-events/spec.md:9–20,63–76`.
|
||
- **Claim / consumers:** next-day hourly get creates new window scope; no exact
|
||
previous row + overlapping old forecast → early return old fallback even with
|
||
newly computed complete candidate. `findMany` fallback has no recency order.
|
||
- **Current mechanism:** resolve conflates saved result identity and requested
|
||
filter. It computes candidate in Events GET, takes write fence, then returns
|
||
first overlap before validating/publishing new source revisions. No overlap
|
||
with old forecast and candidate unavailable instead returns null, although
|
||
intended semantics specifies successful empty filtered saved forecast.
|
||
- **Impact / actual cost:** upstream/calendar and repeated local calculation могут
|
||
не менять visible result; new candidate publication0 despite completed work.
|
||
Age/value wrongness is source-inferred conditional, runtime **UNVERIFIED**;
|
||
**Impact unmeasured**. Не просто лишний loop: repeated work cannot reach UI.
|
||
- **Why now:** hourly rolling windows сдвигаются ежедневно; consumer требует
|
||
исходный saved window/calculatedAt и фильтрацию, а не создание identities по GET.
|
||
- **Minimum change:** в existing forecast owner отделить selection последнего
|
||
complete result от HTTP window filter; candidate publication не блокировать
|
||
только из-за другого window key. Read-only selection должен работать и для
|
||
непересекающегося запроса. Contract migration scope согласовать перед реализацией.
|
||
- **Guarantees at risk:** dependency/source version CAS `80–145`, membership and
|
||
connection generation fence, complete-only publish, exact financial totals,
|
||
unchanged calculatedAt при read filter, old complete при failed update.
|
||
- **Counterarguments / trade-offs:** узкий GET не должен пересчитывать successful
|
||
saved forecast или менять timestamp — existing fallback защищает это. Простое
|
||
удаление early return сломает такой UX; различить producer update и consumer
|
||
selection, а не ослабить проверки. «Latest» требует deterministic order.
|
||
- **Verification or rollback:** save complete W0, update quantities/calendar,
|
||
hourly produce complete W1 overlapping W0 → W1 доступен; narrow/non-overlap GET
|
||
сохраняет исходные time/window и filters; failed W1 сохраняет W0; dependency race
|
||
rejects publication. Existing `upcoming-events.e2e-spec.ts:619–641` защищает narrow
|
||
filter, его сохранить; добавить distinct producer W0→W1 outcome в isolated DB.
|
||
- **Related finding IDs:** FLOW-006, FLOW-009, FLOW-010.
|
||
|
||
### FLOW-008 — Общая freshness policy требует явного решения
|
||
|
||
- **Decision:** INVESTIGATE. **Severity:** P3. **Confidence:** Medium. **Effort:** S.
|
||
- **Evidence:** `frontend/src/app/ui/App.tsx:13–19`;
|
||
`frontend/src/entities/portfolio/api/usePortfolio.ts:3–5`;
|
||
`frontend/src/entities/brokerage-account/api/useBrokerageAccounts.ts:7–17`;
|
||
`frontend/src/features/tbank-connection/model/screen-sync.ts:20–35`;
|
||
`docs/adr/019-hourly-reads-and-online-instrument-reference.md:27–29`;
|
||
`frontend/.ai/conventions/data.md:70–74`.
|
||
- **Claim / consumers:** source включает focus/reconnect; convention говорит focus
|
||
off; ADR «не чаще30мин». StaleTime30s допускает GET после mount/focus/reconnect
|
||
через30s, одновременно с interval30min. Факт policy divergence доказан, но
|
||
трактовка initial loading/retry/manual actions и freshness UX не утверждена.
|
||
- **Current mechanism:** Query defaults+entity freshness+page-native timer;
|
||
staleTime не служит timer или строгим throttle; interval не зависит от staleTime.
|
||
- **Impact / actual cost:** C и detail могут повторять online RPC; B/D/E повторяют
|
||
local work и, пока FLOW-001 не исправлен, scheduling. Request count зависит от
|
||
пользователя/visibility; не фиксирован. **Impact unmeasured.**
|
||
- **Why now:** выбирать caching наугад может ухудшить cold completeness на30мин.
|
||
- **Minimum change:** сначала уточнить правило; затем при подтверждении перенести
|
||
периодические GET к existing Query owner, убрать obsolete page polling и `_scope`.
|
||
Не вводить общий refresh registry. `ponytail-audit native:` уже установленный
|
||
Query interval вместо собственного timer, если его visibility UX подходит.
|
||
- **Guarantees at risk:** initial readiness, cached snapshot, connection reset,
|
||
user retry, backend hourly freshness distinct from client response freshness.
|
||
- **Counterarguments / trade-offs:** более частые local reads ускоряют появление
|
||
первого snapshot; online instrument spec требует online-at-request, а не
|
||
necessarily durable cache. Background-tab interval и reconnect UX требуют выбора.
|
||
- **Verification or rollback:** fake-time App policy test: mount/focus/reconnect/
|
||
timer/manual retry до/после chosen budget; evaluate first-null→complete latency
|
||
на synthetic sources. Не count test wrapper defaults как production policy.
|
||
- **Related finding IDs:** FLOW-001, FLOW-002, FLOW-003.
|
||
|
||
### FLOW-009 — Full snapshot/analytics DTO cost доказан структурно, benefit не установлен
|
||
|
||
- **Decision:** INVESTIGATE. **Severity:** P3. **Confidence:** Medium. **Effort:** M.
|
||
- **Evidence:** `backend/src/modules/investments/portfolio/infrastructure/portfolio.repository.ts:25–47`;
|
||
`backend/src/modules/investments/portfolio/application/portfolio.service.ts:57–59,79–95`;
|
||
`brokerage-accounts/brokerage-accounts.service.ts:17–39`;
|
||
`upcoming-events/upcoming-events.service.ts:60–71,206–239`;
|
||
`income/income-analytics.service.ts:138–162,233–284`;
|
||
`frontend/src/pages/account-income/model/display.ts:97–129`.
|
||
- **Claim / consumers:** single-account read loads all account snapshot JSON before
|
||
service filter; Accounts summary repeats full snapshot hydration and hourly read;
|
||
Events rereads Portfolio/metadata. Income delivers derived buckets/instruments
|
||
unused by current UI. Это реальные opportunities, не подтверждённый slow endpoint.
|
||
- **Current mechanism:** reuse whole saved dataset for independent HTTP resources;
|
||
forecast GET validates/publishes under CAS; income period entries intentionally
|
||
unbounded. Нет runtime hydrate/dehydrate registry frontend.
|
||
- **Impact / actual cost:** logical repeated reads/JSON work; bytes/CPU/SQLite time
|
||
unknown. Removal source calls could change race consistency; **Impact unmeasured**.
|
||
- **Why now:** пригодно для дешёвого isolated trace перед новым summary endpoint/cache.
|
||
- **Minimum change:** измерить per-route bytes/model SQL/JSON time; кандидат —
|
||
передавать account predicate existing repository (single-account read), reuse
|
||
`saved.hourly` при Accounts list, сократить unused DTO только с contract proof.
|
||
No change допустим при дешёвых local reads. Не добавлять BFF endpoint вслепую.
|
||
- **Guarantees at risk:** user/revision/account scope, consistent dependency
|
||
projection, last complete forecast/timestamps, exact unlimited-period entries,
|
||
stable public artifacts и внешние consumers.
|
||
- **Counterarguments / trade-offs:** общий dataset упрощает all aggregation;
|
||
independent GETs дают section error isolation. Spec дохода требует все entries
|
||
сразу (`spec.md:157–179`) и unlimited duration (`10`), поэтому pagination требует
|
||
product/contract decision. «Unused UI field» не доказывает no external consumers.
|
||
- **Verification or rollback:** isolated SQLite one account vs many, same data/DTO,
|
||
error/isolation race; compare bytes and hydration with query trace. Проверить
|
||
всех контрактных consumers до изменения generated snapshot. Если gain neutral,
|
||
оставить существующий путь.
|
||
- **Related finding IDs:** FLOW-005, FLOW-007, FLOW-010.
|
||
|
||
### FLOW-010 — Защищающая сложность и достаточное reuse: KEEP
|
||
|
||
- **Decision:** KEEP. **Severity:** P2. **Confidence:** High. **Effort:** S.
|
||
- **Evidence:** `backend/src/integrations/tbank/tbank-access.service.ts:158–169,262–288`;
|
||
`backend/src/modules/investments/portfolio/infrastructure/portfolio.repository.ts:121–133`;
|
||
`operations/infrastructure/brokerage-operations.repository.ts:128–144,602–698,703–881`;
|
||
`portfolio/infrastructure/forecast.repository.ts:35–46,80–145`;
|
||
`instruments/stocks/application/stock-screener.service.ts:19–26,84–109`;
|
||
`frontend/src/features/tbank-connection/model/connection-boundary.ts:24–39`;
|
||
`frontend/src/features/auth/model/auth.vm.ts:73–90`.
|
||
- **Claim / consumers:** финансовые UI, pagination, forecasts и соединения уже
|
||
используют meaningful completeness/access/CAS boundaries и cache reuse.
|
||
Совпадение keys/SHA/interfaces или цена некоторых reads не оправдывает удаление.
|
||
- **Current mechanism:** private user/revision dataset; conditional work claims;
|
||
staged complete operation publish; signed cursor filter/version binding;
|
||
shared public source coalescing; Query cancel/reset on connection and clear on
|
||
logout; bounded upstream queue/deadline; screener snapshot stable pages.
|
||
- **Impact / actual cost:** prevents cross-connection publication, wrong money,
|
||
duplicate/replayed writes и pagination inconsistency. **Impact unmeasured**,
|
||
это protected guarantee, не confirmed performance defect.
|
||
- **Why now:** любой optimization выше должен сохранить эту equivalence boundary.
|
||
- **Minimum change:** no change to safeguards. Retain search details needed by
|
||
actual card, batch prices, SQLite batches400/100 и correct DTO whitelist.
|
||
- **Guarantees at risk:** authorization/privacy, secret handling, exact currency/
|
||
decimal strings/bigint, rollback/idempotency/CAS, complete-only result,
|
||
controlled scheduler lifecycle, public contract stability.
|
||
- **Counterarguments / trade-offs:** SQL/JSON validation имеет local cost; alternatives
|
||
допустимы только с equal guarantees. Не заменять signed cursor offset у Ledger,
|
||
не хранить user portfolios в generic shared instrument cache.
|
||
- **Verification or rollback:** retain isolated cross-user/revision/late publication,
|
||
cursor, complete-empty/failed-refresh, banking refund/replay/atomicity tests.
|
||
Source inspection не доказывает весь real concurrency/security набор; это
|
||
необходимый regression plan, не заявление «security audit пройден».
|
||
- **Related finding IDs:** FLOW-001, FLOW-002, FLOW-003, FLOW-004, FLOW-005, FLOW-006, FLOW-007, FLOW-009.
|
||
|
||
### Before/after critical-path outline — две главные проблемы
|
||
|
||
**1. FLOW-001, ownership/import.**
|
||
|
||
```text
|
||
BEFORE ordinary income, membership fresh/full coverage:
|
||
access → readPage → membership reservation(write fence + read)
|
||
→ schedule(access → readPage → readPlan(access → coverage)) → readPage
|
||
response local; detached import = m + Σp(R_T ∪ R_H), if missing
|
||
|
||
AFTER intended passive income:
|
||
access → readPage → exact totals/comparisons/entries → response
|
||
import from this read = 0; hourly and explicitly allowed missing Ledger remain
|
||
|
||
BEFORE Ledger first missing period:
|
||
access → validate cursor if present → readPlan chosen + outside lifetime history
|
||
→ local readPage → metadata batch → response
|
||
worker queue = pages(chosen gaps) + pages(outside gaps)
|
||
|
||
AFTER Ledger:
|
||
same access/cursor/coverage/DTO protection, request only chosen gaps
|
||
worker queue = pages(chosen gaps); no hidden membership/portfolio/calendar refresh
|
||
```
|
||
|
||
Fully warm conditional income foreground model reads20→6; this is static count,
|
||
not20/6 speed ratio. Worker total сокращается на `m+Σp(R_H)` плюс выбранные Income gaps, которые
|
||
должны принадлежать разрешённому Ledger owner. Нельзя удалить permitted Ledger
|
||
first-load или выдавать empty completeness за отсутствие операций.
|
||
|
||
**2. FLOW-003, search waterfall.**
|
||
|
||
```text
|
||
BEFORE: Token → Search → [Detail1 || … || DetailN] → PricesBatch → DTO → UI
|
||
AFTER: Token → Search → [[Detail1 || … || DetailN] || PricesBatch] → DTO → UI
|
||
TOTAL: N+2 → N+2 (N>0); empty1 →1
|
||
PATH: Tsearch + max(Tdetails) + Tprices
|
||
→ Tsearch + max(max(Tdetails), Tprices), subject to limiter contention
|
||
```
|
||
|
||
Do not count N concurrent details as N sequential latency stages. A full failed
|
||
detail response remains failed; moving price start earlier can spend1 extra call
|
||
on that error path. No latency gain claimed until limiter-aware synthetic proof.
|
||
|
||
## 5. DELETE / SIMPLIFY / KEEP / INVESTIGATE — summary decision table
|
||
|
||
| Decision | IDs | Минимальная стратегия | Решение сейчас |
|
||
| --- | --- | --- | --- |
|
||
| DELETE | — | obsolete outside-scope refresh branches удаляются внутри FLOW-001, отдельного blanket deletion нет | не удалять весь planInvestmentScreen/status/coverage |
|
||
| FIX | FLOW-001/002/004 | один source owner; readiness guard; existing token translator | confirmed actionable |
|
||
| FIX | FLOW-006/007 | current calendars + producer/consumer forecast distinction | confirmed semantics, regression proof before implementation |
|
||
| SIMPLIFY | FLOW-003 | parallel independent prices with details | count unchanged; latency experiment with rollback |
|
||
| SIMPLIFY | FLOW-005 | bounded page relation loading instead of row loaders | DB work reduction structure known; runtime benefit unmeasured |
|
||
| KEEP | FLOW-010 | native Query sharing, complete snapshots, signed cursor, CAS, transaction, whitelist, limiter, screener snapshot | preserve invariants |
|
||
| INVESTIGATE | FLOW-008 | explicit freshness/readiness policy; native Query interval if suitable | no assumed business decision |
|
||
| INVESTIGATE | FLOW-009 | local hydration/DTO measurement before new cache/endpoint | no current latency finding |
|
||
|
||
Ponytail complexity-pass: `native:` Query-owned interval conditional on agreed
|
||
policy; `shrink:` убрать независимую последовательность prices; `delete:` убрать
|
||
неразрешённые history refresh branches, сохранив source owner и protected guards.
|
||
**net: lines not measured; 0 dependency removals established.** Число строк не
|
||
выдумывается без prepared diff; новые dependency/registry не предлагаются.
|
||
|
||
## 6. Cost & benefit
|
||
|
||
| Strategy / IDs | No change | Expected cost/benefit class | Freshness / UX / rate / maintenance trade-off | Measurement |
|
||
| --- | --- | --- | --- | --- |
|
||
| ownership FLOW-001 | unrelated history continues on reads | runtime RPC, less source queue work, simpler refresh ownership | cold income completeness remains explicit; chosen Ledger fetch stays; no covert full backfill | not measured; request-set delta proven |
|
||
| readiness FLOW-002 | disabled timer requests retained | runtime HTTP + less invalid failures; one shared guard | preserve enabled transitions/session reset/manual retry; avoid cancel/restart storms | not measured; idle tick delta1 or dependent6 inferred |
|
||
| parallelism FLOW-003 | current detail→prices order, fewer price calls on detail fail | critical-path latency, small local code change | no total-call reduction, ≤1 extra price on failure; existing limiter still applies | not measured |
|
||
| token mapping FLOW-004 | permanent auth500 continues | fewer useless HTTP/RPC retries, correct recovery | safe public code; don't invalidate new generation using old refusal | not measured; first-RPC scenario3→1 |
|
||
| batching FLOW-005 | default10 may already be fast | DB/model reads/materialization; some maintenance increase | same transaction/user/refund totals; avoid row N+1, no remote DB claim | not measured |
|
||
| calendar freshness FLOW-006 | stable cache saves calls indefinitely | correctness/currentness, MORE upstream calls | hourly calendar-specific scope, multi-account dedup, no full history/quote force | not measured; added source budget explicit |
|
||
| forecast FLOW-007 | old overlap can suppress candidate | reliable visible update; avoid wasted upstream/calculation | producer/read filter distinct, full fallback/CAS preserved | not measured |
|
||
| cache policy FLOW-008 | inconsistent timers/focus remains | possible HTTP/RPC and cognitive savings | 30min can delay initial complete state; online reference freshness distinct | not measured; INVESTIGATE |
|
||
| DTO/hydration FLOW-009 | independent local reads retain error isolation | possible CPU/bytes/agent navigation reduction | unlimited exact income entries required; contract consumers must agree | not measured; INVESTIGATE |
|
||
|
||
**CI minutes:** не измерялись и не являются целью07. **Agent tokens/cognitive load:**
|
||
ожидаемое качественное упрощение ownership, без numeric savings. **Batching search:**
|
||
GetLastPrices уже batch; удаление N BondBy без equivalent card fields недопустимо.
|
||
**Cache search:** durable reference copy запрещена current online semantics;
|
||
ephemeral coalescing/TTL требует product freshness и token/revision-safe key,
|
||
сейчас не confirmed action. **Removing obsolete refresh:** FLOW-001 даёт
|
||
доказуемый request-set cut; blanket удаление scheduler не эквивалентно.
|
||
|
||
## 7. Top actions — не более 10
|
||
|
||
### Quick wins, подтверждённые механизмом
|
||
|
||
| Priority | Action | Value / risk / effort |
|
||
| --- | --- | --- |
|
||
| 1 | FLOW-002: shared timer readiness guard, no restart active read | explicit invalid HTTP eliminated; small scope/S |
|
||
| 2 | FLOW-004: existing online token translator + HTTP/MSW proof | removes permanent retry loop, correct recovery; S |
|
||
| 3 | FLOW-003: overlap prices/detail with controlled-source check | local change/S; revert if limiter/error cost erases benefit |
|
||
|
||
### Требуют behavioral / DB proof перед выпуском
|
||
|
||
| Priority | Action | Value / risk / effort |
|
||
| --- | --- | --- |
|
||
| 4 | FLOW-001: remove outside-scope backfill + passive Income | strongest source-budget reduction; cold history UX/completeness proof/M |
|
||
| 5 | FLOW-006: calendar-only hourly refresh with per-walk dedup evidence | current calendar guarantee; increases known upstream budget/M |
|
||
| 6 | FLOW-007: distinguish forecast production/read filtering | removes old-candidate suppression; CAS/narrow-read regression/M |
|
||
| 7 | FLOW-005: bounded bank list loader only after isolated cost check | per-row DB reads reduced; refund/owner contract and maintenance/M |
|
||
|
||
ADR нужен только если меняется принятая freshness/cold-history/forecast boundary,
|
||
а не как обязательный документ для guard или Promise.all. FLOW-008/009 не входят
|
||
в подтверждённые top fixes до ответов и измерений. No change — допустимый результат
|
||
измерения для performance-only simplification.
|
||
|
||
## 8. Cross-audit signals
|
||
|
||
- **05 Frontend composition:** проверить consumer resource metadata на AccountPage boundary —
|
||
`frontend/src/pages/account/ui/AccountPage/AccountPage.vm.ts:125–135`; проверить
|
||
передачу в container/view. Presentation расследование здесь не выполнялось.
|
||
- **06 Backend domain style:** отдельные похожие `publicInstrument` projections —
|
||
`catalog-discovery.service.ts:499–530`, `catalog-enrichment.service.ts:646–677`;
|
||
допустимость объединения owner проверить отдельно, whitelist KEEP.
|
||
- **04 Dead code:** production `TbankSyncService.request/dispatch` не найден среди
|
||
callers; controller только status — `tbank-sync.controller.ts:31–41`,
|
||
`tbank-sync.service.ts:54–78`. Удаление недоступной orchestration — другой audit,
|
||
runtime request budget текущих flows от её удаления не меняется.
|
||
- **13 Reliability:** scheduler catch/settle/poll и hourly eligibility —
|
||
`tbank-hourly.scheduler.ts:108–110,178–218`, `tbank-sync.repository.ts:189–201`;
|
||
stalled work/пропущенный срок меняют допустимость следующей работы. Без отдельного
|
||
security/reliability расследования в07, finite max walk duration unknown.
|
||
- **08 Tests:** Ledger observe assertions допускают scheduling, income helper по
|
||
умолчанию observe, scheduler mocked — source refs в FLOW-001/006. Нужен outcome
|
||
proof actual ordinary callers, не performance suite вместо regression.
|
||
- **15 Specs/docs:** старый Ledger cache/upstream requirement
|
||
`brokerage-account-ledger/spec.md:97–110` конфликтует с local/hourly
|
||
`131–140`; online-reference addendum docs/bonds `147–151` сосуществует со старым
|
||
refresh explanation `119–123`. Не восстанавливать obsolete cache автоматически.
|
||
|
||
## 9. Open questions / verification plan
|
||
|
||
### Вопросы, действительно меняющие решение
|
||
|
||
1. **Cold historical Income после пассивного GET:** пользователь сначала открывает
|
||
Ledger, либо требуется иной явный permitted owner? Это меняет UX FLOW-001,
|
||
но не делает текущий hidden membership/backfill разрешённым.
|
||
2. **30-minute client rule:** применяется к periodic revalidation только или также
|
||
focus/mount/reconnect; initial incomplete и user recovery exceptions? Самая
|
||
дешёвая проверка — owner decision + fake-time App policy test, FLOW-008.
|
||
3. **Calendar budget масштаба:** A, unique held UID, MOEX page counts и one walk
|
||
across accounts? Controlled fixtures с repeated UID перед calendar force;
|
||
actual production distribution не необходима для correct outcome proof.
|
||
4. **Forecast producer identity:** один latest complete snapshot на account либо
|
||
поддерживаемые producer windows? Нужен deterministic selection и producer/read
|
||
distinction до FLOW-007, без ослабления source CAS.
|
||
5. **Bank/DTO масштаб и внешние consumers:** действительно ли local reads/bytes
|
||
важны, и кто использует income buckets/instruments/full detail fields? Ответ
|
||
меняет FLOW-005 runtime adoption и FLOW-009 contract cut, не надо нового endpoint
|
||
ради потенциального gain.
|
||
|
||
### Non-production verification
|
||
|
||
| Finding | Самая дешёвая надёжная boundary / expected proof |
|
||
| --- | --- |
|
||
| FLOW-001 | existing isolated SQLite Ledger/Income suite + mocked broker; selected complete, outside missing, observe/no observe; interval set and zero forbidden work |
|
||
| FLOW-002 | existing Vitest/MSW screen-sync test, fake time + real App query options; disabled/session/UID prerequisites; no timer cancel/restart |
|
||
| FLOW-003 | controlled service mock deferred details/prices; unchanged DTO/count; optional limiter-aware synthetic timing, no live credentials |
|
||
| FLOW-004 | configured Nest HTTP error mapping + MSW authenticatedFetch retry test; permanent1 attempt, transient retries retained |
|
||
| FLOW-005 | isolated banking suite, mixed/refund family/cross-user/deleted cases; SQL/model-call trace+timing before adopting batch |
|
||
| FLOW-006 | controlled two-pass hourly with shared UID+changed calendar/failure; real SQLite version/timestamp fallback |
|
||
| FLOW-007 | W0→W1 producer, narrow/non-overlap passive read, dependency race; real DB CAS outcome |
|
||
| FLOW-008/009 | product policy/contract owners + synthetic bytes/read/hydration trace; no release recommendation from intuition |
|
||
|
||
Future verification commands must use local `nvm use --silent`; inspect script
|
||
side effects first. Existing frontend npm test scripts build packages;
|
||
SQLite runner migrates disposable DB; они **не исполнялись** в этом read-only
|
||
аудите. Новые scripts/frameworks/fixtures tools для самого аудита не создавались.
|
||
|
||
### Итоговая проверка отчёта и read-only
|
||
|
||
- Полный prompt и полный отчёт перечитаны; source refs, диапазоны, consumers,
|
||
count units/formulas и все required fields проверены статически.
|
||
- Semantic/Boy Scout review: найденные возможности не превращены в blanket
|
||
removal; public DTO/transaction/CAS/cursor guards сохранены в recommendations;
|
||
нет новой application abstraction/dependency/process gate.
|
||
- `git diff --no-index --check /dev/null "audit-results/07-api-and-data-flow-efficiency-report.md"`
|
||
в root cwd: пустой diagnostic output, exit1 при сравнении нового файла с
|
||
`/dev/null`; whitespace errors не выведены. Exit проверен через
|
||
`check_exit=$?; typeset -p check_exit`: `typeset check_exit=1`.
|
||
- `git diff --check` в root/frontend/backend/specs/docs: пустой output, exit0.
|
||
- Итоговые `git rev-parse HEAD && git status --short` всех пяти repo: SHA как в
|
||
таблице metadata, tracked/staged/unstaged изменений нет. Новый отчёт root
|
||
существует как разрешённый ignored audit artifact; отсутствие его в status
|
||
само по себе не проверка содержимого, поэтому выполнен no-index check.
|
||
`git check-ignore -v` подтвердил root `.gitignore:2:/*`; ignore не менялся.
|
||
- Ограничения: no current runtime measurement/tests, no graph, no fresh OpenAPI
|
||
generation, no current production log/trace; version-specific Prisma SQL
|
||
expansion unknown. Все latency/throughput выигрыши остаются **UNVERIFIED**.
|
||
|
||
## Машинная таблица для консолидации
|
||
|
||
| ID | Decision | Severity | Confidence | Repo | Files | One-line impact | Effort | Related IDs |
|
||
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||
| FLOW-001 | FIX | P2 | High | backend | backend/src/modules/investments/operations/application/brokerage-operations.service.ts; backend/src/modules/investments/operations/application/brokerage-operations-sync.service.ts; backend/src/modules/investments/income/income-analytics.controller.ts | GET plans m+pages(selected gaps+outside history), violates source ownership | M | FLOW-002,FLOW-008,FLOW-010 |
|
||
| FLOW-002 | FIX | P2 | High | frontend | frontend/src/features/tbank-connection/model/screen-sync.ts; frontend/src/entities/bond/api/queries.ts | Manual timer refetch bypasses enabled/session/UID prerequisites | S | FLOW-001,FLOW-008,FLOW-010 |
|
||
| FLOW-003 | SIMPLIFY | P2 | High | backend | backend/src/modules/investments/instruments/application/catalog-discovery.service.ts | Search N+2 RPC retains unnecessary details-to-prices waterfall | S | FLOW-004,FLOW-008,FLOW-010 |
|
||
| FLOW-004 | FIX | P2 | High | backend+frontend | backend/src/integrations/tbank/tbank.client.ts; backend/src/modules/investments/instruments/application/catalog-discovery.service.ts; frontend/src/shared/api/client.ts | Permanent online broker auth becomes500 and up to3 repeated GET/RPC attempts | S | FLOW-003,FLOW-010 |
|
||
| FLOW-005 | SIMPLIFY | P2 | High | backend | backend/src/modules/banking/bank-cards/application/bank-operations.service.ts | Bank history 3+sum(1+t+s+c+r) embedded model reads/page | M | FLOW-009,FLOW-010 |
|
||
| FLOW-006 | FIX | P2 | High | backend | backend/src/modules/investments/tbank-sync/tbank-hourly.scheduler.ts; backend/src/modules/investments/upcoming-events/upcoming-events.service.ts; backend/src/modules/investments/instruments/application/catalog-enrichment.service.ts | Hourly stable calendar reuse can suppress updated future events | M | FLOW-007,FLOW-010 |
|
||
| FLOW-007 | FIX | P2 | High | backend | backend/src/modules/investments/portfolio/infrastructure/forecast.repository.ts; backend/src/modules/investments/portfolio/application/forecast.service.ts | Overlap fallback can suppress newly complete rolling-window candidate | M | FLOW-006,FLOW-009,FLOW-010 |
|
||
| FLOW-008 | INVESTIGATE | P3 | Medium | frontend+docs | frontend/src/app/ui/App.tsx; frontend/src/features/tbank-connection/model/screen-sync.ts; docs/adr/019-hourly-reads-and-online-instrument-reference.md | Conflicting30s/30min/focus policy needs readiness and freshness decision | S | FLOW-001,FLOW-002,FLOW-003 |
|
||
| FLOW-009 | INVESTIGATE | P3 | Medium | backend+frontend | backend/src/modules/investments/portfolio/infrastructure/portfolio.repository.ts; backend/src/modules/investments/income/income-analytics.service.ts; frontend/src/pages/account-income/model/display.ts | Full snapshots/derived DTO work exists but runtime benefit and contract cut unverified | M | FLOW-005,FLOW-007,FLOW-010 |
|
||
| FLOW-010 | KEEP | P2 | High | backend+frontend | backend/src/integrations/tbank/tbank-access.service.ts; backend/src/modules/investments/operations/infrastructure/brokerage-operations.repository.ts; backend/src/modules/investments/portfolio/infrastructure/forecast.repository.ts; frontend/src/features/tbank-connection/model/connection-boundary.ts | Preserve isolation/completeness/CAS/atomicity/typed projection and existing dedup | S | FLOW-001,FLOW-002,FLOW-003,FLOW-004,FLOW-005,FLOW-006,FLOW-007,FLOW-009 |
|
||
|
||
Все Files в машинной таблице — paths относительно workspace.
|
||
Actionable=FIX+SIMPLIFY+DELETE; KEEP и INVESTIGATE исключены из severity totals.
|