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

110 KiB
Raw Permalink Blame History

06 — Backend Architecture

1. Audit metadata

  • Дата: 2026-10-11. Область: ownership доменных инвариантов, application flows, Nest DI, persistence/transactions, scheduler и integration error boundaries.
  • Результат: локальные исправления текущего монолита; оснований вводить CQRS, микросервисы, generic repositories или обязательные interfaces не обнаружено.
  • Affected areas: backend/src/modules/investments/{portfolio,tbank-sync,operations,income,instruments,brokerage-accounts}, integration access/error boundaries. Banking/auth преимущественно защищены решениями KEEP.
  • Фактический backend stack: NestJS 12.0.1, TypeScript 6.0.3, Prisma/client/SQLite adapter 7.10.0, better-sqlite3 13.0.3, gRPC-js 1.14.5; источник — backend/package.json:40–66,104. SQLite embedded; PrismaService владеет подключением, backend — один modular application. Node pin v24.21.0 указан в manifest, runtime этой сессией не запускался.
  • Исполнитель: один независимый агент; дополнительные сабагенты не запускались. Отчёты аудитов 05/07 не читались.
  • Режим: исходники/config четырёх самостоятельных repo и root read-only. Единственная запись — этот новый отчёт через apply_patch.
Repo Абсолютный путь HEAD при начале аудита Начальный git status --short --untracked-files=all
root /Users/ksv741/Project/vibe/WealthLedger dc030f84c979cedb2628ef4bd97c62d6c63a9256 пустой
backend /Users/ksv741/Project/vibe/WealthLedger/backend 0ad84ff4f0b4712abc7e69dc299856f14af6eadd пустой
frontend /Users/ksv741/Project/vibe/WealthLedger/frontend c8821200ce699dab1ce65360d1968d5877a2e78b пустой
specs /Users/ksv741/Project/vibe/WealthLedger/specs fcdc805f6c1b3be6a20a7e858e3e80cbd83a9121 пустой
docs /Users/ksv741/Project/vibe/WealthLedger/docs e6fd4f64b81dd73b33959b845a1babcfac1891c8 пустой

Инструкции, skills и источники

Задание 06-backend-architecture.md прочитано целиком. Перед исследованием каждого repo прочитан его AGENTS.md; backend/frontend — также местный engineering.md. Для backend активированы architecture, HTTP API, errors, data/money/transactions, integrations, security/auth, testing, observability; для frontend consumers — data/contract/auth/errors. Для specs прочитан AI_INDEX.md, затем существующие тематические specs; для docs — README, CONTRIBUTING, текущие объяснения и ADR 019. Root conventions применены только к записи/проверке отчёта.

ponytail-audit загружен первым через skill tool и применён к сложности: недоступная orchestration — DELETE, защищающие механизмы — KEEP. Его complexity-pass отделён от correctness-pass. Также применены codebase-design (locality/interface/deletion test), systematic-debugging (трассировка причины и контрпримеров), verification-before-completion. graphify загружен только для проверки существующего графа: graphify-out/graph.json отсутствует в root/backend, что подтверждено прямым Read; Glob не нашёл граф и в остальных трёх repo. Генерация/обновление/query с записью artifacts не выполнялись; выводы основаны на исходниках.

audit-results/technical-audit-2026-10-11.md прочитан только как реестр гипотез. Все приведённые ниже механизмы/строки/callers проверены заново. Например, тезис «календари никогда не обновляются» уточнён: dividend key меняется вместе с окном, а bond-events key — нет. Новая существенная проверка — выход Ledger backfill за запрошенный интервал и отсутствие hourly metadata у Accounts/Income.

Выполненные команды и ограничения

Проверка Команда/инструмент Результат и предел доказательства
Версия и чистота каждого repo git rev-parse HEAD && git status --short --untracked-files=all, отдельно в пяти cwd SHA выше; начальные status пустые
Inventory rg --files src -g '*.module.ts' | wc -l; аналогично controllers/repositories; rg -c '^model ' prisma/schema.prisma, cwd backend 18 module files, 11 controller files, 8 repository files, 21 Prisma models; это footprint, не число изученных use cases или показатель качества
Публичные artifacts git diff --no-index --stat "/Users/ksv741/Project/vibe/WealthLedger/backend/openapi.json" "/Users/ksv741/Project/vibe/WealthLedger/frontend/src/shared/api/spec/openapi.json", cwd root exit 0, вывода нет: файлы совпадают. Не доказывает свежесть относительно runtime Swagger
Source/consumer tracing Read/Glob/Grep с абсолютными repo dirs; поиск registrations, callers, state transitions, DTO и configured tests Статические механизмы подтверждены; production outcomes и частота ошибок не измерены
Библиотечные факты Context7 /nestjs/docs.nestjs.com, /prisma/web Подтверждены default singleton scope и rollback interactive transaction при throw; не выдаются за тест текущего adapter/version
Итоговые проверки перечитывание отчёта; diff checks и повтор HEAD/status Результаты зафиксированы после записи ниже

Первичные библиотечные свидетельства Context7: Nest provider scopes и Prisma integration/lifecycle; Prisma interactive transaction commit/rollback semantics. Документация подтверждает общую semantics, а не concurrency/stop outcome текущего приложения.

Никакие Node-команды, тесты, install, build, full-suite/verify, codegen, migrations, запуск app, финансовые live API, commit/PR/staging не выполнялись. Тестовые файлы читались как описание regression surfaces, не как свежий PASS. Безопасный существующий тест не запускался: для установленных ветвей достаточно кода, а значимые DB/lifecycle сценарии требуют изолированного HTTP/SQLite runner с запуском приложения, запрещённого этой сессией. При последующей проверке Node обязателен местный nvm use --silent.

Узкие поиски в больших деревьях иногда возвращали усечённый список; важные owners затем читались адресно. Ошибочные предположения об именах app.config.ts, ncr.client.ts, portfolio-value-history.service.ts не использованы как evidence: конфигурация найдена в main.ts, NCR — в ncr-ratings.client.ts; value-history write проверен непосредственно в PortfolioRepository/schema. Игнорируемый OpenAPI прочитан прямо, без доверия глобальному Glob.

Финальная целостность: проверка содержания, counts, точных source ranges и относительных ссылок выполнена; результаты Git-проверок приведены в §9.

2. Executive verdict

  1. Факт / P1 — BE-001: ForecastRepository.resolve возвращает пересекающийся старый forecast до проверки нового candidate. Сдвиг окна способен удерживать старые финансовые суммы после успешного обновления. Сам CAS/dependency fence правильный и подлежит KEEP.
  2. Факт / P1 — BE-002: верхний catch hourly walk не регистрирует сбой; settlement errors подавляются, а polling durable child rows не ограничен. Persistence failure может дать ложный succeeded либо прекратить дальнейшие hourly обновления этого подключения в текущем процессе.
  3. Факт / P2 — BE-003/004: новый schedule-driven owner сосуществует с прежними правилами: изменяемые календари считаются stable, Income GET запрашивает membership, Ledger запускает backfill за пределами выбранного интервала. Это конкретные ownership-разрывы, а не нехватка слоёв.
  4. Факт / P2 — BE-005/008: новые online paths обходят existing invalid-token translator; Accounts/Income не передают durable hourly attempt в resource meta. Ошибка и наблюдаемое состояние теряют смысл на application boundary, хотя общая безопасная HTTP boundary есть.
  5. KEEP: banking intent fingerprints/tombstones, версии/refund transactions, refresh CAS, decimal strings/bigint, connection revision и snapshot/dependency fences защищают реальные сценарии. Их удаление удешевит код за счёт корректности.
  6. INVESTIGATE: публикация прогнозов из GET, семантика отсутствующего MOEX block и завершение detached jobs при shutdown требуют адресного proof/решения. Эти пункты не считаются подтверждёнными actionable defects.
  7. Стоимость: основной cleanup — удалить недоступный screen-command path (BE-007); затем исправить существующих владельцев. Runtime/CI/token savings не измерены; никакие проценты или прогнозы ускорения не заявляются.

Actionable: 8 findings: P0 = 0, P1 = 2, P2 = 5, P3 = 1. По решениям: FIX = 7, DELETE = 1, SIMPLIFY = 0. Отдельно KEEP = 4, INVESTIGATE = 3. Severity KEEP отражает важность сохраняемой гарантии, а не существующий дефект. Confidence High означает подтверждённый механизм по current source, не выполненное runtime-воспроизведение.

3. Inventory & coverage

Source, schema и DI inventory

Область Что реально исследовано Ownership / ограничения
Composition src/app.module.ts:18–38, все 18 местных module registrations, src/main.ts:19–42, Prisma/logging lifecycle Config, Prisma и logging — shared infrastructure; бизнес-модули подключены в одном процессе
Auth controller, Credentials DTO, AuthService полностью, JWT strategy, configured concurrent refresh test DTO/email policy отдельно от session CAS и cookie boundary; logout изучен как альтернативный entrypoint
Banking controller, create/update DTO, card/operation/category services, input, RUB rules, repository, summary use case, relevant SQLite cases Все mutation entrypoints traced; manual-card spec прочитан по финансовым/ownership/replay требованиям. Полный UI аудит не выполнялся
Investments persistence Все 8 concrete repositories; Prisma schema целиком 21 model: User/Session, 3 banking models, 16 T-Bank models. Подсчёт по ^model ; группы установлены по именам схемы
Source-driven updates membership, portfolio, operations, catalog/enrichment, metadata, candles, hourly coordinator Проверены reserve → RPC → staging/publication → settlement, процессные maps/sets/promises и revision fences
Reads/projections Accounts, Portfolio, Ledger, Income, events; stock/bond catalog/income; screener как контрольный пример snapshot Персональные DTO, data:null, complete/empty, timestamps, dependent forecasts, mapping boundaries
Integrations TbankClient полностью, TbankAccess/Token/Membership, metadata owner, MoexClient полностью, NCR client Deadline/retry/auth mapping; no real upstream calls. Глубокий threat model и измерение fan-out вне 06
Public contract/consumers оба предоставленных OpenAPI, выбранные generated schema ranges; Portfolio/Events/Income/bank mutation consumers, shared frontend transport Связь реального caller с backend behaviour; полная route/schema parity без codegen не проверялась
Intended behaviour specs user-authentication, manual-bank-cards, hourly-investment-sync, tbank-data-sync, online-instrument-information, brokerage-account-upcoming-events; docs ADR 019, containers/deployment Requirements отделены от mechanism. Исторические/параллельные аудиты не являются authority

Критическая schema partition: prisma/schema.prisma:25–71,101–146,232–263 связывает private accounts/portfolios/operations/forecasts/work со (userId,revision) и cascade. Public instrument/source/candle данные ключуются provider/UID (148–230); personal holdings/estimates туда не должны попадать. Value-history intentionally переживает replacement (73–99), но visibility определяется подтверждённым membership. Banking FK — Restrict, replay data и tombstones сохраняются (278–334). Payload JSON остаётся внутренним форматом с mappingVersion; comprehensive historical payload migration не проверялась.

DI и shared mutable state

  • TbankModule:10–27 единожды регистрирует client, access, token, membership, metadata/repository; consuming modules импортируют module. InstrumentSourcesModule:12–23 единожды владеет catalog/candle providers. TbankSyncRepositoryModule:5–9 отделяет durable storage от coordinator и предотвращает обратную зависимость Portfolio → Sync orchestration.
  • PortfolioModule:12–23, OperationsModule:11–21, UpcomingEventsModule:11–17, TbankSyncModule:13–27 образуют явную композицию. TbankConnectionModule:9–14 импортирует sync module, но его controller/service не вызывают tick; первый запуск фактически предлагает minute timer, не PUT callback. Точная гарантия «немедленно» требует уточнения (§9).
  • InstrumentsModule:23–33 использует useExisting: TbankAccessService, а не создаёт второй access owner. Mandatory ports/interfaces не нужны: concrete injection сохраняет единый limiter и state. Production forwardRef, request/transient scopes, manual new этих services в просмотренных исходниках не найдены; найденные new AuthService/PortfolioService находятся в unit specs.
  • Stateful owners: Portfolio/Membership pending maps и serialized reservations; Operations ready/runningAttempts; Catalog/Candles/Metadata producers/observers/queues; Screener bounded snapshots; TbankClient FIFO limiter; NCR LRU of shared public registry promises. Их lifetime default singleton, подтверждённый Nest docs. Shared mutation не означает race автоматически: короткая reservation chain и DB attempt fence имеют реальные назначения.
  • Controllers преимущественно ограничены identity/DTO/cookie/делегацией. В StockController:85–99 дополнительный whitelist raw body — transport policy, не доменный расчёт. BankCardsRepository.card:62–67 знает archive/HTTP outcome — существующий cohesive transaction helper; extraction ради «чистого repository» лишь размножит pass-through и не рекомендуется.
  • Преобразования внешних records в Stock/Bond/Portfolio/Money DTO необходимы: currency normalization, nano/units, nullable money, ticker/UID, unknown values, filtering. Две копии publicInstrument — отдельный локальный complexity signal 02, не причина убирать whitelist целиком.

Backend Flow Atlas

Read/write в таблице означает local DB read/write, а не внешнюю финансовую операцию. В изученных flows нет исполнения торговых/банковских переводов в upstream: manual transfer — одна локальная ledger запись.

Flow / entrypoint Chain request/job → DTO → application → domain/persistence → integration → response/settlement Owner, side effects и инварианты
Auth refresh POST auth/refresh → cookie extraction, без body DTO → AuthService.refresh → JWT/type/session/hash checks → conditional Session.updateMany → access JWT → controller cookie + 200 AuthService — session; controller — cookie. R/W session, external integration отсутствует. Один победитель; loser не отзывает winner и не меняет cookie
Banking mutations POST/PATCH/DELETE bank-cards → runtime DTO/verified sub → Card/Operation/CategoryService → normalized input/fingerprint/RUB/date/ownership → BankCardsRepository.write + Prisma transaction → public view / 201,200,204 Operation service — refund/archive/transfer invariants; DB — atomicity, unique identity/CAS; domain — точные суммы. Нет зависимости от T-Bank
Portfolio data GET :accountId/portfolio/all → verified sub/path → PortfolioService.read/saved → access.local + PortfolioRepository.read + metadata.savedMany → currency totals/structure → {data,meta}; job counterpart PortfolioService.request → reserve → tokenForAttempt → GetPortfolio/GetPositions → mapper → publish/history → work GET локален. Source owner отдельно публикует complete snapshot и value-history в одной transaction; unknown metadata не блокирует финансовый GET
Hourly sync native minute tick → DB connections → reserveHourly → membership → portfolios всех account → текущий месячный operations tail → calendars → events.get → settleHourly → resource attempt metadata Coordinator owns cadence/lifecycle, source services own datasets. Никакой общей transaction вокруг сети/всего walk; section success сохраняется отдельно. Ошибки lifecycle — BE-002/006
Forecast publication GET events или hourly events step → strict date DTO/default UTC window → access/portfolio/readSources → exact quantity × calendar money → ForecastService.result → ForecastRepository.resolve dependency transaction → stored payload/filter/metadata Publication fence — repository; candidate calculation — UpcomingEventsService; GET/job оба могут записывать forecast. Window selection — BE-001; граница ownership — BE-013
Online instrument discovery GET stocks/bonds/search/ticker/detail/company → query/UUID/ticker DTO → Stock/BondCatalogService/Company use case → CatalogDiscovery/Enrichment online branch → access.onlineToken → TbankClient search/detail/prices → public projection/DTO → {data,meta} Normal online branch не пишет long-lived catalog и не меняет personal snapshots. observe остаётся альтернативным persisted read. Auth error mapping — BE-005

Две ветки и состояние после ошибки

F1. Auth refresh. backend/src/modules/auth/presentation/auth.controller.ts:81–91,113–138 → application/auth.service.ts:81–114,166–201. Success: проверенный refresh JWT и hash, одно atomic update, новый jti/7-day session, access 15m; cookie устанавливается только после результата. Invalid/expired/superseded: 401; hash mismatch ничего не пишет; expired DB session может удаляться, cookie не очищается. Storage lookup/update failure: 500 safe envelope, controller не успевает set-cookie. Реальная SQLite barrier surface существует в backend/test/app.e2e-spec.ts:161–213; не запускалась. CAS отдельно от полномочий JWT strategy; account authorization нельзя заменить guard. Сопутствующее создание session в register/login не является общей transaction с User: failure после создания User оставит зарегистрированный email; login — существующий recovery, runtime такого отказа не проверен и P1 из него не выводится.

F2. Banking. bank-cards/presentation/bank-cards.controller.ts:94–166,195–203 → соответствующие services. Create replay сначала проверяет owner/fingerprint/tombstone, только новое намерение проходит mutable date/archive/refund validation. Success transfer — одна operation с source/target; баланс derived, отдельного cached balance write нет. Refund сначала CAS-lock исходного expense version, затем aggregate limit и insert/update. Throw после lock откатывает и version, и запись; retry повторяет весь callback, не внешнюю операцию. PATCH использует version; DELETE сохраняет tombstone, повтор DELETE no-op, POST replay deleted — 410. Foreign link/archived card/date/limit/version conflict не оставляют половину transfer/refund. Evidence: bank-operations.service.ts:94–240,251–330, bank-cards.service.ts:74–154, bank-cards.repository.ts:35–73; regression surfaces test/bank-cards.e2e-spec.ts:391–486,622–708 прочитаны.

F3. Portfolio. Local saved читает current revision/access и coherent DB snapshot; all требует наличие обязательных account snapshots и не выдаёт partial positions за complete (portfolio.service.ts:55–122,170–231). Success source attempt: reserve short transaction → два независимых RPC вне transaction → normalize → PortfolioRepository.publish:116–185 claims attempt и пишет snapshot/history. После network failure прежний snapshot/fetchedAt остаётся, work failed; invalid token помечает connection invalid через access. Replacement deletes private rows/work; поздний publish не проходит attempt/revision claim. Metadata requests запускаются после финансового publish и не являются его atomic dependency. Если этот постпубликационный шаг падает, snapshot уже сохранён; fail не перепишет complete work — scheduler может не показать metadata problem. Это согласуется с optional-label характером metadata, но не доказывает полноту календаря.

F4. Hourly. tbank-hourly.scheduler.ts:48–172 и tbank-sync.repository.ts:174–242: timer only proposes, same-process running row запрещает overlap. Первый проход повторно подтверждает membership; accounts sequentially получают Portfolio, monthly operations, event sources. Ordinary source outage обычно фиксируется durable child work и превращается в partial/failed; другие successful sections не откатываются. Invalid membership token прекращает обработку, connection unavailable. Replacement/deletion удаляет rows; старый attempt settlement становится no-op. Unexpected throw, failure to settle child или parent и superseded child отличаются от обычного upstream outage: текущие handlers могут потерять terminal state/ошибку (BE-002). При restart другой processRunId позволяет interrupt/re-reserve, но это не recovery в текущем процессе.

F5. Forecast. upcoming-events.service.ts:54–173,200–239 формирует candidate только когда есть полный Portfolio, calendar completeness и identity/ticker. Unknown payouts — null, offer без придуманной суммы; totals по currency. forecast.service.ts:35–65 передаёт expected portfolio/source versions, forecast.repository.ts:35–175 проверяет их в transaction. При missing calendar/version race возвращается old compatible fallback либо null без partial publication. Однако fallback поиск по intersecting request window удерживает old candidate, а disjoint request при отсутствии candidate теряет old result (BE-001). Success publication и GET filtering сейчас находятся в одном call; scheduler вызывает его только для каждого account, all и instrument payout forecasts могут создаваться из read path — BE-013. Non-happy path с новым отсутствующим calendar действительно сохраняет forecast (test/upcoming-events.e2e-spec.ts:294–318); это не proof того, что уже сохранённый calendar после failed forced refresh всегда исключён из нового candidate.

F6. Online. Production stock/bond controller default sync !== observe → CatalogDiscoveryService.detail/byTicker/onlineSearch:149–165,227–249,295–337; company → CatalogEnrichmentService.section:241–267. Success record → public whitelist → domain-specific DTO; financial snapshots не перезаписываются. No match/null detail: not found; exact ticker ambiguity: conflict; deadline/queue/upstream outage: 503 TBANK_UNAVAILABLE; auth rejection: plain Error доходит до 500, а stored invalid flag даёт правильный 400 — BE-005. Search Promise.all reject не возвращает частичный список; сохранённые personal datasets не затронуты. Persisted observe читает старые records и не выполняет online branch; удаление compatibility без проверки consumers не предлагается.

Transaction & Consistency Boundary Map

Инвариант / владелец Atomic boundary и реальные entrypoints Сеть, error/retry и возможность обхода
Refresh rotation — AuthService Session conditional updateMany по id+old hash; refresh controller, Session schema 265–276 JWT signing до/после CAS, не network transaction. Superseded hash получает 401 без revoke. Проверка logout race/revocation policy относится 13
Bank intent/CAS/refunds — services + repository BankCardsRepository.write охватывает replay lookup, owner, source-version lock, refund sum, insert/update/delete и public view; create/update/delete card/op/category До write нормализуется immutable intent; mutable checks в transaction. Throw rollback; bounded busy retry повторяет callback целиком. DTO alone не владеет финансовым инвариантом
Connection replacement — ConnectionService PUT validateToken до DB; delete old + create revision/membership/accounts + history visibility в одной transaction (tbank-connection.service.ts:31–88) Upstream/encryption/transaction failure сохраняет прежнюю connection. DELETE идемпотентно скрывает history; успешный replacement даже тем же token создаёт revision
Membership — TbankAccessService claim current attempt, account reconciliation, membershipRevision/At, history visibility и completeWork (tbank-access.service.ts:148–238) RPC вне transaction; invalidToken mutation fenced по attempt/revision. Source caller — MembershipService; privileged put создаёт новый membership в отдельной корректной boundary
Portfolio/history — PortfolioRepository claim + snapshot upsert + per-currency attempt-key history points (116–185) RPC завершены; ошибка transaction не оставляет complete-work без snapshot. History only fully valued; late/superseded attempt no-op
Operations — OperationsRepository page staging 602–699; complete marker/identity checks; group publication+coverage+operationsRevision+advanceWork 703–881 RPC page вне transaction. Atomic reconciliation не публикует subset; first missing intervals могут complete отдельно. Broker IDs deduplicate; cursor continuation validated. Wide request planning — BE-004
Shared source/candles — Catalog/Candle repositories publication claims initiator+observer (catalog-discovery.repository.ts:154–278), candle staged range completeness and replacement (candle.repository.ts:137–301) Общая public row не содержит holdings/token error; work remains private. Network failure сохраняет прежнюю source row; failure финализации ломает lifecycle, не atomic DB data
Forecast — ForecastRepository connection/membership fence + expected portfolio/source revision/timestamp checks + payload upsert (35–175) Calculation вне transaction, versions rechecked внутри. Window lookup до fence проверки candidate — BE-001. Same generic owner имеет 3 production consumers: events, stock dividends, bond coupons
Hourly lifecycle — SyncRepository/Scheduler reservation transaction; отдельный conditional settlement по id/attempt/user/revision/state (174–242) Walk не ACID целиком, это правильно. Current-process stalled child rows/parent settlement нуждаются в BE-002; no distributed lease оправдан только single-process assumption

Source / request / schedule ownership и альтернативные entrypoints

Ресурс Intended owner Current callers и обход
Accounts/membership connection validation + hourly refresh PUT и MembershipService/hourly оправданы; Income collectRanges дополнительно вызывает membership — BE-004
Personal Portfolio hourly source service GET Portfolio/Accounts/positions local; PortfolioService.request callers hourly и недоступный screen command. Manual command удалить, source method сохранить
Operations tail/history hourly mutable tail; только missing Ledger interval — request exception readPlan также backfills remaining account history; Income вызывает его для comparison ranges — BE-004. planOperationRead уже не переобновляет complete interval: этот guard сохранить
Event calendars hourly future sources events.refresh → readMany(false) → requestMany(force=false) делает stable reuse — BE-003. Stock/bond payout GET имеют другие ranges/source kinds; это не доказательство freshness hourly calendar
Forecast complete source publication + saved read/filter GET и job входят в один result/resolve; no separate attempt contract. Не вводить новую очередь/CQRS, сначала проверить BE-013
Instrument detail/search/company online request Default path online, persisted observe compatibility; финансовый GET использует только saved labels. Внешний auth boundary пропущен — BE-005
Candles впервые отсутствующий диапазон CandleService.read local coverage → только gap request; native source service staged publication. Completed history reuses coverage; detailed tail policy не является performance аудитом
Screener pages explicit POST snapshot → local GET page stock-screener.service.ts:65–109,131–139: bounded snapshot/user/revision/TTL guards оправданы; page не перечитывает Shares

Integration Error Mapping Matrix

Источник / condition Integration outcome Application/job mapping → HTTP / durable state Retryability и состояние данных
DTO invalid configured ValidationPipe BadRequest filter → 400 VALIDATION_ERROR + pointers (main.ts:31–40, filter 24–47,67–81) До mutation; target/value исключены, internals не выдаются
Domain banking version/refund/archive/intent helpers BadRequest/Conflict/NotFound/Gone 400/409/404/410 BANK_*; controller не делает собственный envelope Immutable intent retry безопасен; failed transaction rollback. Frontend mutation retry false
Refresh invalid/expired/superseded AuthService Unauthorized 401 UNAUTHORIZED; login invalid credentials отдельный code Cookie untouched на failed refresh; no application retry loop
No T-Bank connection / stored invalid flag access.local/current/onlineToken 409 TBANK_NOT_CONNECTED / 400 INVALID_TBANK_TOKEN (tbank-access.service.ts:124–145,262–288) Permanent; no RPC; private read unavailable
gRPC auth rejected Error(INVALID_TBANK_TOKEN) (tbank.client.ts:553–557) PUT translator → 400; durable owners → failed work + invalid connection; online Discovery/Company/Screener → 500 INTERNAL_SERVER_ERROR (BE-005) Client RPC не retry auth. Frontend GET видит 500 и считает transient; personal snapshots не изменяются online path
gRPC deadline/resource exhausted/unavailable one retry then ServiceUnavailable 503 TBANK_UNAVAILABLE; jobs обычно store same failure category Bounded client retry; duplicate nested retry не нужен; old complete snapshots остаются
Local RPC queue full ServiceUnavailable TBANK_UNAVAILABLE 503 или failed child work Не повторяется внутри TbankClient (504–566); scheduler persistence recovery отдельный вопрос
gRPC NOT_FOUND / empty detail shareBy/getInstrumentBy special 404; absent instrument null Discovery detail → 404; metadata lookup stores supported absence bondBy не включён в special 404 branch (559–565), потому raw NOT_FOUND там станет 503; нужен synthetic parity case, не вывод о реальном upstream contract
Ticker ambiguity selected >1 exact matches 409 STOCK_TICKER_AMBIGUOUS / BOND_TICKER_AMBIGUOUS Не temporary outage; не publishing financial absence
MOEX 404 request returns null rows returns []; optional yield null; events may be complete empty No HTTP client retry loop; 10s AbortSignal timeout
MOEX missing block / malformed/cursor parseRows throws ServiceUnavailable MOEX_UNAVAILABLE enrichment stores TBANK_UNAVAILABLE; existing calendar preserved Missing block conflates absence/corruption — BE-014; public safe mapping, loss of provider-specific code in durable child policy не требует нового registry
NCR invalid ISIN/no match null optional rating null Не outage; shared public registry promise cache
NCR timeout/parser/empty registry service-unavailable NCR_UNAVAILABLE enrichment marks unavailable kind/durable failure Failed promise TTL 60s, не false no-rating; personal finance неизменны
DB/unexpected exception throw / callback rollback HTTP filter → 500 safe code; background owners log safe event Correct envelope не чинит missing terminal state. Hourly catch/settle silence — BE-002

Matrix основана на current source: TbankClient:504–588, TbankTokenService:26–35, CatalogEnrichmentService:535–590, MoexClient:119–198, NcrRatingsClient:28–57,64–126, HttpExceptionFilter:19–65. В ответах не обнаружен прямой raw stack/SQL/token output в изученных flows; это не comprehensive security certification.

4. Findings

BE-001 — Request window участвует в идентичности forecast и блокирует новую публикацию

  • Decision: FIX. Severity: P1. Confidence: High.
  • Evidence: backend/src/modules/investments/portfolio/infrastructure/forecast.repository.ts:50–79,80–174; caller backend/src/modules/investments/upcoming-events/upcoming-events.service.ts:152–173,271–279; scheduled caller backend/src/modules/investments/tbank-sync/tbank-hourly.scheduler.ts:166–175; filtering backend/src/modules/investments/portfolio/application/forecast.service.ts:98–120. Intended: specs/openspec/specs/brokerage-account-upcoming-events/spec.md:12–24,63–76. Real consumer: frontend/src/entities/brokerage-event/api/useBrokerageEvents.ts:7–23.
  • Claim: новый полный candidate для сдвинувшегося окна может не публиковаться, хотя все источники получены; disjoint read без candidate не возвращает требуемый saved empty result.
  • Current mechanism: exact key включает account/from/to. Если exact previous отсутствует, repository ищет первый intersecting forecast без ordering и выполняет if (!previous && fallback) return fallback до candidate dependency validation/upsert. Default UTC from меняется ежедневно. При no candidate и непересекающемся запросе fallback отсутствует.
  • Impact / actual cost: Impact unmeasured. Механизм способен удерживать старые projected amounts/calculatedAt/window и исключать новые события после successful hourly cycle. P1 обоснован систематическим блокированием финансовой publication boundary, не размером файла; фактический ущерб/частота не измерены.
  • Why now: hourly owner рассчитывает ежедневно меняющееся default window; это обычный live path, а не только искусственный caller. Existing test backend/test/upcoming-events.e2e-spec.ts:321–331 закрепляет disjoint data:null, не актуальное requirement.
  • Minimum change: в existing owner разделить выбор последнего complete forecast данного account от HTTP window filtering; fallback допускается при missing/incompatible candidate, но не должен veto нового полного candidate. Выбор latest deterministic. Сохранить исходное окно в record/response; wire-поле окна согласовать с 05. При необходимости изменить key — additive migration/backfill с сохранением старых records до proof, не delete/reset JSON history.
  • Guarantees at risk: membership/account scope, portfolio/source revision recheck, original calculatedAt/window, complete-only publication, failure fallback и all-account completeness.
  • Counterarguments / trade-offs: early return сохраняет timestamp при narrow GET — полезная гарантия (test/upcoming-events.e2e-spec.ts:619–641). Простое удаление line 79 позволит narrow GET создать другой прогноз; поэтому один-line removal недостаточен. «Самый новый» выбирается внутри current revision/membership, не глобально.
  • Verification or rollback: isolated SQLite existing upcoming-events suite: два full candidates с соседними default windows и изменённой quantity/calendar; второй становится latest; narrow/disjoint GET сохраняют его identity и original window; dependency race, account removal, source failure сохраняют fallback. Three ForecastService consumers (events/stock dividends/bond coupons) проверить отдельно. Backup/additive schema позволяет code rollback без утраты старых forecasts; DI providers не добавлять.
  • Effort: M. Related finding IDs: BE-003, BE-012, BE-013, BE-014.

BE-002 — Hourly lifecycle может терять ошибку или навсегда ждать orphan child work

  • Decision: FIX. Severity: P1. Confidence: High.
  • Evidence: backend/src/modules/investments/tbank-sync/tbank-hourly.scheduler.ts:93–110,178–218; backend/src/modules/investments/tbank-sync/tbank-sync.repository.ts:189–201,223–242,274–286; producer failure backend/src/modules/investments/instruments/application/catalog-enrichment.service.ts:563–590; Operations sibling backend/src/modules/investments/operations/application/brokerage-operations-sync.service.ts:195–230,296–301; membership sibling backend/src/integrations/tbank/tbank-membership.service.ts:36–44,53–68.
  • Claim: нештатное завершение walk может обозначаться success; transient DB failure может оставить durable child/parent running и исключить будущие hourly attempts в текущем процессе.
  • Current mechanism: верхний catch повторно вызывает settle с прежним failures; пустой список → succeeded. Settlement reject подавляется. Catalog outer catch очищает active/queue/completions без durable finish оставшихся rows. Scheduler polling ждёт, пока raw row перестанет быть running, без deadline/shutdown/attempt fence; reserveHourly same-process running row всегда возвращает null. Operations step+fail reject тоже очищает process activity без гарантии failed row. Любое non-running child принимается terminal, но raw superseded не разрешается к successor.
  • Impact / actual cost: Impact unmeasured. После одного persistence fault последующие обновления всех секций этого connection могут остановиться до restart/replacement, либо metadata сообщит succeeded без полного прохождения. Это серьёзный механизм P1; upstream outage обычно корректно обрабатывается и сам по себе не доказывает stall.
  • Why now: lifecycle — единственный плановый owner personal data; корректная DB atomicity не помогает scheduler, если он не узнаёт terminal outcome.
  • Minimum change: у coordinator различать completion/error/interruption и записывать safe section/cause при outer failure; bounded child wait с expected attempt/successor semantics; после bounded persistence recovery логировать невозможность durable settlement и разрешать диагностируемое восстановление stale same-process work. Использовать существующие rows/logger; не заменять их распределённой очередью. Проверить все source finalizers, не ограничиться scheduler catch.
  • Guarantees at risk: no overlapping walk, no early retry failed attempt, current connection isolation, independent successful section results, no false complete and no raw sensitive logs.
  • Counterarguments / trade-offs: restart другой processRunId восстанавливает eligibility; ошибки могут быть редкими. Но исправная single-process система должна переживать transient write failure без обязательного operator restart. Deadline не должен отменять legitimate long work и немедленно запускать overlapping replacement; re-reservation нуждается в fence.
  • Verification or rollback: controlled-time coordinator test + real disposable SQLite: отказ finish/publishSource после reservation, последующая DB recovery; wait завершается diagnostic failed/partial, next eligible walk допускается, late old publish rejected. Также throw до refreshAccounts, settle failure, connection deletion, operations supersession. Existing upcoming-events.e2e-spec.ts:140–148 scheduler заменяет stub; это не proof. Изменение state semantics без schema migration, preserve rows; rollback code возможен, но не восстанавливает остановленные attempts автоматически.
  • Effort: M. Related finding IDs: BE-006, BE-008, BE-012, BE-015.

BE-003 — Hourly обновление календарей использует stable reuse вместо refresh policy

  • Decision: FIX. Severity: P2. Confidence: High.
  • 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:100–125,184–194,596–625,679–692. Intended: specs/openspec/specs/hourly-investment-sync/spec.md:8–9; downstream ForecastService caller — upcoming-events.service.ts:160–173.
  • Claim: успешно сохранённые event sources могут не перечитываться при очередном hourly cycle, хотя их содержание изменяемо.
  • Current mechanism: refresh вызывает readMany с observe=false, который передаёт requestMany force=false; policy refresh разрешает только quote kinds. Bond-events — stable с постоянным rangeKey; dividends — stable в пределах exact window. Смена dividend window создаёт новый key и может загрузить его заново; это важный контрпример к формулировке «никогда».
  • Impact / actual cost: Impact unmeasured. Bond calendar может бесконечно сохранять старые объявленные/изменённые события; повторные same-window dividend passes игнорируют исправления. Hourly success не означает новое чтение calendar.
  • Why now: этот путь называется refresh и используется actual hourly owner; PUT/membership/portfolio force=true не распространяется на calendars.
  • Minimum change: scheduled event owner должен передать existing force для календарных kinds либо назначить им явную bounded freshness. Stable detail metadata оставить reusable. Перед forced refresh обеспечить сохранение предыдущего forecast на failed calendar, см. BE-013; GET не должен превращаться в refresh command.
  • Guarantees at risk: old complete calendar/forecast survives outage, no personal read RPC, shared source producer isolation, monthly operations/candle history unchanged.
  • Counterarguments / trade-offs: дополнительный upstream budget и rate-limit contention; changed-window dividends уже обновляются. Force всех detail kinds избыточен; ограничить календарями. Объём RPC/latency передан 07, здесь не измерен.
  • Verification or rollback: два successive scheduled passes для одной bond UID и одного dividend window с отличающимся upstream content; timestamps/data/events действительно меняются. Fail второго pass сохраняет прежние rows/forecast и показывает проблему. Local GET не вызывает RPC. DI/schema change не нужен; rollback policy в existing owner.
  • Effort: S. Related finding IDs: BE-001, BE-002, BE-013, BE-014.

BE-004 — Общий operations read planner расширяет request exception, Income обновляет membership

  • Decision: FIX. Severity: P2. Confidence: High.
  • Evidence: Ledger caller backend/src/modules/investments/operations/application/brokerage-operations.service.ts:120–151; Income caller того же файла 391–435; shared planner backend/src/modules/investments/operations/application/brokerage-operations-sync.service.ts:46–126; HTTP callers backend/src/modules/investments/operations/presentation/brokerage-operations.controller.ts:22–39, backend/src/modules/investments/income/income-analytics.controller.ts:24–79; backend/src/modules/investments/income/income-analytics.service.ts:45–93. Intended: specs/openspec/specs/tbank-data-sync/spec.md:9–22; actual frontend frontend/src/entities/income/api/useIncome.ts:14–45.
  • Claim: обычный Income GET инициирует membership и imports; ограниченный Ledger request инициирует также отсутствующую историю вне выбранного диапазона.
  • Current mechanism: controller default observe=false, consumer не посылает sync. collectRanges делает membership.request и schedule readPlan(history=true). Ledger тоже передаёт history=true; readPlan после requested load вычисляет accountHistory, remaining = uncovered(missing, targets) и requests их с priority history. Domain plan уже корректно loads only missing, но application расширяет сами targets.
  • Impact / actual cost: Impact unmeasured. Разные read entrypoints становятся владельцами одной private sync и скрыто расширяют work scope. Это нарушает узкое исключение и увеличивает сложность recovery/supersession. Число RPC и latency не оценены.
  • Why now: ordinary frontend Income query достижим, а текущие tests преимущественно используют observe; Ledger scope-expansion не обнаруживается проверкой одного requested результата.
  • Minimum change: оставить GET Income только local collect/projection; у shared readPlan убрать history-outside-target branch для read callers, сохранить explicit scheduled/request source APIs и только missing requested Ledger intervals. Удалить больше не нужный MembershipService injection из read service после проверки всех callers. Не создавать второй read planner под новым именем. Выбрать cold historical income UX до удаления источника его backfill.
  • Guarantees at risk: first missing Ledger loads permitted data, complete past interval local reuse, monthly tail hourly owner, bounded requested interval, valid cursor/access before any work, independent comparison completeness.
  • Counterarguments / trade-offs: backfill улучшает historical Income availability без ручных действий; отказ может оставить history missing. Это реальная продуктовая цена, но текущая сохранённая policy запрещает hidden full-history work. Решать missing-history ownership явно, а не механически восстанавливать legacy.
  • Verification or rollback: configured HTTP default GET без observe: Income не создаёт membership/operation work; Ledger c narrow past range создаёт intent только внутри range и не outside history. Reopen confirmed interval без RPC; cold/missing comparisons не показывают ложные zero. Caller search ограничить обоими production readPlan paths; DI remove только фактически unused dependency. Code rollback не отменит уже запущенную wide work — regression проверять на disposable DB.
  • Effort: M. Related finding IDs: BE-002, BE-007, BE-008, BE-012.

BE-005 — Online application paths обходят существующий invalid-token перевод

  • Decision: FIX. Severity: P2. Confidence: High.
  • Evidence: backend/src/integrations/tbank/tbank.client.ts:553–565; translator backend/src/integrations/tbank/tbank-token.service.ts:26–35; online calls backend/src/modules/investments/instruments/application/catalog-discovery.service.ts:149–165,227–249,295–317, backend/src/modules/investments/instruments/application/catalog-enrichment.service.ts:241–267; sibling screener backend/src/modules/investments/instruments/stocks/application/stock-screener.service.ts:65–81; HTTP filter backend/src/common/filters/http-exception.filter.ts:19–31,50–64; real consumer policy frontend/src/shared/api/client.ts:162–193.
  • Claim: неизвестный локально, но rejected upstream broker token в online branch возвращается как internal server error, а не permanent connection failure.
  • Current mechanism: access.onlineToken/current проверяют только stored invalid flag. New gRPC auth rejection становится обычным Error; online services не вызывают translator. Filter правильно скрывает Error как 500 INTERNAL_SERVER_ERROR. PUT и durable owners уже имеют другие корректные routes обработки.
  • Impact / actual cost: Impact unmeasured. Клиент теряет INVALID_TBANK_TOKEN recovery code; actual shared GET transport относит 500 к transient. Никакое измерение лишних RPC/latency не выполнено.
  • Why now: online-only policy перевела эти endpoints с durable work mapping на прямую request/error boundary; единого error owner здесь не хватает при уже существующем translator.
  • Minimum change: переиспользовать translateInvalidToken вокруг всех прямых online operations, включая screener. Расположить translation у existing application/access boundary; не менять globally plain Error на HttpException без regression durable callers. Нужен один смысл permanent failure, не новый registry.
  • Guarantees at risk: durable invalid-token state/revision fencing, safe envelopes, no auth retry, absence vs unavailable, online failure не меняет private snapshots. Marking connection invalid из online read — отдельное решение с captured revision, не побочный эффект этого минимального fix.
  • Counterarguments / trade-offs: stored invalidToken уже даёт 400 — баг проявляется только при первом external rejection. Глобальный client mapping короче, но сломает существующие string-code catch owners; безопаснее reuse current translator.
  • Verification or rollback: synthetic client auth rejection через configured stock/bond detail/search/ticker/company/screener endpoints → 400 INVALID_TBANK_TOKEN; source attempt по-прежнему failed+invalid flag, temporary unavailable → 503; no financial DB changes. DI использует уже экспортируемый TbankTokenService, не новую реализацию. Code-only rollback.
  • Effort: S. Related finding IDs: BE-009, BE-012.

BE-006 — Eligibility после длинного walk не пропускает наступивший срок

  • Decision: FIX. Severity: P2. Confidence: High.
  • Evidence: backend/src/modules/investments/tbank-sync/tbank-sync.repository.ts:171–218, caller backend/src/modules/investments/tbank-sync/tbank-hourly.scheduler.ts:48–86; intended scenario specs/openspec/specs/hourly-investment-sync/spec.md:8–25.
  • Claim: после завершения walk, перекрывшего очередной hourly term, следующий minute tick уже eligible, хотя пропущенный term не должен становиться немедленным повтором.
  • Current mechanism: due = now - createdAt >= hourMs; running в том же процессе блокируется, но после completion учитывается только минимальный elapsed hour. Walk 10:00 → завершился 11:10 → tick 11:11 eligible; requirement допускает следующий не ранее 12:00.
  • Impact / actual cost: Impact unmeasured. Более частое чем задано refresh после длительного обхода; changing cadence и retry ownership. Никакой оценки нагрузки из этого не выводится.
  • Why now: reservation — действующий единственный owner cadence. Timer frequency сам по себе не чинит eligibility.
  • Minimum change: вычислять следующий допустимый 60-minute term от attempt start с учётом terms, пропущенных во время работы; завершение/interrupt history не должно сдвигать start base. Сохранить single catch-up после downtime.
  • Guarantees at risk: no overlap, failed attempt no early retry, first new revision eligibility, restart interruption и no replay пропущенных часов.
  • Counterarguments / trade-offs: текущий код удовлетворяет простому «не ранее 60 минут», но не более точному saved scenario. Если deliberate desired policy изменилась, сначала согласовать requirement; doc/script redesign не нужен.
  • Verification or rollback: controlled Date + repository state: short, failed, 10:00–11:10, 10:00–12:10, restart/downtime/new revision, точная граничная eligibility. SQLite reservation race proof отдельно от mocked clock. DI не меняется; formula code rollback.
  • Effort: S. Related finding IDs: BE-002, BE-015.

BE-007 — Недоступная screen-command orchestration сохраняет прежнего владельца обновления

  • Decision: DELETE. Severity: P3. Confidence: High.
  • Evidence: backend/src/modules/investments/tbank-sync/tbank-sync.service.ts:54–311; единственный HTTP consumer backend/src/modules/investments/tbank-sync/tbank-sync.controller.ts:31–41 вызывает только status; registration backend/src/modules/investments/tbank-sync/tbank-sync.module.ts:14–25; helper backend/src/modules/investments/tbank-sync/tbank-screen.ts:9; действующий отдельный consumer plan — backend/src/modules/investments/operations/application/brokerage-operations.service.ts:120–151; sourceStatus использует Operations tbank-sync.service.ts:364–390.
  • Claim: request/dispatch не имеют production entrypoint в текущем checkout и сохраняют лишний manual refresh owner/DI dependencies.
  • Current mechanism: service инжектирует membership, portfolio, metadata, catalog discovery/enrichment, candles ради недоступного dispatch. Controller публикует только GET status. Поиск refs класса/normalizeScreen/planInvestmentScreen и callers в src не выявил production вызова request; OpenAPI contract test также ожидает отсутствие командного route (backend/test/openapi-contract.e2e-spec.ts:30).
  • Impact / actual cost: Impact unmeasured для времени сопровождения/agent tokens. Подтверждён candidate block 54–311 = 258 физических строк (включая blank/comments, метод подсчёта 311 - 54 + 1); это нижняя граница потенциального удаления, не готовый diff и не прогноз total cleanup.
  • Why now: сохранённая policy запрещает manual personal sync; новый hourly owner уже существует. Удаление снижает вероятность повторного подключения legacy flow.
  • Minimum change: удалить request/dispatch и исключительно связанные imports/fields/normalize helper. Сохранить status/sourceStatus и Operations dependency для observe successor. Не удалять planInvestmentScreen целиком, shared screen DTO wire shape, repository read или public GET compatibility без отдельной проверки consumers.
  • Guarantees at risk: current revision status isolation, source/superseded operations observation, public response compatibility; source publication methods не являются dead только потому, что один их caller удаляется.
  • Counterarguments / trade-offs: внешний dynamic caller или будущий manual refresh не подтверждён. Для возврата feature придётся реализовать новый контракт; это не основание поддерживать отключённую orchestration. Persisted screen rows могут оставаться историческим input status, их schema deletion в эту задачу не входит.
  • Verification or rollback: повтор production caller search; targeted configured status/source tests, DTO/OpenAPI parity; реальный AppModule initialization после DI pruning. TbankSyncModule imports InstrumentSources можно убрать только если больше нет consumers в его providers; Portfolio/UpcomingEvents/Operations для hourly оставить. Code rollback без migration.
  • Effort: S. Related finding IDs: BE-004, BE-009.

BE-008 — Accounts/Income resource meta не отражает последнюю durable hourly attempt

  • Decision: FIX. Severity: P2. Confidence: High.
  • Evidence: Accounts backend/src/modules/investments/brokerage-accounts/brokerage-accounts.service.ts:38–76; Income ranges backend/src/modules/investments/operations/application/brokerage-operations.service.ts:438–445,489–540; top-level Income backend/src/modules/investments/income/income-analytics.service.ts:45–70,114–135; defaults backend/src/modules/investments/investment-resource.mapper.ts:60–61,91–103; корректный пример backend/src/modules/investments/portfolio/application/portfolio.service.ts:223–229. Intended specs/openspec/specs/hourly-investment-sync/spec.md:27–28, tbank-data-sync/spec.md:24–29; generated consumer frontend/src/shared/api/generated/schema.d.ts:940–996.
  • Claim: поле присутствует в contract, но metadata разных personal resources содержит разные понятия lastAttempt; successful hourly state может превращаться в none у Accounts/Income.
  • Current mechanism: Accounts читает hourly и отдаёт legacy top-level sync, но meta factory получает work=null и не получает lastAttempt → none. Income ranges/top-level не передают hourlyAttempt вообще; fallback строится из source work, timestamps null, failed problems помечаются section membership даже для operation work. После complete work fallback возвращает none, хотя hourly attempt есть.
  • Impact / actual cost: Impact unmeasured. Consumers, использующие стандартный meta вместо Accounts-only sync, не могут различить absence/succeeded/error history; нарушается единый наблюдаемый lifecycle. Финансовые суммы сами по себе не повреждаются.
  • Why now: schema требует lastAttempt, и реальные Income/Portfolio/Events queries возвращают одинаковую resource envelope. Presence/type parity не обнаруживает semantic omission.
  • Minimum change: передать existing durable hourlyAttempt на Accounts meta и во все Income meta levels с соответствующей operations/account relevance. Reuse helper/repository через имеющуюся module boundary; source work state хранить как техническое наблюдение, не подменять им hourly attempt. Сохранить top-level Accounts sync до consumer migration.
  • Guarantees at risk: current connection privacy, partial independent results, no fake timestamps, source freshness unchanged, no extra personal work on metadata read.
  • Counterarguments / trade-offs: Accounts уже показывает полный state через sync; это оправдывает compatibility, но не неверное meta. Один global partial state может отражать unrelated account issue — определить relevance helper existing policy, не вводить вторую status registry.
  • Verification or rollback: реальные saved hourly none/running/succeeded/partial/failed rows + Accounts/Income/Portfolio GET: lastAttempt state/time/problem scope согласованы; source timestamp/data unchanged. Default Income GET проверять после BE-004; no RPC/work reservation. DI changes ограничить existing repository import/export, schema change не нужен.
  • Effort: S. Related finding IDs: BE-002, BE-004, BE-009.

BE-009 — KEEP: конкретные modules/providers и простой монолит

  • Decision: KEEP. Severity: P3. Confidence: High.
  • Evidence: backend/src/app.module.ts:18–38, backend/src/integrations/tbank/tbank.module.ts:10–27, backend/src/modules/investments/instruments/instrument-sources.module.ts:12–23, backend/src/modules/investments/tbank-sync/tbank-sync-repository.module.ts:5–9, backend/src/modules/investments/instruments/instruments.module.ts:23–33; topology statement docs/architecture/deployment.md:1–8.
  • Claim: текущая composition уже обеспечивает пригодные к тестированию interfaces и общие singleton owners; mandatory ports/CQRS/distributed scheduler не решают подтверждённые defects.
  • Current mechanism: concrete services injected через один owning module; metadata/candle/source и durable work helper отделены по ответственности; useExisting alias не создаёт второй экземпляр. Controllers делегируют, pure calculations отдельно. Single-process ceiling явно помечен ponytail: у hourly scheduler 26–31.
  • Impact / actual cost: Impact unmeasured. Сохранение предотвращает дополнительную DI/config/serialization/migration complexity; текущая latency или экономия не измерены.
  • Why now: findings о смешанном ownership легко ошибочно превратить в большой DDD/CQRS rewrite вместо исправления конкретных state transitions.
  • Minimum change: оставить монолит/concrete injection и исправлять текущих owners; prune только недоступные dependencies по BE-007.
  • Guarantees at risk: единственный limiter/cache/pending owner, простой backup SQLite и independent section publication.
  • Counterarguments / trade-offs: horizontal scaling или несколько backend processes потребуют lease/distributed coordination и другой proof. Фактическая deployed replica count не предоставлена; docs не считаются runtime measurement.
  • Verification or rollback: relevant AppModule initialization/HTTP test при DI pruning; deploy manifest от владельца deployment для multi-process решения. Архитектурная миграция сейчас не нужна.
  • Effort: S. Related finding IDs: BE-002, BE-007, BE-015.

BE-010 — KEEP: banking transactions, versions, intent fingerprints и refresh CAS

  • Decision: KEEP. Severity: P2. Confidence: High.
  • Evidence: backend/src/modules/banking/bank-cards/application/bank-operations.service.ts:152–240,251–330; backend/src/modules/banking/bank-cards/application/bank-cards.service.ts:74–154; backend/src/modules/banking/bank-cards/infrastructure/bank-cards.repository.ts:35–73; backend/prisma/schema.prisma:278–334; auth backend/src/modules/auth/application/auth.service.ts:99–114,166–201; callers controllers и regression surfaces backend/test/bank-cards.e2e-spec.ts:391–486,622–708, backend/test/app.e2e-spec.ts:161–213.
  • Claim: SHA fingerprint/version/tombstone — не искусственная сложность: каждый механизм отличает meaningful competing financial/auth outcomes.
  • Current mechanism: replay сначала проверяет original normalized intent, потом возвращает current record; tombstone запрещает восстановление deleted operation. CAS/refund source-version lock и whole-callback transaction обеспечивают aggregate limit. Refresh old-hash CAS даёт одного победителя, loser cookie не очищается.
  • Impact / actual cost: Impact unmeasured. Удаление даст double-entry, stale overwrite, refund overspend либо session winner revocation. Измеренного incidents/maintenance savings нет.
  • Why now: это защищённые invariants для любого cleanup application/repository layering.
  • Minimum change: сохранить механизмы и cohesive transaction boundaries; не переносить checks за transaction и не заменять DB CAS процессным lock.
  • Guarantees at risk: exact once intent, independent concurrent operations, current replay response, no resurrection, refund sum≤expense, rollback source lock, cookie winner.
  • Counterarguments / trade-offs: fingerprint — не security secret; hash identity — не generic idempotency framework. Runtime uniqueness errors нужно интерпретировать конкретно, но second registry/storage не нужен. Existing DB tests прочитаны, не запускались.
  • Verification or rollback: при изменении owner использовать текущие SQLite cases replay/concurrent refunds/rejected lock rollback/stale update/deleted replay и refresh barrier. Code rollback с сохранением schema/tombstones; их purge требует отдельной data migration.
  • Effort: S. Related finding IDs: BE-011, BE-012.

BE-011 — KEEP: decimal strings, bigint-копейки и meaningful financial mappers

  • Decision: KEEP. Severity: P2. Confidence: High.
  • Evidence: backend/src/common/decimal.ts:19–83; backend/src/modules/banking/bank-cards/domain/rub-money.ts:12–38; backend/src/modules/investments/portfolio/infrastructure/tbank-portfolio.mapper.ts:63–106,109–139; backend/src/modules/investments/income/income-analytics.service.ts:185–198,296–326; backend/src/modules/investments/upcoming-events/upcoming-events.service.ts:119–143,282–299.
  • Claim: эти helpers и mappings выполняют реальные precision/currency/null semantics; замена на JS number или прямой upstream record ухудшит contract.
  • Current mechanism: parsed decimal = bigint+scale, aligned add/subtract/multiply, explicit divide rounding; per-currency aggregation; RUB at most two decimals and signed-64 input boundary; arbitrary exact derived sums; nullable unknown money отделено от known zero.
  • Impact / actual cost: Impact unmeasured. Guards предотвращают потерю копеек/nano, смешивание currencies и ложные нули. Число avoided incidents и runtime cost не измерены.
  • Why now: flatten application layers не должен flatten anti-corruption financial representation.
  • Minimum change: сохранить helpers и projections у current owners; unify только доказанно одинаковую public whitelist, не money/portfolio mapping с разными semantics.
  • Guarantees at risk: exact input/output, unknown≠zero, bond percentage≠nominal money, currency separation, aggregate без arbitrary input cap.
  • Counterarguments / trade-offs: custom decimal имеет собственную ответственность и tests; generic decimal library migration может быть оправдана новыми операциями, которых сейчас не установлено. Одного number для display percentage не следует трактовать как финансовую persistence model.
  • Verification or rollback: existing decimal/RUB/domain tests и configured boundary values; cross-currency returns, non-integer quantity/nano, negative/zero, RUB overprecision/limits. Замена storage type без migration не допустима.
  • Effort: S. Related finding IDs: BE-010, BE-012.

BE-012 — KEEP: connection/attempt/dependency fences и staged complete publication

  • Decision: KEEP. Severity: P2. Confidence: High.
  • Evidence: backend/src/modules/investments/tbank-connection/tbank-connection.service.ts:31–88; backend/src/integrations/tbank/tbank-access.service.ts:148–257; backend/src/modules/investments/portfolio/infrastructure/portfolio.repository.ts:116–185; backend/src/modules/investments/portfolio/infrastructure/forecast.repository.ts:35–49,80–175; backend/src/modules/investments/operations/infrastructure/brokerage-operations.repository.ts:602–699,703–881,932–944; backend/src/modules/investments/instruments/infrastructure/candle.repository.ts:193–301.
  • Claim: conditional publication — необходимая сложность; source result не становится safe только потому, что upstream succeeded.
  • Current mechanism: new connection revision, composite FKs/cascade, attemptId/process owners, claim before publication, exact source/portfolio versions, staging complete markers, work+snapshot+coverage atomic commit. Actual callers — hourly, missing Ledger/candles, forecast consumers и connection PUT/DELETE.
  • Impact / actual cost: Impact unmeasured. Защищает от late/superseded publication, cross-connection financial leak, incomplete reconciliation и confirmed empty при missing dependencies.
  • Why now: BE-001/002/003/004 меняют выбор/порядок work, поэтому именно эти guards должны оставаться regression surface.
  • Minimum change: сохранять claims и compound predicates; чинить selection/lifecycle рядом с ними, не заменять durable identity проверкой только process memory и не удерживать transaction вокруг RPC.
  • Guarantees at risk: previous complete result and timestamps, fail/late no-op, idempotent attempt history points, all source completeness, rollback stage/result consistency.
  • Counterarguments / trade-offs: есть overlap технических pattern fragments между owners. Generic executor/repository абстракция не доказана: publication scopes/rollback groups различны. Attempt-level fences не гарантируют correct cadence и terminal lifecycle, что видно BE-002.
  • Verification or rollback: real SQLite publication race/supersession/connection replacement/dependency mismatch cases; current test/upcoming-events.e2e-spec.ts:344–369,643–668 обеспечивает подходящие сценарии для части boundary, но не был выполнен. Schema migration только additive с backup и isolated restore.
  • Effort: S. Related finding IDs: BE-001, BE-002, BE-004, BE-010, BE-011, BE-013.

BE-013 — INVESTIGATE: один forecast interface объединяет saved read и публикацию

  • Decision: INVESTIGATE. Severity: P2. Confidence: Medium.
  • Evidence: backend/src/modules/investments/upcoming-events/upcoming-events.service.ts:54–173; backend/src/modules/investments/portfolio/application/forecast.service.ts:18–95; backend/src/modules/investments/portfolio/infrastructure/forecast.repository.ts:24–175; other production callers backend/src/modules/investments/instruments/stocks/application/stock-income.service.ts:51–94, backend/src/modules/investments/instruments/bonds/application/bond-income.service.ts:82–125; scheduled event caller backend/src/modules/investments/tbank-sync/tbank-hourly.scheduler.ts:166–175.
  • Claim: request/job share publication ownership; необходимо выяснить, должен ли local GET после failed/unfinished calendar cycle иметь право публиковать projection по новым holdings и старой complete source row. Сам факт local DB write ещё не доказывает неправильный финансовый outcome.
  • Current mechanism: every GET calculates candidate из saved current portfolio/source data и calls resolve(candidate); repo не проверяет calendar attempt outcome. Stored source completeness остаётся complete после failed force; scheduler проверяет children и сам skips get при failed, но другой HTTP caller этого barrier не имеет. Job публикует только per-account events, не all/instrument payout forecasts.
  • Impact / actual cost: UNVERIFIED; Impact unmeasured. Potential relabel of snapshot/calculatedAt после failed source refresh и request-owned all forecast. Достижимость failed existing stable calendar из обычного hourly path ограничена BE-003, поэтому это не добавлено в actionable defects.
  • Why now: BE-003 force refresh расширит реальный non-happy path; BE-001 изменит selected candidate. Нужно не создать regression исправлением рядом.
  • Minimum change: сначала existing upcoming-events fixture: full forecast, same UID quantity change, forced same-calendar failure, subsequent normal GET. Если old forecast заменяется вопреки desired contract — split existing read/publish methods, schedule-specific publication, saved all aggregation; без CQRS/new module. Применимость к stock/bond payout ranges решить отдельно.
  • Guarantees at risk: original holdings/calculatedAt/window, failed update retention, all-account complete contributions, optional metadata independence и accurate data:null.
  • Counterarguments / trade-offs: lazy local projection по полным stored dependencies может быть сознательно корректной моделью и проще отдельного materializer. Read-only для сети не обязательно означает zero DB writes. Без product decision о snapshot identity нельзя рекомендовать полный перенос.
  • Verification or rollback: cheap synthetic DB fixture + existing ForecastRepository and GET boundary, no financial network; определить expected output до migration. При split проверить DI exports и все три callers; старые rows/backfill сохранить для reversible code transition.
  • Effort: M. Related finding IDs: BE-001, BE-003, BE-012.

BE-014 — INVESTIGATE: MOEX missing block и corrupted payload имеют одинаковый смысл

  • Decision: INVESTIGATE. Severity: P2. Confidence: High.
  • Evidence: backend/src/integrations/moex/moex.client.ts:119–177,182–198; consumer backend/src/modules/investments/instruments/application/catalog-enrichment.service.ts:469–472,535–590; retained fixture expectations backend/test/upcoming-events.e2e-spec.ts:540–599; intended specs/openspec/specs/brokerage-account-upcoming-events/spec.md:78–84.
  • Claim: source и saved requirement расходятся: успешный ответ без requested block считается ошибкой, spec считает отсутствием данных. Нужность самой spec semantics не подтверждена upstream contract.
  • Current mechanism: parseRows(undefined) throws MOEX_UNAVAILABLE так же, как wrong columns/row shape; cursor отдельно строго validates complete pagination. Durable layer сохраняет old calendar и failed work.
  • Impact / actual cost: Impact unmeasured. Возможен false unavailable вместо confirmed empty; противоположный механический fix способен публиковать false empty вместо incomplete upstream response.
  • Why now: calendar refresh fixes будут регулярно проходить эту boundary; current tests закрепляют conservative interpretation, но не подтверждают сохранённое requirement.
  • Minimum change: согласовать absence contract для каждого запрошенного блока, затем отличить deliberate missing от malformed/cursor truncation в existing parser. До такого proof guards KEEP; no second error registry/client.
  • Guarantees at risk: complete calendar, malformed rejection, cursor termination, previous forecast preservation, absence≠outage.
  • Counterarguments / trade-offs: conservative failure безопаснее false complete empty. Saved spec — requirement evidence, но не доказательство поведения MOEX; network investigation в сессии не выполнялась.
  • Verification or rollback: synthetic existing fixtures undefined block/empty valid block/wrong columns/invalid cursor; approved saved protocol examples или первичная документация MOEX, без financial live calls. Изменение interpretation code-only, existing records not rewritten automatically.
  • Effort: S. Related finding IDs: BE-001, BE-003, BE-013.

BE-015 — INVESTIGATE: detached source jobs не имеют общей graceful-stop boundary

  • Decision: INVESTIGATE. Severity: P3. Confidence: Medium.
  • Evidence: backend/src/main.ts:40–42; backend/src/modules/investments/tbank-sync/tbank-hourly.scheduler.ts:48–54,84–86; backend/src/prisma/prisma.service.ts:24–30; detached drains backend/src/modules/investments/operations/application/brokerage-operations-sync.service.ts:175–180, backend/src/modules/investments/instruments/application/catalog-enrichment.service.ts:221–225; gRPC clients created backend/src/integrations/tbank/tbank.client.ts:190–255.
  • Claim: остановка timer не равна завершению walk/drains; поведение активных RPC/DB publication при app close/SIGTERM не доказано.
  • Current mechanism: shutdown hooks enabled, Scheduler onModuleDestroy только clearInterval; detached promises не возвращены из hook. Prisma disconnect — отдельный owner. У TbankClient нет module-destroy close implementation. Durable processRunId recovery присутствует, но runtime stop order/grace period не исследованы.
  • Impact / actual cost: UNVERIFIED; Impact unmeasured. Возможны interrupted work и диагностический шум при shutdown; data loss из этого не установлена, DB transaction fences сохраняются.
  • Why now: controlled scheduler lifecycle входит в scope; single-process stop/deploy — обычная операция, не hypothetical horizontal scaling.
  • Minimum change: сначала controlled synthetic app-close case с заблокированным RPC; установить, нужен ли bounded drain/cancel и closing flag у existing owner. Не строить общий job framework/lease ради неподтверждённого исхода.
  • Guarantees at risk: no new reservations after stop, bounded shutdown, rollback in-flight transaction, next process recovery, no pending late publish into new revision.
  • Counterarguments / trade-offs: process termination + durable interruption may be accepted design, docs допускают короткий downtime. Добавление await-all без deadline может само повесить shutdown; timer stop alone и app close outcome следует проверить отдельно.
  • Verification or rollback: existing Nest HTTP fixture с synthetic deferred RPC, app.close → release → restart on disposable SQLite; проверить terminal/recoverable rows, отсутствие post-close unexpected rejection. Production grace period/replica config предоставить отдельно. DI stays existing.
  • Effort: M. Related finding IDs: BE-002, BE-006, BE-009, BE-012.

5. DELETE / SIMPLIFY / KEEP / INVESTIGATE

ID Decision Предмет решения Почему это минимально достаточно
BE-001 FIX Forecast selection/window vs publication Исправить existing owner, сохранить fences и saved filtering
BE-002 FIX Terminal hourly/source work semantics Current durable rows/logger, bounded recovery вместо новой job platform
BE-003 FIX Mutable calendars in scheduled refresh Existing force/policy только calendar kinds
BE-004 FIX Income imports и Ledger scope expansion Убрать скрытых read owners; сохранить narrow missing-load exception
BE-005 FIX Invalid-token online mapping Existing translator и actual caller sweep
BE-006 FIX Skipped hourly term Local eligibility formula, не второй timer
BE-007 DELETE Недоступные request/dispatch Реального production command consumer нет; status/source APIs сохранить
BE-008 FIX Resource-relevant durable attempt metadata Existing hourlyAttempt, без status registry
BE-009 KEEP Concrete DI/simple modular monolith Реальные shared owners; недоказанная нужда в распределении
BE-010 KEEP Financial transaction/CAS/replay/tombstones Предотвращает lost update/refund overflow/resurrection/session winner loss
BE-011 KEEP Exact money and boundary mappers Meaningful precision/currency/null conversion
BE-012 KEEP Connection/attempt/dependency/staging fences Late response и complete-only publication
BE-013 INVESTIGATE Request/job forecast publication interface Product semantics + same-source-failure fixture до переноса
BE-014 INVESTIGATE MOEX missing-block meaning Не превращать corrupted/incomplete calendar в confirmed empty
BE-015 INVESTIGATE Graceful stop of detached jobs Runtime proof до нового coordination code

SIMPLIFY: самостоятельных подтверждённых architecture-level кандидатов нет. Мелкие duplicate mappings/pass-through wrappers переданы 02; отсутствие SIMPLIFY не означает обязанность наполнить таблицу.

Complexity-pass по фактическому формату ponytail-audit:

  • delete: убрать недоступные TbankSyncService.request/dispatch и исключительно связанные state/imports/normalization; replacement — existing hourly owner плюс retained status reader. [backend/src/modules/investments/tbank-sync/tbank-sync.service.ts:54–311]

net: -258 lines, -0 deps possible. 258 — точный размер подтверждённого candidate block по current line range, а не размер подготовленного diff; дополнительные helper/import deletions не оценены. Ни одной dependency, которую можно удалить вместе только с этим path, не установлено.

Что оставить простым монолитом

Auth, banking и investment source owners должны оставаться в одном Nest application. Transactional банковский сценарий не следует дробить на use-case class для каждого write или оборачивать Prisma в generic port. Shared financial records/forecasts требуют concrete publication guards, а не CQRS шины. Hourly coordinator может остаться native timer + durable reservation + source services; каталог — existing online adapters и narrow saved labels/calendar/candle storage. Самый дешёвый полезный рефакторинг здесь — уменьшение альтернативных владельцев и явные terminal outcomes, а не новые уровни.

6. Cost & benefit

Все размеры эффекта, кроме физического блока BE-007 и footprint inventory, not measured. Effort S/M — качественная оценка scope, не часы и не SLA.

Действие Ожидаемый класс benefit Фактическое измерение Regression/security boundary и migration cost
BE-001 latest saved selection reliability/maintenance/cognitive load not measured Narrow/disjoint queries, day shift, all scope, source/portfolio race; возможная additive key/window migration и DTO consumer transition
BE-002 terminal lifecycle reliability/operational recovery not measured Fault-injected settlement + DB recovery, no overlap, correct cadence, old result retained; same schema feasible
BE-003 calendar policy data correctness/freshness not measured; возможный runtime cost увеличивается Force only calendars, bounded limiter, failed-cycle fallback; RPC budget владельцу 07
BE-004 single read ownership maintenance/cognitive load; возможный runtime benefit not measured Cold historical Income UX, exact Ledger range, local confirmed reopen; prune DI only after caller sweep
BE-005 translation error recovery/maintenance not measured All direct online callers + unchanged durable invalid-token semantics; no new registry
BE-006 cadence reliability/predictability not measured Long term, failure, restart; formula/code only
BE-007 deletion cognitive load/maintenance/agent navigation 258-line candidate block; actual diff/token/time not measured Public status retained, no schema purge, DI initialization proof; нет confirmed removable deps
BE-008 metadata contract coherence/diagnosability not measured Resource-relevant state/time, no data timestamp renewal; preserve top-level sync compatibility
KEEP mechanisms avoided regressions/complexity growth not measured Remove only with equivalently strong DB/protocol proof; no replacement framework justified
INVESTIGATE mechanisms prevent wrong migration not measured Один directed fixture/decision для каждой гипотезы до изменения owners

CI minutes, endpoint latency, N+1 impact, total LOC reduction, token savings и hardware/storage capacity в этом аудите не измерялись. Из statically visible extra call нельзя выводить percentage performance win.

7. Top actions

Порядок по value/risk/effort; только actionable findings. KEEP/INVESTIGATE не представлены как обязательные исправления.

Quick wins с targeted regression proof

  1. BE-005: подключить existing invalid-token translator ко всем online callers; один configured HTTP table of outcomes + durable unchanged assertion.
  2. BE-008: дать Accounts/Income correct hourlyAttempt в meta; использовать существующий repository/helper, не менять freshness/data.
  3. BE-006: исправить eligibility после skipped terms; controlled clock plus real reservation proof.
  4. BE-007: удалить недоступный screen request/dispatch, сохранив status/Operations successor и public DTO; DI initialization/contract proof.

Решения с SQLite/lifecycle proof и согласованием ownership

  1. BE-002: устранить false success и orphan-running stall на coordinator и source finalization boundaries; fault injection с recovery прежде refactor.
  2. BE-001: отделить latest complete forecast от query filtering, сохранять original identity/window; при persistent key/wire change — короткое решение/ADR, additive migration и coordinated consumer proof.
  3. BE-004: сузить shared read planner и сделать Income local; согласовать cold-history recovery/UX, не вводя скрытый full-history owner обратно.
  4. BE-003: изменить calendar refresh policy только после failed-existing-calendar proof (BE-013 как prerequisite investigation); измерение бюджета адресовать 07. Исправление selection и calendar freshness — разные regression outcomes.

8. Cross-audit signals

  • 05 API/Data flow: original forecast window требуется specs, но backend/src/modules/investments/upcoming-events/upcoming-events.dto.ts:68–80 и actual generated frontend/src/shared/api/generated/schema.d.ts:1225–1234 поля окна не содержат; согласовать wire/consumer transition вместе с BE-001. Полный contract audit здесь не выполнялся.
  • 05 API/Data flow: Accounts meta.lastAttempt и top-level sync используют разные источники (brokerage-accounts.service.ts:38–76); frontend generated schema.d.ts:940–996 допускает оба. Owning semantic fix — BE-008, audit 05 проверить представление/recovery, не дублировать finding.
  • 05 API/Data flow: auth.dto.ts:43–74 сочетает character MinLength с UTF-8 maximum, тогда как specs/openspec/specs/user-authentication/spec.md:10 требует byte range; parity validation contract — отдельное направление, не session architecture defect 06.
  • 07 Performance: online search details/price ordering catalog-discovery.service.ts:295–317, per-row banking view loading bank-operations.service.ts:41–68,333–365; посчитать fan-out/DB reads и измерить significance, здесь количественного finding нет.
  • 07 Performance: forced mutable calendar refresh (BE-003) и request scope expansion (BE-004) влияют на limiter/budget; representative connections/accounts/instruments нужен для capacity, не для доказательства ownership defect.
  • 02 Complexity: duplicate publicInstrument в catalog-discovery.service.ts:499–530 и catalog-enrichment.service.ts:646–677, локальные Stock/Bond catalog facades имеют meaningful DTO mapping; не удалять последние по одному caller/размеру.
  • 08/09 Tests: test/upcoming-events.e2e-spec.ts:321–331 ожидает disjoint null, 140–148 подменяет scheduler; проверить соответствие scenarios и добавить real hourly terminal/cadence proof. Тесты не запускались, качество всей suite не оценивалось.
  • 13 Reliability/Security: refresh CAS/revoke interleaving и valid access после logout (auth.service.ts:116–140,183–199, jwt.strategy.ts:21–25) проверить относительно принятой stateless-access policy; не заявляется exploit в 06.
  • 15 Specs/Docs: docs/architecture/containers.md:142–148 описывает request-driven startup recovery, ADR 019 9–29 — hourly owner; specs/AI_INDEX.md:15–31 не выводит на hourly/data-sync/online specs. Документальный маршрут расходится; новые conventions не требуются.

9. Open questions / verification plan

Вопрос, меняющий решение Самый дешёвый proof / требуемый input Что меняет
Latest forecast — один saved account identity/window или отдельные query scopes? Как сохранять/агрегировать all? Desired output для full day-1/day-2/narrow/disjoint/all; existing SQLite fixture без upstream сети BE-001 migration/key selection и BE-013 GET/publication ownership
Можно ли после failed existing calendar обновить amounts от нового Portfolio? Same UID full row + изменённая quantity + forced calendar failure + normal GET; сравнить record/calculatedAt/metadata BE-013 → FIX только при demonstrated forbidden replacement; prerequisite BE-003
Кто загружает missing historical Income после удаления скрытого read backfill? Product decision: предварительный Ledger или явный ограниченный owner; inspect current cold response fixture Scope удаления BE-004 и сохранённый user recovery
Что означает успешный missing MOEX block? Approved protocol docs/synthetic saved samples: missing vs empty valid vs malformed/cursor BE-014 → parser FIX либо уточнение requirement; complete-only guarantees сохраняются
Нужен ли graceful drain, или deliberate restart interruption достаточно? Controlled deferred RPC/app.close/restart на disposable DB; deployed grace period/replica count от владельца deploy BE-015 и допустимый local lifecycle fix, no distributed lease по умолчанию
«Первый обход без ожидания срока» допускает minute tick delay? onModuleInit/PUT timing controlled test + точное ожидание; scheduler 48–50, connection controller 43–50 Нужен ли local tick-on-success/start; в actionable не добавлено без timing contract
Есть ли внешние status/observe consumers? Backend snapshot route inventory + объявленные внешние consumers; текущий frontend production use inventory недостаточен для external отрицания Безопасный объём BE-007/legacy compatibility; не удалять public GET автоматически
Должен ли bondBy NOT_FOUND быть public not-found, как shareBy? Synthetic TbankClient error matrix + approved upstream protocol contract; никаких real broker calls Расширение BE-005 mapping без превращения true outage в false absence

Итоговая проверка отчёта и workspace

  • Все 9 разделов и три обязательные карты присутствуют. В 15 карточках имеются все поля задания; IDs последовательны BE-001…BE-015, related IDs существуют.
  • Actionable counts: FIX 7 + DELETE 1 = 8; severity 2 P1 + 5 P2 + 1 P3; P0 нет. KEEP 4 и INVESTIGATE 3 отдельно. Top actions = 8, без включения нерешённых гипотез.
  • Relative links проверены по прочитанным существующим targets; source references — workspace-relative точные диапазоны текущего HEAD, а не offsets прежнего аудита. Boy Scout review отчёта: ownership, отсутствие дублирующих performance/security findings, доказательства и контрпримеры проверены; приложение не менялось.
  • git diff --no-index --check /dev/null "audit-results/06-backend-architecture-report.md" (cwd root): exit 1, диагностического вывода нет; для no-index это наличие diff нового непустого файла относительно /dev/null, whitespace errors не найдены. Exit отдельно проверен через shell $?, а не выведен из отсутствия stdout.
  • git diff --check && git rev-parse HEAD && git status --short --untracked-files=all, отдельно в root/backend/frontend/specs/docs: все checks exit 0, status пустые, все пять HEAD совпадают с §1. Root report находится вне nested repos; root tracking allowlist/ignore не менялся. Пустой root status не заменяет просмотр нового игнорируемого отчёта: файл перечитан непосредственно.
  • Статический контроль карточек через rg -c в root: 15 заголовков BE, решений FIX/DELETE/KEEP/INVESTIGATE соответственно 7/1/4/3 и 165 обязательных field-bullets (11 на карточку; Severity/Confidence размещены в Decision bullet, Related IDs — в Effort bullet). Counts/severity и machine-readable rows дополнительно сверены вручную.

Машинно-считываемая таблица для консолидации

ID Decision Severity Confidence Repo Files One-line impact Effort Related IDs
BE-001 FIX P1 High backend src/modules/investments/portfolio/infrastructure/forecast.repository.ts; src/modules/investments/upcoming-events/upcoming-events.service.ts Пересекающийся saved forecast блокирует новый complete candidate; disjoint fallback теряется M BE-003,BE-012,BE-013,BE-014
BE-002 FIX P1 High backend src/modules/investments/tbank-sync/tbank-hourly.scheduler.ts; src/modules/investments/tbank-sync/tbank-sync.repository.ts; src/modules/investments/instruments/application/catalog-enrichment.service.ts; src/modules/investments/operations/application/brokerage-operations-sync.service.ts Persistence failure даёт false success либо running stall до restart/replacement M BE-006,BE-008,BE-012,BE-015
BE-003 FIX P2 High backend src/modules/investments/upcoming-events/upcoming-events.service.ts; src/modules/investments/instruments/application/catalog-enrichment.service.ts Stable calendar reuse не гарантирует обновления будущих событий hourly owner S BE-001,BE-002,BE-013,BE-014
BE-004 FIX P2 High backend src/modules/investments/operations/application/brokerage-operations.service.ts; src/modules/investments/operations/application/brokerage-operations-sync.service.ts; src/modules/investments/income/income-analytics.controller.ts Income импортирует personal data; Ledger backfill выходит за requested interval M BE-002,BE-007,BE-008,BE-012
BE-005 FIX P2 High backend src/integrations/tbank/tbank-token.service.ts; src/modules/investments/instruments/application/catalog-discovery.service.ts; src/modules/investments/instruments/application/catalog-enrichment.service.ts; src/modules/investments/instruments/stocks/application/stock-screener.service.ts Online invalid broker token теряет permanent code и становится HTTP 500 S BE-009,BE-012
BE-006 FIX P2 High backend src/modules/investments/tbank-sync/tbank-sync.repository.ts Длинный walk допускает немедленный запуск после пропущенного hourly term S BE-002,BE-015
BE-007 DELETE P3 High backend src/modules/investments/tbank-sync/tbank-sync.service.ts; src/modules/investments/tbank-sync/tbank-screen.ts; src/modules/investments/tbank-sync/tbank-sync.module.ts Недоступный manual command сохраняет альтернативную orchestration и DI navigation S BE-004,BE-009
BE-008 FIX P2 High backend src/modules/investments/brokerage-accounts/brokerage-accounts.service.ts; src/modules/investments/operations/application/brokerage-operations.service.ts; src/modules/investments/income/income-analytics.service.ts Accounts/Income meta не отражает actual durable hourly attempt S BE-002,BE-004,BE-009
BE-009 KEEP P3 High backend src/app.module.ts; src/integrations/tbank/tbank.module.ts; src/modules/investments/instruments/instrument-sources.module.ts Concrete singleton owners и простой монолит достаточно глубоки для текущей задачи S BE-002,BE-007,BE-015
BE-010 KEEP P2 High backend src/modules/banking/bank-cards/application/bank-operations.service.ts; src/modules/banking/bank-cards/infrastructure/bank-cards.repository.ts; src/modules/auth/application/auth.service.ts; prisma/schema.prisma Transaction/CAS/replay guards сохраняют financial integrity и session winner S BE-011,BE-012
BE-011 KEEP P2 High backend src/common/decimal.ts; src/modules/banking/bank-cards/domain/rub-money.ts; src/modules/investments/portfolio/infrastructure/tbank-portfolio.mapper.ts Exact money/currency/null semantics нельзя заменить прямыми number/upstream records S BE-010,BE-012
BE-012 KEEP P2 High backend src/integrations/tbank/tbank-access.service.ts; src/modules/investments/portfolio/infrastructure/forecast.repository.ts; src/modules/investments/operations/infrastructure/brokerage-operations.repository.ts; src/modules/investments/instruments/infrastructure/candle.repository.ts Revision/attempt/dependency fences защищают late и incomplete publication S BE-001,BE-002,BE-004,BE-010,BE-011,BE-013
BE-013 INVESTIGATE P2 Medium backend src/modules/investments/portfolio/application/forecast.service.ts; src/modules/investments/upcoming-events/upcoming-events.service.ts; src/modules/investments/instruments/stocks/application/stock-income.service.ts; src/modules/investments/instruments/bonds/application/bond-income.service.ts UNVERIFIED: GET и job делят forecast publication при failed existing source M BE-001,BE-003,BE-012
BE-014 INVESTIGATE P2 High backend src/integrations/moex/moex.client.ts; test/upcoming-events.e2e-spec.ts Missing block трактуется как ошибка вопреки spec; безопасная absence semantics требует proof S BE-001,BE-003,BE-013
BE-015 INVESTIGATE P3 Medium backend src/modules/investments/tbank-sync/tbank-hourly.scheduler.ts; src/prisma/prisma.service.ts; src/integrations/tbank/tbank.client.ts UNVERIFIED: detached jobs не возвращают completion в graceful shutdown boundary M BE-002,BE-006,BE-009,BE-012