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

87 KiB
Raw Blame History

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

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, important defaults, 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

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.

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.

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.