From d07fbfabb1295780952a554185989d2d5b5c8349 Mon Sep 17 00:00:00 2001 From: codex-bot Date: Sat, 17 Oct 2026 19:38:03 +0300 Subject: [PATCH] docs: archive technical audit 2026-10-11 in develop (#36) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Переносит архив `audits/2026-10-11/` (19 файлов) из codex/audit-remediation-2026-10-11 в develop и добавляет точечный allowlist в .gitignore. --- .gitignore | 13 + audits/2026-10-11/README.md | 114 ++ ...chitecture-and-module-boundaries-report.md | 820 +++++++++++++ ...2-complexity-and-overengineering-report.md | 916 +++++++++++++++ .../03-duplication-and-reusability-report.md | 890 ++++++++++++++ .../reports/04-dead-code-and-legacy-report.md | 746 ++++++++++++ .../05-frontend-architecture-report.md | 1038 +++++++++++++++++ .../reports/06-backend-architecture-report.md | 494 ++++++++ .../07-api-and-data-flow-efficiency-report.md | 970 +++++++++++++++ ...8-test-quality-and-effectiveness-report.md | 430 +++++++ ...-architecture-and-infrastructure-report.md | 440 +++++++ .../reports/10-custom-scripts-and-tooling.md | 504 ++++++++ ...-validation-and-ci-cd-efficiency-report.md | 490 ++++++++ ...2-dependencies-and-configuration-report.md | 328 ++++++ .../13-reliability-and-security-report.md | 355 ++++++ .../14-ai-agent-workflow-efficiency.md | 342 ++++++ .../reports/15-sdd-specs-and-documentation.md | 375 ++++++ .../reports/previous/complexity-audit.md | 765 ++++++++++++ .../previous/technical-audit-2026-10-11.md | 347 ++++++ audits/2026-10-11/triage.md | 372 ++++++ 20 files changed, 10749 insertions(+) create mode 100644 audits/2026-10-11/README.md create mode 100644 audits/2026-10-11/reports/01-architecture-and-module-boundaries-report.md create mode 100644 audits/2026-10-11/reports/02-complexity-and-overengineering-report.md create mode 100644 audits/2026-10-11/reports/03-duplication-and-reusability-report.md create mode 100644 audits/2026-10-11/reports/04-dead-code-and-legacy-report.md create mode 100644 audits/2026-10-11/reports/05-frontend-architecture-report.md create mode 100644 audits/2026-10-11/reports/06-backend-architecture-report.md create mode 100644 audits/2026-10-11/reports/07-api-and-data-flow-efficiency-report.md create mode 100644 audits/2026-10-11/reports/08-test-quality-and-effectiveness-report.md create mode 100644 audits/2026-10-11/reports/09-test-architecture-and-infrastructure-report.md create mode 100644 audits/2026-10-11/reports/10-custom-scripts-and-tooling.md create mode 100644 audits/2026-10-11/reports/11-validation-and-ci-cd-efficiency-report.md create mode 100644 audits/2026-10-11/reports/12-dependencies-and-configuration-report.md create mode 100644 audits/2026-10-11/reports/13-reliability-and-security-report.md create mode 100644 audits/2026-10-11/reports/14-ai-agent-workflow-efficiency.md create mode 100644 audits/2026-10-11/reports/15-sdd-specs-and-documentation.md create mode 100644 audits/2026-10-11/reports/previous/complexity-audit.md create mode 100644 audits/2026-10-11/reports/previous/technical-audit-2026-10-11.md create mode 100644 audits/2026-10-11/triage.md diff --git a/.gitignore b/.gitignore index 78f5dab..9c7838e 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,16 @@ !/.gitea/ISSUE_TEMPLATE/refactor.md !/.gitea/ISSUE_TEMPLATE/engineering.md !/.gitea/PULL_REQUEST_TEMPLATE.md +!/audits/ +/audits/* +!/audits/2026-10-11/ +/audits/2026-10-11/* +!/audits/2026-10-11/README.md +!/audits/2026-10-11/triage.md +!/audits/2026-10-11/reports/ +/audits/2026-10-11/reports/* +!/audits/2026-10-11/reports/[0-9][0-9]-*.md +!/audits/2026-10-11/reports/previous/ +/audits/2026-10-11/reports/previous/* +!/audits/2026-10-11/reports/previous/complexity-audit.md +!/audits/2026-10-11/reports/previous/technical-audit-2026-10-11.md diff --git a/audits/2026-10-11/README.md b/audits/2026-10-11/README.md new file mode 100644 index 0000000..db73641 --- /dev/null +++ b/audits/2026-10-11/README.md @@ -0,0 +1,114 @@ +# Technical Audit Remediation — 2026-10-11 + +Исторический аудит WealthLedger от 11 октября 2026 года: root coordination, +frontend, backend, specs и docs. Программа управляется Gitea Issues; этот +архив сохраняет доказательства и решения triage, а не текущие статусы задач. + +Главный Issue: [Technical Audit Remediation — WealthLedger — 2026-10-11](https://git.personal.ksv741.ru/WealthLedger/main/issues/23). +Нормализованный реестр и задачи: [triage](triage.md). + +## Исследованные revisions + +HEAD повторно проверены при remediation planning; application checkout чистые. +Все пять исходных commits существуют в соответствующих remote repositories. + +| Repository | Verified revision | +| --- | --- | +| [WealthLedger/main](https://git.personal.ksv741.ru/WealthLedger/main) | `dc030f84c979cedb2628ef4bd97c62d6c63a9256` | +| [WealthLedger/frontend](https://git.personal.ksv741.ru/WealthLedger/frontend) | `c8821200ce699dab1ce65360d1968d5877a2e78b` | +| [WealthLedger/backend](https://git.personal.ksv741.ru/WealthLedger/backend) | `0ad84ff4f0b4712abc7e69dc299856f14af6eadd` | +| [WealthLedger/specs](https://git.personal.ksv741.ru/WealthLedger/specs) | `fcdc805f6c1b3be6a20a7e858e3e80cbd83a9121` | +| [WealthLedger/docs](https://git.personal.ksv741.ru/WealthLedger/docs) | `e6fd4f64b81dd73b33959b845a1babcfac1891c8` | + +## Отчёты + +Все 18 источников прочитаны. Специализированные отчёты — основной источник; +предыдущие отчёты дают дополнительные свидетельства и гипотезы. Повторение +вывода в прежнем отчёте не считается независимым подтверждением. + +| № | Источник / архив | +| --- | --- | +| 01 | [Architecture & Module Boundaries](reports/01-architecture-and-module-boundaries-report.md) | +| 02 | [Complexity & Overengineering](reports/02-complexity-and-overengineering-report.md) | +| 03 | [Duplication & Reusability](reports/03-duplication-and-reusability-report.md) | +| 04 | [Dead Code & Legacy](reports/04-dead-code-and-legacy-report.md) | +| 05 | [Frontend Architecture](reports/05-frontend-architecture-report.md) | +| 06 | [Backend Architecture](reports/06-backend-architecture-report.md) | +| 07 | [API & Data Flow Efficiency](reports/07-api-and-data-flow-efficiency-report.md) | +| 08 | [Test Quality & Effectiveness](reports/08-test-quality-and-effectiveness-report.md) | +| 09 | [Test Architecture & Infrastructure](reports/09-test-architecture-and-infrastructure-report.md) | +| 10 | [Custom Scripts & Tooling](reports/10-custom-scripts-and-tooling.md) | +| 11 | [Validation & CI/CD Efficiency](reports/11-validation-and-ci-cd-efficiency-report.md) | +| 12 | [Dependencies & Configuration](reports/12-dependencies-and-configuration-report.md) | +| 13 | [Reliability & Security](reports/13-reliability-and-security-report.md) | +| 14 | [AI Agent Workflow Efficiency](reports/14-ai-agent-workflow-efficiency.md) | +| 15 | [SDD, Specs & Documentation](reports/15-sdd-specs-and-documentation.md) | +| 16 | [complexity-audit.md](reports/previous/complexity-audit.md) | +| 17 | [technical-audit-2026-10-11.md](reports/previous/technical-audit-2026-10-11.md) | +| 18 | `audit-results/АудитЭффективности.md` — publication blocked, см. ниже | + +Фактическое имя последнего источника — `АудитЭффективности.md`, а не +`АудитЭффективности(1).md`. Соответствие установлено по содержимому. + +## Ключевые выводы + +- P1: hourly lifecycle способен сохранить ложный успех или зависнуть после + persistence failure; overlapping forecast fallback блокирует новый complete + candidate. Runtime-воспроизведение этих дефектов в planning не выполнялось. +- Переход к hourly/local/online ownership неполон: calendars, Income/Ledger, + query timer и UI feedback сохраняют несовместимые части прежнего поведения. +- Тесты требуют различающих stimuli и completion barriers; настоящая SQLite, + financial precision, auth CAS, replay и publication fences сохраняются. +- Дорогая повторная preparation сокращается в существующих workflow; новые + слои, shared tooling, validators и постоянная audit automation не нужны. +- Specs/current docs требуют точечной reconciliation. Исторические ADR, + OpenSpec archives и стандартные lifecycle skills остаются неизменяемыми. + +## Сохранность и публикация + +17 опубликованных отчётов копируются byte-for-byte из `audit-results/`; +содержание, исходные имена и исторические утверждения не исправляются. +Внутренние workspace-relative ссылки оригиналов сохранены как evidence; +актуальные remote permalinks и навигация принадлежат README/triage/Issues. +Оригинальные 18 локальных файлов не удаляются. SHA manifests и новые +integrity validators не вводятся: версии архива принадлежат Git. + +`gitleaks dir audit-results --redact=100 --no-banner --log-level error` +завершился с exit 0. Дополнительно выполнен содержательный privacy review. +Все пять remote repositories публичные. Отчёт `АудитЭффективности.md` +содержит идентификаторы и названия частных OpenCode sessions: неизменённый +оригинал нельзя безопасно публиковать здесь. Он сохранён в исходном локальном +месте; все RC-01–RC-18 рассмотрены в triage без частных session identifiers. +Блокер снимается предоставлением подходящего приватного Git owner/location +либо явным решением владельца о допустимости публикации этих metadata. +Редактирование оригинала или публикация приватных conversations не предлагаются. + +Архив публикуется в task branch `codex/audit-remediation-2026-10-11` от свежего +`origin/develop`. Локальный commit без подтверждённого push не считается +публикацией. Release/version bump из названия программы не следует. + +Первый архивный commit `0d6344abfdf9c4c6139e4d2b50bf3bd3b2ee690e` успешно +отправлен и прочитан через Gitea; отчёты в нём неизменяемы. Финальные README и +triage доступны в той же remote task branch и имеют фактические Issue URLs. + +## Gitea программа + +39 нормализованных engineering topics поставляются 43 working Issues: +40 новых и 3 повторно используемых. Главный navigation Issue создан отдельно; +итого 41 новое Issue и 44 participating Issues. Все новые задачи неназначены; +существующие assignees/тексты сохранены, audit supplements добавлены comments. + +| Repository | Milestone | Participating Issues | +| --- | --- | --- | +| main | [milestone 1](https://git.personal.ksv741.ru/WealthLedger/main/milestone/1) | 3, включая главный Issue | +| backend | [milestone 4](https://git.personal.ksv741.ru/WealthLedger/backend/milestone/4) | 23 | +| frontend | [milestone 3](https://git.personal.ksv741.ru/WealthLedger/frontend/milestone/3) | 11 | +| specs | [milestone 5](https://git.personal.ksv741.ru/WealthLedger/specs/milestone/5) | 5 | +| docs | [milestone 2](https://git.personal.ksv741.ru/WealthLedger/docs/milestone/2) | 2 | + +Milestones не имеют due date. Переиспользованы organization labels; +8 новых repo-scoped labels — 5 audit и 3 investigation. Создание org labels +отвергнуто недостатком organization-owner permission; repo-scoped fallback +подтверждён. Для reused Issues назначение labels сделано через replace_labels: +обычный issue update в MCP их не применяет. Оба ограничения API обработаны. +Native dependencies установлены и прочитаны обратно, cycles отсутствуют. diff --git a/audits/2026-10-11/reports/01-architecture-and-module-boundaries-report.md b/audits/2026-10-11/reports/01-architecture-and-module-boundaries-report.md new file mode 100644 index 0000000..bfc2a44 --- /dev/null +++ b/audits/2026-10-11/reports/01-architecture-and-module-boundaries-report.md @@ -0,0 +1,820 @@ +# Аудит 01 — Architecture & Module Boundaries + +## 1. Audit metadata + +- **Audit ID/name:** 01 / Architecture & Module Boundaries. +- **Дата:** 2026-10-11. +- **Исполнитель:** один независимый исполнитель направления 01. Другие + субагенты не запускались, новые отчёты других направлений не читались. +- **Workspace:** `/Users/ksv741/Project/vibe/WealthLedger`. +- **Режим:** исследование read-only; единственная запись — этот отчёт вне + четырёх самостоятельных checkout. + +| Checkout | Абсолютный путь | Проверенный HEAD | Начальный status | Итоговый status после записи отчёта | +| --- | --- | --- | --- | --- | +| root | `/Users/ksv741/Project/vibe/WealthLedger` | `dc030f84c979cedb2628ef4bd97c62d6c63a9256` | clean | clean | +| frontend | `/Users/ksv741/Project/vibe/WealthLedger/frontend` | `c8821200ce699dab1ce65360d1968d5877a2e78b` | clean | clean | +| backend | `/Users/ksv741/Project/vibe/WealthLedger/backend` | `0ad84ff4f0b4712abc7e69dc299856f14af6eadd` | clean | clean | +| specs | `/Users/ksv741/Project/vibe/WealthLedger/specs` | `fcdc805f6c1b3be6a20a7e858e3e80cbd83a9121` | clean | clean | +| docs | `/Users/ksv741/Project/vibe/WealthLedger/docs` | `e6fd4f64b81dd73b33959b845a1babcfac1891c8` | clean | clean | + +SHA/status получены самостоятельно командами `git rev-parse HEAD` и +`git status --short --untracked-files=all` из каждого владельца. Пустой вывод +status означает отсутствие staged, unstaged и видимых untracked изменений; +ignored файлы этим не инвентаризируются. + +**Инструкции и skills.** Первым загружен установленный `ponytail-audit`. +Применён его complexity-only проход: поиск недоступной orchestration, +лишней indirection и механизма, заменяемого возможностью установленного stack. +Correctness-сигналы отделены от этого прохода. Полная структура отчёта следует +протоколу `audit-prompts/01-architecture-and-module-boundaries.md`, прочитанному +целиком. Прочитаны AGENTS каждого checkout, местные architecture/engineering +и активированные data, auth/security, integrations, HTTP/errors, testing, +frontend TypeScript/React и UI conventions. Root conventions использованы +только для root и сохранения отчёта. Дополнительно применены +`feature-sliced-design` с reference `layer-structure.md` и +`verification-before-completion`. Локальный FSD profile не заменён общими +рекомендациями skill. + +**Graphify limitation.** Skill загружен, но существующий +`graphify-out/graph.json` отсутствует в root, frontend, backend, specs и docs +по проверке известных абсолютных путей. Query существующего графа невозможен. +Графы и дополнительные artifacts не генерировались; выполнена статическая +трассировка source/imports/DI. Это не отказ от аудита из-за отсутствия графа. + +**Выполненные проверки:** + +1. Git HEAD/status в пяти checkout в начале, перед сохранением и после + сохранения; SHA совпадают, source/config trees чистые. + `git diff --check` в каждом checkout — без ошибок. Сам ignored отчёт + отдельно проверен `git diff --no-index --check /dev/null audit-results/01-architecture-and-module-boundaries-report.md` — без whitespace errors. +2. Чтение manifests, module registrations, public exports, controllers, + application owners, relevant persistence/schema, query consumers и + контраргументов; поиск через Read/Glob/Grep из соответствующих repo paths. +3. В frontend, после активации местной Node: + `source "$HOME/.nvm/nvm.sh" && nvm use --silent && ./node_modules/.bin/depcruise src packages --config dependency-cruiser.cjs --output-type err-long`. + Exit 0: **420 modules, 1773 dependencies, no dependency violations**. + Это прямой запуск установленного анализатора; `npm run deps` не запускался, + потому что его pre-step собирает packages + (`frontend/package.json:24-26,33`). +4. Из root: + `git diff --no-index --stat "backend/openapi.json" "frontend/src/shared/api/spec/openapi.json"`. + Exit 0, различий файлов на диске нет. Это не свежая генерация и не + доказательство runtime schema parity. +5. Context7: документация TanStack Query `/tanstack/query` по polling, + `refetchInterval` и отличию automatic fetching от manual `refetch`. + Справочный запрос не обращался к прикладным финансовым API. + +**Уточнение scope:** dead-code и documentation/index вопросы перенесены в +короткие Cross-audit signals для направлений 04 и 15. При корректировке +перечитан и изменён только этот отчёт; проверки source graph и OpenAPI из +первичного аудита повторно не запускались. Counts, related IDs и таблицы +относятся к оставшимся архитектурным findings. + +**Не запускались:** install, build, codegen/OpenAPI generation, migrations, +application startup, full-suite/verify, SQLite/E2E/unit tests, нагрузочные +проверки, commit/PR и реальные внешние API. `references/**`, dated audits, +архивы и исходники не изменялись. Предыдущий +`audit-results/technical-audit-2026-10-11.md` прочитан только как реестр +гипотез; его результаты измерений не использованы вместо текущих проверок. + +Все `path:line` ниже отсчитываются от workspace root и относятся к указанным +HEAD. Ссылка на тест означает статически просмотренный сценарий либо будущую +проверку, но не результат запуска теста в этом аудите. + +## 2. Executive verdict + +1. **Факт:** модульный NestJS monolith и локальный frontend FSD profile + жизнеспособны. Прослеженные Nest module imports не образуют цикла; + frontend dependency-cruiser не обнаружил циклов/forbidden imports по + текущим правилам. Не доказана необходимость CQRS, обязательных ports, + дополнительного state store или микросервисов. +2. **Подтверждённый boundary risk / ARCH-002:** read orchestration операций + шире разрешённой первой загрузки. Income GET инициирует membership work, + а общий `readPlan` с `history: true` может загрузить отсутствующую историю + вне запрошенного периода. Это конкурирующая refresh policy рядом с hourly + owner, а не недостаток слоёв. +3. **Подтверждённый ownership drift / ARCH-003:** периодический GET принадлежит + одновременно query owner, глобальному QueryClient и timer в feature + подключения. Разрешённый import `pages → features` не делает этот owner + осмысленным; timer не знает query eligibility. +4. **KEEP / ARCH-005–007:** auth coordination, account/connection fences, + общая conditional publication и banking transaction/replay boundaries + имеют конкретных production consumers. Их удаление не является + допустимым сокращением архитектуры. +5. **UNVERIFIED / ARCH-008–009:** перенос event-specific semantics из generic + forecast и удаление persisted `observe` reference-path требуют проверки + product semantics/совместимости. Наличие нескольких consumers и публичного + параметра — реальные контраргументы против механического удаления. + +**Подсчёт:** actionable = `DELETE`, `SIMPLIFY`, `FIX` в разделе 4. +**P0: 0; P1: 0; P2: 2; P3: 0. KEEP: 3; INVESTIGATE: 2.** +Severity не превращена в порядок cleanup. Для KEEP использован P3, поскольку +текущий дефект не заявляется; важность защищаемых инвариантов описана отдельно. +Для INVESTIGATE severity — уровень рассмотрения гипотезы, не подтверждение +сбоя. Runtime/performance impact не измерялся. + +## 3. Inventory & coverage + +### 3.1. Фактический layout и слои + +**Backend.** Manifest объявляет NestJS 12, TypeScript 6, Prisma 7 и +better-sqlite3; schema задаёт SQLite, а не Postgres +(`backend/package.json:40-66,104-105`, `backend/prisma/schema.prisma:1-9`). +Прочитаны регистрации всех найденных `src/**/*.module.ts`: +composition root, Auth, BankCards, BrokerageAccounts, Portfolio, Operations, +Income, UpcomingEvents, Instruments, InstrumentSources, TbankConnection, +TbankSync, TbankSyncRepository, Tbank/Moex/Ncr, Prisma и Logging. +Это DI inventory, а не предположение по названиям директорий. + +```text +AppModule + ├─ Auth / BankCards + ├─ BrokerageAccounts → Portfolio + ├─ Income → Operations + ├─ Instruments → InstrumentSources + Portfolio + ├─ UpcomingEvents → InstrumentSources + Portfolio + ├─ TbankConnection → TbankSync + └─ TbankSync → Portfolio + Operations + UpcomingEvents + InstrumentSources + +Personal source owners → TbankSyncRepositoryModule (нижний durable store) +Business/source owners → TbankModule → gRPC client / access / membership +InstrumentSources → MoexModule + NcrModule +Global infrastructure: ConfigModule, PrismaModule, LoggingModule +``` + +Контекст legacy DI: зарегистрированный status service содержит также +screen-command orchestration и зависимости source owners +(`backend/src/modules/investments/tbank-sync/tbank-sync.service.ts:42-52,54-311`), +но HTTP controller вызывает только status +(`backend/src/modules/investments/tbank-sync/tbank-sync.controller.ts:31-40`). +Это часть фактической dependency map; решение о dead-code closure принадлежит +направлению 04. + +Intended ownership сравнивался с принятым ADR 019: hourly walk обновляет +personal sources, обычные GET локальны, Ledger/candles имеют узкие first-load +исключения (`docs/adr/019-hourly-reads-and-online-instrument-reference.md:9-29`). +Current ownership показан production traces ниже; расхождения read initiators +оценены в ARCH-002. Актуальность docs/index рассматривается направлением 15. + +Важный контрпример предполагаемому циклу: +`TbankSyncModule → PortfolioModule → TbankSyncRepositoryModule` не возвращается +в `TbankSyncModule`. Store module регистрирует только repository +(`backend/src/modules/investments/tbank-sync/tbank-sync-repository.module.ts:1-9`). +Также `investment-sync-plan → income.api` достигает только pure +`planIncomePeriod`, не `IncomeAnalyticsService` +(`backend/src/modules/investments/income/income.api.ts:1`). +Source import cycle, Nest module cycle и runtime DI cycle не отождествляются. +Полный backend AST/DI graph checker не запускался; отсутствие всех возможных +транзитивных source cycles не заявляется. + +**Frontend.** Manifest задаёт React/Vite/TypeScript, TanStack Query, +openapi-fetch/openapi-react-query и private local packages +(`frontend/package.json:39-62,112-116,128-130`). Реальное направление: + +```text +app/providers/routes → pages/{account,accounts,income,events,operations,...} +pages → features/{auth,tbank-connection,instrument-search,instrument-details} +pages → entities/{portfolio,income,broker-operation,brokerage-event,bond,...} +pages → widgets/portfolio-structure +features/entities → shared/{api,lib,i18n,ui} +src consumers → packages/ui,icons; packages/ui → packages/tokens +``` + +Routes lazy-import page public APIs +(`frontend/src/app/routes/AppRoutes/AppRoutes.tsx:9-103`), account VM собирает +entity queries через публичные index +(`frontend/src/pages/account/ui/AccountPage/AccountPage.vm.ts:6-18,66-84`). +Query options принадлежат entities; shared/api владеет transport, session-aware +eligibility и resource projection. Пример: +`frontend/src/entities/portfolio/api/usePortfolio.ts:11-25` → +`frontend/src/shared/api/resource-query.ts:85-149` → +`frontend/src/shared/api/authenticated-query.ts:34-50` → +`frontend/src/shared/api/client.ts:53-119,284-290`. + +Размеры 420/1773 получены из единственного текущего запуска depcruise на +`src packages`, с `doNotFollow: node_modules`, TS pre-compilation dependencies, +source aliases и исключением package dist +(`frontend/dependency-cruiser.cjs:78-87`, +`frontend/tsconfig.dependency-cruiser.json:1-12`). Они включают объекты графа, +в том числе test/story source, и не означают 420 runtime business modules. +Правила проверяют imports, public APIs, cycles и direction +(`frontend/dependency-cruiser.cjs:1-76`), но не ownership refresh policy. + +### 3.2. Сквозные production traces + +| Capability | Controller/query → application → rules → storage/integration | Проверенная boundary | +| --- | --- | --- | +| Auth/session | App/AuthProvider → auth VM → shared authenticatedFetch; AuthController → AuthService → Session/JWT | В браузере один session owner и callback-based transport; cookie пишет controller; rotation CAS остаётся у AuthService. Evidence: `frontend/src/features/auth/model/auth.vm.ts:29-151`; `backend/src/modules/auth/presentation/auth.controller.ts:81-118`; `backend/src/modules/auth/application/auth.service.ts:81-114,166-201` | +| Accounts/portfolio | Account VM → usePortfolio → PortfolioController → PortfolioService.read/saved → PortfolioRepository; отдельно hourly → PortfolioService.request → gRPC → mapper → publish | HTTP read локален; production producer и publisher сохранены отдельно от read composition. Evidence: `frontend/src/pages/account/ui/AccountPage/AccountPage.vm.ts:66-84`; `backend/src/modules/investments/portfolio/presentation/portfolio.controller.ts:19-32`; `backend/src/modules/investments/portfolio/application/portfolio.service.ts:55-59,113-232,234-284` | +| Instrument discovery | BondsPage VM → useBondSearch → BondCatalogService → CatalogDiscoveryService.onlineSearch → TbankClient; source calendars отдельно через CatalogEnrichmentService | Default reference read online; persisted observe branch остаётся доступен. Evidence: `frontend/src/pages/bonds/ui/BondsPage/BondsPage.vm.ts:9-13`; `frontend/src/entities/bond/api/queries.ts:97-104`; `backend/src/modules/investments/instruments/bonds/application/bond-catalog.service.ts:25-55`; `backend/src/modules/investments/instruments/application/catalog-discovery.service.ts:84-94,295-337` | +| Sync/forecast | Native tick → reserveHourly → membership → portfolio → operations → events.refresh/get → ForecastService → ForecastRepository | Coordinator задаёт порядок, owners выполняют sources. Event GET также рассчитывает candidate и проходит write-capable resolve. Evidence: `backend/src/modules/investments/tbank-sync/tbank-hourly.scheduler.ts:48-65,76-110,127-175`; `backend/src/modules/investments/upcoming-events/upcoming-events.service.ts:54-173`; `backend/src/modules/investments/portfolio/infrastructure/forecast.repository.ts:24-46,170-175` | +| Banking operations | Frontend bank mutation → BankCardsController → BankOperationsService → operationInput/validateOperation → BankCardsRepository.write → SQLite; summary → GetBankSummaryUseCase → pure bankSummary | Banking не зависит от broker integration; transaction охватывает idempotency, refund validation, versioned write и response read. Evidence: `frontend/src/entities/bank-card/api/mutations.ts:29-82`; `backend/src/modules/banking/bank-cards/presentation/bank-cards.controller.ts:127-166`; `backend/src/modules/banking/bank-cards/application/bank-operations.service.ts:94-240`; `backend/src/modules/banking/bank-cards/application/get-bank-summary.use-case.ts:12-35` | + +### 3.3. Ownership map — специальный результат + +Пять центральных capabilities ниже разделены по решениям/состояниям/записям, +чтобы несколько технических participants не выдавать за нескольких owners. + +| Capability / решение и состояние | Current owner | Competing owner | Recommended owner | Evidence | +| --- | --- | --- | --- | --- | +| Auth/session: локальная identity, refresh hash, rotation | Backend AuthService; browser auth VM — только клиентская session | Конкурирующий session writer в прослеженных путях не найден; shared client получает callbacks | Сохранить это разделение; shared client не становится auth domain owner | `backend/src/modules/auth/application/auth.service.ts:81-114,166-201`; `frontend/src/features/auth/model/auth.vm.ts:29-115,143-151` | +| Accounts/portfolio: создание/replacement connection и начального membership | TbankConnectionService.put, одна транзакция новой revision | TbankAccessService.publishMembership — последующее подтверждение текущей revision, а не competing replacement | Lifecycle подключения остаётся в connection module; последующие membership writes у существующего access owner | `backend/src/modules/investments/tbank-connection/tbank-connection.service.ts:31-72`; `backend/src/integrations/tbank/tbank-access.service.ts:148-237` | +| Accounts/portfolio: право видеть счёт и membership freshness | TbankAccessService.local; source producer TbankMembershipService; refresh cadence у hourly | Income collectRanges инициирует refresh membership | Hourly инициирует повторное membership чтение; personal GET использует только local boundary | `backend/src/integrations/tbank/tbank-access.service.ts:260-288`; `backend/src/modules/investments/tbank-sync/tbank-hourly.scheduler.ts:113-125`; `backend/src/modules/investments/operations/application/brokerage-operations.service.ts:428-435` | +| Accounts/portfolio: positions, точные totals, сохранённый portfolio | PortfolioService/PortfolioRepository; AccountsService только строит summary | Legacy screen dispatcher содержит portfolio branch без найденного production entrypoint; это DI-контекст, не текущий refresh initiator | Portfolio source owner сохраняется; инициирование — hourly, projection — существующие readers | `backend/src/modules/investments/portfolio/application/portfolio.service.ts:179-198,234-284`; `backend/src/modules/investments/portfolio/infrastructure/portfolio.repository.ts:116-184`; `backend/src/modules/investments/brokerage-accounts/brokerage-accounts.service.ts:16-39`; `backend/src/modules/investments/tbank-sync/tbank-sync.service.ts:99-115` | +| Instrument discovery: поиск/identity и публичная projection | CatalogDiscoveryService + stock/bond catalog services | Default online и persisted observe представляют разные поколения read policy | Default online owner сохранить; удаление persisted compatibility пока INVESTIGATE | `backend/src/modules/investments/instruments/application/catalog-discovery.service.ts:84-167,220-292`; `backend/src/modules/investments/instruments/bonds/application/bond-catalog.service.ts:25-88` | +| Instrument discovery: сохранённые calendars/quotes/candles | CatalogEnrichmentService/CatalogDiscoveryRepository и CandleService/CandleRepository | Metadata lookup хранит другую identity projection; само по себе не competing calendar owner | Сохранить source-specific owners, не сливать financial sources с online reference answers | `backend/src/modules/investments/instruments/application/catalog-enrichment.service.ts:100-160,164-230`; `backend/src/modules/investments/instruments/application/candle.service.ts:101-124`; `backend/prisma/schema.prisma:148-229` | +| Sync/forecast: момент запуска личной работы и durable attempt | TbankHourlyScheduler + TbankSyncRepository; Ledger — узкое first-load исключение | Income scheduling и readPlan history expansion — реальные read initiators; TbankSyncService.request/dispatch — legacy DI-контекст | Hourly — единственный repeat-refresh initiator; Ledger запускает только missing requested interval | `backend/src/modules/investments/tbank-sync/tbank-hourly.scheduler.ts:76-175`; `backend/src/modules/investments/operations/application/brokerage-operations-sync.service.ts:92-124`; `specs/openspec/specs/tbank-data-sync/spec.md:9-22` | +| Sync/forecast: window, event amounts, filtering и complete publication | UpcomingEventsService рассчитывает события; ForecastService фильтрует events; ForecastRepository выбирает overlapping window и публикует | Event semantics распределена между capability и generic portfolio forecast owner; generic publication имеет ещё stock/bond consumers | Сохранить один общий fenced publisher; event window/filter policy предпочтительно у Events, но перенос после semantic proof | `backend/src/modules/investments/upcoming-events/upcoming-events.service.ts:119-173,271-299`; `backend/src/modules/investments/portfolio/application/forecast.service.ts:98-123`; `backend/src/modules/investments/portfolio/infrastructure/forecast.repository.ts:50-79,179-186` | +| Banking operations: intent, refund eligibility, journal write, version | BankOperationsService внутри BankCards module; repository владеет общей DB transaction boundary | UI хранит draft/intent, но не authority для refund limit; competing server writer не найден | Сохранить cohesive transactional owner и pure money rules; не дробить transaction по картам/категориям/возвратам | `backend/src/modules/banking/bank-cards/application/bank-operations.service.ts:94-240,251-330`; `backend/src/modules/banking/bank-cards/infrastructure/bank-cards.repository.ts:35-66`; `backend/src/modules/banking/bank-cards/domain/rub-money.ts:12-38` | + +### 3.4. Dependency/Boundary matrix — специальный результат + +| From → To | Фактическая зависимость и consumer | Сопоставление с intended boundary / оценка | +| --- | --- | --- | +| Frontend app → pages/features | Lazy routes и AuthProvider: `frontend/src/app/routes/AppRoutes/AppRoutes.tsx:9-103`; `frontend/src/app/ui/App.tsx:10-37` | Корректная верхняя composition root | +| Frontend pages → entities/features | Account VM queries и screen timer: `frontend/src/pages/account/ui/AccountPage/AccountPage.vm.ts:6-18,66-84` | Public imports соблюдены; semantic owner timer требует ARCH-003 | +| Frontend entities → shared/api | usePortfolio/useBondSearch: `frontend/src/entities/portfolio/api/usePortfolio.ts:1-25`; `frontend/src/entities/bond/api/queries.ts:97-104` | Transport общий, endpoint identity/policy локальны; сохранить | +| Frontend features/auth → shared transport | Callback registration: `frontend/src/features/auth/model/auth.vm.ts:143-151`; `frontend/src/shared/api/client.ts:28-35,122-125` | Обратный callback не является forbidden source import shared → feature | +| Frontend src → private packages | App theme и exports: `frontend/src/app/ui/App.tsx:7,25-28`; `frontend/packages/ui/package.json:9-19,26-33` | Реальные boundaries для app/catalog consumers; не дополнительные runtime services | +| Frontend packages → src | Запрет `frontend/dependency-cruiser.cjs:18-23` | В текущем source graph нарушений не найдено | +| Backend HTTP → application | Portfolio/Income/Bank controllers: `backend/src/modules/investments/portfolio/presentation/portfolio.controller.ts:17-31`; `backend/src/modules/investments/income/income-analytics.controller.ts:24-79`; `backend/src/modules/banking/bank-cards/presentation/bank-cards.controller.ts:127-166` | Controller делегирует, identity из verified user; сохраняется | +| Backend Income → Operations | `backend/src/modules/investments/income/income.module.ts:3-12`; `backend/src/modules/investments/income/income-analytics.service.ts:45-93` | Аналитика должна читать ledger data; скрытая scheduling branch — ARCH-002 | +| Backend UpcomingEvents/Instruments → Portfolio forecast | `backend/src/modules/investments/upcoming-events/upcoming-events.module.ts:5-15`; `backend/src/modules/investments/instruments/stocks/application/stock-income.service.ts:85-94`; `backend/src/modules/investments/instruments/bonds/application/bond-income.service.ts:116-125` | Общее publication/coherence имеет несколько consumers; event-specific window protocol — ARCH-008 | +| Backend TbankSync → resource owners | `backend/src/modules/investments/tbank-sync/tbank-sync.module.ts:14-25`; scheduler `:113-175` | Направление coordinator → owners обосновано; broad legacy DI описана как контекст inventory | +| Backend resource owners → sync store | Portfolio/Operations/Events imports: `backend/src/modules/investments/portfolio/application/portfolio.service.ts:20-23`; `backend/src/modules/investments/operations/application/brokerage-operations.service.ts:20-23`; `backend/src/modules/investments/upcoming-events/upcoming-events.service.ts:23-26` | Store module экспортирует repository и не импортирует coordinator. Цикл не доказан. Прямой файл вместо `*.api.ts` отступает от общего documented import profile, но это реально экспортированный provider, не доказательство плохого owner | +| Backend business → Tbank integration | `backend/src/integrations/tbank/tbank.module.ts:10-25`; `backend/src/modules/investments/portfolio/portfolio.module.ts:3-21` | Client/cache/limiter регистрируются у одного integration owner | +| Backend integration → business modules | Просмотр imports в integrations и `backend/src/integrations/tbank/tbank-access.service.ts:9-15` | В просмотренных import paths обратного обращения к business module нет; Prisma access — documented technical persistence, не HTTP/domain dependency | +| Backend BankCards → Auth/Prisma/domain | `backend/src/modules/banking/bank-cards/bank-cards.module.ts:3-19`; `backend/src/modules/banking/bank-cards/application/bank-operations.service.ts:3-29` | Cohesive CRUD service использует tx client по локальному profile; обязательный repository wrapper на каждый query не нужен | +| Backend domain → framework/storage | ESLint guard: `backend/eslint.config.mjs:126-168`; pure RUB rules `backend/src/modules/banking/bank-cards/domain/rub-money.ts:1-38` | Прямые imports ограничены; эта проверка не доказывает транзитивный граф или DI. Не выдаётся новый blanket lint verdict | +| Backend → frontend contract | Nest metadata → предоставленный JSON → generated frontend types: `backend/scripts/generate-openapi.cjs:19-35`; `frontend/scripts/check-openapi.mjs:10-26` | Передаётся artifact, не shared TypeScript source соседнего checkout. Файлы snapshot на диске совпадают; fresh parity UNVERIFIED | +| Specs/docs → application | Specs behavioral input; docs объясняют architecture: `specs/AI_INDEX.md:3-13`; `docs/README.md:52-67` | Не runtime/source dependency; intended policy сопоставлена с production ownership, documentation/index maintenance относится к audit 15 | + +Обнаруженные прямые imports экспортированных backend providers не +приравниваются к доказанным DI/circular failures. Например, +`backend/src/modules/investments/instruments/stocks/application/stock-screener.service.ts:10-13` +читает integration implementation entry вместо `tbank.api.ts`; provider +зарегистрирован в TbankModule, альтернативная implementation не заявлена. +Смысл boundaries проверен по consumers, а не по наличию barrel-файла. + +### 3.5. SRP, cohesion, DIP/ISP, OCP/LSP + +- **SRP/cohesion:** Auth session, connection lifecycle, source membership, + portfolio publication и banking journal имеют разные причины изменения + и реальные write boundaries. BankCardsService, BankOperationsService, + ExpenseCategoriesService и summary use case в одном module не требуют + отдельных Nest modules. Legacy screen dispatcher соединяет те же source + owners на уровне DI; dead-code решение вынесено в cross-audit signal 04. +- **Coupling:** Income зависит от read service, который также инициирует + membership/full-history work; фронтенд-страницы зависят от connection feature + ради периодических resource reads. Это проверенные связи, не вывод по имени + класса. Forecast window меняется сразу в Events и generic portfolio + persistence/service — ARCH-008. +- **DIP/ISP:** Concrete clients/repositories допустимы и имеют test seams. + `TbankConnectionService` использует `Pick` + (`backend/src/modules/investments/tbank-connection/tbank-connection.service.ts:11-19`), + а screener получает narrow access contract через `useExisting` + (`backend/src/modules/investments/instruments/stocks/application/stock-screener.service.ts:50-62`; + `backend/src/modules/investments/instruments/instruments.module.ts:24`). + Это не второй client instance и не доказательство ненужности интерфейса. +- **OCP/LSP:** not applicable для исследованных бизнес-границ: сменяемых + production implementations одного domain contract не установлено. + Stock/bond services — разные capabilities, а не заявленные взаимозаменяемые + subclasses. Нарушение LSP по различию их ответов не придумано. + +### 3.6. Границы покрытия + +Выборка risk-/cost-based: пять central capabilities, все Nest registrations, +frontend import graph, три подробных page/query consumers (Account, Bonds, +AccountEvents), auth/connection boundaries и banking write workflow. +Основные гипотезы проверены по caller, source mechanism и контрпримеру. +Не подсчитывались LOC savings, complexity scores или количество API calls. +Не исследовались подробно UI state/rendering, duplicate fragments, +test effectiveness, scheduler error correctness, API latency, dependency +security, migrations и эксплуатационные данные. Эти области не закрываются +данным audit 01. + +## 4. Findings + +### ARCH-002 — Ограничить read orchestration операции запрошенным missing interval + +- **Decision:** FIX. +- **Severity:** P2. +- **Confidence:** High. +- **Evidence:** `backend/src/modules/investments/income/income-analytics.controller.ts:24-79`; + `backend/src/modules/investments/income/income-analytics.service.ts:45-93`; + `backend/src/modules/investments/operations/application/brokerage-operations.service.ts:120-150,391-435`; + `backend/src/modules/investments/operations/application/brokerage-operations-sync.service.ts:46-124`; + `specs/openspec/specs/tbank-data-sync/spec.md:9-22`; + `specs/openspec/specs/brokerage-account-ledger/spec.md:131-144`. +- **Runtime consumer:** frontend income queries не передают `sync=observe` + (`frontend/src/entities/income/api/useIncome.ts:14-44`). IncomeController + передаёт `observe=false`, затем summary/analytics вызывают `collectRanges`. + Второй caller `readPlan` — production Ledger read через + `backend/src/modules/investments/operations/presentation/brokerage-operations.controller.ts:22-38`. +- **Claim:** разрешение читать personal result сейчас включает право + инициировать более широкую личную синхронизацию. Граница first-load не + удерживается общим application owner. +- **Current mechanism:** при обычном Income GET `collectRanges` запрашивает + membership, а после завершения запускает readPlan с `history: true`. + Ledger тоже передаёт `history: true`. Общий readPlan сначала загружает + missing target, затем вычисляет `accountHistory` и ставит в работу + `remaining` вне targets. Это отдельная ветвь execution, не только metadata. + Все production callers `readPlan` найдены: Ledger и collectRanges. +- **Impact / actual cost:** экран аналитики может инициировать membership + refresh; узкий Ledger interval может инициировать постороннюю историю + счёта. Ответ продолжает возвращаться локально, но ownership работы и объём + разрешённого side effect отличаются от narrow exception. **Impact + unmeasured:** число фактических RPC, latency и частота этого состояния + не измерялись. +- **Why now:** source owners уже вызваны hourly coordinator + (`backend/src/modules/investments/tbank-sync/tbank-hourly.scheduler.ts:113-175`). + Предыдущая broad-history policy пережила переход и создаёт вторую причину + выполнения work. +- **Minimum change:** убрать membership/scheduling из обычного income read; + оставить ему local operations projection. В общем readPlan убрать branch + расширения на `accountHistory`, сохранив coverage, first missing target, + revision/access checks и source request. Повторный refresh mutable tail + остаётся у hourly. Cold historical income должен честно возвращать + incomplete/unavailable; если нужна самостоятельная first-load возможность + для Income, принять это явно как изменение существующего исключения, + не спрятать в generic read helper. +- **Guarantees at risk:** полнота selected/previous/year comparisons, account + access, cursor binding, atomic publication, невыдача частичных totals за + полный результат. Удалять весь BrokerageOperationsSyncService, staging или + first-load Ledger exception нельзя. +- **Counterarguments / trade-offs:** history prefetch делает Income cold start + удобнее; это реальная продуктовая польза, а не бессмысленная indirection. + Полный сохранённый прошлый target сам по себе не повторно загружается: + `planOperationRead` добавляет только missing fragments + (`backend/src/modules/investments/operations/domain/operations-sync-plan.ts:174-195`). + Поэтому finding не утверждает безусловный refresh всех интервалов. + Старое ledger requirement всё ещё описывает upstream TTL + (`specs/openspec/specs/brokerage-account-ledger/spec.md:97-110`), а новое + `:131-140` задаёт first-load/local reads: конфликт нужно сделать явным. +- **Verification or rollback:** controlled integration case с membership старше + TTL и narrow missing historical interval: Income GET не создаёт membership + work; Ledger work ограничен этим interval; complete previous interval + остаётся local; incomplete income не превращается в ноль. Проверить оба + callers общего helper, не только IncomeController. Откат source policy + совместим со schema; product clarification — в существующем ADR/spec. +- **Effort:** M. +- **Related finding IDs:** ARCH-006. + +### ARCH-003 — Вернуть периодическое чтение владельцу query + +- **Decision:** SIMPLIFY. +- **Severity:** P2. +- **Confidence:** High. +- **Evidence:** `frontend/src/features/tbank-connection/model/screen-sync.ts:7-35,37-61`; + `frontend/src/features/tbank-connection/index.ts:1-2`; + `frontend/src/entities/bond/api/queries.ts:97-104`; + `frontend/src/entities/portfolio/api/usePortfolio.ts:19-25`; + `frontend/src/app/ui/App.tsx:13-21`. +- **Runtime consumer:** Account VM `:78-84`, BondsPage VM `:9-13` и + AccountEvents VM `:62-75` по путям из раздела 3. Остальные найденные + consumers — Accounts, AccountIncome, AccountOperations, Stock и Bond page + VMs. Это прямые вызовы feature public API, а не timer в тестовом коде. +- **Claim:** connection feature владеет cadence всех переданных resource GET, + хотя их inputs/eligibility/cache policy принадлежат queries. Это competing + lifecycle owner, который можно убрать возможностями уже установленного + TanStack Query. +- **Current mechanism:** hook игнорирует `_scope`, сохраняет resources в ref + и каждые 30 минут вызывает manual refetch всех ресурсов. В resource contract + нет `isEnabled`; при этом useResourceQuery это поле предоставляет + (`frontend/src/shared/api/resource-query.ts:132-146`). Query owners задают + enabled/staleTime, App включает focus/reconnect refetch. Timer и эти policy + owners действуют независимо. +- **Impact / actual cost:** review inputs/query eligibility требует перехода + из entity в стороннюю connection feature; Bonds query, отключённый для + короткого поиска, всё равно передан timer. Manual refetch не следует + automatic enabled policy. **Impact unmeasured:** число нежелательных + requests, влияние на backend и время сопровождения не измерены. +- **Why now:** screen descriptor больше не выбирает server work; сохранять + screen-sync boundary для обычного чтения нет основания. Depcruise это + пропускает, потому что `pages → features` разрешён. +- **Minimum change:** перенести polling в options существующих query owners + через `refetchInterval` и убрать page timer calls/неиспользуемые descriptors. + Connection feature оставить владельцем только PUT/DELETE и успешного + connection cache reset. Retry отдельного query остаётся в page VM. Если + остаётся реальный общий retry use case, сохранить только его cohesion, + не переносить timer целиком в новый общий hook. +- **Guarantees at risk:** no polling для disabled/unauthenticated queries, + отсутствие old-session/old-connection данных, сохранение cached successful + result, background-tab semantics и ограничение частоты. Нельзя выдавать + механическую замену таймера за полностью эквивалентный scheduling contract. +- **Counterarguments / trade-offs:** общий screen timer сейчас синхронно + предлагает чтение нескольких resources, что удобно для page composition. + Query-native polling по observers может иначе вести себя в background tab + и не делает multi-resource read атомарным. Source cadence и согласованность + resource results принадлежат backend; общий frontend timer их всё равно + не гарантирует. Product правило «не чаще 30 минут» нужно согласовать с + mount/focus/reconnect, а не просто добавить второй interval. +- **Verification or rollback:** fake-time/MSW через существующую boundary: + короткий поиск и отсутствующий account/uid не вызываются, active query + повторяется в принятом режиме, session/connection switch отменяет старый + результат, повтор не инициирует backend screen command. Проверить focus и + hidden-tab behavior. Откат query options/page imports, schema не меняется. +- **Effort:** M. +- **Related finding IDs:** ARCH-002, ARCH-005, ARCH-006. + +### ARCH-005 — Сохранить разделение session owner и authenticated transport + +- **Decision:** KEEP. +- **Severity:** P3. +- **Confidence:** High. +- **Evidence:** `frontend/src/features/auth/model/auth.vm.ts:29-115,121-151`; + `frontend/src/shared/api/client.ts:28-35,53-119,122-125`; + `frontend/src/shared/api/authenticated-query.ts:34-50`; + `backend/src/modules/auth/application/auth.service.ts:166-201`; + `backend/src/modules/auth/presentation/auth.controller.ts:81-118`. +- **Runtime consumer:** App монтирует AuthProvider + (`frontend/src/app/ui/App.tsx:31-37`); все generated authenticated requests + проходят shared fetch, session-aware queries используют его session + snapshot. Backend refresh endpoint вызывает CAS rotation и после успеха + пишет cookie. +- **Claim:** queue/single-flight/callback boundary здесь защищает реальные + session transitions; это не пустой wrapper над fetch. +- **Current mechanism:** auth VM владеет in-memory access token, cookie-write + queue, refresh promise и generation; transport добавляет Bearer и делает + ограниченный auth retry через callback. Backend conditional update сравнивает + прежний refresh hash и требует одного победителя. +- **Impact / actual cost:** frontend feature и shared transport имеют разные + причины изменения без source import shared → feature. Сохранена граница + session policy/HTTP mechanics. **Impact unmeasured:** runtime savings или + полная race coverage здесь не измерялись. +- **Why now:** при ARCH-003 нельзя одновременно упростить native polling и + удалить session-aware eligibility/late-response protection. +- **Minimum change:** не менять auth owner/queue ради сокращения файлов; + не добавлять refresh loops в pages/entities. +- **Guarantees at risk:** identity isolation, один refresh winner, + недопущение old-session response, cookie ordering и logout cache clearing. +- **Counterarguments / trade-offs:** эта модель ограничена текущей вкладкой; + сама по себе не решает межвкладочную coordination. Это documented ceiling, + а не основание добавлять distributed token store/Web Locks. +- **Verification or rollback:** при будущих изменениях — существующие + `frontend/src/features/auth/model/auth.test.tsx`, + `frontend/src/shared/api/client.test.ts` и configured backend auth/SQLite + scenarios. В этом аудите tests не запускались; rollback не требуется. +- **Effort:** S (сохранение existing boundary, без текущего patch). +- **Related finding IDs:** ARCH-003, ARCH-006. + +### ARCH-006 — Сохранить singleton source owners и fenced publication + +- **Decision:** KEEP. +- **Severity:** P3. +- **Confidence:** High. +- **Evidence:** `backend/src/integrations/tbank/tbank.module.ts:10-25`; + `backend/src/modules/investments/instruments/instrument-sources.module.ts:12-21`; + `backend/src/modules/investments/tbank-sync/tbank-sync-repository.module.ts:1-9`; + `backend/src/modules/investments/portfolio/infrastructure/portfolio.repository.ts:116-184`; + `backend/src/modules/investments/portfolio/infrastructure/forecast.repository.ts:35-46,80-159`; + `backend/src/modules/investments/tbank-connection/tbank-connection.service.ts:31-72`. +- **Runtime consumer:** hourly вызывает source owners, personal readers + используют их snapshots/status, Events/StockIncome/BondIncome используют + общий forecast publisher. Connection replacement — отдельный producer + новой revision, а не обход publication owner. +- **Claim:** DI/store split и conditional publication защищают реальные + source lifecycles. Удалять их вместе с legacy dispatcher нельзя. +- **Current mechanism:** Tbank client/access/membership зарегистрированы у + одного module owner; source work имеет user/revision/attemptId; + portfolio publication claim и write находятся в transaction; forecast + проверяет membership/portfolio/source versions. Sync repository module + отделяет durable store от coordinator, устраняя обратный module edge. +- **Impact / actual cost:** сохраняется общий client/limiter и один publication + fence вместо переносов cache/private state в consumers. **Impact unmeasured:** + производительность и полнота lifecycle correctness не подтверждены этим + архитектурным проходом. +- **Why now:** ARCH-002 ограничивает read initiators, а не независимые source + owners; ARCH-008 не должен размножить publisher по каждому forecast. +- **Minimum change:** сохранить concrete owners, module registration и DB + fences; исправления scheduler/policy выполнять внутри текущих границ. + Не вводить interface/adapter на каждый provider и не сливать user connection + lifecycle с low-level RPC client. +- **Guarantees at risk:** connection isolation, CAS/conditional publication, + complete snapshot preservation, shared retry/limiter и process lifecycle. +- **Counterarguments / trade-offs:** global Prisma provider и concrete DI не + равны идеальной строгой layer inversion, но соответствуют существующей + single-process monolith topology. ProcessRunId не distributed lease; + scheduler прямо фиксирует ceiling + (`backend/src/modules/investments/tbank-sync/tbank-hourly.scheduler.ts:28-31`). + Multi-process deployment потребовал бы отдельного решения, не текущего cleanup. +- **Verification or rollback:** будущий patch обязан доказать late publication + после replacement, superseded attempt и rollback на изолированной SQLite; + сохранить target source-owner contracts. В аудите — static verification, + не proof реальных races; rollback не требуется. +- **Effort:** S (без изменения защищённой boundary). +- **Related finding IDs:** ARCH-002, ARCH-003, ARCH-005, ARCH-008, ARCH-009. + +### ARCH-007 — Сохранить cohesive banking transaction и точные money rules + +- **Decision:** KEEP. +- **Severity:** P3. +- **Confidence:** High. +- **Evidence:** `backend/src/modules/banking/bank-cards/application/bank-operations.service.ts:94-240,251-330`; + `backend/src/modules/banking/bank-cards/infrastructure/bank-cards.repository.ts:35-66,96-123`; + `backend/src/modules/banking/bank-cards/domain/rub-money.ts:12-38`; + `backend/src/modules/banking/bank-cards/application/get-bank-summary.use-case.ts:12-35`; + `frontend/src/entities/bank-card/api/mutations.ts:29-82`. +- **Runtime consumer:** BankCardsController вызывает journal operations и + summary use case; frontend mutations передают intent и после server outcome + invalidируют bank GET subtree. +- **Claim:** DB transaction callback с concrete Prisma tx client и локальные + domain helpers — достаточная граница для банковского use case. Декомпозиция + каждой validation/query в отдельный service не даст доказанной пользы. +- **Current mechanism:** create intent fingerprint сравнивается после owner + check, tombstone не позволяет replay recreate; refund validation и version + claim выполняются внутри той же write boundary. RUB normalization переводит + decimal string в bigint-копейки с проверками currency/scale/range. Summary + получает согласованные inputs через repository и вызывает pure calculation. +- **Impact / actual cost:** сохраняется локально обозримый атомарный workflow + без broker coupling. **Impact unmeasured:** DB latency, maintenance cost и + фактические concurrent outcomes не измерялись. +- **Why now:** audit complexity не должен объявлять fingerprints/CAS/tx лишней + indirection или переносить journal authority во frontend. +- **Minimum change:** сохранить transactional owner и money boundary; + не вводить generic repository, port на каждый query или module на каждую + banking entity. Read-side optimizations — отдельный предмет audit 07. +- **Guarantees at risk:** точность денег, refund limit, rollback, user ownership, + lost-response replay protection, immutable operation identity и versions. +- **Counterarguments / trade-offs:** repository сейчас также возвращает + application conflicts (`card(active)`), а CRUD service делает direct queries. + Это documented local compromise, не чистая domain-only модель. Перенос + ради одинаковой структуры без самостоятельного consumer только создаст + passthrough layer и может разорвать transaction. +- **Verification or rollback:** при изменении — существующие configured + banking SQLite cases для refund, intent replay, versions/rollback и pure + money boundary. Отсутствие запуска здесь явно сохраняется как limitation; + текущий patch/rollback не нужен. +- **Effort:** S (сохранить, без нового кода). +- **Related finding IDs:** —. + +### ARCH-008 — Уточнить event-specific boundary общего forecast publisher + +- **Decision:** INVESTIGATE. +- **Severity:** P2. +- **Confidence:** Medium. +- **Evidence:** `backend/src/modules/investments/upcoming-events/upcoming-events.service.ts:152-173,271-299`; + `backend/src/modules/investments/portfolio/application/forecast.service.ts:18-65,98-123`; + `backend/src/modules/investments/portfolio/infrastructure/forecast.repository.ts:50-79,179-186`; + `backend/src/modules/investments/instruments/stocks/application/stock-income.service.ts:85-94`; + `backend/src/modules/investments/instruments/bonds/application/bond-income.service.ts:116-125`. +- **Runtime consumer:** EventController → Events.get и scheduler + refreshEvents → Events.get; ещё два независимых consumers generic publisher — + StockIncome и BondIncome. +- **Claim:** распределение event semantics между Events, generic + ForecastService и repository доказано. Выгода отдельной write/read boundary + и конкретное правило latest/window selection — **UNVERIFIED**. +- **Current mechanism:** Events кодирует account/window в scopeKey; repository + раскодирует его для overlapping fallback; ForecastService снова + раскодирует для event filtering и пересчитывает currency totals. + `result` одновременно resolve/publish и response projection. GET + сохраняет candidate, хотя не делает upstream RPC. +- **Impact / actual cost:** изменение смысла event window требует учитывать + три owners/файла, generic storage знает private wire-like scope protocol. + **Impact unmeasured:** стоимость поддержки и runtime failure rate не + измерены; это не основание объявлять общий publisher бесполезным. +- **Why now:** hourly owner и local GET нуждаются в понятном разграничении + «публиковать новый полный forecast» и «отфильтровать сохранённый». Оно + важнее механического перемещения файлов portfolio → events. +- **Minimum change:** сначала определить latest-complete forecast identity + и window semantics. Если подтверждено, перенести только event window/filter + policy в существующий Events owner и дать repository явный selection input; + общий conditional publisher и stock/bond consumers сохранить. Новый + ForecastModule/ports/CQRS не обоснован. +- **Guarantees at risk:** partial result fallback, membership/source-version + coherence, точные currency totals, account/all scope и late publication. +- **Counterarguments / trade-offs:** общий publisher реально обслуживает + несколько forecast capabilities и содержит защитные checks. Local + calculation/write внутри GET не равнозначны personal sync/RPC. Repository + может обоснованно выбирать fallback по persistence identity; сначала нужно + доказать, что текущая window policy не сама intended contract. +- **Verification or rollback:** самый дешёвый решающий proof — controlled + два последовательных complete forecasts с новым днём, затем narrow и + non-overlap GET; проверить время, totals, account/all и preservation при + partial update. Correctness вопрос selection передан audit 06. После + согласования boundary — reversible local source diff, без удаления schema. +- **Effort:** M. +- **Related finding IDs:** ARCH-006. + +### ARCH-009 — Не удалять persisted observe path без compatibility evidence + +- **Decision:** INVESTIGATE. +- **Severity:** P2. +- **Confidence:** Medium. +- **Evidence:** `backend/src/modules/investments/instruments/application/catalog-discovery.service.ts:66-94,143-174,220-267`; + `backend/src/modules/investments/instruments/stocks/presentation/stock.controller.ts:58-82,112-119`; + `frontend/src/shared/api/resource-query.ts:69-127,132-149`; + `specs/openspec/specs/online-instrument-information/spec.md:8-17`; + `docs/adr/019-hourly-reads-and-online-instrument-reference.md:22-26`. +- **Runtime consumer:** HTTP `sync=observe` переводит instrument reads в + persisted branch; обычный frontend Bonds query не передаёт этот параметр. + Backend financial calendars/candles действительно используют source + repository независимо от публичного reference search. +- **Claim:** dual reference read policy существует, но отсутствие внешних + consumers observe/status и безопасный объём удаления — **UNVERIFIED**. + Нельзя считать все persisted instrument sources dead из-за online default. +- **Current mechanism:** default detail/search/ticker работают online, observe + читает catalog discovery/enrichment snapshots. Frontend resource wrapper + всё ещё предоставляет специальный observe method с transport-mode + substitution. +- **Impact / actual cost:** поддерживаются два пути identity/metadata ответа. + **Impact unmeasured:** размер реально устранимого графа, LOC, память и + dependency savings не измерены. +- **Why now:** online default и public observe используют разные read paths. + Для выбора boundary нужно отличить reference compatibility от source данных, + необходимых текущим calendars/forecasts/candles. +- **Minimum change:** собрать compatibility evidence для public reference + observe contract; при подтверждении отсутствия необходимости удалить только + его старую discovery orchestration/transport mode. Не удалять общую schema, + source rows, financial calendar owner или candle FK без отдельного решения. +- **Guarantees at risk:** existing public query contract, instrument identity, + persisted financial sources и стабильные labels личных snapshots. +- **Counterarguments / trade-offs:** tests и HTTP parameter являются реальными + consumers/contract surface, даже если текущая SPA использует online default. + Accepted ADR прямо требует отдельной совместимой миграции старых reference + rows; отсутствие production call site внутри SPA не доказывает отсутствие + других клиентов или FK consumers. +- **Verification or rollback:** offline inventory public contract и provider + call sites, подтверждение владельцем списка внешних consumers; затем + targeted reference HTTP cases и source calendar/candle proof. Production + telemetry/реальные API в аудите не запрашивались. Пока решение INVESTIGATE, + deletion/rollback не выполняются. +- **Effort:** M. +- **Related finding IDs:** ARCH-006. + +## 5. DELETE / SIMPLIFY / KEEP / INVESTIGATE + +| Decision | IDs | Краткий итог | +| --- | --- | --- | +| DELETE | — | Подтверждённых архитектурных deletion findings в собственном scope нет | +| SIMPLIFY | ARCH-003 | Native polling у query owner вместо screen timer в connection feature | +| FIX | ARCH-002 | Narrow first-load boundary операций | +| KEEP | ARCH-005 | Session owner, single-flight/cookie queue, callback authenticated transport, backend rotation CAS | +| KEEP | ARCH-006 | Singleton source owners, lower durable store module, connection/attempt fences и общий conditional publisher | +| KEEP | ARCH-007 | Banking transaction/replay/version boundary и decimal/bigint money rules | +| INVESTIGATE | ARCH-008 | Event-specific window/selection/read-write ownership; generic publisher имеет реальные consumers | +| INVESTIGATE | ARCH-009 | Public observe compatibility и persisted reference orchestration; financial sources не объявлены dead | + +### Valid architectural decisions — специальный результат + +1. **Modular monolith + concrete providers.** AppModule явно собирает + capabilities (`backend/src/app.module.ts:18-36`). Установка обязательных + ports/adapters на каждый класс не решит найденные initiator conflicts. +2. **Отдельные connection lifecycle и integration.** PUT валидирует upstream + до DB transaction и атомарно публикует новую revision/membership + (`backend/src/modules/investments/tbank-connection/tbank-connection.service.ts:31-72`). + Это не тот же owner, что shared gRPC protocol/client. +3. **Sync repository module ниже coordinator.** Его существование обрывает + предполагаемую циклическую зависимость resource → coordinator + (`backend/src/modules/investments/tbank-sync/tbank-sync-repository.module.ts:1-9`). + Наличие одного класса не делает module лишним. +4. **Общий fenced publication, а не независимый write каждого HTTP reader.** + Сохранить проверки dependency versions и fallback на complete result + (`backend/src/modules/investments/portfolio/infrastructure/forecast.repository.ts:35-46,80-159`). +5. **Banking journal остаётся локальной транзакционной capability.** + Fingerprint, tombstone, version и refund checks имеют разные защитные роли; + common transaction сохраняет их совместно — ARCH-007. +6. **Frontend layer/public API + верхняя page composition.** Entity queries + переиспользуются account/details pages; большой связный VM не нужно + дробить только по размеру. Green dependency graph не выбирает owner policy. +7. **Private UI/tokens foundation для app и catalog.** Два production/tooling + consumers используют один theme entry: + `frontend/src/app/ui/App.tsx:7,25-28` и + `frontend/.storybook/preview.tsx:14,27-35`. + Package boundary здесь оправдана; independent registry/runtime service + не требуется. +8. **Artifact вместо shared source между repo.** Backend владеет исполняемым + Nest contract; frontend использует предоставленный JSON и offline types, + specs — behavioral requirements, docs — explanations + (`backend/scripts/generate-openapi.cjs:19-35`; + `frontend/scripts/check-openapi.mjs:10-26`; `specs/AI_INDEX.md:11-13`). + Добавление соседнего checkout в CI не требуется. + +## 6. Cost & benefit + +| ID | Ожидаемый класс экономии / пользы | Фактическое измерение | Обязательная regression boundary | +| --- | --- | --- | --- | +| ARCH-002 | Maintenance/предсказуемость side effects; потенциально runtime upstream budget | not measured; реальная frequency/RPC/latency неизвестны | Narrow first-load, access, complete coverage, no partial income totals | +| ARCH-003 | Cognitive load, lifecycle ownership; потенциально меньше ненужных GET | not measured; нельзя обещать ускорение или уменьшение requests в процентах | Enabled/session eligibility, polling cadence, cached result/late response | +| ARCH-005–007 | Сохранение correctness/security/data guarantees без новых layers | not measured; экономия не заявляется | Auth CAS, connection fencing, banking atomicity/replay/money | +| ARCH-008–009 | Возможное уменьшение смешанных semantics/compatibility paths | not measured; решение UNVERIFIED | Complete forecast, public contract, financial source/FK consumers | + +CI-minute saving, dependency removal и runtime ускорение не подтверждены. +Никаких новых dependencies для предложенных boundary changes не требуется. +Все количества findings подсчитаны по decision rows раздела 4; остальные +числа — вывод depcruise или constants исходников с указанными путями. + +**Complexity-only проход ponytail-audit в архитектурном scope:** + +- `native:` screen GET timer/descriptor. Замена: `refetchInterval` существующих + query owners, с явной cadence policy. + [`frontend/src/features/tbank-connection/model/screen-sync.ts:15-35`, ARCH-003] + +`stdlib:`, `yagni:` и `shrink:` не добавлены ради заполнения списка: одна +implementation, небольшой public API или concrete repository сами по себе +не являются достаточным основанием удалить boundary. Correctness-related +readPlan ограничение находится в ARCH-002, не выдано за вывод skill. + +**net:** возможное сокращение строк **не измерено**; кандидаты на удаление +dependencies **не установлены**. Числовой `-N lines` без подготовленного diff +был бы выдуманной метрикой. + +## 7. Top actions + +Подтверждённые действия сгруппированы по value/risk/effort, а не severity +cleanup. Новые архитектурные слои для них не нужны. + +### Quick wins + +Отдельных low-risk quick wins в оставшихся архитектурных findings не +установлено: оба actionable изменения требуют boundary proof. + +### Решения с test/product confirmation + +1. **ARCH-002 / M:** сузить общий operations readPlan до missing targets, + убрать Income membership/scheduling. До acceptance явно решить cold + historical Income UX; proof должен охватить оба production callers. +2. **ARCH-003 / M:** передать cadence query owners и убрать connection timer. + Согласовать focus/reconnect/mount/hidden-tab смысл правила 30 минут и + проверить disabled/session cases существующим MSW/fake-time boundary. + +ARCH-008/009 не представлены как подтверждённые deletion actions: сначала +дешёвый semantic/compatibility proof из раздела 9. + +### Minimal boundary changes — специальный результат + +| ID | Текущая boundary | Минимальная достаточная альтернатива | Что сохраняется | +| --- | --- | --- | --- | +| ARCH-002 | Resource read неявно разрешает membership и broad history scheduling | Income — read-only; Ledger — missing requested interval; hourly — repeat refresh | Existing read service, sync worker, coverage, staging/CAS | +| ARCH-003 | Connection feature инициирует timer для unrelated query resources | Query options владеют periodic GET; page VM выбирает resources/inputs; connection feature — lifecycle mutation/reset | Generated keys, shared transport, existing session/connection boundaries | +| ARCH-008 (условно) | Generic portfolio forecast знает events window/filter semantics | Events задаёт event selection/projection input; existing publisher сохраняет common fences | Несколько forecast consumers; изменение после semantic proof | +| ARCH-009 (условно) | Public reference observe смешан с persisted source machinery | Удалить только доказанно ненужную compatibility orchestration | Financial calendars, candles, private snapshot labels и FK | + +Цена предложений — local source diffs и targeted boundary proof. +Дополнительных runtime containers, очередей, generic scheduler, CQRS, +обязательных interfaces и новых CI analyzers не предлагается. + +## 8. Cross-audit signals + +- **Audit 04 (бывший ARCH-001):** dead-code closure manual dispatcher — `backend/src/modules/investments/tbank-sync/tbank-sync.service.ts:54-311`; production HTTP entry предоставляет только status: `backend/src/modules/investments/tbank-sync/tbank-sync.controller.ts:31-40`. +- **Audit 15 (бывший ARCH-004):** stale Container/read-index относительно hourly ownership — `docs/architecture/containers.md:142-148`, `specs/AI_INDEX.md:15-31`; intended policy: `docs/adr/019-hourly-reads-and-online-instrument-reference.md:9-29`. +- **Audit 06:** forecast next-day/fallback selection и GET publication semantics — + `backend/src/modules/investments/portfolio/infrastructure/forecast.repository.ts:50-79`; + architectural ownership отмечен ARCH-008, correctness здесь не воспроизводился. +- **Audit 06:** hourly termination/persistence outcome — + `backend/src/modules/investments/tbank-sync/tbank-hourly.scheduler.ts:93-110,178-218`; + отдельный implementation/lifecycle вопрос внутри сохраняемого owner. +- **Audit 06/07:** calendar refresh force/TTL policy — + `backend/src/modules/investments/upcoming-events/upcoming-events.service.ts:182-239`, + `backend/src/modules/investments/instruments/application/catalog-enrichment.service.ts:164-193`; + не основание удалять source boundary. +- **Audit 07:** online search fan-out и banking list loader — + `backend/src/modules/investments/instruments/application/catalog-discovery.service.ts:295-317`, + `backend/src/modules/banking/bank-cards/application/bank-operations.service.ts:333-365`; + performance не измерялся. +- **Audit 08:** ordinary Income GET и scheduling proof вместо одного observe path — + `backend/test/tbank-income-local.e2e-spec.ts:249-278`; + эффективность suites отдельно не оценивалась. +- **Audit 05/13:** timer manual-refetch eligibility при коротком поиске и смене + session — `frontend/src/features/tbank-connection/model/screen-sync.ts:28-35`, + `frontend/src/entities/bond/api/queries.ts:97-104`; + архитектурный owner — ARCH-003, UI/security последствия требуют своего proof. + +## 9. Open questions / verification plan + +| Вопрос, который меняет решение | Самая дешёвая решающая проверка | Как меняется решение | +| --- | --- | --- | +| Может ли cold historical Income самостоятельно загружать прошлые периоды? | Product confirmation существующего tbank-data-sync исключения; controlled test ordinary Income GET с cold coverage/stale membership | ARCH-002 остаётся FIX; либо read-only Income, либо явно расширенное узкое first-load requirement, без membership/full-history prefetch | +| Что означает 30-minute client cadence для focus/reconnect/mount/background? | Согласовать формулировку ADR 019 `:27-29`; fake-time/MSW active/disabled query и visibility cases | Определяет query options ARCH-003; не оправдывает connection-owned timer | +| Как выбрать latest complete event forecast независимо от запрошенного окна? | Два successive-day complete publications, narrow/non-overlap GET и partial update; проверить account/all totals и timestamps | ARCH-008 может стать SIMPLIFY/FIX с переносом только event-specific policy; общий fenced publisher KEEP | +| Нужен ли public instrument `sync=observe` внешнему consumer/старому сохранённому work? | Offline inventory OpenAPI/callers и подтверждение владельцем consumer compatibility; targeted reference/status cases | ARCH-009 становится DELETE/SIMPLIFY только для совместимо удаляемого path; source schema не удаляется автоматически | +| Свеж ли предоставленный OpenAPI artifact относительно этих HEAD? | В отдельной разрешённой implementation/verification сессии штатная backend generation/parity и frontend offline check | Здесь сравнение только existing files; новые checker/shared-source dependency не предлагаются | + +**Review completeness перед завершением:** подтверждены девять общих +разделов, Ownership map, Dependency/Boundary matrix, valid architectural +decisions, minimal boundary changes и финальная таблица. По scoped `rg -c` +найдено семь finding headers; каждое из четырнадцати обязательных полей +присутствует семь раз, распределение по findings сверено вручную. +Actionable findings — только ARCH-002 и ARCH-003; KEEP — ARCH-005–007, +INVESTIGATE — ARCH-008–009. Внутренние related IDs ссылаются только на эти +семь findings; вынесенные вопросы 04/15 не входят в counts и consolidation. +Сверка source evidence/consumer/counterarguments проведена статически; +неподтверждённые решения отмечены UNVERIFIED. После сохранения повторены +HEAD/status и `git diff --check` во всех пяти checkout: source/config +не изменились. Новый ignored отчёт отдельно прошёл whitespace-проверку; +routing затрагивает только root audit artifact. Boy Scout review текста +не выявил новых source changes, новых механизмов или неподтверждённых числовых +оценок экономии. Application build/full-suite для этого artifact не запускались. + +## Final machine-readable consolidation table + +| ID | Decision | Severity | Confidence | Repo | Files | One-line impact | Effort | Related IDs | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | +| ARCH-002 | 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.service.ts | Personal reads могут инициировать membership и историю вне requested interval | M | ARCH-006 | +| ARCH-003 | SIMPLIFY | P2 | High | frontend | src/features/tbank-connection/model/screen-sync.ts; src/entities/bond/api/queries.ts; src/entities/portfolio/api/usePortfolio.ts; src/app/ui/App.tsx | Connection timer конкурирует с query lifecycle owner и не учитывает eligibility | M | ARCH-002,ARCH-005,ARCH-006 | +| ARCH-005 | KEEP | P3 | High | frontend,backend | frontend/src/features/auth/model/auth.vm.ts; frontend/src/shared/api/client.ts; backend/src/modules/auth/application/auth.service.ts | Session/transport split сохраняет single-flight, identity isolation и rotation CAS | S | ARCH-003,ARCH-006 | +| ARCH-006 | KEEP | P3 | High | backend | src/integrations/tbank/tbank.module.ts; src/modules/investments/tbank-sync/tbank-sync-repository.module.ts; src/modules/investments/portfolio/infrastructure/portfolio.repository.ts; src/modules/investments/portfolio/infrastructure/forecast.repository.ts | Singleton owners и fenced publication сохраняют source lifecycle и connection isolation | S | ARCH-002,ARCH-003,ARCH-005,ARCH-008,ARCH-009 | +| ARCH-007 | KEEP | P3 | High | frontend,backend | backend/src/modules/banking/bank-cards/application/bank-operations.service.ts; backend/src/modules/banking/bank-cards/infrastructure/bank-cards.repository.ts; backend/src/modules/banking/bank-cards/domain/rub-money.ts; frontend/src/entities/bank-card/api/mutations.ts | Cohesive transaction защищает money, refunds, versions и replay | S | - | +| ARCH-008 | INVESTIGATE | P2 | Medium | backend | src/modules/investments/upcoming-events/upcoming-events.service.ts; src/modules/investments/portfolio/application/forecast.service.ts; src/modules/investments/portfolio/infrastructure/forecast.repository.ts | UNVERIFIED: event semantics распределена; новая write/read boundary требует semantic proof | M | ARCH-006 | +| ARCH-009 | INVESTIGATE | P2 | Medium | backend,frontend | backend/src/modules/investments/instruments/application/catalog-discovery.service.ts; backend/src/modules/investments/instruments/stocks/presentation/stock.controller.ts; frontend/src/shared/api/resource-query.ts | UNVERIFIED: удаление persisted observe path требует compatibility/FK proof | M | ARCH-006 | diff --git a/audits/2026-10-11/reports/02-complexity-and-overengineering-report.md b/audits/2026-10-11/reports/02-complexity-and-overengineering-report.md new file mode 100644 index 0000000..5dc550a --- /dev/null +++ b/audits/2026-10-11/reports/02-complexity-and-overengineering-report.md @@ -0,0 +1,916 @@ +# Аудит 02 — Complexity & Overengineering + +## 1. Audit metadata + +- **Audit ID/name:** 02 / Complexity & Overengineering. +- **Дата:** 2026-10-11. +- **Исполнитель:** независимый исполнитель направления 02; другие сабагенты + не запускались. Выводы других новых отчётов не читались. +- **Workspace:** `/Users/ksv741/Project/vibe/WealthLedger`. +- **Режим:** исследование, read-only для application source, конфигураций, + тестов, specs, docs и root source/config. Единственная запись этого аудита — + `audit-results/02-complexity-and-overengineering-report.md` через `apply_patch`. + +| Checkout | Абсолютный путь | Проверенный HEAD | Начальный status | +| --- | --- | --- | --- | +| root | `/Users/ksv741/Project/vibe/WealthLedger` | `dc030f84c979cedb2628ef4bd97c62d6c63a9256` | clean | +| frontend | `/Users/ksv741/Project/vibe/WealthLedger/frontend` | `c8821200ce699dab1ce65360d1968d5877a2e78b` | clean | +| backend | `/Users/ksv741/Project/vibe/WealthLedger/backend` | `0ad84ff4f0b4712abc7e69dc299856f14af6eadd` | clean | +| specs | `/Users/ksv741/Project/vibe/WealthLedger/specs` | `fcdc805f6c1b3be6a20a7e858e3e80cbd83a9121` | clean | +| docs | `/Users/ksv741/Project/vibe/WealthLedger/docs` | `e6fd4f64b81dd73b33959b845a1babcfac1891c8` | clean | + +SHA получены самостоятельно командой `git rev-parse HEAD` из каждого checkout; +clean означает пустой вывод +`git status --porcelain=v1 --untracked-files=all`, а не отсутствие ignored files. +Root allowlist исключает audit-results и nested repos: +`.gitignore:1–2`. Поэтому новый отчёт проверяется отдельно от tracked diff. + +**Инструкции и skills.** Сначала загружен установленный `ponytail-audit` +через Skill tool, прочитан полный протокол +`audit-prompts/02-complexity-and-overengineering.md:1–67`. +Использованы его ladder/hunt, теги `delete:`, `native:`, `yagni:`, `shrink:` +и ранжирование по возможному сокращению сложности. Полный формат отчёта задан +пользовательским протоколом; численная оценка net LOC не выдумывается. +Прочитаны AGENTS каждого checkout, local architecture/engineering и +активированные frontend UI/data/TS/testing, backend HTTP/data/integrations/ +security/errors/observability/testing conventions. Root conventions применены +только к размещению и проверке отчёта. Загружен +`verification-before-completion` для завершающей проверки. + +`graphify` загружен, но существующий graph отсутствует: прямое чтение +`graphify-out/graph.json` в root и каждом из четырёх checkout вернуло +File not found. Graph extraction, query CLI, установка и создание artifacts +не выполнялись. Исследование продолжено статическими трассировками. + +**Что выполнялось:** специализированные Read/Grep/Glob, поиск callers из +директорий соответствующих repo, read-only Git-команды для SHA/status; +при проверке native-замены прочитан уже установленный dependency source. +Завершающие проверки перечислены в разделе 9. + +**Что не выполнялось:** install, codegen, migrations, build, full-suite, +targeted runtime tests, запуск приложения, browser/network сценарии, +обращения к реальным внешним API, commit, PR. Старый +`audit-results/technical-audit-2026-10-11.md` прочитан только как список +гипотез; его findings, оценки LOC и результаты проверок не перенесены как +собственные измерения. Остальные новые отчёты не открывались. + +**Статус доказательств:** High подтверждает механизм и callers по текущему +source, а не runtime-воспроизведение. `UNVERIFIED` означает, что конкретная +потеря гарантии или безопасный объём удаления ещё не доказаны. У всех оценок +выигрыша — `Impact unmeasured`, если отдельно не указано иное. + +## 2. Executive verdict + +1. **Факт:** наибольшая подтверждённая лишняя координация в живом frontend — + `useScreenSync`: страницы передают descriptor и массив resources в отдельный + timer, хотя query-owner уже управляет lifecycle. Все изученные production + callers используют только его побочный эффект. **CPLX-001 / SIMPLIFY / P2.** +2. **Факт:** живой Ledger сначала вычисляет интервал, превращает его обратно + в строки screen intent, прогоняет через многоцелевой planner и извлекает + только operations/screen intervals. Для этого caller достаточно имеющихся + `operationIntervals` и `readPlan`. **CPLX-002 / SIMPLIFY / P2.** Удаление + всего planner не следует из этого вывода. +3. **Факт, небольшая стоимость:** двухcallback presentation VM скринера + не владеет state, fetching, lifecycle или отдельным consumer. Локальная + замена сохраняет accessibility; это необязательный cleanup. + **CPLX-003 / SIMPLIFY / P3.** +4. **Риск и недоказанная возможность упрощения:** forecast связывает request + window, persisted identity и выбор fallback; online/observe инструментов + обслуживают разные модели; income read содержит асинхронное планирование + импорта. Смешение моделей доказано, но безопасное удаление всех ветвей — + **UNVERIFIED**, поэтому **CPLX-004–006 / INVESTIGATE**, а не массовый delete. +5. **Факт:** conditional publication, source-version fences, точная денежная + арифметика, durable reservation, банковские fingerprints/CAS/tombstones и + стабильные screener snapshots имеют конкретные текущие сценарии. + **CPLX-007–010 / KEEP.** Их сокращение до «обычного GET/CRUD/timer» создаст + больше проблем, чем снимет сложности. +6. **Итог:** подтверждены **3 actionable findings: P0=0, P1=0, P2=2, P3=1**; + отдельно **KEEP=4**, **INVESTIGATE=3**. Новые слои, общий workflow engine, + generic repository или новая dependency для решения не нужны. Это + risk-/cost-based выборка, а не сертификат отсутствия других проблем. + +## 3. Inventory & coverage + +### 3.1. Фактическая карта и метод + +Проверены независимые checkout и composition roots. Backend — NestJS modular +monolith, Prisma/SQLite, конкретные application providers; frontend — +React/TypeScript/Vite, TanStack Query, Mantine, generated OpenAPI types. +Основания: `backend/src/app.module.ts:18–38`, +`backend/package.json:40–66`, `frontend/package.json:39–62`, +`frontend/src/app/ui/App.tsx:13–44`. Архитектурная convention сама по себе +не использована как доказательство целесообразности реализации. + +Отбор выполнялся по распределению transitions, глубине делегирования, +вариантам режима чтения и числу преобразований одного input. Затем проверялись +entrypoint, runtime consumer, source-owner, persistence и контрпример. +Ни LOC, ни наличие отдельного файла не являлись критерием дефекта. + +Выборка репрезентативна для сложной investment coordination и финансовых writes: +есть browser query lifecycle, periodic producer, conditional persistence, +cursor/history, multi-period analytics, online reference, command snapshot +и transactional replay. Auth refresh, все integration parsers, все UI forms, +весь банковский read layer и каждый controller exhaustively не трассировались. +Мёртвый код, DRY, tooling/test infrastructure и performance исследовались +только до передачи signal соответствующему направлению. Generated/vendor +деревья не сканировались как собственные production abstractions проекта. + +**Метод чисел в этом отчёте:** + +- 8 flows — ручная нумерация F1–F8 ниже, по различным production entrypoints. + Это не автоматически рассчитанный coverage percentage. +- 8 production call sites `useScreenSync` — уникальные page VM из поиска + `useScreenSync` в `frontend/src`, без import/export и test matches; все + восемь VM прочитаны. Это accounts, account, account-operations, + account-events, account-income, bonds, bond, stock. +- 2 effects и 1 interval в hook — декларации в + `frontend/src/features/tbank-connection/model/screen-sync.ts:23–35`. + Это локальные конструкции, а не число browser timers во всей системе. +- 2 callbacks presentation VM — декларации строк 9 и 13; 3 consumers forecast + — UpcomingEventsService, StockIncomeService, BondIncomeService по imports + и вызовам `forecasts.result` в production source. +- Counts findings/severity получаются из consolidation table. Число ветвей + всего repo, cyclomatic complexity, milliseconds, CI minutes, token savings, + bundle size и net LOC не измерялись. + +### 3.2. Восемь живых production flows + +| Flow / owner | Реальный entrypoint и цепочка | Поддерживаемые сценарии и состояния | Сопоставление сложности со сценарием | +| --- | --- | --- | --- | +| **F1. Обзор счёта и перечитывание**; page VM + entity queries | Route `/accounts/:accountId`: `frontend/src/app/routes/AppRoutes/AppRoutes.tsx:203–210` → `pages/account/ui/AccountPage/AccountPage.tsx:9–34` → VM `:66–135` → `entities/portfolio/api/usePortfolio.ts:19–25` → `shared/api/resource-query.ts:99–149` → `backend/src/modules/investments/portfolio/presentation/portfolio.controller.ts:25–31` → `application/portfolio.service.ts:113–231` → `infrastructure/portfolio.repository.ts:25–47` | 5 resources в overview; без content loading/error, успешный empty/loaded, partial metadata, cached content при background read | Query/session/resource projection и локальный server read оправданы. Отдельный screen timer дублирует query scheduling; scope не влияет на него. CPLX-001 | +| **F2. Часовой обход**; scheduler и source owners | `backend/src/app.module.ts:35` → `tbank-sync/tbank-sync.module.ts:24–25` → `tbank-hourly.scheduler.ts:48–87` → `TbankSyncRepository.reserveHourly` `tbank-sync.repository.ts:174–220` → membership/portfolio/operations/events `scheduler:113–175` → settle `:178–218` | Первая/последующая попытка, overlap, restart, changed connection, failed/partial/succeeded; проход membership → portfolios → operations → events | Нативный timer только предлагает работу; durable row и process flag имеют разные горизонты жизни. Polling отдельных workers и completion promises различаются по owner. KEEP координации; ошибка lifecycle передана audit 13 | +| **F3. Сохранённый прогноз событий**; UpcomingEvents/Forecast owners | `backend/src/modules/investments/upcoming-events/upcoming-events.controller.ts:22–38` → `upcoming-events.service.ts:54–173,200–239` → local portfolio + saved calendars → `portfolio/application/forecast.service.ts:18–95` → `portfolio/infrastructure/forecast.repository.ts:24–175` | One account/all, известные/неизвестные monetary values, complete candidate, прежний full result при failed dependencies, другое UTC-окно | Фильтрация/расчёт и publication fence — разные обязанности. Парсинг окна из строкового ключа и поиск пересекающегося fallback смешивают read selection и publication. CPLX-004; fences CPLX-007 | +| **F4. Ledger, cursor и первая загрузка интервала**; Operations owner | `backend/src/modules/investments/operations/presentation/brokerage-operations.controller.ts:22–38` → `application/brokerage-operations.service.ts:69–152` → screen planner → `application/brokerage-operations-sync.service.ts:46–126` → domain interval/coverage → repository → saved page и cursor `service:152–388` | One account/all, explicit/default period, instrument/categories, first/cursor page, complete/missing coverage, historical cold read, moving tail | Подпись cursor и interval normalization сохраняются. Date → ISO → screen → source union → intervals является обходным маршрутом для уже выбранного интервала. CPLX-002 | +| **F5. Income summary/analytics**; Income + Operations owners | `backend/src/modules/investments/income/income-analytics.controller.ts:24–79` → `income-analytics.service.ts:45–165` → `operations/application/brokerage-operations.service.ts:391–574` → local snapshot, optional membership.done → schedule → readPlan → totals/comparisons/buckets | Summary выбранного периода, analytics selected/previous/year, неизвестная граница истории, missing/partial/complete, observe/default read | Несколько comparison periods и точные суммы нужны. Два read modes с discovery/schedule расширяют lifecycle обычного чтения; удалить без решения cold history нельзя. CPLX-006 | +| **F6. Online поиск/detail/ticker инструмента**; Catalog owner | `frontend/src/pages/bonds/ui/BondsPage/BondsPage.vm.ts:9–39` → `entities/bond/api/queries.ts:97–104` → `backend/src/modules/investments/instruments/bonds/presentation/bond.controller.ts:55–89` → `application/bond-catalog.service.ts:25–88` → `instruments/application/catalog-discovery.service.ts:84–337` → TbankClient; `sync=observe` идёт через saved discovery/enrichment | Поиск, UID detail, ticker ambiguity/not found, online и saved/observe variants; bond/share mapping | Catalog service не passthrough: переводит provider money/dates/fields. Legacy observe path доступен через публичный query flag, поэтому отсутствие нового frontend caller не доказывает мёртвость backend ветви. CPLX-005 | +| **F7. Скринер акций**; Screener owner | Route `/stocks`: `frontend/src/app/routes/AppRoutes/AppRoutes.tsx:176–183` → `pages/stocks/ui/StocksPage/StocksPage.vm.ts:29–115` → POST/GET `backend/src/modules/investments/instruments/stocks/presentation/stock.controller.ts:85–109` → `application/stock-screener.service.ts:65–139` | Fresh search, stable pages, empty success, expiry, foreign user/revision, invalid page/input, поздний frontend ответ | Snapshot identity, bounds и latest-request guard нужны. Отдельная translation-only presentation VM не добавляет поведения. CPLX-003, CPLX-010 | +| **F8. Создание/изменение/повтор банковской операции**; BankOperations owner | `backend/src/modules/banking/bank-cards/presentation/bank-cards.controller.ts:133–166` → `application/bank-card-input.ts:125–142` → `application/bank-operations.service.ts:94–330` → `infrastructure/bank-cards.repository.ts:35–73` → SQLite transaction/constraints | Expense/topup/transfer/refund, replay same ID, changed intent, stale version, dependent refunds, delete/tombstone, rollback | Проверки денег/input, replay, ownership, CAS исходной покупки и транзакция защищают разные инварианты. Они не лишние fallback/guards. CPLX-009 | + +Пути в цепочках, сокращённые до `application/...`, относятся к module, +названному в той же строке; полные evidence основных решений приведены ниже. +Эти flows живы по routes/module wiring/callers; выполнение в production +или latency не наблюдались. + +### 3.3. Проверенные seams, guards, flags и контраргументы + +| Кандидат / качественный сигнал | Независимая проверка | Итог | +| --- | --- | --- | +| Framework interfaces/strategy, конкретные repositories | `backend/src/prisma/prisma.service.ts` и scheduler используют lifecycle contracts; архитектура и DI roots не требуют port на каждый service. Поиск `interface`, `implements`, factory/strategy/registry в `backend/src` не выявил подтверждённого generic plugin engine в выбранных flows | Не объявлять framework seam ненужным из-за одной реализации | +| `StockScreenerService` / `TbankAccess` / `TBANK_ACCESS` | Узкий access type `stock-screener.service.ts:50–63`; production `useExisting` `instruments.module.ts:24`; test seam `stock-screener.service.spec.ts:25–38` | Один production provider не означает бесполезность seam; не создавать второй adapter и не считать это actionable | +| `GetStockCompanyUseCase` похож на wrapper | `get-stock-company.use-case.ts:25–38,41–88` выбирает sector-dependent metrics, currency/finite value rules и wire projection | Meaningful boundary conversion; не объединять с gRPC client | +| Generic `ForecastService` | Три production callers: `upcoming-events.service.ts:160`, `stock-income.service.ts:85`, `bond-income.service.ts:116`; общие publication guarantees | Generic здесь обслуживает существующие разные payloads; не переписывать в три независимых persistence flow | +| Generic resource/session wrappers | `frontend/src/shared/api/authenticated-query.ts:34–58`, `resource-query.ts:37–67,99–102,129–149` обслуживают несколько endpoint queries и confirmed access loss | Session gate, cache purge и resource envelope сохранять при CPLX-001/005 | +| Presentation VM выглядит формальным | Stocks VM — только labels; `CardPagePresentation.vm.ts:18–53,65–73` имеет настоящий scroll/layout lifecycle | CPLX-003 только для Stocks; не обобщать на все presentation hooks | +| Flags `ticking`, durable `state`, `processRunId`, `attemptId` | Tick guard `scheduler:56–71`; durable/restart logic `repository:189–201`; publication identities `portfolio.repository.ts:122–147` | Не одинаковое состояние: обход процесса, lifetime попытки, fencing старой работы | +| Public metadata `freshness`, `completeness`, `availability`, `lastAttempt` | `investment-resource.mapper.ts:18–68`; spec `tbank-data-sync/spec.md:24–37` | Один общий boolean уничтожит различие старого полного результата и новой неуспешной попытки. Derived completeness не доказывает, что его можно убрать из wire contract | +| Internal `meta.sync` и новые публичные states | `investment-resource.mapper.ts:65–68` задаёт non-enumerable compatibility property; её читают `forecast.service.ts:69–75`, enrichment `:323–328` | Migration bridge имеет runtime consumers; нельзя просто удалить property как «не сериализуется» | +| Fallback после неуспеха | `forecast.repository.ts:78–97,130–159` и тест `upcoming-events.e2e-spec.ts:294–318,344–363` | Old complete snapshot и rejection устаревшего candidate нужны; подозрителен выбор identity/window, не само сохранение fallback | +| Catch → payload fallback | `forecast.service.ts:98–123` | Parsed string key и generic payload скрывают причинность; malformed saved data/ключи не исследованы runtime. Потенциальное упрощение требует CPLX-004, не механического удаления guards | +| Calendar `stable/quote` policy | `catalog-enrichment.service.ts:189–193,679–692`, hourly caller `scheduler:166–175` | Корректность refresh policy — audit 13/07, не доказательство ненужности source-owner | +| Runtime guards поверх типов | Screener `item` `:112–127`, money `rub-money.ts:12–31`, company metrics `get-stock-company.use-case.ts:71–87` | DTO/TS types не гарантируют shape внешних records и persisted values. KEEP | +| Config switches | `backend/src/config/env.validation.ts:9–19,21–114` ограничивает rate limit, endpoint/CA и logging; реальные integration/client inputs | Не найдено подтверждения speculative compatibility registry; эксплуатационные knobs не удаляются по одному отсутствующему override | + +### 3.4. Complexity justification table — специальные результаты + +| Flow | Supported scenarios | Safety invariants | Suspect complexity | Removal candidate | Risk of change | Decision | +| --- | --- | --- | --- | --- | --- | --- | +| F1 overview/periodic read | Mounted resources, changing IDs, cached success/error | Enabled/session gate, canonical key, AbortSignal/cache reset, read-only retry | Timer + resourcesRef + screen descriptor над query lifecycle | Собственный scheduler hook после переноса cadence к query options; без новой registry | Medium: background/focus/reconnect и reset cadence не полностью эквивалентны | SIMPLIFY / CPLX-001 | +| F2 hourly | First/due/restart/overlap, independent sections | Durable single-attempt, connection isolation, safe terminal outcome | In-memory и durable state рядом; разные completion APIs | Только несущественные presentation/helpers после отдельного lifecycle proof; timer/reservation не удалять | High: restart/late result/partial result | KEEP / CPLX-008 | +| F3 forecast | Complete candidate, missing calendar, saved fallback, window/all | Exact Money, atomic dependency validation, membership/connection fence | Window спрятано в scopeKey; generic read selection и publication в одной resolve | Локально разделить latest complete selection и request filter после решения window identity | High: stored data compatibility и сохранённый calculatedAt | INVESTIGATE / CPLX-004; KEEP / CPLX-007 | +| F4 ledger | Account/all, default/explicit dates, cursor, first missing range | Exact [from,to), normalization/month splitting, authorization, cursor binding | Range → ISO screen → Source union → intervals | Вызов broad planner в этом reader; напрямую использовать существующий normalizer | Medium: end-inclusive/exclusive, movingEnd и unfiltered source coverage | SIMPLIFY / CPLX-002 | +| F5 income | Selected/previous/year, cold/incomplete history, default/observe | Exact per-currency sums, known vs unknown, current connection | Snapshot → membership.done → deferred schedule → readPlan → repeat read | Request-driven membership/schedule только после определения cold-history owner | High: исчезновение первого доступного результата | INVESTIGATE / CPLX-006 | +| F6 online instruments | Search/detail/ticker, ambiguity, observe cache | Public shape, unknown fields, source-vs-personal isolation | Online и durable/saved reference paths под одним bool | Legacy observe backend branch только после consumer/contract decision; не calendars/candles | High: публичный flag и persisted consumers | INVESTIGATE / CPLX-005 | +| F7 screener | New search, stable page, empty/expired/foreign result | Search ownership/revision, bounds, stable order, latest frontend intent | Translation-only VM; кажущаяся лишней input allowlist | Только Stocks presentation hook; snapshot и raw-body rejection оставить | Low для callbacks; High для snapshots/validation | SIMPLIFY / CPLX-003; KEEP / CPLX-010 | +| F8 banking write/replay | Create/update/delete/refund, lost response, collision | Kopecks exactness, transaction rollback, CAS, original fingerprint/tombstone | Много guards и replay-before-validation | Подтверждённых removal candidates в write path нет | High: двойное денежное влияние/over-refund | KEEP / CPLX-009 | + +### 3.5. Калибровка + +**«Сложно, но нужно», случай A — forecast publication (CPLX-007).** +Реальный scenario: quantity/source revision меняется между расчётом и записью. +Owner повторно проверяет membership, portfolios и calendar versions внутри +transaction (`forecast.repository.ts:35–49,80–145`). Изученный SQLite test +меняет portfolio до настоящего `resolve` +(`backend/test/upcoming-events.e2e-spec.ts:344–363`), а другой меняет +dataRevision (`:490–513`). Простое `upsert(candidate)` уменьшает код, но +публикует смесь наблюдений. Сохранять fence независимо от решения об окне. + +**«Сложно, но нужно», случай B — banking replay и refund (CPLX-009).** +Реальный scenario: полный refund сохранён, HTTP-ответ потерян, одновременно +идёт второй refund или исправление покупки. Сначала replay идентичного +намерения, затем validation нового действия; CAS покупки и refund sum остаются +в transaction (`bank-operations.service.ts:152–196,214–240`). Проверки +`bank-cards.e2e-spec.ts:462–489,622–653` различают эти исходы. Проверять +refund limit до replay или использовать только unique ID проще внешне, +но теряются successful retry и collision protection. + +**«Просто, но рискованно» — убрать raw-body allowlist скринера, полагаясь +на `forbidNonWhitelisted` (CPLX-010).** +`backend/src/main.ts:31–38` сначала применяет global transform/whitelist без +forbidNonWhitelisted. Локальный body pipe +`stock.controller.ts:91` работает уже после global pipe; explicit +`Object.keys(request.body)` `:94–97` смотрит исходное тело. +Порядок проверен по установленному Nest source: +`backend/node_modules/@nestjs/core/router/router-execution-context.js:161–170` +соединяет `pipes.concat(paramPipes)`, +`backend/node_modules/@nestjs/common/pipes/validation.pipe.js:68–85,100–101` +преобразует body в entity и возвращает entity для argument. +Следовательно, декларация local forbid не является достаточным доказательством +rejection неизвестного original field. Изученный configured HTTP test +`backend/test/stocks.e2e-spec.ts:73–87` требует 400 для unknown field. +Этот test не запускался, но существует конкретный механизм, объясняющий +guard. Не удалять guard как redundant; глобальное изменение pipe policy +ради одного endpoint увеличит scope и сложность проверки. + +## 4. Findings + +Actionable здесь означает только `DELETE`, `SIMPLIFY` или `FIX`. +KEEP — явно защищённая текущая сложность, INVESTIGATE — требование evidence +перед изменением. Severity у KEEP=P3 обозначает отсутствие установленной +проблемы текущего механизма; серьёзность возможного неправильного удаления +описана в Guarantees at risk и не подменяет severity finding. + +### CPLX-001 — native: перенести периодическое чтение к query-owner + +- **Decision:** SIMPLIFY. +- **Severity:** P2. +- **Confidence:** High. +- **Evidence:** `frontend/src/features/tbank-connection/model/screen-sync.ts:7–61`; + `frontend/src/shared/api/authenticated-query.ts:34–58`; + `frontend/src/entities/portfolio/api/usePortfolio.ts:19–25`; + `frontend/src/entities/bond/api/queries.ts:97–104`; + `frontend/src/app/ui/App.tsx:13–19`. +- **Runtime consumer:** eight page VM calls: accounts `AccountsPage.vm.ts:23`, + account `AccountPage.vm.ts:78–84`, operations `AccountOperationsPage.vm.ts:96–99`, + events `AccountEventsPage.vm.ts:67–75`, income `AccountIncomePage.vm.ts:66–69`, + bonds `BondsPage.vm.ts:13`, bond `BondPage.vm.ts:45–55`, + stock `StockPage.vm.ts:45–55` под `frontend/src/pages//ui//`. + Все восемь используют вызов как statement, не читают возвращённый model. +- **Claim:** один read lifecycle сейчас разнесён по query options и собственному + page-level timer с mutable ref; это подтверждённая лишняя coordination boundary. +- **Current mechanism:** каждый mounted caller ставит interval; отдельный effect + обновляет resourcesRef; timer вызывает ручной refetch каждого текущего resource. + `_scope` никак не используется. В том же hook рассчитываются refresh/retry/ + source/transport flags, которые эти production callers не используют. +- **Impact / actual cost:** для диагностики cadence нужно проследить page VM, + hook, entity policy и App defaults. Проверка enabled находится в query-owner, + но timer вызывает manual refetch без неё. **Impact unmeasured**: число лишних + запросов, runtime latency и трудозатраты не измерены. +- **Why now:** текущие страницы уже перешли к периодическому read-only чтению, + descriptor больше не обозначает команду синхронизации. +- **Minimum change:** использовать существующий `refetchInterval` на нужных + entity query observers вместо timer/ref координации; убрать hook/call + descriptors, когда все текущие callers переведены. Не включать polling + глобально для всех API и не создавать registry. Явно согласовать текущие + focus/reconnect/stale policies у тех же владельцев. +- **Guarantees at risk:** auth/session enabled, смена connection/key, + cached snapshot/retry, HTTP-only read, отсутствие запросов с пустым UID, + прежняя политика background-tab и общий cadence. +- **Counterarguments / trade-offs:** единый timer даёт общий момент перечитывания + нескольких resources; native timers привязаны к observers и могут менять фазу. + Однако текущий hook не выполняет barrier или атомарный snapshot для группы. + Native option существует в установленной dependency: `frontend/package.json:46`; + `node_modules/@tanstack/query-core/src/queryObserver.ts:371–430` проверяет enabled + при scheduling и отдельно управляет background/focus. Manual `refetch` + `:279–285,340–362` идёт другим путём. Один refetchInterval **сам по себе не + ограничивает** focus/mount/reconnect reads до 30 минут. Переход требует решения + о политике, не нового scheduler. +- **Verification or rollback:** существующий screen-sync test `:21–80` сохраняет + HTTP-only semantics, но не доказывает timer boundary. Перед изменением добавить + минимальную MSW/fake-time проверку meaningful/disabled input, смены key, + unmount и background/refetch policy; сохранить access/cache tests. Rollback — + revert локальной migration, без schema/данных. +- **Effort:** M. +- **Related finding IDs:** CPLX-005. + +### CPLX-002 — shrink: Ledger не нужен round-trip через общий screen planner + +- **Decision:** SIMPLIFY. +- **Severity:** P2. +- **Confidence:** High. +- **Evidence:** `backend/src/modules/investments/operations/application/brokerage-operations.service.ts:74–151`; + `backend/src/modules/investments/investment-sync-plan.ts:77–105,121–140,271–284,301–327`; + `backend/src/modules/investments/operations/domain/operations-sync-plan.ts:18–64`; + `backend/src/modules/investments/operations/application/brokerage-operations-sync.service.ts:46–126`. +- **Runtime consumer:** оба живых routes через + `backend/src/modules/investments/operations/presentation/brokerage-operations.controller.ts:22–38` + вызывают `BrokerageOperationsService.read`; после planner consumer берёт только + operations источники с priority=screen. +- **Claim:** source union и screen abstraction здесь не представляют бизнес- + возможность caller; уже выбранный interval временно расширяется до screen + plan и снова сужается до того же operations interval. +- **Current mechanism:** `ledgerRange` вычисляет [from,to); reader превращает + from/to в ISO, вычитает 1 ms из exclusive end, вызывает planner с пустыми + instruments и known-membership flag. Planner снова вызывает `ledgerRange`, + добавляет membership/history sources, нормализует operations и сортирует + priorities. Reader отбрасывает всё, кроме operations/screen, и передаёт readPlan. +- **Impact / actual cost:** изменение first-load boundary требует понимания + обширного screen vocabulary, priority фильтра и обратимого date conversion. + Это engineering/cognitive cost, не доказанный performance bottleneck. + **Impact unmeasured.** +- **Why now:** live caller уже account/range-based, а screen planner обслуживает + модель orchestration, которую read endpoint не исполняет целиком. +- **Minimum change:** построить intervals из уже выбранных `selected`, `from`, + `to` при known membership и `from < to`, передать их в существующий + `operationIntervals`, затем существующий `sync.readPlan`. Сохранить текущий + unfiltered source import: instrument/categories ограничивают read result, + не должны незаметно сужать completeness базового источника. +- **Guarantees at risk:** [from,to) и inclusive HTTP end, subsecond cursor end, + empty/all scopes, membership unknown, month splitting, movingEnd, + authorization/revision, coverage. Политику history/backfill не менять этим cleanup. +- **Counterarguments / trade-offs:** screen planner централизует expansion для + другого caller `tbank-sync.service.ts:143–180`; его полное удаление не предлагается. + Нормализацию нельзя заменить простым единственным диапазоном: она группирует, + объединяет и делит по месяцам (`operations-sync-plan.ts:21–61`). + Default planner `force=false` уже не передаёт instrumentUid в import + (`investment-sync-plan.ts:139`); это поведение следует сохранить явно. +- **Verification or rollback:** сравнить observable first-load intervals до/после + для known/unknown membership, one/all, explicit/default end и cursor; + использовать изученные SQLite scenarios + `backend/test/tbank-ledger-local.e2e-spec.ts:113–137,138–228,229–268` + для точности/cursor/local repeat. Они не запускались. Rollback — revert + только caller simplification; shared normalizer/repository остаются прежними. +- **Effort:** S. +- **Related finding IDs:** CPLX-006, CPLX-008. + +### CPLX-003 — yagni: translation-only Stocks presentation VM + +- **Decision:** SIMPLIFY. +- **Severity:** P3. +- **Confidence:** High. +- **Evidence:** `frontend/src/pages/stocks/ui/StocksPage/StocksPagePresentation.vm.ts:1–20`; + `frontend/src/pages/stocks/ui/StocksPage/StocksPage.view.tsx:205–208,259–268`; + `frontend/.ai/conventions/ui.md:58–89`. +- **Runtime consumer:** единственный обнаруженный production import/call — + `StocksPage.view.tsx:28,208`. Поиск имени по frontend source не обнаружил + другого consumer или самостоятельного test/story seam этого hook. +- **Claim:** отдельная VM содержит только два translation callbacks для + Pagination labels; собственной lifecycle/data/form responsibility нет. +- **Current mechanism:** view уже вызывает `useTranslation`, затем ещё один + hook вызывает translation и возвращает getControlProps/getItemProps. +- **Impact / actual cost:** дополнительный import/file переход для изменения + доступного названия; стоимость мала. **Impact unmeasured**. Не заявляется + React performance improvement или необходимость менять всю page architecture. +- **Why now:** подходящий cleanup при следующем касании pagination, не отдельная + срочная задача и не основание дробить большой view. +- **Minimum change:** оставить два типизированных callback references рядом + с Pagination в существующем presentation owner, используя уже доступный `t`; + убрать отдельный hook/file. Не убирать aria-label и не создавать shared helper. +- **Guarantees at risk:** translation update, имена previous/next/page, + presentation-only boundary, отсутствие inline functions в JSX props. +- **Counterarguments / trade-offs:** local convention предпочитает actions в VM + и extraction helpers; формальная uniformity может быть важнее маленького + выигрыша. Решение допустимо как узкое UI-only упрощение с именованными const + callbacks; если правило трактуется как запрет любых таких callbacks во view, + сначала согласовать узкую convention exemption, а не переносить этот код в + data VM. `CardPagePresentation.vm.ts:18–53,65–73` — контрпример с настоящим + layout lifecycle, его не объединять по аналогии. +- **Verification or rollback:** semantic review labels + существующая + `StocksPage.test.tsx`; простая проверка keyboard/pagination names при будущем + изменении. Новый unit suite только для декларации callback не нужен. + Rollback — вернуть файл и import без изменения endpoint/state. +- **Effort:** S. +- **Related finding IDs:** CPLX-010. + +### CPLX-004 — request window, persisted identity и forecast fallback смешаны + +- **Decision:** INVESTIGATE. +- **Severity:** P2. +- **Confidence:** Medium. +- **Evidence:** `backend/src/modules/investments/upcoming-events/upcoming-events.service.ts:152–173`; + `backend/src/modules/investments/portfolio/infrastructure/forecast.repository.ts:50–79,179–186`; + `backend/src/modules/investments/portfolio/application/forecast.service.ts:77–93,98–123`; + `backend/prisma/schema.prisma:133–145`. +- **Runtime consumer:** F3 HTTP reads и hourly `events.get` + `tbank-hourly.scheduler.ts:166–175`; тот же ForecastService обслуживает + `stock-income.service.ts:85–110` и `bond-income.service.ts:116–140`. +- **Claim:** вычисленный request scopeKey используется как PK, query filter + и протокол разбора window. Это делает simple saved-read policy зависимой + от generic persistence parsing и cross-window fallback. Безопасность замены + latest-result моделью — **UNVERIFIED**. +- **Current mechanism:** exact-key row → parse requested key → scan matching + events keys → first overlapping/account match → membership fallback → + candidate/no-candidate branches → version validation → dependency equality + → upsert → повторный parse/filter/re-total в service. +- **Impact / actual cost:** read/publication semantics труднее объяснить и + менять независимо; catch возвращает исходный payload, если parsing/filter + падает. **Impact unmeasured**; не заявляется частота wrong-window response. +- **Why now:** новые specs требуют saved calculatedAt/original window при + другом request window, включая пустое пересечение + `specs/openspec/specs/brokerage-account-upcoming-events/spec.md:10–20,63–76`. +- **Minimum change:** пока только определить identity последнего полного + результата и разделение read filter/publication в существующих owners. + После proof возможна локальная замена cross-window parser/selection; + schema migration и универсальный forecast engine не назначаются заранее. +- **Guarantees at risk:** сохранённый calculatedAt, window/empty success, + source/portfolio version fencing, membership isolation, старые rows, + stock/bond payloads и per-currency exact totals. +- **Counterarguments / trade-offs:** scope keys нужны для независимых payloads + и старых windows. ADR 018 `docs/adr/018-minimal-runtime-coordination.md:42–45` + объясняет rejection incompatible candidate; SQLite test + `backend/test/upcoming-events.e2e-spec.ts:321–339` требует data=null для + непересекающегося window, тогда как новая spec требует empty saved result. + Это реальный конфликт evidence, а не разрешение удалить все fallback. + `:619–640` отдельно защищает unchanged calculatedAt при узком окне. +- **Verification or rollback:** cheapest proof — controlled two successful + day/window publications, narrower/disjoint GET, changed dependency перед + publish и one/all scope в existing SQLite suite после решения семантики. + Проверить representative persisted keys/payloads на disposable данных. + Runtime proof и data inspection не выполнялись. До этого rollback-план + для изменения identity нельзя считать определённым. +- **Effort:** M. +- **Related finding IDs:** CPLX-007, CPLX-008. + +### CPLX-005 — online/observe compatibility раздваивает reference lifecycle + +- **Decision:** INVESTIGATE. +- **Severity:** P2. +- **Confidence:** Medium. +- **Evidence:** `backend/src/modules/investments/instruments/application/catalog-discovery.service.ts:84–140,143–217,220–337`; + `backend/src/modules/investments/instruments/bonds/presentation/bond.controller.ts:55–89`; + `backend/src/modules/investments/instruments/application/catalog-enrichment.service.ts:233–330`; + `frontend/src/shared/api/resource-query.ts:69–127,144`. +- **Runtime consumer:** `BondController.search/get/getByTicker` и аналогичные + `StockController:58–82,112–119` принимают public `sync=observe`. Новые frontend + queries обычно вызывают default online mode; frontend `.observe()` найден + только в `resource-query.test.tsx:63,167`, не в production callers. +- **Claim:** один public reference use case поддерживает online answer и + persisted/durable resource model с иной error/availability semantics. + Нужность сохранять backend compatibility branch сегодня — **UNVERIFIED**. +- **Current mechanism:** false → online token/search/details/prices; true → + saved discovery/source reads, dependency composition, sync metadata и alias + verification. Frontend observation helper отдельно меняет transport key, + отменяет старый query и пишет envelope в canonical cache. +- **Impact / actual cost:** два набора branches/metadata/state policies при + изменении search/detail/company; труднее определить владельца freshness. + **Impact unmeasured**. RPC fan-out/latency не оценивались в этом направлении. +- **Why now:** `specs/openspec/specs/online-instrument-information/spec.md:8–17` + и ADR 019 `:22–26` описывают online-only reference, но публичный flag и + persistence consumers ещё существуют. +- **Minimum change:** получить consumer/compatibility decision для конкретных + reference endpoints. При доказанном отсутствии обязательства сохранить old + behavior — локально оставить online path и удалить лишь его legacy alternative. + Frontend unused observation export передан audit 04, не объявлен отдельным + delete finding здесь. +- **Guarantees at risk:** public HTTP contract, offline clients, сохранённые + labels/calendar/candle dependencies, canonical cache identity и confirmed + access invalidation. Не удалять CatalogEnrichment/Repository целиком: F3 + использует saved calendar sources. +- **Counterarguments / trade-offs:** backend branch имеет реальный entrypoint; + отсутствие вызова в SPA не доказывает отсутствие внешнего consumer. Shared + repository также обслуживает личные forecast dependencies, а старые FK + защищают свечи. Generated transport/DTO mapping не является лишним wrapper. +- **Verification or rollback:** inventory callers/опубликованного query contract, + targeted default-vs-observe HTTP cases на controlled upstream; отдельно + проверить calendar/candle source readers. Никаких реальных API calls для proof + не требуется. Сохранять совместимость до ответа; data deletion требует + отдельного disposable migration/rollback proof и в этот audit не входит. +- **Effort:** M. +- **Related finding IDs:** CPLX-001, CPLX-007. + +### CPLX-006 — income read содержит собственную асинхронную import coordination + +- **Decision:** INVESTIGATE. +- **Severity:** P2. +- **Confidence:** Medium. +- **Evidence:** `backend/src/modules/investments/operations/application/brokerage-operations.service.ts:391–436`; + `backend/src/modules/investments/income/income-analytics.service.ts:45–93`; + `backend/src/modules/investments/income/income-analytics.controller.ts:24–79`; + `backend/src/modules/investments/operations/application/brokerage-operations-sync.service.ts:46–126`. +- **Runtime consumer:** оба summary и оба analytics routes передают + `query.sync === 'observe'`; обычный GET выбирает branch с schedule. +- **Claim:** read result и запуск работы соединены через snapshot reads, + membership completion и deferred schedule. Это дополнительный owner + lifecycle личных данных; возможность удалить его целиком без ухудшения + cold history — **UNVERIFIED**. +- **Current mechanism:** local access → readPage → define schedule → default + membership.request → discovery.done.then(schedule) либо await schedule → + repeat readPage → periods/coverage/meta → exact income aggregation. +- **Impact / actual cost:** response может быть вычислен до завершения + deferred work; изменение period/source policy нужно согласовывать с Ledger + и hourly owner. **Impact unmeasured**, без оценки RPC/latency. +- **Why now:** `specs/openspec/specs/tbank-data-sync/spec.md:9–22` оставляет + исключение первой загрузки только Ledger/candles; ADR 019 `:9–16` описывает + ту же границу. В live read code сохранилась более широкая координация. +- **Minimum change:** определить, кто и как делает отсутствующий history + доступным income. Если это уже Ledger/source owner, убрать request-driven + membership/schedule из этого reader, сохранив periods/coverage/unknown results. + Не создавать новый IncomeSyncService до необходимости. +- **Guarantees at risk:** cold-start выбранного и comparison periods, + неполнота не превращается в ноль, membership/revision isolation, + exact totals и passive observe semantics. +- **Counterarguments / trade-offs:** обычное income чтение сейчас может быть + единственным действием пользователя для получения старых операций; + hourly обновляет mutable tail (`scheduler:154–160`), а не всякий history + interval. Механическое удаление ветки оставит данные неполными дольше. + Сложность агрегирования по валютам/периодам не является этим finding. +- **Verification or rollback:** один controlled cold income GET без observe + на пустой coverage и один после Ledger load; проверить lastSuccessfulAt, + selected/previous/year, отсутствие membership RPC и old complete reads. + Нужен product/behavior answer о доступности истории. Режим работы + непроверен runtime; откат локального reader возможен, но UX меняется. +- **Effort:** M. +- **Related finding IDs:** CPLX-002, CPLX-008. + +### CPLX-007 — сохранить точность и conditional forecast publication + +- **Decision:** KEEP. +- **Severity:** P3. +- **Confidence:** High. +- **Evidence:** `backend/src/modules/investments/portfolio/infrastructure/forecast.repository.ts:35–49,80–145,154–174`; + `backend/src/modules/investments/upcoming-events/upcoming-events.service.ts:119–130,282–299`; + `backend/src/common/decimal.ts:19–48,59–83`; + `backend/test/upcoming-events.e2e-spec.ts:294–318,344–363,490–513`. +- **Runtime consumer:** F3 плюс stock dividends/bond coupons forecast callers; + pure decimal helpers используются для candidate и output totals. +- **Claim:** guards отвергают stale/несогласованный candidate; точный Money + сохраняет currency и неизвестное значение. Это подтверждённая необходимая + сложность, независимо от спорного scope/window selection. +- **Current mechanism:** transaction fence connection/membership, portfolio + version set, source revision+fetchedAt, прежний result при mismatch, + exact bigint+scale calculations, publication только согласованного candidate. +- **Impact / actual cost:** сопровождение dependency lists и decimal types + стоит внимания; **Impact unmeasured**. Потеря fence создаст неправильную + смесь количества и календаря; Number-замена теряет exact decimal semantics. +- **Why now:** эти guards находятся рядом с CPLX-004 и особенно уязвимы для + ошибочного удаления вместе с parser/fallback logic. +- **Minimum change:** оставить эти механизмы; при будущем window cleanup + сохранять существующую transaction/version validation boundary. +- **Guarantees at risk:** financial accuracy, cross-currency separation, + unknown≠zero, immutable successful calculatedAt, connection isolation. +- **Counterarguments / trade-offs:** «проверили access до расчёта» не защищает + от изменения до записи; test seams здесь доходят до real repository. + Batch reads source versions также учитывают SQLite parameter limit + (`forecast.repository.ts:99–128`), не заменять без proof одним giant query. +- **Verification or rollback:** существующие cited controlled publication + scenarios и decimal cases при любом изменении. Они изучены, но не запущены. + Удаление protections не предлагается; rollback не требуется этому audit. +- **Effort:** S. +- **Related finding IDs:** CPLX-004, CPLX-005, CPLX-009. + +### CPLX-008 — сохранить durable reservation и source-owner boundaries + +- **Decision:** KEEP. +- **Severity:** P3. +- **Confidence:** High. +- **Evidence:** `backend/src/modules/investments/tbank-sync/tbank-hourly.scheduler.ts:28–31,48–111,113–175`; + `backend/src/modules/investments/tbank-sync/tbank-sync.repository.ts:174–242`; + `backend/src/modules/investments/portfolio/application/portfolio.service.ts:244–306`; + `backend/src/modules/investments/portfolio/infrastructure/portfolio.repository.ts:116–147`; + `backend/prisma/schema.prisma:232–262`. +- **Runtime consumer:** registered scheduler F2 и portfolio/operations/events + source owners; timer не является источником личного HTTP-read lifecycle. +- **Claim:** process-level flags/pending maps и durable attempt identity + защищают разные случаи. Замена всего на один interval и Promise.all не + эквивалентна существующему безопасному publication contract. +- **Current mechanism:** tick guard → transaction reservation по current + connection/hourly scope → process restart distinction → sequence sources + → own worker completion → conditional publication → durable settlement. +- **Impact / actual cost:** диагностика требует нескольких owners; + **Impact unmeasured**. Удаление лишит restart/late response fencing и + независимого сохранения успеха одного ресурса при сбое другого. +- **Why now:** это самая видимая orchestration chain, но она обслуживает + текущий hourly requirement и не должна становиться жертвой LOC cleanup. +- **Minimum change:** оставить source owners и короткие DB fences; lifecycle + ошибки исправлять в них по audit 13, не введением общей distributed workflow + платформы. Single-process предел уже отмечен `ponytail:` в source. +- **Guarantees at risk:** no overlapping attempts, cancelled old work, + current revision ownership, transactions вне network calls, error isolation. +- **Counterarguments / trade-offs:** текущий deployment single-process, + поэтому новый distributed lease заранее не оправдан. KEEP не означает, + что все catch/poll/settle branches корректны: `scheduler:108–110,187–218` + дают отдельный reliability signal, который не лечится удалением reservation. +- **Verification or rollback:** будущие controlled clock/restart/supersession + и persistence-failure checks на durable boundary. Full suites и scheduler + runtime не запускались; отсутствие ошибки не утверждается. +- **Effort:** S. +- **Related finding IDs:** CPLX-002, CPLX-006, CPLX-007. + +### CPLX-009 — сохранить banking intent/CAS/tombstone/rollback + +- **Decision:** KEEP. +- **Severity:** P3. +- **Confidence:** High. +- **Evidence:** `backend/src/modules/banking/bank-cards/application/bank-operations.service.ts:152–196,214–240,251–330`; + `backend/src/modules/banking/bank-cards/application/bank-card-input.ts:74–81,125–142`; + `backend/src/modules/banking/bank-cards/infrastructure/bank-cards.repository.ts:35–73`; + `backend/src/modules/banking/bank-cards/domain/rub-money.ts:12–38`; + `backend/prisma/schema.prisma:310–333`. +- **Runtime consumer:** F8 create/update/delete HTTP routes + `bank-cards.controller.ts:133–166`. +- **Claim:** fingerprint, mutable version, original-purchase CAS и deletedAt + не являются несколькими способами обозначить одну state; они защищают + collision, concurrent edits, refund serialization и resurrection отдельно. +- **Current mechanism:** normalize exact input → original fingerprint → + transaction → replay existing result before new-operation validation → + ownership/links/date checks → purchase CAS/refund sum → write/version/tombstone. +- **Impact / actual cost:** guards делают write path длиннее; + **Impact unmeasured**. Их удаление допускает второе денежное влияние, + over-refund или восстановление deleted record после lost response. +- **Why now:** эти механизмы легко ошибочно принять за слишком defensive + CRUD, но они требуются `manual-bank-cards/spec.md:527–589` и различаются + в studied test outcomes. +- **Minimum change:** оставить защищающую transaction и exact arithmetic; + не вводить generic repository/IdempotencyService вокруг этой локальной модели. +- **Guarantees at risk:** exact RUB kopecks, replay/collision protection, + lost update, refund ceiling, ownership, atomic rollback и delete persistence. +- **Counterarguments / trade-offs:** unique ID отсекает duplicate row, но + сам по себе не различает изменённый intent и replay после edit/delete. + Mutex только в приложении не заменит durable version/restart semantics. + Retry transaction может повторять callback; держать внешний side effect + вне callback — обязательный предел модели. +- **Verification or rollback:** `backend/test/bank-cards.e2e-spec.ts:391–422` + replay/collision/delete, `:462–489` refund/replay, + `:622–653` race и rollback rejected source lock. Тесты прочитаны, не запущены. + При будущем изменении нужен real isolated SQLite proof, не mock call count. +- **Effort:** S. +- **Related finding IDs:** CPLX-007. + +### CPLX-010 — сохранить identity и runtime границы screener результата + +- **Decision:** KEEP. +- **Severity:** P3. +- **Confidence:** High. +- **Evidence:** `backend/src/modules/investments/instruments/stocks/application/stock-screener.service.ts:49–63,65–139`; + `backend/src/modules/investments/instruments/stocks/presentation/stock.controller.ts:85–109`; + `backend/src/main.ts:31–38`; + `frontend/src/pages/stocks/ui/StocksPage/StocksPage.vm.ts:58–101`. +- **Runtime consumer:** F7 POST creates searchId; GET page reads именно этот + bounded snapshot, а frontend выбирает результат последнего user intent. +- **Claim:** snapshot/ownership/revision/page validation и local late-result + guard представляют текущие сценарии; они не оправдывают лишнюю presentation + VM, но сами не являются overengineering. +- **Current mechanism:** full selected result один раз → bounded Map/TTL → + stable paging по searchId → user/current-revision/bounds checks; raw body + allowlist поверх transformed DTO; requestId guard на поздние frontend results. +- **Impact / actual cost:** bounded memory и дополнительный search identity; + **Impact unmeasured**. «Проще запрашивать каталог каждой страницей» теряет + стабильность, а удаление guards теряет observable input/owner contract. +- **Why now:** этот flow содержит и actionable CPLX-003, и необходимые seams; + требуется не смешивать простую presentation simplification с state deletion. +- **Minimum change:** сохранить snapshot/guards; не добавлять Redis, cursor + service или adapter с единственной реализацией. Узкий `TBANK_ACCESS` + `instruments.module.ts:24` имеет test seam и не требует cleanup отдельно. +- **Guarantees at risk:** непересекающиеся стабильные страницы, successful + empty result, чужой user/revision, expiry, unknown-input rejection, + latest frontend search after out-of-order responses. +- **Counterarguments / trade-offs:** память process-local, restart теряет + search; TTL/limit делают этот предел явным. Default query library нельзя + механически поставить вместо command search model. Raw body guard не + избыточен при порядке global/local pipes — калибровка 3.5. +- **Verification or rollback:** existing + `stock-screener.service.spec.ts:40–96` (stable/foreign/revision/invalid cases), + configured HTTP `backend/test/stocks.e2e-spec.ts:65–105`. + Frontend latest-result proof проверять с completion barrier, не по уже + видимому newer result. Все эти tests в этом аудите не запускались. +- **Effort:** S. +- **Related finding IDs:** CPLX-003. + +## 5. DELETE / SIMPLIFY / KEEP / INVESTIGATE + +| Decision | IDs | Что сокращать или сохранять | Граница решения | +| --- | --- | --- | --- | +| DELETE | — | Самостоятельных подтверждённых DELETE findings в scope 02 нет | Отсутствие consumers у manual sync и frontend observe передано 04, не пересчитано как complexity finding | +| SIMPLIFY | CPLX-001 | Screen timer/ref/descriptor → existing query scheduling | Только текущие mounted investment reads; auth/cache guards оставить | +| SIMPLIFY | CPLX-002 | Date/screen/source round-trip → existing operation interval owner | Не удалять весь planner или менять coverage/history policy | +| SIMPLIFY | CPLX-003 | Translation-only VM → local presentation callbacks | Aria labels, typed references и presentation isolation сохранить | +| KEEP | CPLX-007 | Exact arithmetic и conditional forecast publication | Даже при изменении identity/window | +| KEEP | CPLX-008 | Durable attempt/source-owner coordination | Reliability fixes не равны удалению fence | +| KEEP | CPLX-009 | Banking intent fingerprints/CAS/tombstones/transactions | Replay до validation нового refund обязателен | +| KEEP | CPLX-010 | Stable bounded screener snapshot, input/owner guards и late-response fence | Не полагаться на local forbid после global whitelist | +| INVESTIGATE | CPLX-004 | Forecast window/identity/read selection | UNVERIFIED safe replacement; specs/test expectation conflict | +| INVESTIGATE | CPLX-005 | Legacy observe reference lifecycle | UNVERIFIED external consumer/compatibility obligation | +| INVESTIGATE | CPLX-006 | Request-driven income schedule | UNVERIFIED cold-history behavior после удаления | + +**Ponytail shortlist, от большего ожидаемого сокращения coordination к меньшему:** + +- `native:` убрать собственный screen read scheduler; использовать установленный + Query `refetchInterval` с owner policy. [CPLX-001; + `frontend/src/features/tbank-connection/model/screen-sync.ts:22–61`] +- `shrink:` убрать Ledger round-trip через screen plan; использовать существующий + interval normalizer. [CPLX-002; + `backend/src/modules/investments/operations/application/brokerage-operations.service.ts:120–151`] +- `yagni:` объединить translation-only presentation hook с его view; + сохранить оба accessible callbacks. [CPLX-003; + `frontend/src/pages/stocks/ui/StocksPage/StocksPagePresentation.vm.ts:1–20`] + +**net:** сокращение строк не измерено: implementation diff не создавался; +подтверждённых dependency-кандидатов на удаление — **0**. Это число означает +результат рассматриваемых simplifications, не полный dependency audit. + +## 6. Cost & benefit + +| ID / группа | Ожидаемый класс экономии | Фактическое измерение | Обязательные регрессии / предел | +| --- | --- | --- | --- | +| CPLX-001 | Cognitive load, maintenance, меньше собственного scheduling/state code | not measured; runtime/requests/CI minutes/agent tokens не измерены | Enabled/session, changing key, background policy, cached result, retry только GET; нельзя обещать общий 30-minute throttle одним interval | +| CPLX-002 | Cognitive load, навигация, maintenance диапазонов | not measured; performance не оценивалась | Date bounds/subseconds/all/unknown membership, normalization, coverage и cursor | +| CPLX-003 | Малое сокращение code navigation и maintenance | not measured | Translation/accessibility; отсутствие роста generic UI abstraction | +| CPLX-004–006 | Возможное уменьшение mixed lifecycle/compatibility cost | not measured; выгода условна до verification | Public contract, cold history, saved forecast identity и source-owner boundaries | +| CPLX-007–010 | Сохранение защищающих гарантий; положительная cleanup экономия не заявляется | not measured; изученный source/test intent не равен runtime proof | Financial accuracy, CAS/transaction/replay/owner isolation, stable paging, lifecycle | + +Не заявлены проценты ускорения, число удаляемых LOC, CI minutes или экономия +agent tokens. Снижение навигации — качественное следствие удаления лишнего +перехода, а не измерение productivity. + +### 6.1. Before/After logic outline — три подтверждённых случая + +Это варианты изменения, **не внесённые исправления**. + +#### A. CPLX-001 — periodic investment reads + +**Before** + +```text +page создаёт entity queries +page передаёт scope и resources в useScreenSync +effect обновляет mutable resourcesRef +другой effect держит timer до unmount +tick вручную вызывает refetch каждого resource +App/каждый query отдельно задают focus/reconnect/stale policy +hook вычисляет ещё refresh/retry/status model, который page не читает +``` + +**After** + +```text +page создаёт те же entity queries +owner options задают periodic read interval для этих observers +Query enabled/session/key/cleanup владеют запуском и отменой +view продолжает читать data/meta/query error +retry существующего владельца повторяет только HTTP read +focus/reconnect/stale policy согласована у тех же query owners +``` + +**Потери/добавленная сложность:** у query owners появляются явные interval/ +background options; observer timers имеют иную фазу, чем один page timer. +Если нужно opt-in только для части callers, потребуется минимальный existing +query option input. Не создавать новый polling registry или второй scheduler; +если обязательна атомарная групповая свежесть, одной native option недостаточно, +но текущий hook тоже её не обеспечивает. + +#### B. CPLX-002 — Ledger first-load intervals + +**Before** + +```text +access.local → selected accounts +ledgerRange(query) → from/to (exclusive end) +cursor при наличии задаёт прежний to +Date → ISO; to - 1ms → screen intent +planInvestmentScreen(operations, empty instruments, membershipKnown) + → membership source + normalized screen operations + history sources + → priority sort +filter kind=operations, priority=screen; снова собрать intervals +sync.readPlan(intervals, current options) +saved page/coverage/cursor +``` + +**After** + +```text +access.local → те же selected accounts +тот же ledgerRange и cursor end +при membershipKnown и from < to: + operationIntervals(selected accounts × already computed [from,to)) +sync.readPlan(intervals, те же options) +тот же saved page/coverage/cursor +``` + +**Потери/добавленная сложность:** reader должен явно сохранить membership +guard и unfiltered source-import semantics. Оставить общую date policy и +normalization у прежних helpers; иначе локальное сокращение превратится в +новую конкурирующую interval implementation. Изменение history budget не +включать в этот before/after. + +#### C. CPLX-003 — Pagination labels + +**Before** + +```text +StocksPageView: useTranslation +→ useStocksPagePresentationVm: useTranslation + → два typed translation callbacks +← callbacks object +Pagination получает callback references +``` + +**After** + +```text +StocksPageView: useTranslation +два typed callback references рядом с presentation +Pagination получает те же aria-label values +``` + +**Потери/добавленная сложность:** небольшое исключение uniform VM layout и +локальные closures в view. Если convention запрещает их абсолютно, выигрыш +мал и правило нужно сначала согласовать; не вводить shared utility/data-VM +зависимость ради устранения одного файла. Accessibility не упрощается. + +## 7. Top actions + +Только подтверждённые actionable направления; investigation не выдаётся за +готовое действие удаления. Ранжирование — value/risk/effort, не severity alone. + +### Quick wins + +1. **CPLX-002:** локально убрать Ledger → screen-planner round-trip с сохранением + `operationIntervals/readPlan` и current options. Самая небольшая содержательная + change boundary; обязательна проверка exact interval/cursor semantics. +2. **CPLX-003:** при ближайшем изменении Pagination убрать translation-only hook. + Небольшая стоимость/выгода; не делать отдельный архитектурный рефакторинг. + +### Решения, требующие policy/test подтверждения + +3. **CPLX-001:** заменить page timer existing Query scheduling и согласовать + focus/reconnect/background/key-change policy. Перед implementation нужен + маленький controlled boundary proof; ADR нужен только если меняется + продуктовый cadence contract, а не ради применения query option. + +CPLX-004–006 требуют ответов раздела 9 до назначения cleanup. KEEP-механизмы +не помещены в список «улучшений», чтобы не увеличивать backlog без необходимости. + +## 8. Cross-audit signals + +Каждый пункт — передача темы владельцу, без второго полноценного аудита. + +| Тема | Evidence | Рекомендуемый audit ID | +| --- | --- | --- | +| Нет найденного production consumer `TbankSyncService.request`; controller предоставляет только status, shared planner всё ещё нужен Ledger | `backend/src/modules/investments/tbank-sync/tbank-sync.service.ts:54–311`; `backend/src/modules/investments/tbank-sync/tbank-sync.controller.ts:34–40`; поиск `TbankSyncService` по backend source | 04 — Dead Code & Legacy | +| Frontend `.observe()` consumer обнаружен только в tests; backend observe route при этом жив | `frontend/src/shared/api/resource-query.ts:69–127`; `frontend/src/shared/api/resource-query.test.tsx:63,167`; `backend/src/modules/investments/instruments/bonds/presentation/bond.controller.ts:55–89` | 04 — Dead Code & Legacy | +| Две publicInstrument projections требуют отдельного DRY ownership review | `backend/src/modules/investments/instruments/application/catalog-discovery.service.ts:499–530`; `backend/src/modules/investments/instruments/application/catalog-enrichment.service.ts:646–677` | 03 — Duplication & Reusability | +| Общий catch/settle/poll hourly lifecycle и stable calendar refresh policy | `backend/src/modules/investments/tbank-sync/tbank-hourly.scheduler.ts:108–110,187–218`; `backend/src/modules/investments/instruments/application/catalog-enrichment.service.ts:189–193,679–692` | 13 — Reliability & Security | +| Default income GET/source history ownership и readPlan backfill выходят за complexity cleanup | `backend/src/modules/investments/operations/application/brokerage-operations.service.ts:408–435`; `backend/src/modules/investments/operations/application/brokerage-operations-sync.service.ts:102–124`; `specs/openspec/specs/tbank-data-sync/spec.md:9–22` | 06 — Backend Architecture; 07 — API/Data Flow | +| Overview source metadata не передаётся в loaded view | `frontend/src/pages/account/ui/AccountPage/AccountPage.vm.ts:125–135`; `frontend/src/pages/account/ui/AccountPage/AccountPage.tsx:24–34` | 05 — Frontend Architecture | +| Search detail RPC fan-out и banking list operationView повторные reads | `backend/src/modules/investments/instruments/application/catalog-discovery.service.ts:308–317`; `backend/src/modules/banking/bank-cards/application/bank-operations.service.ts:41–68,361–364` | 07 — API/Data Flow Efficiency | +| Forecast old-range expectation конфликтует с новой saved-window spec; документация читает разные поколения owner model | `backend/test/upcoming-events.e2e-spec.ts:321–331`; `specs/openspec/specs/brokerage-account-upcoming-events/spec.md:18–20`; `docs/architecture/containers.md:142–148`; `docs/adr/019-hourly-reads-and-online-instrument-reference.md:9–16` | 08 — Test Quality; 15 — Specs & Documentation | + +Ни один signal не включён в actionable count 02 как отдельное исправление +correctness/security/performance/dead code/tests/tooling. + +## 9. Open questions / verification plan + +| Вопрос, действительно меняющий решение | Самый дешёвый способ проверки | Влияние на finding | +| --- | --- | --- | +| Интервал 30 минут относится только к periodic tick или также mount/focus/reconnect; нужна ли background вкладка? | Согласовать смысл `docs/adr/019-hourly-reads-and-online-instrument-reference.md:27–29`, затем controlled Query/MSW fake-time case с существующими policies | CPLX-001: native замена подтверждена, точная cadence policy ещё требует решения | +| Ledger нормализация должна быть upstream-unfiltered при UI instrument filter? | Сравнить current planner `force=false` и existing normalizer outputs на one/all, default dates, subsecond cursor и unknown membership | CPLX-002: ограничивает безопасный минимальный diff | +| Допустимы ли два trivial presentation callbacks во view при текущей convention? | Узкое semantic решение по `frontend/.ai/conventions/ui.md:58–89`; проверить typed callback refs и aria labels | CPLX-003: может оставить P3 cleanup отложенным без новой abstraction | +| Что является identity последнего полного event forecast; какой новый полный результат заменяет старый window? | Согласовать current specs/test conflict; two-window publication + narrow/disjoint read + dependency race в existing SQLite suite | CPLX-004: до ответа safe simplification UNVERIFIED | +| Есть ли обязательные внешние consumers saved `sync=observe` reference endpoints? | Проверить предоставленный public contract и сведения владельца о consumers; targeted HTTP default/observe case | CPLX-005: отсутствие SPA consumer не разрешает breaking backend delete | +| Кто обеспечивает cold historical income после отказа от request-driven membership/schedule? | Один cold income GET и один после Ledger interval load на isolated fixture; уточнить UX owner истории | CPLX-006: отсутствие полного результата нельзя заменять нулём | + +**Минимальные будущие проверки, не запущенные в этом аудите:** существующие +frontend MSW/RTL boundary cases для CPLX-001/003 и isolated SQLite scenarios +для CPLX-002/004/006; published contract/consumers для CPLX-005. Не требуется +новая audit test infrastructure. Статический audit не доказывает отсутствие +временных race или актуальность реальных данных. + +### 9.1. Проверка выполнения самого аудита + +- Полный протокол прочитан; сохранены 9 общих разделов. +- В inventory: 8 live flows с entrypoints/owners/callers, complexity justification + table, seams/guards/fallback/states/compatibility assessment. +- Калибровка: два «сложно, но нужно» и один «просто, но рискованно». +- Findings CPLX-001–010 имеют все обязательные поля; actionable/KEEP/INVESTIGATE + и unmeasured/UNVERIFIED разделены. +- Before/After: три подтверждённых случая и явная added-complexity цена каждой + рекомендации. Финальная consolidation table содержит только эти 10 IDs. +- Boy Scout semantic review отчёта: проверены scope 02, конкретные владельцы, + реальные consumers, контраргументы, preservation точности/transactions/CAS/ + replay/auth/cache/lifecycle; source cleanup не выполнялся. +- После записи выполнены `git rev-parse HEAD`, + `git status --porcelain=v1 --untracked-files=all`, `git diff --check` и + `git diff --cached --check` из каждого из пяти checkout: HEAD совпадает + с таблицей metadata, status/diff checks без вывода. Все source checkout + остались clean; staged/unstaged/new source изменений не появилось. +- `git check-ignore -v audit-results/02-complexity-and-overengineering-report.md` + подтвердил root ignore `.gitignore:2`. Сам новый файл проверен отдельно: + `git diff --no-index --check /dev/null audit-results/02-complexity-and-overengineering-report.md` + без whitespace errors. +- Read/Grep нового отчёта подтвердили разделы 1–9, special results и финальную + таблицу. `rg --count` по heading pattern вернул 10 findings; по field-label + pattern — 150 полей (14 обязательных + Runtime consumer у каждого из 10). + Таблица вручную сверена с полями; aggregation учитывает только 10 строк + consolidation, не дополнительные сводные таблицы выше. + +**Ограничения:** static risk-based выборка, без existing graph, без runtime +tests/build/full-suite, без реальной сети и persisted production data inspection. +Generated schema/lockfiles/tools не менялись; архивы/references/dated audits +не изменялись. Package manifests и installed dependency source изучались, +но dependency/security/performance audit не выполнялся. Некоторые initial +path guesses отсутствовали; последующие evidence взяты только из реально +прочитанных paths, не из предположений о layout. + +## Machine-readable consolidation table + +| ID | Decision | Severity | Confidence | Repo | Files | One-line impact | Effort | Related IDs | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | +| CPLX-001 | SIMPLIFY | P2 | High | frontend | src/features/tbank-connection/model/screen-sync.ts; src/entities/portfolio/api/usePortfolio.ts; src/entities/bond/api/queries.ts; src/app/ui/App.tsx | Page timer/ref добавляет второй read lifecycle; эффект не измерен | M | CPLX-005 | +| CPLX-002 | SIMPLIFY | P2 | High | backend | src/modules/investments/operations/application/brokerage-operations.service.ts; src/modules/investments/investment-sync-plan.ts; src/modules/investments/operations/domain/operations-sync-plan.ts | Ledger разворачивает выбранный interval через общий screen plan; эффект не измерен | S | CPLX-006,CPLX-008 | +| CPLX-003 | SIMPLIFY | P3 | High | frontend | src/pages/stocks/ui/StocksPage/StocksPagePresentation.vm.ts; src/pages/stocks/ui/StocksPage/StocksPage.view.tsx | Два translation callbacks требуют отдельного файла; эффект не измерен | S | CPLX-010 | +| CPLX-004 | INVESTIGATE | P2 | Medium | backend | src/modules/investments/portfolio/infrastructure/forecast.repository.ts; src/modules/investments/portfolio/application/forecast.service.ts; src/modules/investments/upcoming-events/upcoming-events.service.ts | Window identity смешана с fallback/publication; безопасная замена UNVERIFIED | M | CPLX-007,CPLX-008 | +| CPLX-005 | INVESTIGATE | P2 | Medium | backend,frontend | backend/src/modules/investments/instruments/application/catalog-discovery.service.ts; backend/src/modules/investments/instruments/application/catalog-enrichment.service.ts; frontend/src/shared/api/resource-query.ts | Online/observe сохраняют два lifecycle; обязательство compatibility UNVERIFIED | M | CPLX-001,CPLX-007 | +| CPLX-006 | INVESTIGATE | P2 | Medium | backend | src/modules/investments/operations/application/brokerage-operations.service.ts; src/modules/investments/income/income-analytics.service.ts; src/modules/investments/income/income-analytics.controller.ts | Income read также планирует импорт; безопасное удаление cold-history пути UNVERIFIED | M | CPLX-002,CPLX-008 | +| CPLX-007 | KEEP | P3 | High | backend | src/modules/investments/portfolio/infrastructure/forecast.repository.ts; src/common/decimal.ts; src/modules/investments/upcoming-events/upcoming-events.service.ts | Точная арифметика и conditional publication защищают согласованный monetary snapshot | S | CPLX-004,CPLX-005,CPLX-009 | +| CPLX-008 | KEEP | P3 | High | backend | src/modules/investments/tbank-sync/tbank-hourly.scheduler.ts; src/modules/investments/tbank-sync/tbank-sync.repository.ts; src/modules/investments/portfolio/infrastructure/portfolio.repository.ts | Durable attempts/source fences защищают restart и позднюю публикацию | S | CPLX-002,CPLX-006,CPLX-007 | +| CPLX-009 | KEEP | P3 | High | backend | src/modules/banking/bank-cards/application/bank-operations.service.ts; src/modules/banking/bank-cards/infrastructure/bank-cards.repository.ts; src/modules/banking/bank-cards/domain/rub-money.ts | Intent/CAS/tombstones/transactions защищают точные replay/refund semantics | S | CPLX-007 | +| CPLX-010 | KEEP | P3 | High | backend,frontend | backend/src/modules/investments/instruments/stocks/application/stock-screener.service.ts; backend/src/modules/investments/instruments/stocks/presentation/stock.controller.ts; frontend/src/pages/stocks/ui/StocksPage/StocksPage.vm.ts | Snapshot и owner/input/late-result guards имеют текущие сценарии и consumers | S | CPLX-003 | diff --git a/audits/2026-10-11/reports/03-duplication-and-reusability-report.md b/audits/2026-10-11/reports/03-duplication-and-reusability-report.md new file mode 100644 index 0000000..01eab43 --- /dev/null +++ b/audits/2026-10-11/reports/03-duplication-and-reusability-report.md @@ -0,0 +1,890 @@ +# Аудит 03 — Duplication & Reusability + +## 1. Audit metadata + +- **Audit ID/name:** 03 / Duplication & Reusability. +- **Дата:** 2026-10-11. +- **Workspace:** `/Users/ksv741/Project/vibe/WealthLedger`. +- **Режим:** независимое read-only исследование; единственный записанный + артефакт этого исполнителя — настоящий отчёт вне четырёх checkout. +- **Протокол:** `audit-prompts/03-duplication-and-reusability.md`, прочитан + целиком. Другие новые отчёты не читались и не перезаписывались. +- **Skill:** установленный `ponytail-audit` загружен через Skill tool до + исследования. Его complexity-only подход применён к предложениям + `SIMPLIFY`; подтверждённое рассогласование validation policy разобрано + отдельно по контракту аудита, не приписано correctness-проверке skill. + Дополнительно использованы `feature-sliced-design` для ownership, + `systematic-debugging` для трассировки причины DRY-001 и + `verification-before-completion` для проверки результата исследования. +- **Graphify:** skill загружен. По известному абсолютному пути root + `graphify-out/graph.json` отсутствует; поиски этого artifact внутри четырёх + checkout также не дали результатов. Glob может пропускать ignored paths, + поэтому отсутствие любого скрытого графа не доказано. Доступного existing + graph для query не обнаружено; анализ продолжен статически. Генерация, + установка, индексация и создание graphify artifacts не выполнялись. + +### Репозитории и самостоятельно снятое состояние + +Все пути в таблице абсолютные. В остальных evidence путь начинается с имени +checkout и считается относительно workspace. + +| Repo | Path | HEAD SHA | Начальный `git status --short` | После исследования и сохранения отчёта | +| --- | --- | --- | --- | --- | +| root | `/Users/ksv741/Project/vibe/WealthLedger` | `dc030f84c979cedb2628ef4bd97c62d6c63a9256` | пустой | пустой; отчёт ignored | +| frontend | `/Users/ksv741/Project/vibe/WealthLedger/frontend` | `c8821200ce699dab1ce65360d1968d5877a2e78b` | пустой | пустой | +| backend | `/Users/ksv741/Project/vibe/WealthLedger/backend` | `0ad84ff4f0b4712abc7e69dc299856f14af6eadd` | пустой | пустой | +| specs | `/Users/ksv741/Project/vibe/WealthLedger/specs` | `fcdc805f6c1b3be6a20a7e858e3e80cbd83a9121` | пустой | пустой | +| docs | `/Users/ksv741/Project/vibe/WealthLedger/docs` | `e6fd4f64b81dd73b33959b845a1babcfac1891c8` | пустой | пустой | + +`AGENTS.md` каждого checkout прочитан до исследования его содержимого. +Прочитаны локальные architecture/engineering и активированные темы: +frontend data, TypeScript/React, UI, testing, Storybook; backend HTTP API, +data, errors, security/auth, integrations, testing. Specs выбирались через +`AI_INDEX.md`; docs — через README/CONTRIBUTING и тематические страницы. +Root conventions не применялись к соседним checkout. Локальный FSD profile +и его реальные import restrictions важнее общего совета о размещении CRUD. + +### Выполненные проверки и границы исполнения + +1. В каждом repo самостоятельно выполнены `git rev-parse HEAD` и + `git status --short`: в начале, после исследования и перед завершением. +2. Использованы Read/Glob/Grep по конкретным repo paths для поиска символов, + callers, требований и существующих тестовых сценариев. +3. Выполнен read-only + `git diff --no-index --stat backend/openapi.json frontend/src/shared/api/spec/openapi.json` + из root: различий нет. Это сравнение existing файлов, а не доказательство + свежести генерации относительно runtime. +4. Проверены `git diff --check` и отдельно whitespace нового ignored отчёта + через `git diff --no-index --check /dev/null audit-results/03-duplication-and-reusability-report.md`. + Проверен `git check-ignore -v` для отчёта; root allowlist сохранён. +5. Выполнена содержательная сверка отчёта с девятью общими разделами, + специальными результатами, всеми finding fields и финальной таблицей. + Read-only `rg --count-matches` подтвердил 9 общих заголовков, 10 DRY sections, + 150 полей (15 на finding, включая Runtime consumer) и 3 Ownership model + для 3 SIMPLIFY. Агрегатный подсчёт дополнен чтением каждого блока. + +Тесты не запускались. Node/npm-команды, install, codegen, migrations, build, +full-suite/verify, commit/PR и обращения к внешним API не выполнялись. +Исходники, конфигурации, snapshots, dated audits, архивы и references не +изменялись. Файлы БД, `.env`, секреты и реальные пользовательские данные не +читались. Два узких installed-library файла прочитаны только для проверки +семантики `MinLength`, не как кандидаты на ручной DRY cleanup. + +`audit-results/technical-audit-2026-10-11.md` использован как набор гипотез: +F12 и F15 независимо подтверждены исходниками и callers. Другие прежние +рекомендации не принимались автоматически. Числа о LOC, fan-out и времени из +него не перенесены как собственные измерения. + +При сводной перепроверке независимо повторён source trace discovery и всех +production callers `useScreenSync`. Исправлена прежняя ошибка reachability: +catalog HTTP paths не запускают discovery producer, а возвращаемый screen +retry не имеет найденных production consumers. DRY-009 сужен до живого +enrichment и используемых repository fences. Соседние отчёты не читались; +reachability/deletion discovery переданы audit 04, без требования KEEP этого +producer. Исходные SHA при перепроверке совпадают с таблицей выше. + +## 2. Executive verdict + +1. **Факт — DRY-001:** одна password policy имеет разные единицы в frontend + и backend. UI допускает не-ASCII пароль, который отклоняет API. Это + значимое локальное рассогласование, а не основание удалить серверную + валидацию или создать межрепозиторную библиотеку validators. +2. **Факт — DRY-002:** одинаковая whitelist-проекция instrument поддерживается + в двух сервисах одного backend module. У правила один владелец, одинаковые + типы, ошибки и причины изменения. Локальный mapper даст самый убедительный + reuse при сохранении самого whitelist. +3. **Риски сопровождения — DRY-003/004:** frontend повторяет список платёжных + систем внутри одной формы и один набор access-loss codes в трёх контекстах. + Сегодня копии согласованы; текущих пользовательских дефектов не доказано. + Это небольшие P3 cleanup-кандидаты, не обязательная большая миграция. +4. **Факт — DRY-005–009:** денежные trust boundaries, stock/bond семантика, + session-aware query wrappers, generated API copies и живой enrichment с + repository publication fence имеют конкретные гарантии. DRY-009 не требует + сохранять discovery `request/refresh/drain`: current HTTP consumers их + обходят, найденный caller `refresh` — только недоступный manual dispatch. +5. **Непроверенная гипотеза — DRY-010:** часть screen-level periodic-read + orchestration может перейти к query owners. Наличие нескольких triggers + подтверждено, но равноценность замены и runtime-экономия — `UNVERIFIED`. + Все восемь production callers игнорируют hook return; общей кнопки retry + из этого hook не найдено. Реальный counterargument — mount-based timer phase + с чтением текущего resource array. Разные `staleTime` не доказывают DRY-долг. + +**Итог по реестру:** 4 actionable findings: P0 — 0, P1 — 0, P2 — 1, +P3 — 3; отдельно KEEP — 5, INVESTIGATE — 1. Actionable здесь означает +`FIX`, `SIMPLIFY` или `DELETE`; DELETE-кандидатов в scope 03 не подтверждено. +Подсчёт — по уникальным DRY-ID в разделе 4, без повторного подсчёта строк +Knowledge Register и Cross-audit signals. P2 у KEEP означает значимость +защищаемого поведения, а не urgency cleanup. + +## 3. Inventory & coverage + +### Фактически прослеженные области + +| Область | Прослеженный flow / объекты | Граница вывода | +| --- | --- | --- | +| Auth validation | `AuthForm` → Zod schemas → auth provider/client → `CredentialsDto` через configured pipe → `AuthController` → `AuthService`; spec и docs | Установлены разные единицы длины; HTTP-воспроизведение не запускалось | +| Catalog projection | Stock/Bond controllers → catalog services → online discovery либо read-only observe; quote/company paths → enrichment section/load | Две одинаковые detail-проекции; catalog HTTP paths не запускают discovery producer | +| Banking forms | `form.schema.ts`, fields/form VM, generated bank-card types, backend DTO/input/RUB helpers, operation validation/transaction | Разделены локальная копия словаря и необходимое повторение trust-boundary checks | +| Frontend errors | client permanent classification, resource access-group invalidation, Account/Accounts VM и error views | Совпадает один набор codes; recovery actions и Accounts subset различаются | +| Stock/Bond UI | Page containers/VM/views, payout components, `instrument-details` model/UI, entity queries | Подтвержден existing reuse и реальная разница dividend/coupon и денежных единиц | +| Query identity/lifecycle | `apiQueryClient`, authenticated/resource queries, account/portfolio/stock/bond/bank hooks, connection boundary, screen timer | Не обнаружен competing handwritten key для изученных investment queries; банковский `all` — отдельный агрегат | +| Backend state transitions | Discovery entry-point trace отдельно; живой enrichment reservation/drain → repository publishSource/finish | KEEP касается enrichment/shared fence, не discovery producer; reachability/deletion — audit 04 | +| Contracts/config | manifests, `main.ts`, `openapi.ts`, frontend dependency-cruiser, API snapshot и точечные generated declarations | Generated files не исследовались как ручные дубли; freshness генерации не доказана | +| Calendar/money owners | existing investment calendar, instrument-market, shared display, common decimal, T-Bank protobuf decimal | Не предложена общая библиотека на основании похожего имени или арифметики | + +Выборка risk-/cost-based: приоритет получили публичные проекции, валидируемые +политики, money/null semantics, recovery и повторяющиеся transitions. +Тесты читались как свидетельства задуманного поведения и кандидаты для +будущей проверки, не проводился отдельный аудит их качества или factories. + +**Метод чисел:** количество копий получено symbol/content search с ручным +сопоставлением конкретных реализаций и consumers. Для `publicInstrument` +найдены две definitions и четыре call sites: discovery `:157,323`, enrichment +`:263,349`. Для access-loss набора — три полных места; transport содержит +ещё `UNAUTHORIZED`, а Accounts VM использует только два connection codes. +Для payment systems — две независимо редактируемые frontend runtime lists; +backend tuple и производные artifacts перечислены отдельно. Глобальные +проценты дублей, прочитанных файлов, LOC и coverage не вычислялись. + +Восемь production call sites `useScreenSync` получены поиском символа в +`frontend/src` с исключением declarations/re-exports/tests и чтением каждого +полного VM. Во всех случаях вызов — standalone expression, без присваивания, +destructuring или передачи return дальше; таблица ниже фиксирует полный набор. + +### Уточнённый consumer trace: discovery и живой enrichment + +| Вход | Подтверждённая цепочка / evidence | Достигает discovery `request/drain`? | +| --- | --- | --- | +| Catalog HTTP endpoints | `backend/src/modules/investments/instruments/stocks/presentation/stock.controller.ts:58–82,112–119`; `backend/src/modules/investments/instruments/bonds/presentation/bond.controller.ts:55–89` передают `query.sync === 'observe'`; `backend/src/modules/investments/instruments/stocks/application/stock-catalog.service.ts:22–89` и `backend/src/modules/investments/instruments/bonds/application/bond-catalog.service.ts:25–88` вызывают только `search/detail/byTicker` | Нет при обоих значениях observe | +| Default `search` | `backend/src/modules/investments/instruments/application/catalog-discovery.service.ts:84–86,295–337`: ранний return `onlineSearch`, RPC напрямую через client | Нет | +| Default `detail` / `byTicker` | Тот же файл `:149–166,227–249`: online branches; `byTicker(false)` вызывает `detail` с default false | Нет | +| Observe branches | Тот же файл `:86–93,168–190,251–267`: `read(..., true)` и `enrichment.readMany(..., true)`; `read` запускает request только при `!observe` (`:66–70`) | Нет | +| Discovery producer entry | Тот же файл `:340–386,395–396`: `request` ставит drain, `refresh` вызывает request. Единственный найденный внешний production-text caller refresh — `backend/src/modules/investments/tbank-sync/tbank-sync.service.ts:192–201` внутри `dispatch` | Только dead-parent-reachable по найденному src trace, не текущий HTTP flow | +| Parent entry point | `backend/src/modules/investments/tbank-sync/tbank-sync.service.ts:54–78`: dispatch запускается только из request; `backend/src/modules/investments/tbank-sync/tbank-sync.controller.ts:31–41` вызывает только status; `backend/src/modules/investments/tbank-sync/tbank-sync.module.ts:23–25` регистрирует service, экспортирует scheduler, но не запускает request | Production consumer request не найден; DI registration не является вызовом | +| Реальный enrichment producer | `backend/src/modules/investments/instruments/stocks/presentation/stock.controller.ts:122–129` → `backend/src/modules/investments/instruments/stocks/application/stock-market.service.ts:34–45` → `backend/src/modules/investments/instruments/application/catalog-enrichment.service.ts:234–241,269–289,113–125,189–225,497–577` | Это другой, живой producer: `quote(false)` → `section` → `readMany(false)` → при missing/stale `requestMany` → drain → publishSource | + +В таблице «тот же файл» означает указанный выше полный path discovery service. +Поиск ссылок `CatalogDiscoveryService`/`discovery.refresh` и +`TbankSyncService` в `backend/src` не обнаружил другого producer entry point. +Прямые вызовы `read(false)`/`request` существуют в +`backend/test/tbank-catalog-discovery.e2e-spec.ts:86,184–187,202–203`; они +подтверждают test reachability, но не production reachability. Удаление +неживой producer-ветки и точные границы compatibility — предмет audit 04. + +### Production callers `useScreenSync`: return не используется + +| Call site | Фактический consumer | +| --- | --- | +| `frontend/src/pages/account/ui/AccountPage/AccountPage.vm.ts:78–84` | Standalone вызов для пяти resources; return отброшен | +| `frontend/src/pages/accounts/ui/AccountsPage/AccountsPage.vm.ts:23` | Standalone вызов для accounts; return отброшен | +| `frontend/src/pages/account-events/ui/AccountEventsPage/AccountEventsPage.vm.ts:67–75` | Standalone вызов для events; return отброшен | +| `frontend/src/pages/account-income/ui/AccountIncomePage/AccountIncomePage.vm.ts:66–69` | Standalone вызов для analytics; return отброшен | +| `frontend/src/pages/account-operations/ui/AccountOperationsPage/AccountOperationsPage.vm.ts:96–99` | Standalone вызов для operations; return отброшен | +| `frontend/src/pages/stock/ui/StockPage/StockPage.vm.ts:45–55` | Standalone вызов для stock sections; return отброшен | +| `frontend/src/pages/bond/ui/BondPage/BondPage.vm.ts:45–55` | Standalone вызов для bond sections; return отброшен | +| `frontend/src/pages/bonds/ui/BondsPage/BondsPage.vm.ts:13` | Standalone вызов для search result; return отброшен | + +Только test harness сохраняет return и вызывает retry/refresh: +`frontend/src/features/tbank-connection/model/screen-sync.test.tsx:37–50,68–78`. +Реальный production side effect hook — timer +`frontend/src/features/tbank-connection/model/screen-sync.ts:22–35`, не его +return `:53–61`. Timer effect имеет `[]`: его clock начинается при mount, +не перезапускается из-за resource array change; callback читает актуальный ref. + +**Вне охвата:** полный clone detector по всему tree, generated/vendor/proto +duplication, зависимости как предмет отдельного аудита, dead code (04), +крупные module boundaries (01), эффективность API (07), качество tests (08), +test infrastructure/factories (09), docs/SDD duplication (15). Root release/CI +и похожие configs самостоятельных repos не объявлены DRY-долгом: они не +должны становиться общей обязательной tooling-зависимостью. + +### Knowledge Duplication Register + +| Rule/behavior | Copies | Divergence risk | Single owner? | Extraction trade-off | Decision | +| --- | --- | --- | --- | --- | --- | +| Password 12–72 UTF-8 bytes | FE `auth.schema.ts:10–11,36–43`; BE `auth.dto.ts:16–17,43–74`; snapshot `:5246–5250` | Нижняя граница уже разошлась по единицам | Да: auth policy; server владеет enforcement, spec фиксирует требование | Согласовать две независимые проверки; TS generator не реализует byte validation | FIX / DRY-001 | +| Detail instrument whitelist | `catalog-discovery.service.ts:499–531`; `catalog-enrichment.service.ts:646–678` | Новый field/filter требует двух правок | Да: instruments boundary | Один локальный mapper; не объединять online и durable execution | SIMPLIFY / DRY-002 | +| Runtime payment systems формы | FE `form.schema.ts:65–69`; `BankCardFields.vm.ts:19–22`; BE tuple `bank-card.dto.ts:45` | FE schema и selectable options могут расходиться | Да внутри FE bank-card; BE отдельно владеет wire vocabulary | Tuple у существующей schema; generated union проверяет допустимость, но не даёт runtime list | SIMPLIFY / DRY-003 | +| Четыре access-loss codes | FE `client.ts:162–169`; `resource-query.ts:42–51`; `AccountPage.vm.ts:59–64` | Добавление code может изменить retry, purge и page state по-разному | Да: API code taxonomy; actions имеют разных owners | Один predicate, сохранить отдельные действия и дополнительный auth code | SIMPLIFY / DRY-004 | +| Independent banking validation | FE `form.schema.ts:8–42`, `OperationForm.schema.ts:53–131`; BE `rub-money.ts:12–30`, `bank-operations.service.ts:94–210` | Значения policy требуют согласования; implementations специально различаются | Server — authority, browser — UX snapshot | Shared runtime validator не заменит ownership/CAS/transaction и ухудшит repo autonomy | KEEP / DRY-005 | +| Dividend/coupon forecast и UI | BE stock `stock-income.service.ts:114–178`, bond `bond-income.service.ts:145–201`; FE payout views | Опасность скрыть payment/record/date и per-share/per-bond differences | Нет для всего процесса; общие primitives уже имеют owners | Generic payout с mode/flags объединит разные причины изменения | KEEP / DRY-006 | +| Stock/Bond query declarations | FE `stock/api/queries.ts:21–95`, `bond/api/queries.ts:21–104` | Малое локальное повторение, разные endpoints/accessGroup/contracts | Два entity owners; transport общий | Оставить endpoint-specific generated options, не строить string-templated query factory | KEEP / DRY-006 | +| Query wrapper / Mantine primitives | `authenticated-query.ts:34–59`, `resource-query.ts:37–67,84–149`; UI packages | Удаление defaults потеряет session/access/resource/ARIA поведение | Да для каждой существующей focused boundary | Нативные controls плюс project defaults уже используются; passthrough переименование не предлагается | KEEP / DRY-007 | +| OpenAPI → snapshot → TS aliases | BE DTO/Swagger, FE snapshot/generated, `bank-card/api/types.ts`, `auth-context.ts` | Freshness artifact остаётся отдельным вопросом | Backend editable source; FE локальный consumer artifact | Сохранить offline generation, не создавать shared DTO package/runtime validator | KEEP / DRY-008 | +| Known UI errors vs open wire string | FE `problem-details.ts:1–28`; generated `schema.d.ts:731–747` | Unknown code должен сохранять fallback | Server code owners и FE known subset имеют разные задачи | Не превращать UI enum в закрытый весь wire contract | KEEP / DRY-008 | +| Живой enrichment vs похожий discovery producer | BE enrichment `:113–125,189–225,497–593`; discovery `:66–70,84–86,149–166,227–267,340–495` | Generic extraction из неживой ветки добавит framework; изменение enrichment может потерять batching/fences | Enrichment владеет текущим producer; общая repository fence уже есть | Сохранить только используемые enrichment guarantees; discovery reachability/deletion — audit 04, не KEEP | KEEP / DRY-009 (узкий scope) | +| Periodic reads и freshness triggers | FE `screen-sync.ts:22–35`; восемь standalone page calls; `App.tsx:13–20`; entity stale times | Триггеры распределены; return/retry не используются, runtime waste не измерен | Scheduling owner требует решения; freshness не равно interval | Query-owned interval требует проверки mount phase/latest-resource semantics, не гипотетической общей кнопки | INVESTIGATE / DRY-010 | + +В таблице короткие имена относятся к точным полным paths из findings ниже. +Скопированные generated значения не посчитаны независимо editable знаниями. + +### Intentional Duplication to Keep + +| Место / evidence | Runtime consumer | Почему повторение оправдано | Что нельзя потерять | +| --- | --- | --- | --- | +| `frontend/src/entities/bank-card/model/money.ts:4–23` и `backend/src/modules/banking/bank-cards/domain/rub-money.ts:12–30` | Form draft → normalized request → server application input | UI допускает пробелы/запятую/leading zeros; server принимает канонический RUB decimal и проверяет storage range | bigint precision, positive checks, ошибки вместо округления | +| `frontend/src/pages/card/ui/OperationForm/OperationForm.schema.ts:53–131` и `backend/src/modules/banking/bank-cards/application/bank-operations.service.ts:94–210` | Draft validation и create/update operation | UI проверяет cached список; server повторно читает authoritative user-owned records в transaction | Cross-user isolation, refund CAS/limit, rollback, replay | +| `frontend/src/entities/stock/api/queries.ts:21–95`; `frontend/src/entities/bond/api/queries.ts:21–104`; `backend/src/modules/investments/instruments/stocks/application/stock-income.service.ts:114–178`; `backend/src/modules/investments/instruments/bonds/application/bond-income.service.ts:145–201` | Stock/Bond route и HTTP resource | Сходная композиция, разные payloads, null/date rules, quote units; общие chart/position/layout уже переиспользуются | Не смешать проценты номинала и Money, registry и payment date | +| `backend/src/modules/investments/instruments/application/catalog-enrichment.service.ts:113–125,189–225,497–577`; `backend/src/modules/investments/instruments/infrastructure/catalog-discovery.repository.ts:154–179` | Живой quote(false) → section → readMany(false) → enrichment producer → publishSource | Local enrichment использует batching/observers, repository уже разделяет authorization/publication checks; generic queue из сходства с неживым discovery не нужна | Authorized producer, private work handles, conditional publication только используемого enrichment; discovery producer не входит в KEEP | +| `backend/src/common/money.types.ts:1`; `backend/src/common/money.dto.ts:3–10`; `frontend/src/shared/api/generated/schema.d.ts:763–795` | Swagger/ValidationPipe и typed API client | Runtime metadata и независимые репозитории требуют разных форм представления одного контракта | Offline frontend input, open unknown errors, отсутствие Swagger в pure domain | + +Эта выборка показывает несколько разных оснований KEEP, а не исключения, +добавленные после обязательного требования «всё объединить». + +## 4. Findings + +### DRY-001 — Password policy расходится по единицам, а не по месту проверки + +- **Decision:** FIX. +- **Severity:** P2. +- **Confidence:** High. +- **Evidence:** `frontend/src/features/auth/model/auth.schema.ts:10–11,36–43`; + `backend/src/modules/auth/presentation/auth.dto.ts:16–17,43–74`; + `backend/src/main.ts:31–39`; + `specs/openspec/specs/user-authentication/spec.md:9–10,109–110`. + Installed semantics независимо проверены в + `backend/node_modules/class-validator/cjs/decorator/string/MinLength.js:14–29` + и `backend/node_modules/validator/lib/isLength.js:23–26`. +- **Claim:** две проверки одного принятого правила имеют несовместимый + минимум: frontend — bytes, backend — character length. +- **Current mechanism:** browser считает обе границы TextEncoder-ом; + `CredentialsDto` использует `MinLength(12)` плюс только upper-byte validator. + Например, `абвгде` — шесть кириллических символов / двенадцать UTF-8 байтов: + проходит FE правило 12–72, не проходит BE минимум 12 characters. Это вывод + по коду и encoding, не результат выполненного HTTP-теста. +- **Runtime consumer:** `frontend/src/features/auth/ui/AuthForm/AuthForm.tsx:24–40` + выбирает обе schemas; provider отправляет password без изменения + (`frontend/src/features/auth/model/auth.vm.ts:14–24,121–132`). Register/login + принимают тот же DTO (`backend/src/modules/auth/presentation/auth.controller.ts:55–79`), + далее password поступает в bcrypt (`backend/src/modules/auth/application/auth.service.ts:35–78`). +- **Impact / actual cost:** UI принимает draft, API отказывает; AuthForm + показывает общий requestError вместо успешного auth. Impact unmeasured: + частота таких паролей и пользовательских отказов неизвестна. +- **Why now:** уже доказан drift единицы policy; изменение локально у auth + owner и не требует нового validator framework. +- **Minimum change:** обе byte-границы проверять существующим + `PasswordBytesConstraint`, убрать несовместимый character minimum. + Согласовать Swagger описание bytes: snapshot сейчас содержит character + `minLength: 12` (`frontend/src/shared/api/spec/openapi.json:5246–5250`), что + не описывает byte-policy. При реализации явно исправить metadata и передать + обновлённый artifact потребителю, не править generated TS вручную. +- **Guarantees at risk:** server-side validation, bcrypt 72-byte ceiling, + отсутствие trim/normalization password, совместимость login и error pointers. +- **Counterarguments / trade-offs:** character minimum может быть отдельным + security/product решением, но такого изменения не подтверждают текущая spec + и docs (`docs/domains/authentication/overview.md:14–21`). Independent + validation нужна; общего исполняемого пакета между repos не нужно. +- **Verification or rollback:** будущая минимальная проверка — frontend schema + и configured HTTP DTO boundary на 11/12/72/73 bytes, ASCII/кириллице/emoji, + password whitespace без изменения. Existing DTO checks находятся в + `backend/src/common/dto-validation.spec.ts:13–30`; HTTP flow должен сохранить + 400/VALIDATION_ERROR и success auth. Не менять stored hashes; rollback — + возврат DTO/metadata/artifact совместно. В этом аудите проверки не запускались. +- **Effort:** M. +- **Related finding IDs:** DRY-005, DRY-008. + +### DRY-002 — Одна instrument detail projection имеет две идентичные копии + +- **Decision:** SIMPLIFY. +- **Severity:** P3. +- **Confidence:** High. +- **Evidence:** `backend/src/modules/investments/instruments/application/catalog-discovery.service.ts:499–531`; + `backend/src/modules/investments/instruments/application/catalog-enrichment.service.ts:646–678`. +- **Claim:** `publicInstrument` копирует один и тот же allowlist для одного + `TbankBond | TbankShare` input, включая одинаковый brand.logoName whitelist. +- **Current mechanism:** две чистые функции без side effects или различающейся + error/null политики. Обе сохраняют optional/undefined fields; отсутствие + instrument обрабатывают callers, а не функция. +- **Runtime consumer:** online detail/search — + `backend/src/modules/investments/instruments/application/catalog-discovery.service.ts:143–165,295–325`; + company online и durable detail/bond-current load — + `backend/src/modules/investments/instruments/application/catalog-enrichment.service.ts:241–267,339–350`. + Ответы преобразуют + `backend/src/modules/investments/instruments/stocks/application/stock-catalog.service.ts:59–89` + и `backend/src/modules/investments/instruments/bonds/application/bond-catalog.service.ts:58–88`; + сохранение проходит через + `backend/src/modules/investments/instruments/infrastructure/catalog-discovery.repository.ts:154–223,252–276`. +- **Impact / actual cost:** изменение разрешённого field требует двух правок; + drift может сделать online и saved representation разными. Текущего drift + этих функций нет. Impact unmeasured; экономия maintenance/cognitive load. +- **Why now:** существует реальный reuse внутри одного module и нет + необходимости менять execution model ради него. +- **Minimum change:** оставить одну чистую функцию у instruments boundary, + например `instruments/infrastructure/public-instrument.mapper.ts`, и два + локальных imports. Не создавать Nest provider, generic mapper или общий + cross-repo package. +- **Ownership model:** этот module-owned mapper вызывают discovery и + enrichment application services; он зависит только от integration record + types. Application → infrastructure уже используется этими владельцами; + новых cross-module exports или forbidden imports не требуется. +- **Guarantees at risk:** explicit allowlist, отсутствие private quantities + и owner fields, nested brand whitelist, UID check, null/undefined semantics, + publication fences и оригинальное fetchedAt. +- **Counterarguments / trade-offs:** online и saved execution действительно + различаются. Общая projection не должна объединять их orchestration. Skinny + discovery payload + `backend/src/modules/investments/instruments/infrastructure/catalog-discovery.repository.ts:358–387` + имеет другие обязанности и + `update: {}`; это другой shape, не цель reuse полной detail projection. + Reachability/deletion этого discovery publication path относится к audit 04; + DRY-002 не требует сохранять его или discovery producer. +- **Verification or rollback:** future check одного share и bond с extra + private fields, unknown/null optional fields и лишним brand field через + обе paths; сохранить UID mismatch guard и CAS. Existing discovery assertions + `backend/test/tbank-catalog-discovery.e2e-spec.ts:119–177,197–229` защищают + близкую boundary через direct service calls, но не доказывают production + reachability discovery producer или полный online/enrichment parity. + Rollback — вернуть два локальных вызова/definitions без schema migration. +- **Effort:** S. +- **Related finding IDs:** DRY-006, DRY-009. + +### DRY-003 — Select options и Zod enum дублируют словарь одной bank-card формы + +- **Decision:** SIMPLIFY. +- **Severity:** P3. +- **Confidence:** High. +- **Evidence:** `frontend/src/entities/bank-card/model/form.schema.ts:65–69`; + `frontend/src/entities/bank-card/ui/BankCardForm/BankCardFields.vm.ts:19–25`; + `backend/src/modules/banking/bank-cards/presentation/bank-card.dto.ts:45,80–83,245–246`; + `frontend/src/shared/api/generated/schema.d.ts:767–795`. +- **Claim:** в frontend одну runtime vocabulary вручную поддерживают и schema, + и selectable options. Здесь одинаковы owner, причина изменения и semantics. +- **Current mechanism:** schema содержит `""` и три payment-system значения; + fields VM независимо содержит те же три значения. `""` — UI sentinel, + преобразуемый в `null`, а не четвёртая платёжная система. +- **Runtime consumer:** fields VM передаёт options форме; form VM использует + schema и отправляет `values.paymentSystem || null` + (`frontend/src/entities/bank-card/ui/BankCardForm/BankCardForm.vm.ts:30–39,50–65`). +- **Impact / actual cost:** при изменении словаря можно получить selectable, + но невалидное значение или валидное, но недоступное для выбора. Сегодня + списки совпадают. Impact unmeasured; малая maintenance cost. +- **Why now:** один локальный export может удалить вторую editable копию без + новых layers, dependencies или API change. +- **Minimum change:** существующая `form.schema.ts` владеет typed const tuple + реальных payment systems; Zod добавляет пустой sentinel, VM строит labels + из этого tuple. Проверять tuple через generated `BankCard`/request union + аналогично existing `model/operation-kinds.ts:1–8`. +- **Ownership model:** schema и fields VM остаются в `entities/bank-card`; + VM импортирует local schema export. Не нужен `shared/constants`, новый slice + или import backend checkout. `satisfies` проверяет допустимость values, + но не является доказательством exhaustive покрытия future enum. +- **Guarantees at risk:** clearable Select, `null` в API, `""` в form draft, + сохранение выбранного значения, перевод labels и independent server checks. +- **Counterarguments / trade-offs:** словарь маленький и меняется редко; + это optional cleanup при работе над формой. Generated TS union не является + runtime list; вводить второй generator ради этих трёх values невыгодно. +- **Verification or rollback:** existing visible selection/save case + `frontend/src/entities/bank-card/ui/BankCardForm/BankCardForm.test.tsx:86–105`, + clearing-to-null `:55–84`; при реализации проверить каждый option и empty + sentinel, затем local type/dependency checks. Rollback локальный, API прежний. +- **Effort:** S. +- **Related finding IDs:** DRY-005, DRY-008. + +### DRY-004 — Access-loss taxonomy повторяется, recovery actions объединять не надо + +- **Decision:** SIMPLIFY. +- **Severity:** P3. +- **Confidence:** High. +- **Evidence:** `frontend/src/shared/api/resource-query.ts:37–67`; + `frontend/src/shared/api/client.ts:162–179`; + `frontend/src/pages/account/ui/AccountPage/AccountPage.vm.ts:59–64,86–96`. +- **Claim:** один набор `FORBIDDEN`, `INVALID_TBANK_TOKEN`, `NOT_FOUND`, + `TBANK_NOT_CONNECTED` независимо перечислен для resource loss, permanent + transport failure и блокирующего account state. +- **Current mechanism:** resource отменяет/очищает access group; transport + запрещает retry и отдельно включает `UNAUTHORIZED`; Account VM выбирает + error вместо snapshot. Совпадает membership классификации, не actions. +- **Runtime consumer:** `useResourceQuery` применяет classifier в effect + (`frontend/src/shared/api/resource-query.ts:99–102`); `fetchAppRead` вызывает + transient classifier (`frontend/src/shared/api/client.ts:127–150,172–179`); + `frontend/src/pages/account/ui/AccountPage/AccountPage.tsx:9–12` использует VM state. +- **Impact / actual cost:** новая access-loss причина потребует синхронно + изменить три места, иначе retry/cache/page state могут разойтись. Сейчас + набор согласован. Impact unmeasured; maintenance risk, не доказанный дефект. +- **Why now:** existing owner `shared/api/problem-details.ts` уже владеет + известными codes и unknown fallback; классификацию можно расширить там. +- **Minimum change:** один predicate именно для этих четырёх codes у этого + owner; transport добавляет свой `UNAUTHORIZED`, resource и Account VM + сохраняют прежние actions. Не создавать error registry или общий UI handler. +- **Ownership model:** `client.ts`/`resource-query.ts` используют local import; + Account VM — public `@/shared/api`. Никаких dependencies из shared в + features/entities/pages. Это API error taxonomy, не банковский расчёт. +- **Guarantees at risk:** unknown fallback, auth refresh отдельно от broker + access, cached data при temporary failures и clearing после confirmed loss. +- **Counterarguments / trade-offs:** + `frontend/src/pages/accounts/ui/AccountsPage/AccountsPage.vm.ts:16–19` намеренно + имеет subset из двух connection codes; расширять его до четырёх нельзя. + Error views имеют разные settings/back actions + (`frontend/src/pages/account/ui/AccountPage/components/AccountPageError.view.tsx:15–43`, + `frontend/src/pages/accounts/ui/AccountsPage/components/AccountsPageError.view.tsx:22–38`); + эти branches остаются у pages. Если название helper скрывает это различие, + оставить текущие короткие списки дешевле плохого generic handler. +- **Verification or rollback:** future MSW checks для membership four codes, + special `UNAUTHORIZED`, `TBANK_UNAVAILABLE`, unknown/missing code; разные + actions остаются. Existing boundary tests: + `frontend/src/shared/api/resource-query.test.tsx:174–209`, + `frontend/src/shared/api/client.test.ts:261–281`. Rollback — вернуть predicates, + не менять error wire values или response shapes. +- **Effort:** S. +- **Related finding IDs:** DRY-007, DRY-008. + +### DRY-005 — Banking validation и точные money representations оставить на обеих границах + +- **Decision:** KEEP. +- **Severity:** P2. +- **Confidence:** High. +- **Evidence:** `frontend/src/entities/bank-card/model/money.ts:4–23`; + `frontend/src/entities/bank-card/model/form.schema.ts:8–42`; + `frontend/src/pages/card/ui/OperationForm/OperationForm.schema.ts:53–131`; + `backend/src/modules/banking/bank-cards/domain/rub-money.ts:12–38`; + `backend/src/modules/banking/bank-cards/application/bank-operations.service.ts:94–210`. +- **Claim:** повторение validation защищает разные trust boundaries; money + helpers также не являются одинаковым public interface. +- **Current mechanism:** UI draft понимает comma/trim/leading zeros, возвращает + null/field errors и проверяет cached данные. Server требует canonical RUB + decimal, storage range и positive operation amount; затем authoritative + records, ownership и refunds проверяются в write transaction. +- **Runtime consumer:** BankCardForm и OperationForm → typed mutation → + DTO/input → `createCard`/`createOperation`. Server write boundary: + `backend/src/modules/banking/bank-cards/infrastructure/bank-cards.repository.ts:35–59`; + replay fingerprint и ownership: + `backend/src/modules/banking/bank-cards/application/bank-cards.service.ts:74–95`, + `backend/src/modules/banking/bank-cards/application/bank-operations.service.ts:214–239`. +- **Impact / actual cost:** дополнительное сопровождение policy values есть, + но удаление server validation позволяет обойти browser checks, а reuse + UI normalizer изменит server semantics. Impact unmeasured. +- **Why now:** DRY-001 показывает риск расхождения значений, но не оправдывает + универсальный shared validator или замену bigint на floating point. +- **Minimum change:** сохранить независимые implementations; согласовывать + конкретные policy values и примеры при изменении границы. Existing + `backend/src/common/decimal.ts:19–83` уже обслуживает server arithmetic; + `backend/src/integrations/tbank/decimal.ts:7–13` занимается protobuf units/nano + отдельно. +- **Guarantees at risk:** signed-64-bit bounds input, precision сумм сверх + storage range, positive/zero distinction, user ownership, CAS, transaction + rollback и replay protection. +- **Counterarguments / trade-offs:** параллельные constants требуют parity + review; frontend/backend не должны делить исполняемые business modules. + `rubMoney` форматирует вычисленную сумму и не обязан ограничивать её input + storage range, в отличие от `parseRubMoney`. +- **Verification or rollback:** future changes проверять existing boundary + cases `frontend/src/entities/bank-card/model/form-validation.test.ts:13–48` + и `backend/src/modules/banking/bank-cards/domain/rub-money.spec.ts:4–56`. + Финансовые changes дополнительно требуют реальной изолированной transaction + proof. В аудите ничего не удалено, rollback не требуется. +- **Effort:** S. +- **Related finding IDs:** DRY-001, DRY-003, DRY-008. + +### DRY-006 — Stock/Bond сходство не оправдывает generic page/query/payout pipeline + +- **Decision:** KEEP. +- **Severity:** P2. +- **Confidence:** High. +- **Evidence:** `frontend/src/pages/stock/ui/StockPage/StockPage.vm.ts:23–98`; + `frontend/src/pages/bond/ui/BondPage/BondPage.vm.ts:23–98`; + `backend/src/modules/investments/instruments/stocks/application/stock-income.service.ts:114–178`; + `backend/src/modules/investments/instruments/bonds/application/bond-income.service.ts:145–201`. +- **Claim:** одинаковая композиция закрывает разные business semantics; + действительно общие primitives уже переиспользуются. +- **Current mechanism:** dividends фильтруют cancelled и выбирают timing по + paymentDate с recordDate fallback; coupons несут number/type/periodDays и + timing по paymentDate. Quotes stock — Money, bond — percent of nominal, + clean/dirty/ACI. Страницы используют общий instrument-details layout, + section, chart, position и operation rows, но разные company/metrics/payouts. +- **Runtime consumer:** route containers + `frontend/src/pages/stock/ui/StockPage/StockPage.tsx:10–21` и + `frontend/src/pages/bond/ui/BondPage/BondPage.tsx:10–21`; + reused composition — + `frontend/src/pages/stock/ui/StockPage/StockPage.view.tsx:38–72`, + `frontend/src/pages/bond/ui/BondPage/BondPage.view.tsx:134–205`. + HTTP consumers — + `backend/src/modules/investments/instruments/stocks/presentation/stock.controller.ts:170–181`, + `backend/src/modules/investments/instruments/bonds/presentation/bond.controller.ts:126–134`. +- **Impact / actual cost:** текущая локальная duplication проще universal + hook с динамическим endpoint, type casts и flags для income kind/company/ + metrics/price unit. Impact unmeasured; savings от такой extraction не доказаны. +- **Why now:** защищает от ложного DRY на основании похожего JSX и одинакового + UUID regex. У entity hooks разные wire contracts и accessGroup usage. +- **Minimum change:** сохранить endpoint-specific entity query declarations + и page composition. Existing `features/instrument-details` остаётся общим + владельцем focused presentation, без объединения stock/bond entities. +- **Guarantees at risk:** exact decimal currency/percent semantics, unknown + amount vs zero, registry/payment dates, cancelled dividends, enabled inputs, + stable generated query keys и отсутствие запрещённых sibling imports. +- **Counterarguments / trade-offs:** horizon constants и часть grouping похожи; + отдельное маленькое правило может заслужить reuse при реальном совместном + изменении. Это не основание сейчас переносить весь orchestration или делать + boolean-driven payout component. +- **Verification or rollback:** future common changes должны сохранять + `backend/src/modules/investments/instruments/stocks/application/stock.spec.ts:10–37,45–69`, + `backend/src/modules/investments/instruments/bonds/application/bond.spec.ts:27–113,139–176` + и видимые stock/bond + payout fields. Local import boundaries проверяет + `frontend/dependency-cruiser.cjs:42–64`. В аудите изменения нет. +- **Effort:** S. +- **Related finding IDs:** DRY-002, DRY-007, DRY-010. + +### DRY-007 — Query wrappers и UI primitives несут project responsibility + +- **Decision:** KEEP. +- **Severity:** P2. +- **Confidence:** High. +- **Evidence:** `frontend/src/shared/api/authenticated-query.ts:34–59`; + `frontend/src/shared/api/resource-query.ts:37–67,84–149`; + `frontend/packages/ui/src/LoadingSkeleton/LoadingSkeleton.tsx:5–17`; + `frontend/packages/ui/src/StateView/StateView.tsx:5–27`. +- **Claim:** эти wrappers не просто воспроизводят API библиотеки под новым + названием; они задают конкретные session/resource/ARIA/presentation defaults. +- **Current mechanism:** authenticated query совмещает session и caller + enabled; resource wrapper очищает access group, проецирует envelope data/meta, + сохраняет transport status и refetch envelope. LoadingSkeleton задаёт busy + status/label, StateView — единый alert/layout; EntryCard — общую композицию, + tone и действия (`frontend/packages/ui/src/EntryCard/EntryCard.tsx:24–81`). +- **Runtime consumer:** portfolio query + `frontend/src/entities/portfolio/api/usePortfolio.ts:19–25`, banking query + `frontend/src/entities/bank-card/api/queries.ts:28–44`; instrument sections + используют LoadingSkeleton + (`frontend/src/features/instrument-details/ui/InstrumentSection.view.tsx:43–54`), + Account/Accounts error views используют StateView. +- **Impact / actual cost:** удаление wrappers размножит session gates, + resource access handling и accessibility defaults по callers. Impact + unmeasured; независимая стоимость текущих abstractions не измерялась. +- **Why now:** ponytail ladder о library/native API не означает удаления + useful project defaults. Mantine controls уже используются напрямую там, + где нет дополнительной ответственности + (`frontend/src/features/auth/ui/AuthForm/AuthForm.tsx:49–83`). +- **Minimum change:** сохранить focused wrappers и primitives. Не добавлять + собственные Button/Select passthrough wrappers. DRY-004 может упростить + классификацию внутри existing owner, не заменить whole wrapper. +- **Guarantees at risk:** no fetch before session, combining enabled, unknown + resource при успешном HTTP, access-loss cache clearing, сохранение cached + snapshot и доступные loading/error states. +- **Counterarguments / trade-offs:** wrapper вручную выбирает часть Query + return API; при новом consumer нужно расширять осмысленно. Observation + compatibility может быть отдельным dead-code вопросом audit 04, но её + возможная ненужность не доказывает ненужность wrapper целиком. +- **Verification or rollback:** existing resource boundary assertions + `frontend/src/shared/api/resource-query.test.tsx:72–138,174–209` + различают transport success, + projection и clearing. Future UI changes сохраняют keyboard/ARIA basics; + tests приложения не заменяют browser proof. В аудите изменений нет. +- **Effort:** S. +- **Related finding IDs:** DRY-004, DRY-006, DRY-010. + +### DRY-008 — Generated contracts и known-error subset не являются competing DTO truths + +- **Decision:** KEEP. +- **Severity:** P3. +- **Confidence:** High. +- **Evidence:** `backend/src/openapi.ts:4–10`; + `backend/.ai/conventions/http-api.md:69–81`; + `frontend/package.json:22–23`; + `frontend/src/entities/bank-card/api/types.ts:1–34`; + `frontend/src/features/auth/model/auth-context.ts:14–15`; + `frontend/src/shared/api/generated/schema.d.ts:731–795`. +- **Claim:** reviewed transport types выводятся из provided snapshot; не + найдена независимая handwritten DTO-копия для рассмотренных bank/auth flows. +- **Current mechanism:** editable contract — BE Nest DTO/Swagger; provided + snapshot — вход FE offline generation; aliases используют `paths`. + Form values выводятся из schemas. `MoneyDto implements Money` + (`backend/src/common/money.dto.ts:3–10`) сохраняет runtime Swagger metadata + отдельно от pure `backend/src/common/money.types.ts:1`. +- **Runtime consumer:** `frontend/src/shared/api/client.ts:1–6` импортирует + generated paths; form/auth/bank query callers используют derived aliases. + Known UI enum `frontend/src/shared/api/problem-details.ts:1–28` обслуживает + recovery, в то время как + generated ProblemDetails.code остаётся открытой строкой. +- **Impact / actual cost:** artifact freshness требует сопровождения, но + удаление snapshots или общий исполняемый DTO package нарушит independent + repo inputs. Impact unmeasured; текущего file-to-file drift OpenAPI нет. +- **Why now:** наличие нескольких файлов с одной shape не означает несколько + редактируемых источников значения. DRY-001 и DRY-003 касаются runtime policies, + которые TypeScript declarations не реализуют автоматически. +- **Minimum change:** оставить существующий contract flow и local aliases; + согласовывать metadata при DRY-001. Сохранить runtime known subset ошибок с + fallback, не закрывать wire string ради auto-generated exhaustive enum. +- **Guarantees at risk:** compatible wire contract, offline FE generation, + независимость CI/checkout, unknown-code fallback, domain без Swagger. +- **Counterarguments / trade-offs:** равенство existing OpenAPI файлов не + подтверждает runtime freshness, а typed `satisfies` не проверяет все future + enum values. Это ограничение существующего flow, не доказательство нужности + ещё одного hash/validator registry. Fresh generation parity — UNVERIFIED. +- **Verification or rollback:** при будущих API changes — предусмотренные + backend contract checks и frontend offline `openapi:check`, без чтения + соседнего source как обязательного входа. В аудите выполнено только сравнение + existing artifacts; generation/build не запускались. +- **Effort:** S. +- **Related finding IDs:** DRY-001, DRY-003, DRY-004, DRY-005. + +### DRY-009 — Живой enrichment/shared fence: не вводить generic queue из сходства с discovery + +- **Decision:** KEEP. +- **Severity:** P2. +- **Confidence:** High. +- **Evidence:** `backend/src/modules/investments/instruments/application/catalog-enrichment.service.ts:113–125,189–225,497–577`; + `backend/src/modules/investments/instruments/infrastructure/catalog-discovery.repository.ts:154–179,280–318`; + `backend/src/modules/investments/instruments/stocks/application/stock-market.service.ts:34–45`. + Reachability counterexample: + `backend/src/modules/investments/instruments/application/catalog-discovery.service.ts:66–70,84–93,149–190,227–267`; + `backend/src/modules/investments/tbank-sync/tbank-sync.service.ts:54–78,192–201`; + `backend/src/modules/investments/tbank-sync/tbank-sync.controller.ts:31–41`. +- **Claim:** KEEP касается используемых enrichment batching/observer/publication + guarantees и общей repository fence. Сходство с discovery не оправдывает + generic queue и не является основанием сохранять discovery producer. +- **Current mechanism:** enrichment `requestMany` при missing/stale sources + резервирует work и ставит drain; price batch ограничен одной user/revision, + поздние observers используют `publishSource` с initiating attempt. Repository + отдельно claim-ит initiator и observer. Discovery default HTTP branches online, + observe branches read-only; `read(true)` пропускает request. +- **Runtime consumer:** `backend/src/modules/investments/instruments/stocks/presentation/stock.controller.ts:122–129` + → `backend/src/modules/investments/instruments/stocks/application/stock-market.service.ts:34–45` + → enrichment `section('quote', false)` + (`backend/src/modules/investments/instruments/application/catalog-enrichment.service.ts:234–241,269–289`) + → `readMany(false)` (`:113–125`) + → при missing/stale `requestMany` (`:189–225`) → drain/publishSource (`:497–577`). + Bond quote имеет тот же живой entry point + (`backend/src/modules/investments/instruments/bonds/presentation/bond.controller.ts:92–99`; + `backend/src/modules/investments/instruments/bonds/application/bond-market.service.ts:38–49`). + Ни default catalog search/detail/byTicker, ни observe calls discovery producer + не запускают. Единственный найденный внешний production-text refresh caller + находится в manual dispatch без найденного production request consumer. +- **Impact / actual cost:** generic queue ради устранения сходства с + dead-parent-reachable кодом добавит callbacks/flags вместо устранения + неиспользуемого пути. Изменение живого enrichment может потерять authorization + и publication fences. Impact unmeasured; runtime savings не измерены. +- **Why now:** исправлен ложный runtime consumer прежней редакции. Наличие + definitions, DI registration и direct tests не доказывает producer reachability. +- **Minimum change:** не вводить generic queue; сохранить гарантии живого + enrichment и используемые им `reserve/publishSource/finish`. DRY-002 извлекает + stateless projection независимо. Discovery `request/refresh/drain` не входит + в KEEP; reachability/deletion и compatibility reads — предмет audit 04. +- **Guarantees at risk:** connection revision/attempt identity, isolation + initiating credentials, private observer handles, late-publication fences, + same-owner batching и conditional finish именно живого enrichment. +- **Counterarguments / trade-offs:** discovery producer в source действительно + похож и исполняется из direct tests, но current catalog HTTP paths его + обходят; это аргумент за отдельный dead-code разбор, не за сохранение двух + queues. KEEP не распространяется на весь CatalogDiscoveryService/repository, + unused methods или каждый completion helper enrichment. Равноценность общей + queue и снижение total cost — UNVERIFIED. +- **Verification or rollback:** при изменении живой boundary нужны controlled + quote/enrichment scenarios с late replace/delete, invalid initiating token, + batch/observer isolation и conditional publication. Direct discovery tests + (`backend/test/tbank-catalog-discovery.e2e-spec.ts:184–187,202–203`) не являются + proof текущего HTTP flow. Реализация в этом аудите не изменена; rollback + source не требуется. Audit 04 определяет deletion scope отдельно. +- **Effort:** S. +- **Related finding IDs:** DRY-002. + +### DRY-010 — Scheduling ownership требует проверки, глобальная freshness policy преждевременна + +- **Decision:** INVESTIGATE. +- **Severity:** P2. +- **Confidence:** Medium. +- **Evidence:** `frontend/src/features/tbank-connection/model/screen-sync.ts:20–35,53–61`; + `frontend/src/app/ui/App.tsx:13–20`; + `frontend/src/entities/portfolio/api/usePortfolio.ts:3–5,19–25`; + `frontend/src/entities/brokerage-account/api/useBrokerageAccounts.ts:3–18`; + `specs/openspec/specs/brokerage-accounts/spec.md:68,96–98`; + `specs/openspec/specs/portfolio-view/spec.md:122–124`. +- **Claim:** у одного HTTP-read flow есть page timer, query freshness и app + refetch triggers. Возможность уменьшить собственную coordination реальна, + но их полное объединение не является доказанным behavior-preserving cleanup. +- **Current mechanism:** screen hook каждые 1 800 000 ms вызывает refetch + текущего resource array; отдельные hooks задают 30 seconds для portfolio и + 30 minutes для accounts; app включает focus/reconnect. Spec специально + различает freshness и periodic interval, а focus refetch запрещает. +- **Runtime consumer:** Account VM передаёт пять resources + (`frontend/src/pages/account/ui/AccountPage/AccountPage.vm.ts:78–84`); + Stock/Bond — свои section queries; BondsPage + передаёт search result (`frontend/src/pages/bonds/ui/BondsPage/BondsPage.vm.ts:10–13`). + Все восемь production call sites перечислены в разделе 3: ни один не + сохраняет/передаёт return hook. `refresh/retry` используются только test + harness (`frontend/src/features/tbank-connection/model/screen-sync.test.tsx:37–50,68–78`), + не кнопкой текущего production UI. +- **Impact / actual cost:** maintenance scheduling policy распределено по + owners; runtime число лишних GET, interval phase, background-tab behavior и + эффект от native query-owned interval не измерены. Impact unmeasured. +- **Why now:** важно не заменять все staleTime на один constant ради DRY. + Неиспользуемый return/retry не является основанием сохранять timer; + его reachability/deletion относится к audit 04. +- **Minimum change:** сначала проверить семантику interval относительно + последнего успешного read, focus/reconnect, disabled inputs и активных + observers. Затем решить, достаточно ли существующих query options для + periodic reads. До подтверждения не создавать global query factory или + universal policy object. +- **Guarantees at risk:** caller enabled/session gates, canonical cache key, + cleanup при unmount, HTTP-only periodic read, сохранение snapshot и отсутствие + hidden personal-import commands. +- **Counterarguments / trade-offs:** существующий timer реально имеет общую + mount-based фазу: effect с `[]` не перезапускается при смене resource array, + а callback читает актуальный ref + (`frontend/src/features/tbank-connection/model/screen-sync.ts:22–35`). Query-owned timers + могут изменить фазу, поведение при смене inputs или background/observer + semantics. Общая кнопка retry из return не является counterargument — + production consumer не найден. Разные 30 sec/30 min в specs не противоречат + друг другу. UNVERIFIED: equivalence замены, необходимость общего clock + как требования и фактические savings. +- **Verification or rollback:** дешевейшее подтверждение — controlled-time + MSW сценарий существующего query/page owner, assert на видимые данные и GET + при mount/focus/interval/unmount/disabled search, затем сверка native options + со штатной документацией используемой версии. Никакой реальной сети и + нагрузочного benchmark для первого решения не нужно. Future rollback — + вернуть только scheduling options/timer, сохранив generated keys. +- **Effort:** M. +- **Related finding IDs:** DRY-006, DRY-007. + +## 5. DELETE / SIMPLIFY / KEEP / INVESTIGATE + +| Decision | IDs / область | Итог | +| --- | --- | --- | +| DELETE | — | Подтверждённых удалений в scope 03 нет; недоступные flows относятся к audit 04 | +| SIMPLIFY | DRY-002 | Один module-local instrument projection mapper; whitelist сохраняется | +| SIMPLIFY | DRY-003 | Один typed runtime tuple у текущей bank-card schema | +| SIMPLIFY | DRY-004 | Один code-membership predicate у existing API error owner; actions остаются разными | +| FIX | DRY-001 | Согласовать единицу password minimum и API metadata, сохранить две trust-boundary checks | +| KEEP | DRY-005 | Money precision, server validation, transaction/CAS/refund/replay protections | +| KEEP | DRY-006 | Stock/Bond contracts и business/render semantics; existing focused reuse достаточен | +| KEEP | DRY-007 | Authenticated/resource wrappers и доступные UI primitives | +| KEEP | DRY-008 | Provider-owned OpenAPI, consumer artifacts, derived aliases, open-code fallback | +| KEEP | DRY-009 | Только гарантии живого enrichment и используемая repository fence; discovery producer — audit 04, не KEEP | +| INVESTIGATE | DRY-010 | Scheduling simplification только после проверки эквивалентности | + +### Minimal ownership model для reuse + +| Правило | Единственная implementation / value | Callers | Запрещённое направление / лишняя abstraction | +| --- | --- | --- | --- | +| Detail projection / DRY-002 | Локальный `instruments/infrastructure/public-instrument.mapper.ts` | Discovery/enrichment application внутри instruments | Не в global common/utils, не integration → business DTO, не DI provider | +| FE payment list / DRY-003 | Typed tuple в existing `entities/bank-card/model/form.schema.ts` | Zod enum и bank-card fields VM | Не frontend → backend checkout, не shared → entity, не новый generator | +| Access-loss membership / DRY-004 | Predicate в existing `shared/api/problem-details.ts` | Local client/resource; Account VM через `shared/api/index.ts` | Не shared → features/pages, не generic recovery component | +| Password policy / DRY-001 | BE CredentialsDto validator — enforcement; FE auth schema — UX implementation той же spec | Register/login boundaries | Не удалять server check, не shared cross-repo executable validator | +| Shared instrument presentation / KEEP | Existing `features/instrument-details` | Stock/Bond pages | Не entity sibling import, не generic boolean-driven page | + +Сравнение вариантов для всех `SIMPLIFY`: + +- **Оставить копии:** допустимо для P3, если область не меняется; сохраняется + несколько мест правки. Для DRY-002 benefit выше: одна visibility projection. +- **Перенести к owner:** предпочтительно для DRY-002–004; число business + owners не меняется, imports локальны или уже разрешены. +- **Генерировать:** полезно для wire types и уже выполняется; byte validators, + recovery actions и runtime UI lists нельзя получить из erased TS declarations + автоматически. Дополнительный generator здесь дороже tuple/predicate. +- **Сделать shared:** допустим только existing focused API code owner DRY-004. + Межрепозиторный shared package и огромный `shared/utils` создадут release, + build, trust-boundary и layer coupling без подтверждённой потребности. + +## 6. Cost & benefit + +| ID | Класс ожидаемой пользы | Фактическое измерение | Обязательная regression boundary | +| --- | --- | --- | --- | +| DRY-001 | Reliability/UX и maintenance согласованной policy | not measured; несовместимость доказана по source | UTF-8 границы, HTTP validation, password без преобразования, published metadata | +| DRY-002 | Cognitive load, maintenance; один visibility owner | not measured; две definitions подтверждены | Whitelist/nested brand, optional fields, online/saved parity, UID/CAS | +| DRY-003 | Небольшая maintenance/cognitive экономия | not measured; две local lists подтверждены | Все options, empty sentinel и null request | +| DRY-004 | Maintenance при изменении code taxonomy | not measured; три полных набора подтверждены | Unknown/temporary errors, auth special case, cache clearing и разные actions | +| DRY-005–009 | Избежать лишнего coupling и удаления используемой защиты | not measured | Money precision, server authority, live enrichment publication/session boundaries, accessibility; discovery producer исключён | +| DRY-010 | Возможная экономия собственных scheduling constructs | UNVERIFIED / not measured | Mount timer phase/latest-resource ref, enabled, active observers, cached snapshot, HTTP-only reads; unused return/retry не защищается | + +Никаких claims об ускорении, CI minutes, bundle size или agent-token savings +не сделано. Для выбранных cleanup нет подтверждённого кандидата на удаление +dependency. Net LOC не вычислялся: нет implementation diff, а helper imports, +public exports и необходимые regression checks также стоят кода. + +### Complexity-only pass в формате ponytail-audit + +- `shrink:` убрать вторую `publicInstrument`; одна instruments boundary projection. [DRY-002; backend/src/modules/investments/instruments/application/catalog-discovery.service.ts:499–531; backend/src/modules/investments/instruments/application/catalog-enrichment.service.ts:646–678] +- `shrink:` убрать повторные полные access-loss списки; один predicate у existing API code owner, actions отдельно. [DRY-004; frontend/src/shared/api/client.ts:162–169; frontend/src/shared/api/resource-query.ts:42–51; frontend/src/pages/account/ui/AccountPage/AccountPage.vm.ts:59–64] +- `shrink:` убрать вторую runtime payment-system list формы; typed tuple existing schema. [DRY-003; frontend/src/entities/bank-card/model/form.schema.ts:65–69; frontend/src/entities/bank-card/ui/BankCardForm/BankCardFields.vm.ts:19–22] + +`net: -N lines, -0 deps possible.` Здесь `N = not measured`: сокращение строк +без подготовленного и проверенного diff не оценено; 0 относится к предложенным +удалениям deps, не к исчерпывающему dependency audit. DRY-001 и защищаемые +инварианты не включены в complexity-only cuts. + +## 7. Top actions + +Все четыре шага ниже подтверждены source analysis; ранжирование учитывает +value/risk/effort, а не число одинаковых строк. + +### Требует behavior/contract подтверждения + +1. **DRY-001 / M:** восстановить согласованный byte minimum у auth validator, + согласовать metadata и artifact; проверить один набор UTF-8 границ на + frontend и configured backend boundary. Новый ADR нужен только при решении + действительно изменить принятую password policy, а не при её восстановлении. + +### Quick wins при работе над соответствующим owner + +2. **DRY-002 / S:** один local projection mapper с сохранением allowlist, + null/undefined и используемых enrichment publication fences. Это не требует + сохранения discovery producer; его deletion scope — audit 04. Максимальный + подтверждённый reuse benefit среди structural кандидатов этого аудита. +3. **DRY-003 / S:** использовать один typed runtime tuple для schema и options; + сохранить пустое form value и API null. Никакой tooling migration. +4. **DRY-004 / S:** один predicate для четырёх codes; сохранить отдельный auth + `UNAUTHORIZED`, Accounts subset и page recovery actions. Выполнять только + с boundary proof сохранения temporary-error snapshot. + +DRY-010 остаётся verification question о timer phase, не подтверждённым шагом +migration и не защитой неиспользуемого return/retry. DRY-009 не требует +сохранения discovery `request/refresh/drain`; их reachability/deletion — audit 04. +KEEP findings не превращены в backlog на дополнительные layers/tests/rules. + +## 8. Cross-audit signals + +- **04 / Dead Code & Legacy:** observation compatibility в `frontend/src/shared/api/resource-query.ts:69–127`; проверить actual production consumers перед удалением, не удалять весь resource wrapper. +- **04 / Dead Code & Legacy:** `backend/src/modules/investments/instruments/application/catalog-discovery.service.ts:66–70,84–86,149–190,227–267,340–495` и `backend/src/modules/investments/tbank-sync/tbank-sync.service.ts:54–78,192–201`; default paths online, observe read-only, внешний refresh caller только в manual dispatch без найденного production request entry point; DRY-009 не защищает этот producer от удаления. +- **04 / Dead Code & Legacy:** return/refresh/retry `frontend/src/features/tbank-connection/model/screen-sync.ts:37–61` потребляется только tests `screen-sync.test.tsx:37–50,68–78`; все восемь production calls отбросили return; deletion scope отдельно от живого timer `:22–35`. +- **13 / Reliability & Security:** online catalog calls `backend/src/modules/investments/instruments/application/catalog-discovery.service.ts:149–165,295–317` и existing token translator `backend/src/integrations/tbank/tbank-token.service.ts:26–35`; проверить публичное invalid-token recovery, здесь отдельный error/security аудит не проводился. +- **05 / Frontend Architecture:** ownership периодического чтения и app focus/reconnect `frontend/src/features/tbank-connection/model/screen-sync.ts:20–35`, `frontend/src/app/ui/App.tsx:13–20`; связан с DRY-010, не повод унифицировать разные staleTime. +- **08 / Test Quality:** proof для DRY-001 должен различать byte/character limits; рассмотренный `backend/src/common/dto-validation.spec.ts:13–30` использует ASCII happy path, полноценный аудит auth tests не выполнялся. +- **15 / SDD, Specs & Documentation:** `docs/adr/019-hourly-reads-and-online-instrument-reference.md:27–29`, `specs/openspec/specs/portfolio-view/spec.md:122–124`, `specs/AI_INDEX.md:15–31`; различать общую формулировку read interval и endpoint-specific freshness, не объявлять docs копии автоматически лишними. + +## 9. Open questions / verification plan + +| Вопрос, реально меняющий решение | Дешевейшая проверка | Что изменит ответ | +| --- | --- | --- | +| Есть ли принятое изменение password minimum с bytes на characters? | Проверить accepted auth decision/requirement у owner; existing spec и docs уже говорят о byte-policy и непринятом drift | Изменяет целевую policy DRY-001; без нового решения восстановить bytes | +| Четыре codes по-прежнему имеют одинаковое membership во всех трёх contexts? | Existing MSW boundaries плюс unknown/temporary/auth code cases, без network | Если meanings расходятся, DRY-004 становится KEEP коротких local predicates | +| Periodic reads должны синхронно повторять весь экран или каждый active resource после собственного успешного read? | Controlled-time MSW scenario mount/focus/reconnect/interval/unmount; сопоставить Accounts/Portfolio specs | Определяет, нужна ли screen clock и эквивалентен ли query-owned interval в DRY-010 | +| Как native interval действует при disabled query и background tab в установленной версии? | Сверка документации версии и targeted existing owner test с disabled search, без install | Определяет безопасность и размер scheduling cleanup; пока UNVERIFIED | + +### Финальная проверка полноты и ограничений + +- Присутствуют все девять общих разделов, Knowledge Duplication Register, + Intentional Duplication to Keep, minimal ownership model и сравнение вариантов + local copies / owner / generated / shared. +- Для каждого из десяти DRY-ID заполнены Decision, Severity, Confidence, + Evidence, Claim, Current mechanism, Runtime consumer, Impact / actual cost, + Why now, Minimum change, Guarantees at risk, Counterarguments / trade-offs, + Verification or rollback, Effort и Related finding IDs. Для каждого + SIMPLIFY дополнительно указан owner и допустимое направление imports. +- Четыре actionable выводы отделены от пяти KEEP и одного INVESTIGATE. + Неизмеренные savings и непроверенная эквивалентность явно обозначены. +- Повторный consumer trace отделяет живой enrichment/shared fence DRY-009 + от discovery producer без найденного production entry point; восемь + standalone `useScreenSync` calls не используют return. Связанные register, + intentional KEEP, summary/top/minimum change и final table согласованы. +- Boy Scout review отчёта: не добавлены competing policy values, generic + shared owner, duplicate findings для одного механизма или обязательная + tooling-зависимость от соседнего checkout. Исходники не правились. +- После записи проверены собственный новый текст, whitespace и повторные + HEAD/status всех пяти repos. Tracked/staged/unstaged source изменений нет. + Отчёт ignored root allowlist-ом и существует только в разрешённом месте. +- Статический audit не является runtime bug reproduction, успешным запуском + tests, подтверждением DB atomicity, свежей генерацией OpenAPI или измерением + производительности. Эти ограничения сохраняются после clean Git status. + +## Machine-readable consolidation table + +| ID | Decision | Severity | Confidence | Repo | Files | One-line impact | Effort | Related IDs | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | +| DRY-001 | FIX | P2 | High | backend,frontend | backend/src/modules/auth/presentation/auth.dto.ts:43–74; frontend/src/features/auth/model/auth.schema.ts:36–43; frontend/src/shared/api/spec/openapi.json:5246–5250 | Разные единицы password minimum дают UI/API отказ | M | DRY-005,DRY-008 | +| DRY-002 | SIMPLIFY | P3 | High | backend | backend/src/modules/investments/instruments/application/catalog-discovery.service.ts:499–531; backend/src/modules/investments/instruments/application/catalog-enrichment.service.ts:646–678 | Две одинаковые detail projections требуют двух правок whitelist | S | DRY-006,DRY-009 | +| DRY-003 | SIMPLIFY | P3 | High | frontend | frontend/src/entities/bank-card/model/form.schema.ts:65–69; frontend/src/entities/bank-card/ui/BankCardForm/BankCardFields.vm.ts:19–22 | Schema и selectable options вручную повторяют один словарь | S | DRY-005,DRY-008 | +| DRY-004 | SIMPLIFY | P3 | High | frontend | frontend/src/shared/api/client.ts:162–179; frontend/src/shared/api/resource-query.ts:37–67; frontend/src/pages/account/ui/AccountPage/AccountPage.vm.ts:59–64 | Три набора access-loss codes могут расходиться; actions оставить локальными | S | DRY-007,DRY-008 | +| DRY-005 | KEEP | P2 | High | backend,frontend | frontend/src/entities/bank-card/model/money.ts:4–23; frontend/src/pages/card/ui/OperationForm/OperationForm.schema.ts:53–131; backend/src/modules/banking/bank-cards/domain/rub-money.ts:12–38; backend/src/modules/banking/bank-cards/application/bank-operations.service.ts:94–210 | Independent validation сохраняет precision и server trust boundary | S | DRY-001,DRY-003,DRY-008 | +| DRY-006 | KEEP | P2 | High | backend,frontend | frontend/src/pages/stock/ui/StockPage/StockPage.vm.ts:23–98; frontend/src/pages/bond/ui/BondPage/BondPage.vm.ts:23–98; backend/src/modules/investments/instruments/stocks/application/stock-income.service.ts:114–178; backend/src/modules/investments/instruments/bonds/application/bond-income.service.ts:145–201 | Generic pipeline скроет разные dividend/coupon и quote semantics | S | DRY-002,DRY-007,DRY-010 | +| DRY-007 | KEEP | P2 | High | frontend | frontend/src/shared/api/authenticated-query.ts:34–59; frontend/src/shared/api/resource-query.ts:37–149; frontend/packages/ui/src/LoadingSkeleton/LoadingSkeleton.tsx:5–17; frontend/packages/ui/src/StateView/StateView.tsx:5–27 | Wrappers несут session/resource/accessibility defaults | S | DRY-004,DRY-006,DRY-010 | +| DRY-008 | KEEP | P3 | High | backend,frontend | backend/src/openapi.ts:4–10; frontend/src/entities/bank-card/api/types.ts:1–34; frontend/src/shared/api/generated/schema.d.ts:731–795; frontend/src/shared/api/problem-details.ts:1–28 | Generated copies сохраняют независимые repo inputs и open-code fallback | S | DRY-001,DRY-003,DRY-004,DRY-005 | +| DRY-009 | KEEP | P2 | High | backend | backend/src/modules/investments/instruments/stocks/application/stock-market.service.ts:34–45; backend/src/modules/investments/instruments/application/catalog-enrichment.service.ts:113–125,189–225,497–577; backend/src/modules/investments/instruments/infrastructure/catalog-discovery.repository.ts:154–179 | Сохранить live enrichment guarantees/shared fence; discovery producer не входит в KEEP, deletion — audit 04 | S | DRY-002 | +| DRY-010 | INVESTIGATE | P2 | Medium | frontend | frontend/src/features/tbank-connection/model/screen-sync.ts:22–35,53–61; frontend/src/app/ui/App.tsx:13–20; frontend/src/pages/account/ui/AccountPage/AccountPage.vm.ts:78–84; frontend/src/entities/portfolio/api/usePortfolio.ts:3–5 | Проверить equivalence timer phase; восемь production callers не используют return/retry, runtime savings UNVERIFIED | M | DRY-006,DRY-007 | diff --git a/audits/2026-10-11/reports/04-dead-code-and-legacy-report.md b/audits/2026-10-11/reports/04-dead-code-and-legacy-report.md new file mode 100644 index 0000000..a279ea0 --- /dev/null +++ b/audits/2026-10-11/reports/04-dead-code-and-legacy-report.md @@ -0,0 +1,746 @@ +# Аудит 04 — Dead Code & Legacy + +## 1. Audit metadata + +- **Audit ID / name:** 04 / Dead Code & Legacy. +- **Дата:** 2026-10-11. +- **Исполнитель:** один независимый исполнитель направления 04; другие + субагенты не запускались. +- **Результат:** исследование reachability и безопасных границ удаления. + Изменён только этот отчёт вне application checkout. +- **Workspace:** `/Users/ksv741/Project/vibe/WealthLedger`. + +| Checkout | Абсолютный путь | HEAD, проверенный самостоятельно | Начальный status | +| --- | --- | --- | --- | +| root | `/Users/ksv741/Project/vibe/WealthLedger` | `dc030f84c979cedb2628ef4bd97c62d6c63a9256` | чистый | +| frontend | `/Users/ksv741/Project/vibe/WealthLedger/frontend` | `c8821200ce699dab1ce65360d1968d5877a2e78b` | чистый | +| backend | `/Users/ksv741/Project/vibe/WealthLedger/backend` | `0ad84ff4f0b4712abc7e69dc299856f14af6eadd` | чистый | +| specs | `/Users/ksv741/Project/vibe/WealthLedger/specs` | `fcdc805f6c1b3be6a20a7e858e3e80cbd83a9121` | чистый | +| docs | `/Users/ksv741/Project/vibe/WealthLedger/docs` | `e6fd4f64b81dd73b33959b845a1babcfac1891c8` | чистый | + +**Метод фиксации:** в каждом checkout выполнены +`git rev-parse HEAD && git status --porcelain=v1`; пустой porcelain означает +отсутствие staged, unstaged и видимых Git untracked changes, а не отсутствие +ignored файлов. Root allowlist исключает `audit-results/` +(`.gitignore:1–7`), поэтому отчёт не должен появляться в обычном root status. + +**Инструкции:** прочитаны AGENTS каждого checkout. Для backend и frontend +прочитаны их собственные architecture, engineering и активированные +conventions: HTTP/data/testing/integrations/security для backend; +TypeScript/React/data/UI/testing/Storybook для frontend. Root conventions +использованы только для root-границы отчёта. Specs выбраны через `AI_INDEX.md` +и точные тематические targets; три актуальных требования нового потока, +которых нет в индексе, прочитаны по известным абсолютным путям. Вложенных +AGENTS в source-областях frontend/backend поиск не обнаружил. + +**Skills:** установленный `ponytail-audit` загружен первым через Skill tool. +Применены его `delete:` / `shrink:` теги, ранжирование по сокращению ненужной +maintenance surface и запрет исправлений. Схема отчёта расширена обязательным +протоколом 04. Числовой `net` не выдуман: deletion diff не создавался. +Также загружены `graphify` и `verification-before-completion`. + +**Graphify:** существующего `graphify-out/graph.json` не обнаружено в root +и четырёх checkout: проверены списки корневых директорий, дополнительно Glob +в frontend/backend. Graph query не выполнялся; graph generation, cache, +manifest и другие artifacts не создавались. Отсутствие существующего graph +— ограничение; выводы получены статической трассировкой актуальных source. + +**Что выполнялось:** Read/Glob/Grep из соответствующих repo directories, +read-only `rg -n -c` для явно указанных символов и `git` inspection. +`technical-audit-2026-10-11.md` прочитан только как список гипотез; +его выводы и оценка `~550 lines` не использованы как доказательство. +Другие новые отчёты аудиторов не читались и не изменялись. + +**Что не выполнялось:** Node-команды, install, build, codegen, migrations, +full-suite, targeted tests, запуск приложения, запросы к реальным API, +изменение config/source/lockfiles, commits, PR и генерация исследовательских +инструментов. Это статический аудит, не runtime-воспроизведение. +`npm run knip`, `deps`, frontend test scripts предварительно вызывают +`build:packages` (`frontend/package.json:14–26`); их запуск нарушил бы +заданную read-only границу. Их результаты из прежнего отчёта не наследуются. + +## 2. Executive verdict + +1. **Факт:** command-часть `TbankSyncService.request → dispatch` не имеет + production entrypoint. Nest создаёт service для `status`, но controller + предоставляет только GET. Удаление command-closure обосновано — DEAD-001. +2. **Факт:** вслед за ней обнаружен менее очевидный dead producer: + `CatalogDiscoveryService.request/refresh/drain`. Реальные HTTP search, + detail и ticker читают онлайн либо вызывают persisted reader только с + `observe=true`; старый request-driven импорт каталога запускают прямые + тесты и недоступная manual orchestration — DEAD-002. +3. **Факт:** frontend `observe()` используется только двумя прямыми + тестовыми вызовами; все восемь production consumers `useScreenSync` + игнорируют его return value, а `_scope` вообще не читается. + Можно удалить эти API-хвосты, сохранив действующий timer — DEAD-004/005. +4. **Факт и граница:** статусный GET и backend `sync=observe` действительно + публично достижимы. Отсутствие их использования в текущей SPA не доказывает + отсутствия внешних consumers или старых DB rows. Их удаление сейчас + **не подтверждено** — DEAD-006/007, `INVESTIGATE`. +5. **Риск ошибочной зачистки:** planner операций, hourly lifecycle, candle + loader, shared source repository и connection/attempt fences остаются + живыми. Массовое удаление по словам `screen`, `catalog` или `sync` может + остановить данные либо каскадно удалить свечи — DEAD-008–011, `KEEP`. +6. **Итог:** 5 actionable findings: **P0 = 0, P1 = 0, P2 = 2, P3 = 3**. + Отдельно **KEEP = 4**, **INVESTIGATE = 2**. Для непроверенной внешней + совместимости указан `UNVERIFIED`; экономия времени/строк не измерена. + +## 3. Inventory & coverage + +### 3.1. Фактическая карта entrypoints + +| Вход / поток | Проверенная цепочка и owner | Следствие для reachability | +| --- | --- | --- | +| SPA bootstrap | `frontend/src/main.tsx:1–23` → `src/app/ui/App.tsx:23–43` → `src/app/routes/AppRoutes/AppRoutes.tsx:9–103,105–257` | Lazy imports являются настоящими consumers page exports. CSS, i18n и dayjs locale — side effects, не orphan imports. | +| SPA routes | `AppRoutes.tsx:105–257`: login/register; cards/card; stocks/stock; bonds/bond; accounts/account/events/income/operations; settings; wildcard | Страницы доступны через router, включая динамические chunks; отсутствие статического прямого import не делает их мёртвыми. | +| Backend HTTP | `backend/src/main.ts:44–60` → `src/app.module.ts:18–36` → controllers | Prefix, validation, cookies, error boundary и shutdown hooks задаются `src/main.ts:19–42`; controller decorators являются registration. | +| Controller families | `backend/src/modules/auth/presentation/auth.controller.ts:51`; `backend/src/modules/banking/bank-cards/presentation/bank-cards.controller.ts:79`; `backend/src/modules/investments/brokerage-accounts/brokerage-accounts.controller.ts:15`; `backend/src/modules/investments/portfolio/presentation/portfolio.controller.ts:15`; `backend/src/modules/investments/operations/presentation/brokerage-operations.controller.ts:18`; `backend/src/modules/investments/income/income-analytics.controller.ts:20`; `backend/src/modules/investments/upcoming-events/upcoming-events.controller.ts:18`; `backend/src/modules/investments/instruments/stocks/presentation/stock.controller.ts:48`; `backend/src/modules/investments/instruments/bonds/presentation/bond.controller.ts:37`; `backend/src/modules/investments/tbank-connection/tbank-connection.controller.ts:33`; `backend/src/modules/investments/tbank-sync/tbank-sync.controller.ts:31` | Проверены registrations всех этих семейств поиском `@Controller`, root module и нужными application chains; это не полный семантический аудит CRUD/auth. | +| Hourly job | `backend/src/modules/investments/tbank-sync/tbank-sync.module.ts:24–25` → scheduler `onModuleInit:48–50` → `tick:56–71` → `reserveHourly` / `walk` | Один native timer, durable work и source owners; запуск не зависит от frontend. | +| Source queues | Portfolio `queueSnapshot`, operations sync, metadata, enrichment, candles используют `setImmediate`; declarations найдены в source, живые примеры traced ниже | Удаление manual dispatcher не даёт права удалять все очереди с похожими именами. | +| Production CLI | `backend/package.json:15–18`; `compose.yaml:24–38` | Backend запускает `dist/src/main.js`; отдельная migration service — Prisma CLI. В manifest не найден CLI, вызывающий manual sync или persisted catalog producer. | +| Test dynamic entrypoint | `backend/test/fixtures/tbank-sync-crash.cjs:1–17` | Динамические require ведут в operations sync/repository, не в `TbankSyncService` или `CatalogDiscoveryService`. Fixture не запускался. | +| OpenAPI tooling | `backend/scripts/generate-openapi.cjs:19–31` → AppModule preview → `src/openapi.ts:6–10` | Generator читает зарегистрированный controller contract; не вызывает service methods по именам. Наличие DTO decorator само по себе не публикует DTO. | +| Storybook / packages | `frontend/.storybook/main.ts:3–7`; `packages/ui/package.json:9–22`; `src/app/ui/App.tsx:3–8` | Stories — дополнительные entrypoints; UI CSS явно side-effectful. Feature/query candidates не экспортируются public UI package. | +| Env / imports | `backend/src/config/env.validation.ts:9–33,102–113`, `.env.example:1–10`, `src/app.module.ts:18–36`; `frontend/vite.config.ts:54–61`, `tsconfig.app.json:8–20` | Env задаёт транспорт/config, но не включает manual sync module/method или старую catalog producer ветку. Alias `@/` ведёт в source. | + +### 3.2. Метод отбора и счёта + +Исследованы **12 кандидатов C01–C12**, отобранных по стоимости сопровождения +и риску ошибочного удаления: старая orchestration, связанные producer paths, +orphan DTO, frontend API-хвосты, публичная compatibility и защищённые live +owners. Число 12 — число строк Reachability Ledger, не результат analyzer. +Findings — 11: C01/C02 образуют одну deletion closure DEAD-001. + +Порядок: declaration → exports/DI/registration → все найденные callers → +реальный runtime entrypoint → external/public accessibility → контрпример. +Поиск применялся к source, затем отдельно tests/scripts/artifacts, чтобы +не принять direct test invocation за пользовательский путь. + +**Воспроизводимые search counts:** это количество matching lines, а не число +AST references; вывод вручную разделён по declarations/imports/calls. +Поиск не считается самостоятельным доказательством отсутствия callers. + +| Поиск и границы | Результат и независимая проверка | +| --- | --- | +| Q01, backend source/tests/scripts, команда ниже | `TbankSyncService` найден только в своём service, controller и module; `sync.request(` — 0 строк. Controller calls только `status`. Для `normalizeScreen(`: 1 declaration, 1 вызов в dead request, 9 строк вызовов в `tbank-screen.spec.ts`. DTO Accepted — 1 declaration, 0 consumers. | +| Backend: owner-aware поиск `this.discovery.*` в instruments и чтение обоих catalog services | 6 live delegation call sites: search/detail/byTicker у stocks и bonds. Ни один не вызывает `read/request/refresh` напрямую. `discovery.refresh` найден в manual dispatcher. В discovery owner три вызова `this.read` находятся после online-return ветки и получают `observe=true`; `this.request` — в `read(!observe)` и `refresh`. | +| Q02, frontend source/packages/Storybook/scripts, команда ниже | `observe()` — 2 строки, обе в `resource-query.test.tsx:63,167`; production вызовов 0. `useScreenSync(` — 8 production call lines, 2 test call lines, 1 declaration. Все 8 VM прочитаны целиком: return hook не захватывается. | +| Q03, frontend source/packages/Storybook, команда ниже | `tbank-sync` — 2 строки JSON snapshot и 1 generated route declaration; runtime call site 0. Литерал `sync: "observe"` — 1 строка в `observationKey`. Accepted DTO — 0. | +| Q04, backend OpenAPI artifact, команда ниже | 2 matching lines: GET path и operationId; Accepted DTO отсутствует. Artifact не регенерировался, поэтому свежесть runtime parity — `UNVERIFIED`; controller/source metadata дополнительно проверены. | + +Команды выполнены с `workdir` соответствующего checkout, без `cd` и записи +output-файлов. Exact regex приведены вне таблицы, чтобы Markdown не подменял +alternation и не создавал лишние колонки. + +```sh +# Q01 — backend +rg -n -c 'TbankSyncService|sync\.request\(|normalizeScreen\(|TbankSyncAcceptedDto|discovery\.(read|request|refresh)\(' src test scripts --glob '*.{ts,cjs,mjs}' +# Q02 — frontend +rg -n -c '\.observe\(|\["observe"\]|\[.observe.\]|\buseScreenSync\(' src packages .storybook scripts --glob '*.{ts,tsx,cjs,mjs}' --glob '!**/generated/**' --glob '!**/dist/**' +# Q03 — frontend +rg -n -c 'tbank-sync|TbankSyncAcceptedDto|sync: "observe"' src packages .storybook --glob '!**/dist/**' +# Q04 — backend +rg -n -c 'TbankSyncAcceptedDto|TbankSyncController_status|/api/v1/tbank-sync' openapi.json +``` + +**Dynamic checks:** source search по `ModuleRef`, reflection/getMetadata, +computed `request/observe`, `import()/require`, factory/alias DI и cron/timers +не выявил alternate entrypoint кандидатов на DELETE. Проверены явные +`@Injectable`, `@Module`, `@Controller`, `@Get`, `ApiExtraModels`, `useExisting` +и module exports. В backend `useFactory` используется для logging/auth, +`TBANK_ACCESS` alias — для instruments (`instruments.module.ts:24`); +кандидатов он не dispatch-ит. В frontend dynamic imports представлены явными +router chunks; Storybook собирает stories по glob. No-match ограничен +этими checkout: неизвестный deployed consumer не исключён. + +### 3.3. Coverage и ограничения выборки + +- Сфокусированы investments synchronization/catalog и frontend consumers, + где после перехода архитектуры есть сильные гипотезы с большими closures. +- Банковские/auth routes проверены на существование root registration; + correctness их бизнес-логики здесь не исследовалась. +- Tests прочитаны как callers и deletion dependencies. Тестовое покрытие + проекта, mutation score и качество всех assertions не оценивались. +- Specs/docs рассматриваются как intended behavior и compatibility context, + а не доказательство текущего исполнения. Архивы не являются candidates. +- Generated/dist, coverage, реальные `.env`, DB, deployment logs и секреты + не служили source call graph; lockfiles/dependency removal вне scope 04. +- Runtime trace, bundle size, CI minutes, tokens и размер потенциального diff + не измерены. Статическая карта не доказывает отсутствие внешних клиентов. + +## 4. Findings + +Severity actionable findings отражает текущую engineering cost, а не обещанную +экономию. В KEEP/INVESTIGATE Severity обозначает риск ошибочного удаления; +эти строки не являются подтверждёнными дефектами и не включены в actionable +severity totals. Ни один finding не предлагает исправление в этом аудите. + +### DEAD-001 — Удалить недоступную manual screen orchestration вместе с closure + +- **Decision:** DELETE. +- **Severity:** P2. +- **Confidence:** High. +- **Evidence:** `backend/src/modules/investments/tbank-sync/tbank-sync.service.ts:41–51,54–311`; `tbank-sync.controller.ts:31–41`; `tbank-sync.module.ts:23–25`; `tbank-sync.repository.ts:62–109,135–169`; `tbank-screen.ts:8–122`; `tbank-screen.spec.ts:1–75`. Последние пять путей относительны той же `backend/src/modules/investments/tbank-sync/`. +- **Claim:** у `request → normalizeScreen → reserve → setImmediate(dispatch)` + нет production entrypoint; соответственно manual child planner и writer + progress поддерживаются без пользовательского сценария. +- **Current mechanism / runtime consumer:** service зарегистрирован Nest, + но его единственный production consumer — constructor controller, + вызывающий `status`. `request` не имеет route/scheduler decorator, + service не экспортирован TbankSyncModule, reflection dispatcher не найден. + `normalizeScreen` фактически вызывают direct unit tests и недоступный request. +- **Impact / actual cost:** лишняя orchestration source owners, ручные + reservations, field normalization и отдельный тестовый сценарий; + **Impact unmeasured**. Runtime RPC savings не заявляются: путь уже недоступен. +- **Why now:** текущие source entrypoints и + `specs/openspec/specs/tbank-data-sync/spec.md:9–25` передали обновление hourly + owner и запрещают manual commands. Это не предположение только по ADR. +- **Minimum change:** удалить request/dispatch, `reservations`, exclusive + DI/imports/constants, repository `reserve/progress`, normalization и её + tests. Сохранить весь status reader и его compatibility types, пока DEAD-006 + не решён; сохранить shared source owners и operations planner. +- **Guarantees at risk:** access/revision checks внутри живого status, + source CAS, hourly lifecycle, shared metadata/portfolio/candle owners. + Не удалять эти owners вместе с исключёнными constructor injections. +- **Counterarguments / trade-offs:** создание service Nest не равно вызову + всех его методов. Возвращение manual sync было бы новым продуктовым решением; + restore из Git возможно. Старые screen work могут ещё читаться status: + поэтому reader не входит в DELETE. +- **Verification or rollback:** после отдельного cleanup — type/DI proof, + отсутствие POST command в configured OpenAPI, HTTP status compatibility, + существующие local-read/hourly scenarios. Rollback — вернуть code-only + closure одним revert; DB/schema не менять. +- **Effort:** M. +- **Related finding IDs:** DEAD-002, DEAD-006, DEAD-008, DEAD-009, DEAD-011. + +### DEAD-002 — Удалить недоступный persisted catalog discovery producer + +- **Decision:** DELETE. +- **Severity:** P2. +- **Confidence:** High. +- **Evidence:** `backend/src/modules/investments/instruments/application/catalog-discovery.service.ts:39–64,66–86,143–149,220–227,340–496`; `instruments/stocks/application/stock-catalog.service.ts:21–31,59–83`; `instruments/bonds/application/bond-catalog.service.ts:24–34,58–82`; `instruments/infrastructure/catalog-discovery.repository.ts:341–464`; `backend/test/tbank-catalog-discovery.e2e-spec.ts:85–118,179–196,231–244,270–299`. Три укороченных `instruments/` пути относительны `backend/src/modules/investments/`. +- **Claim:** producer `request/refresh/drain` production-недостижим в текущем + graph. После DEAD-001 исчезает даже его единственный caller из недоступной + ветки. HTTP `sync=observe` сохраняет reader, но не включает этот writer. +- **Current mechanism / runtime consumer:** ordinary search вызывает + `onlineSearch`; ordinary detail/byTicker выходят через online ветки. + Только observe-ветки доходят до `read`, всегда с `true`, и потому пропускают + `if (!observe) request`. Единственный caller `refresh` в production source + — недоступный manual dispatcher. + Прямые тесты `service.read(..., false/default)` и `service.request(...)` + реально запускают queue, но не являются HTTP consumers. +- **Impact / actual cost:** producer map/queue, forced joins, completions, + logger dependency и отдельная persisted discovery publication поддерживают + прежнее поколение поведения. **Impact unmeasured**; это сокращение maintenance, + не устранение измеренного fan-out живого online search. +- **Why now:** онлайн-входы уже существуют; persisted discovery writer не + обслуживает ни их, ни hourly events. Online-only requirement независимо + прочитан: `specs/openspec/specs/online-instrument-information/spec.md:8–17`. +- **Minimum change:** после DEAD-001 удалить Producer/state/request/refresh/drain, + dead `read(!observe)` branch и только exclusive repository `publish`. + Сохранить reader `read/work`, `enrichment`, normalization/selection и online + error projection. Не удалять repository `reserve`, `finish`, `readSources`, + `publishSource`: их использует live CatalogEnrichmentService. +- **Guarantees at risk:** shared source writes, CAS/invalid-token isolation, + legacy observe read, UID/FIGI identity и candle FK. Schema и данные не входят + в эту code-only deletion. +- **Counterarguments / trade-offs:** public `instruments.api.ts:2` экспортирует + provider внутри приложения, а не published npm method contract; + `backend/package.json:6,11–38` — private application без CLI producer. + HTTP compatibility может оправдать persisted reader (DEAD-007), но не writer, + которого эти routes не вызывают. Прямые tests доказывают механизм, + не пользовательскую reachability. +- **Verification or rollback:** переназначить/удалить direct producer-only + cases в `test/tbank-catalog-discovery.e2e-spec.ts`; сохранить проверку живого + online selection/whitelist и legacy reader через существующие HTTP suites. + Проверить unchanged `publishSource`/candles на isolated SQLite в cleanup PR. + Rollback — code-only revert без DB migration. +- **Effort:** M. +- **Related finding IDs:** DEAD-001, DEAD-007, DEAD-010, DEAD-011. + +### DEAD-003 — Удалить orphan `TbankSyncAcceptedDto` + +- **Decision:** DELETE. +- **Severity:** P3. +- **Confidence:** High. +- **Evidence:** `backend/src/modules/investments/tbank-sync/tbank-sync.dto.ts:212–218`; `tbank-sync.controller.ts:13–27,34–40`; `backend/test/openapi-contract.e2e-spec.ts:27–55`; `frontend/src/shared/api/spec/openapi.json:5102–5123`. Controller путь относителен той же backend sync directory. +- **Claim:** DTO command acceptance объявлен, но не используется controller, + service, ApiExtraModels, scripts или публикуемым snapshot. +- **Current mechanism / runtime consumer:** module DTO загружается живым GET, + и decorators этой orphan class регистрируют её собственные metadata. + Но нет consumer, который включал бы её в response schema или исполнял её + validation. Status class — отдельный live consumer `investmentScreenSchema`. +- **Impact / actual cost:** небольшой устаревший export рядом с действующими + response DTO; **Impact unmeasured**. Его наличие может создавать ложное + впечатление сохранённой POST capability. +- **Why now:** POST отсутствует в actual controller и snapshot; existing + contract test явно ожидает отсутствие `/api/v1/tbank-sync` command route. +- **Minimum change:** удалить только class `TbankSyncAcceptedDto`. + `InvestmentScreenDto`, screen schema, derived DTO и `TbankSyncStatusDto` + остаются нужны public status response. +- **Guarantees at risk:** accidental изменение GET schema при избыточном + удалении shared schema; сама Accepted class не является опубликованным DTO. +- **Counterarguments / trade-offs:** `export class` и decorators не равны + published contract; generator следует зарегистрированным routes. + Artifact parity со свежей сборкой здесь не запускалась — `UNVERIFIED`. +- **Verification or rollback:** targeted OpenAPI schema comparison при cleanup; + Accepted absent, Status и его discriminator unchanged. Rollback — вернуть class. +- **Effort:** S. +- **Related finding IDs:** DEAD-001, DEAD-006. + +### DEAD-004 — Удалить only-test frontend `observe` API + +- **Decision:** DELETE. +- **Severity:** P3. +- **Confidence:** High. +- **Evidence:** `frontend/src/shared/api/resource-query.ts:69–82,104–127,144`; `src/shared/api/resource-query.test.tsx:40–71,139–173`; `src/shared/api/index.ts:12–15`; `frontend/package.json:2–5`; `src/features/tbank-connection/model/screen-sync.ts:28–50`. Укороченные `src/` пути относительны frontend. +- **Claim:** `observationKey` и возвращаемый `observe` не используются + production callers; они поддерживают отдельный transport режим только для + direct tests. Удаление frontend callback не означает удаления backend query. +- **Current mechanism / runtime consumer:** live `useResourceQuery` вызывается + entity hooks, но `observe()` — только tests `:63,167`. Callback вручную + переписывает queryKey для transport, cancel/query/select policy; + production screen timer вызывает обычный `refetch`. +- **Impact / actual cost:** дополнительный путь cancel/cache replacement и + его own tests без UI consumer; **Impact unmeasured**. Bundle/runtime эффект + не измерен, tree-shaking не проверялся. +- **Why now:** текущий runtime использует resource GET и `refetch`; источник + attempt state — metadata, а не orchestration notification. +- **Minimum change:** убрать `observationKey`, `observe`, его return member + и exclusive imports (`QueryFunctionContext`, локальный import session snapshot). + Удалить первый observe-only test; race-сценарий `:139–173` перенести на живой + cancellation/refetch boundary, если он нужен для общей гарантии. +- **Guarantees at risk:** не менять `useAuthenticatedQuery`, + `invalidateResourceAccess`, projected data/meta, canonical query identity, + session switching и обычный `refetch` envelope. +- **Counterarguments / trade-offs:** `.ai/conventions/data.md:61–63` всё ещё + описывает наблюдение, но правило не создаёт отсутствующий production caller. + SPA private, feature не public package; dynamic/router/story consumers + проверены. Backend `sync=observe` остаётся доступен внешнему HTTP клиенту; + наличие реального такого клиента здесь не подтверждено. +- **Verification or rollback:** существующие resource tests `:72–138,174–210` + и auth/connection boundary tests при cleanup; synthetic MSW, без реальной сети. + Rollback — вернуть callback/tests, никакой data migration. +- **Effort:** S. +- **Related finding IDs:** DEAD-005, DEAD-007, DEAD-011. + +### DEAD-005 — Сократить unused facade `useScreenSync`, сохранив live timer + +- **Decision:** SIMPLIFY. +- **Severity:** P3. +- **Confidence:** High. +- **Evidence:** `frontend/src/features/tbank-connection/model/screen-sync.ts:1–18,22–61`; `screen-sync.test.tsx:21–80` в той же directory; `src/pages/accounts/ui/AccountsPage/AccountsPage.vm.ts:21–24`; `src/pages/account/ui/AccountPage/AccountPage.vm.ts:78–84`; `src/pages/account-operations/ui/AccountOperationsPage/AccountOperationsPage.vm.ts:96–99`; `src/pages/account-events/ui/AccountEventsPage/AccountEventsPage.vm.ts:67–75`; `src/pages/account-income/ui/AccountIncomePage/AccountIncomePage.vm.ts:66–69`; `src/pages/bonds/ui/BondsPage/BondsPage.vm.ts:12–13`; stock/bond VM `:45–55`. Все `src/` пути относительны frontend; полные stock/bond пути перечислены в Ledger C06. +- **Claim:** `_scope`, returned manual refresh/retry/status facade и требуемые + только этой facade resource fields не имеют production consumers. + Timer `:28–35` имеет восемь route-reachable callers и не является dead code. +- **Current mechanism / runtime consumer:** все 8 VM вызывают hook как отдельное + expression statement. Возвращённые `refresh/refreshing/retry/sourceError/ + transportError` не попадают в VM/view. Scope не читается даже самим hook. + Единственные вызовы returned commands — прямые tests `:50,78`. +- **Impact / actual cost:** caller собирает screen descriptor, hook создаёт + неиспользуемые commands/status, тип навязывает `queryKey/meta/isError/isFetching`. + **Impact unmeasured**; allocations/время render не измерены. +- **Why now:** hook уже стал периодическим GET reader, но сохранил facade + прежней screen orchestration. +- **Minimum change:** убрать scope аргумент и все восемь descriptor objects; + убрать only-test returned commands/status и exclusive fields/imports. + Оставить `resourcesRef`, актуализацию resources, native interval и cleanup. + Перепривязку timer к Query options этот finding не требует. +- **Guarantees at risk:** нельзя удалить сам hook вызов: он обеспечивает + reread открытого экрана. Нельзя убрать cleanup или обновление resourcesRef: + смена route/filter должна читать актуальные queries. +- **Counterarguments / trade-offs:** тесты refresh/retry действительно исполняют + callbacks, но не делают их доступными пользователю. UX потребность в retry + может потребовать нового подключения к view; сейчас такого consumer нет. + Таймерная политика живого пути — отдельный audit 02/07 вопрос. +- **Verification or rollback:** удалить direct facade-only cases, + проверить существующий hook с fake time: reread актуального resource и + прекращение после unmount. Page behavior и GET-only boundary сохранить. + Rollback — вернуть facade/caller descriptors без изменений данных. +- **Effort:** S. +- **Related finding IDs:** DEAD-004, DEAD-009. + +### DEAD-006 — Статусный GET и старые `screen:` rows требуют compatibility proof + +- **Decision:** INVESTIGATE. +- **Severity:** P2. +- **Confidence:** High. +- **Evidence:** `backend/src/modules/investments/tbank-sync/tbank-sync.controller.ts:21–41`; `tbank-sync.service.ts:313–391`; `tbank-sync.repository.ts:111–133`; `frontend/src/shared/api/spec/openapi.json:5102–5185`; `backend/test/openapi-contract.e2e-spec.ts:30–55`; `docs/architecture/http-api.md:46–52`. Укороченные sync пути относительны backend sync directory. +- **Claim:** GET `/api/v1/tbank-sync/{id}` локально не вызывается SPA, но + зарегистрирован, авторизован и опубликован. Нельзя назвать его confirmed dead + или удалить screen reader вместе с недоступным writer. Наличие реальных + внешних consumers — UNVERIFIED; High относится к reachability. +- **Current mechanism / runtime consumer:** любой авторизованный HTTP consumer + может вызвать controller → `status`. Reader обслуживает `screen:` intent и + source works; access/revision проверяется до и после observation. + Старые screen work могли сохраниться от предыдущей версии приложения. +- **Impact / actual cost:** retained compatibility несёт reader/types/schema + complexity; **Impact unmeasured**. Ошибочное удаление — breaking response/route. +- **Why now:** writer можно удалить отдельно, оставив минимальную проверенную + границу совместимости. Deadline поддержки в прочитанных источниках не найден. +- **Minimum change:** сейчас не удалять endpoint/reader/derived DTO. Получить + список поддерживаемых consumers и решить срок поддержки; затем отдельно + согласовать withdrawal route либо поддержку только source status. +- **Guarantees at risk:** user/connection ownership, old work observation, + stable scope/date discriminator и публичная wire compatibility. +- **Counterarguments / trade-offs:** docs говорит, что command/status не текущая + AS-IS модель; это подтверждает legacy intent, но не отсутствие live HTTP route + и external clients. Отсутствие frontend calls — слабое отрицательное evidence. +- **Verification or rollback:** самый дешёвый proof — ответ owner API о + consumers плюс существующая обезличенная access-log выборка и aggregate count + `screen:` work из disposable DB copy. Ничего из этого здесь не получено. + Rollback будущего изменения — возврат endpoint/schema до destructive migration. +- **Effort:** M. +- **Related finding IDs:** DEAD-001, DEAD-003, DEAD-007. + +### DEAD-007 — Backend observe/persisted reference compatibility внешне достижима + +- **Decision:** INVESTIGATE. +- **Severity:** P2. +- **Confidence:** High. +- **Evidence:** `backend/src/modules/investments/instruments/stocks/presentation/stock.controller.ts:58–82,112–144`; `instruments/bonds/presentation/bond.controller.ts:55–89,102–114`; `instruments/application/catalog-discovery.service.ts:84–141,168–217,251–293`; `backend/src/modules/investments/investment-resource.dto.ts:15–24`; `docs/architecture/http-api.md:50–52`; `docs/adr/019-hourly-reads-and-online-instrument-reference.md:22–26`. Укороченные instruments пути относительны `backend/src/modules/investments/`. +- **Claim:** persisted catalog/detail/ticker read branches являются legacy + public contract, а не confirmed dead. Из DELETE old producer не следует + DELETE observe readers или shared instrument storage. Нужность долгой + поддержки — UNVERIFIED; High относится к подтверждённому механизму. +- **Current mechanism / runtime consumer:** query DTO допускает `sync=observe`; + controllers переводят его в boolean, передают catalog services, которые + достигают reader branches. Это реальный внешний вход даже без SPA callback. +- **Impact / actual cost:** две модели справочного чтения рядом — online и + persisted compatibility; **Impact unmeasured**. API withdrawal требует + consumer transition, а blanket data purge рискует candle coverage. +- **Why now:** online-only intended behavior уже принят, но compatibility + сохранена source/HTTP metadata. Нужно ограничить именно поддержку старого + reader, а не удалять shared business/persistence primitives по названию. +- **Minimum change:** сохранить текущий wire parameter/reader; отдельно решить + endpoint-specific retirement. При подтверждённой ненужности можно перестать + направлять reference reads в old branches, затем удалить их closure. + Не удалять общую `InvestmentReadQueryDto` для всех ресурсов автоматически. +- **Guarantees at risk:** passive observation, distinction unknown/empty/ + ambiguous lookup, account access, precision, persisted calendars и candles. +- **Counterarguments / trade-offs:** requirement запрещает долговременную + самостоятельную reference copy, но не доказывает безопасность удаления + `TbankInstrument` identity: у неё cascade FK на candles/source rows + (`backend/prisma/schema.prisma:148–229`). Внешние consumers не проверены. +- **Verification or rollback:** consumer list + metadata/access logs без + реальных API calls; agreed contract transition и isolated DB/FK proof перед + любым data removal. Rollback code routing возможен, восстановление данных + потребует backup; поэтому data purge сейчас не предложен. +- **Effort:** M. +- **Related finding IDs:** DEAD-002, DEAD-004, DEAD-006, DEAD-010, DEAD-011. + +### DEAD-008 — Planner и interval helpers живы через Account Ledger + +- **Decision:** KEEP. +- **Severity:** P2. +- **Confidence:** High. +- **Evidence:** `backend/src/modules/investments/investment-sync-plan.ts:67–140,271–328`; `operations/application/brokerage-operations.service.ts:16–19,69–85,120–150,203–209`; `operations/application/brokerage-operations-sync.service.ts:9,106–112`; `investment-sync-plan.spec.ts:100–134`. Укороченные пути относительны `backend/src/modules/investments/`. +- **Claim:** нельзя удалить `planInvestmentScreen`, `accountHistory`, + `ledgerRange` или их файл вслед за manual sync: они обслуживают живой Ledger. +- **Current mechanism / runtime consumer:** operations controller → read → + planner для `screen='operations'`, membership gating и selected interval → + `sync.readPlan`. History/read metadata также вызывает accountHistory; + interval helpers используются независимо от dead request. +- **Impact / actual cost:** broad screen vocabulary содержит прежние ветки, + но ядро живо; **Impact unmeasured**. Whole-file deletion ломает получение + первого отсутствующего интервала, границы времени и history state. +- **Why now:** это конкретный counterexample прежней гипотезе «screen planner + принадлежит только manual orchestration». +- **Minimum change:** сохранить live planner/helpers. После DEAD-001 допустим + отдельный узкий reachability pass по вариантам screen, если это стоит усилий; + автоматическое удаление всех planner tests недопустимо. +- **Guarantees at risk:** account scope, membershipKnown gating, inclusive/ + exclusive end, historical boundaries, нерасширение произвольного интервала. +- **Counterarguments / trade-offs:** немало branches после removal manual writer + станет only-test reachable. Это повод сузить конкретные варианты с equivalence + proof, а не удалить финансовые interval rules. В 04 это не generic refactor. +- **Verification or rollback:** current ledger HTTP/local-read/interval tests + при любом будущем narrowing; сравнить selected intervals, unknown membership, + all/account и date-only/instant boundaries. Сейчас deletion отсутствует. +- **Effort:** S. + Здесь и в других KEEP effort относится к сохранению deletion boundary; + narrowing/migration оцениваются отдельно. +- **Related finding IDs:** DEAD-001, DEAD-009. + +### DEAD-009 — Hourly scheduler является runtime owner, а не legacy provider + +- **Decision:** KEEP. +- **Severity:** P2. +- **Confidence:** High. +- **Evidence:** `backend/src/app.module.ts:14,35`; `src/modules/investments/tbank-sync/tbank-sync.module.ts:24–25`; `tbank-hourly.scheduler.ts:28–87,119–175`; `tbank-sync.repository.ts:174–243,245–286`; `compose.yaml:32–35`; `specs/openspec/specs/hourly-investment-sync/spec.md:8–40`. Укороченные sync пути относительны backend sync directory. +- **Claim:** отсутствие HTTP callers `tick` или прямых тестов не делает + scheduler dead; Nest lifecycle запускает его native timer. +- **Current mechanism / runtime consumer:** `onModuleInit → setInterval(tick)` + читает connections, резервирует work и через setImmediate запускает owners + membership/portfolio/operations/events; `onModuleDestroy` очищает timer. + Метаданные ресурсов читают repository hourly result. +- **Impact / actual cost:** runtime coordination необходима для обновления + данных при закрытой SPA; **Impact unmeasured**. Удаление отключило бы + normal personal refresh. Корректность lifecycle отдельно не сертифицируется. +- **Why now:** whole-folder cleanup `tbank-sync/` выглядел бы коротким, + но смешивает dead manual writer с current root entrypoint. +- **Minimum change:** сохранить scheduler, registration, repository hourly + методы и DTO; убирать только manual command closure DEAD-001. +- **Guarantees at risk:** запуск без экрана, single-attempt fence, generation, + остановка timer, section result visibility и отсутствие catch-up очереди. +- **Counterarguments / trade-offs:** наличие lifecycle hook не доказывает + правильность всех failure branches. Подозрения на их correctness направлены + владельцу audit 13/08, а не использованы для рекомендации DELETE. +- **Verification or rollback:** перед cleanup DI/module proof и существующий + source-owner behavior; scheduler error/time scenarios — отдельные проверки + при изменении его поведения. Сейчас механизм сохраняется. +- **Effort:** S. +- **Related finding IDs:** DEAD-001, DEAD-005, DEAD-008, DEAD-011. + +### DEAD-010 — Candle loader и staging не удаляются вместе с manual force caller + +- **Decision:** KEEP. +- **Severity:** P2. +- **Confidence:** High. +- **Evidence:** `backend/src/modules/investments/instruments/application/candle.service.ts:101–124,206–261,264–326`; `instruments/stocks/application/stock-market.service.ts:54–64`; `instruments/bonds/application/bond-market.service.ts:58–68`; `instruments/infrastructure/candle.repository.ts:137–228,257–284`; `backend/test/tbank-candles-persistence.e2e-spec.ts:117–200,486–501`; `specs/openspec/specs/tbank-data-sync/spec.md:16–22`. Укороченные instruments пути относительны `backend/src/modules/investments/`. +- **Claim:** `CandleService.request`, queue/stage/publication и coverage + runtime-required: read запускает первое получение отсутствующего диапазона. +- **Current mechanism / runtime consumer:** GET stock/bond candles → market + service → `history.read` → при отсутствии coverage `request → drain/load`. + Отдельно enrichment.ensureDetail использует shared sources. Manual force + — не единственный caller source owner. +- **Impact / actual cost:** механизм обеспечивает запрошенный диапазон, + точные decimal strings, empty coverage и transactional publication; + **Impact unmeasured**. Удаление owner сделает cold candles недоступными. +- **Why now:** соседство с manual request повышает риск удалить весь loader, + его source repository или associated tests без проверки hidden read caller. +- **Minimum change:** сохранять ordinary request/staging/coverage. Нужность + отдельно forced/priority вариантов после DEAD-001 **UNVERIFIED**: нужен + variant-level pass, в этом отчёте whole-loader deletion отвергнута. +- **Guarantees at risk:** точность, rollback неполного reconciliation, + absence ≠ empty, unchanged coverage вне окна и connection fencing. +- **Counterarguments / trade-offs:** часть direct force tests относится к old + command variants; это не делает всю suite или transactional primitive legacy. + Default method caller в `read:121–123` — положительное доказательство жизни. +- **Verification or rollback:** при любой variant deletion оставить ordinary + period, reopen, exact decimals, closed empty и failed publication cases; + isolated SQLite proof, не mock транзакции. Сейчас deletion не требуется. +- **Effort:** S. +- **Related finding IDs:** DEAD-001, DEAD-002, DEAD-007, DEAD-011. + +### DEAD-011 — Shared source CAS, identity и FK защищают живые use cases + +- **Decision:** KEEP. +- **Severity:** P1. +- **Confidence:** High. +- **Evidence:** `backend/src/modules/investments/instruments/application/catalog-enrichment.service.ts:64–87,164–225,545–579`; `instruments/infrastructure/catalog-discovery.repository.ts:154–179,184–249,280–339,466–487`; `backend/src/modules/investments/upcoming-events/upcoming-events.service.ts:182–239`; `backend/src/modules/investments/portfolio/application/portfolio.service.ts:264–283`; `backend/prisma/schema.prisma:148–229,232–262`. Укороченные instruments пути относительны `backend/src/modules/investments/`. +- **Claim:** общая `CatalogDiscoveryRepository` не является legacy repository + целиком. `reserve/finish/publishSource`, attempt/initiator fences и instrument + identities остаются runtime-required после removal discovery producer. +- **Current mechanism / runtime consumer:** hourly `events.refresh → readSources` + запускает enrichment source work; candles.ensureDetail делает то же для + dependency. Enrichment.reserve/drain обращается в shared repository, где + transaction проверяет origin и observer attempt до публикации public payload. + Live portfolio owner также вызывает metadata.request для своих positions. +- **Impact / actual cost:** защита от late/superseded/invalid-token publication + и сохранение data relationships; **Impact unmeasured**. Удаление ради меньшего + числа проверок может изменить чужую source revision/timestamp; удаление + instrument identity каскадирует source/candle/coverage rows. +- **Why now:** это важная граница closure DEAD-002: method `publish` exclusive + старому discovery producer, method `publishSource` — живому enrichment. +- **Minimum change:** сохранять shared owner, registration, transaction/CAS, + identity и schema. Code-only deletion может затронуть только exclusive + discovery writer `publish`, а не весь repository или таблицы. +- **Guarantees at risk:** connection isolation, conditional publication, + invalid-token boundary, identity reuse, candle data и PII-safe projection. +- **Counterarguments / trade-offs:** online-only reference requirement не + делает DB identity disposable. Название repository и один прежний caller + не определяют его реальную область ответственности. Data migration, если + потребуется, должна быть совместимой и отдельно доказанной. +- **Verification or rollback:** targeted source/candle persistence и late + connection-switch proof при cleanup; перед destructive migration — + disposable backup/restore и FK проверка. В этом аудите DB не открывалась. +- **Effort:** S. +- **Related finding IDs:** DEAD-001, DEAD-002, DEAD-004, DEAD-007, DEAD-009, DEAD-010. + +## 5. DELETE / SIMPLIFY / KEEP / INVESTIGATE + +| Decision | IDs | Подтверждённая граница | +| --- | --- | --- | +| DELETE | DEAD-001 | Только manual writer/reservation/progress/normalization closure; status и live owners сохраняются. | +| DELETE | DEAD-002 | Только persisted discovery producer и exclusive repository publish; shared source repository и observe reader сохраняются. | +| DELETE | DEAD-003 | Только Accepted DTO; status screen schema сохраняется. | +| DELETE | DEAD-004 | Только frontend observe callback/transport key rewrite; auth/access/refetch и backend API сохраняются. | +| SIMPLIFY | DEAD-005 | Scope/returned command facade убрать; timer/current resources/cleanup сохранить. | +| KEEP | DEAD-008 | Live Ledger planner, history и interval boundaries. | +| KEEP | DEAD-009 | Registered hourly scheduler, durable work и lifecycle. | +| KEEP | DEAD-010 | Candle first-range load, staging, rollback и coverage. | +| KEEP | DEAD-011 | Shared enrichment repository, CAS, identity/FK и metadata source owner. | +| INVESTIGATE | DEAD-006 | Public status GET/old screen rows: consumers/retirement неизвестны. | +| INVESTIGATE | DEAD-007 | Public backend observe/persisted reference readers: transition и безопасная data граница неизвестны. | + +FIX не назначен: correctness живых путей не превращалась в параллельный аудит. + +### 5.1. Special result — Reachability Ledger + +`confirmed dead` означает отсутствие функционального production consumer в +проверенном source graph; import metadata registration при этом может исполняться. +`only-test reachable` означает реальный тестовый вызов при отсутствии production +entrypoint. External-use неизвестность всегда указана отдельно. + +| Candidate | Reachability evidence: declaration → exports/registration → callers → runtime entrypoints → external accessibility | Dynamic/external risks | Affected tests/docs/config | Minimum safe deletion | Rollback | Confidence | +| --- | --- | --- | --- | --- | --- | --- | +| C01 — `TbankSyncService.request/dispatch`; **confirmed dead**; DEAD-001 | Declaration `backend/src/modules/investments/tbank-sync/tbank-sync.service.ts:54–78`; DI `tbank-sync.module.ts:24`, только class construction → controller calls `status:40`, command callers 0 → no method decorator/timer → no command route, только GET. Все короткие пути в backend sync directory. | DI не auto-dispatch-ит methods; module exports только scheduler. Alternate computed/reflection/script invocation не найден. | Normalize tests C02; controller/status DTO остаются; intended boundary `specs/openspec/specs/tbank-data-sync/spec.md:9–25`. | Writer closure, не service целиком. | Code-only revert. | High | +| C02 — `normalizeScreen`, repo screen writers; **only-test reachable**; DEAD-001 | `backend/src/modules/investments/tbank-sync/tbank-screen.ts:9–11` export → import в dead service `:28,55` → 9 test call lines → no runtime entrypoint → не public HTTP validator текущего GET. Repo `reserve:62–109/progress:135–169` callers только request/dispatch. | `InvestmentScreenDto` жив через Swagger/status; нельзя удалить его по association. Helpers не имеют top-level work/timer. | `tbank-screen.spec.ts:1–75` только manual descriptor behavior; status schema tests сохранить. | Helper/test file и exclusive repo methods удалить после C01. | Вернуть closure. | High | +| C03 — `CatalogDiscoveryService.request/refresh/drain`; **only-test reachable + dead-parent reachable**; DEAD-002 | `backend/src/modules/investments/instruments/application/catalog-discovery.service.ts:340–496` → provider/API registration `instrument-sources.module.ts:18–21`, `instruments.api.ts:2` → public methods search/detail/byTicker, но online return или observe=true reader → HTTP не попадает в producer; dead manual refresh `tbank-sync.service.ts:197`. | Живые exported provider/HTTP reader не равны live producer. No lifecycle writer hook; setImmediate только внутри недоступного request. | Direct service cases `backend/test/tbank-catalog-discovery.e2e-spec.ts:85–322`; update factory/selection checks, reader compatibility. | Producer state/methods + exclusive repo `publish`, оставить shared `reserve/finish/publishSource`. | Code-only revert, rows/schema untouched. | High | +| C04 — `TbankSyncAcceptedDto`; **confirmed dead export**; DEAD-003 | `backend/src/modules/investments/tbank-sync/tbank-sync.dto.ts:212–218` → export with decorators → 0 consumers/ApiExtraModels → no POST response metadata → отсутствует в обоих известных OpenAPI artifacts. | Class decorators исполняются на import, но не создают route. Shared `investmentScreenSchema` используется Status. Fresh parity не запускалась. | `backend/test/openapi-contract.e2e-spec.ts:30–55`; no Accepted-only test/config consumer. | Только orphan class. | Вернуть class. | High | +| C05 — frontend `observe/observationKey`; **only-test reachable**; DEAD-004 | `frontend/src/shared/api/resource-query.ts:71,104,144` → hook public index `src/shared/api/index.ts:12–15` → tests `resource-query.test.tsx:63,167` → production pages используют refetch → private SPA, не public package export. | Dynamic router imports и stories не используют callback; backend wire mode остаётся reachable C09. | Два observe scenarios; сохранить successful-null/refetch/access tests. Convention data `:61–63` — intended old flow, не runtime. | Callback/helper/exclusive imports; safe race proof перевести на живой caller при необходимости. | Вернуть code/tests. | High | +| C06 — `_scope` и returned screen facade; **live timer / only-test commands**; DEAD-005 | `frontend/src/features/tbank-connection/model/screen-sync.ts:15–22,37–61` → feature `index.ts:1` → 8 VM call sites → все calls expression statements в lazy routed pages → callback exposure вне SPA не найден. | Удаление всего hook опасно: `:28–35` timer жив. Typed resource fields не означают usage после facade removal. | `screen-sync.test.tsx:22–80` direct commands; all page callers. | Только scope/unused commands/status/fields; live effect сохранить. | Вернуть facade/call signatures. | High | +| C07 — `planInvestmentScreen/accountHistory/ledgerRange`; **runtime required**; DEAD-008 | `backend/src/modules/investments/investment-sync-plan.ts:67,287,301` exports → operations source imports → `brokerage-operations.service.ts:80,120,203`, sync service `:106` → Account Ledger HTTP → real public read capability. | Частично legacy vocabulary не делает файл orphan. No DI требуется чистым exported functions. | `investment-sync-plan.spec.ts:100–134`, interval/ledger cases; historical screen cases отдельно проверить после C01. | Whole-file deletion запрещён; variant narrowing требует proof. | Not applicable: KEEP. | High | +| C08 — status GET и screen reader; **externally reachable/unknown + legacy documented contract**; DEAD-006 | `backend/src/modules/investments/tbank-sync/tbank-sync.service.ts:313–391` → `@Get(':id')` → controller `:40` → guarded HTTP + snapshot `frontend/src/shared/api/spec/openapi.json:5102–5185` → любой authorized consumer с work ID. | Local SPA calls 0; external consumers/old rows UNVERIFIED. Reflection не нужна: route явный. | Status DTO, screen discriminator, repository read/progress types, contract tests, old DB work. | Пока deletion запрещён; отдельное compatibility решение. | Вернуть route/schema; data не удалять. | High reachability | +| C09 — catalog `observe` reader branches; **externally reachable + legacy documented contract**; DEAD-007 | `backend/src/modules/investments/instruments/application/catalog-discovery.service.ts:84–141,168–217,251–293` → stock/bond catalog services → controller query.sync → runtime HTTP `?sync=observe` → snapshot query enum и docs compatibility. | External use/retirement UNVERIFIED. Pub DTO decorators и router сохраняют вход без frontend observe. | Legacy reference schemas/read tests, shared DTO, persisted data; online requirement `online-instrument-information/spec.md:8–17`. | Сначала consumer transition; не удалить общую storage identity/все query modes. | Code routing revert; data restore потребует backup. | High reachability | +| C10 — hourly scheduler; **runtime required**; DEAD-009 | `backend/src/modules/investments/tbank-sync/tbank-hourly.scheduler.ts:33–56` → providers/root import → Nest `onModuleInit` → interval.tick + setImmediate.walk → personal source owners, а не внешняя command API. | Lifecycle/reflection registration — positive entrypoint. Env feature switch не найден; single-process deployment `compose.yaml:34`. | Hourly resource DTO/repo + mapper/source tests, hourly spec; scheduler stubs не доказательство смерти. | KEEP provider/root import/durable hourly rows. | Not applicable: KEEP. | High | +| C11 — candle request/staging; **runtime required**; DEAD-010 | `backend/src/modules/investments/instruments/application/candle.service.ts:101–123,206–261` → source module exports → stock/bond market.history.read → controller candle GET → first missing range source work. | Dead manual force caller не отменяет ordinary read caller. Variant-level force нужность UNVERIFIED. | `backend/test/tbank-candles-persistence.e2e-spec.ts:117–200,486–501`; candle coverage/decimal tests; shared source/FK. | KEEP loader/repository; force variants не удалять без отдельного trace. | Not applicable: KEEP. | High | +| C12 — shared source repository/CAS/identity; **runtime required**; DEAD-011 | `backend/src/modules/investments/instruments/infrastructure/catalog-discovery.repository.ts:154–179,280–339,466–487` → DI `instrument-sources.module.ts:19` → enrichment request/publishSource/finish → hourly events and candle ensureDetail → resource HTTP consumes snapshots. | Имя `CatalogDiscovery` вводит в заблуждение; FK Cascade на instrument → candles/coverage `backend/prisma/schema.prisma:199,213,227`. Source origin/observer fence — live invariant. | Source/candle persistence, calendar consumers, Prisma schema; data purge не выполнялся. | KEEP shared repo/CAS/identity; exclusive discovery publish отделим C03. | Code rollback; destructive data rollback только backup. | High | + +**Полные production call sites C06:** + +| Page | Exact path:line-range | +| --- | --- | +| Accounts | `frontend/src/pages/accounts/ui/AccountsPage/AccountsPage.vm.ts:21–24` | +| Account | `frontend/src/pages/account/ui/AccountPage/AccountPage.vm.ts:78–84` | +| Operations | `frontend/src/pages/account-operations/ui/AccountOperationsPage/AccountOperationsPage.vm.ts:96–99` | +| Events | `frontend/src/pages/account-events/ui/AccountEventsPage/AccountEventsPage.vm.ts:67–75` | +| Income | `frontend/src/pages/account-income/ui/AccountIncomePage/AccountIncomePage.vm.ts:66–69` | +| Bonds | `frontend/src/pages/bonds/ui/BondsPage/BondsPage.vm.ts:12–13` | +| Stock | `frontend/src/pages/stock/ui/StockPage/StockPage.vm.ts:45–55` | +| Bond | `frontend/src/pages/bond/ui/BondPage/BondPage.vm.ts:45–55` | + +### 5.2. Special result — Deletion Closure Plan + +План предназначен для последующей отдельной реализации. В рамках аудита +ни один пункт не применён. Это method-level closure, а не whole-folder purge. + +| Candidate | Reachability evidence | Dynamic/external risks | Affected tests/docs/config | Minimum safe deletion | Rollback | Confidence | +| --- | --- | --- | --- | --- | --- | --- | +| C01/C02 / DEAD-001 | No request root; normalize/repo writers only under dead writer, Ledger planner live | Status route и old screen rows отдельно C08; service/controller DI сохраняются | `tbank-screen.ts`, `tbank-screen.spec.ts`; service imports/injections/fields; repo reserve/progress; `tbank-sync.module.ts` reviewed, сохраняет hourly. Existing docs hourly model уже не требует command; archived ADR/spec не менять. | Удалить request/dispatch и reservation field; helper/test files; repo screen reserve/progress; exclusive instrument/candle/metadata/portfolio/membership DI service dependencies. Оставить repository/access/operations, runId/startedAt/status и screen intent/progress types reader. | Revert только code/test deletion. | High | +| C03 / DEAD-002 | HTTP ordinary online; observe reader read(true); refresh only dead C01 | Source repository жив и backend observe externally reachable | Service imports `randomUUID`, logger, attempt type, missing-revision constant/state; constructor factory `backend/test/tbank-catalog-discovery.e2e-spec.ts:28–35`; producer-only cases; source module remains. Online selection/whitelist cases сохранить/перепривязать. Docs online intended model не требует producer; archives retain. | Удалить Producer/state/request/refresh/drain и request branch read; удалить только repository `publish:341–464`. Убрать exclusive logger constructor/import. Не удалять `CatalogDiscoveryService` provider целиком, shared repository registration, `reserve/finish/publishSource/readSources/workMany`, catalog selection, online client или schema. | Code/test revert; сохранённые reference rows остаются readable. | High | +| C04 / DEAD-003 | Declaration only, not in response/ApiExtraModels/artifacts | Status shared screen schema остаётся published | DTO class only; contract test existing. No script/config exclusive to Accepted найден. | Удалить `TbankSyncAcceptedDto:212–218`; сохранить соседние live classes/imports, которыми они пользуются. | Вернуть class. | High | +| C05 / DEAD-004 | 2 direct tests, no production observe symbol consumer | Auth/access effects и server query mode сохраняются | Helper/callback/imports/return member; tests `:40–71,139–173`; frontend data convention old observe paragraph requires owner follow-up, not archive deletion. Feature index `useResourceQuery` остаётся. | Удалить only-test observe path; не удалять resource hook или session snapshot implementation, используемую authenticated-query. Guard/cancellation race проверять через оставшийся live boundary. | Вернуть path/tests. | High | +| C06 / DEAD-005 | 8 callers ignore return, scope unused | Live timer — dangerous to remove automatically | 8 VM descriptor arguments; screen hook resource type; direct refresh/retry tests; mocked facade `frontend/src/pages/account-events/ui/AccountEventsPage/AccountEventsPage.test.tsx:15` пересмотреть по новым exports. Feature barrel остаётся. | Убрать scope/facade, narrowing inputs до использованных timer полей; оставить useEffect/ref/interval cleanup. Rename/move/new helper не требуются. | Вернуть signature/facade/caller descriptors. | High | +| C08/C09 / DEAD-006/007 | Explicit HTTP GET/query registration | **ОПАСНА автоматическая зачистка:** external compatibility и existing rows UNVERIFIED | Contract/source DTO/read tests и published frontend artifact; old rows и FK; potential API retirement notice | Не удалять до согласованного support/consumer решения. Получить evidence, затем отдельная route/reader closure. Destructive schema/data removal — отдельный совместимый шаг. | Restore routing/schema, не полагаться на revert для удалённых данных. | High reachability; deletion unverified | +| C07/C10/C11/C12 / DEAD-008–011 | Positive Ledger/lifecycle/candle/enrichment call traces | **ОПАСНА автоматическая зачистка:** live behavior, generation/CAS, rollback и cascade data loss | Planner/interval, hourly, source/candle/metadata tests; schema; module registration | KEEP; никакой whole-file/whole-folder/schema deletion по совпадению legacy vocabulary. | Не требуется без изменения; data migration отдельно. | High | + +**Важная closure-граница:** убрать injection из TbankSyncService не означает +убрать соответствующий provider из TbankModule/PortfolioModule/InstrumentSourcesModule. +Наличие status branch и shared repository читается независимо от отсутствия writer. +Документальные references закрываются корректировкой текущего описания только +при реальном изменении поведения; история ADR/OpenSpec не стирается. + +## 6. Cost & benefit + +| IDs | Ожидаемый класс экономии | Фактическое измерение | Обязательные регрессии / безопасная граница | +| --- | --- | --- | --- | +| DEAD-001/002 | Maintenance, cognitive load, navigation/agent context; меньше old producer tests | not measured; нет готового diff, baseline CI timings или token профиля | Не менять public status/observe, source owners, CAS, hourly/candle/ledger; DI compilation + applicable isolated tests при cleanup. Runtime запросы уже недоступных paths не объявлены экономией. | +| DEAD-003 | Малое сокращение obsolete API surface | not measured | Status OpenAPI schema unchanged; no whole DTO/schema purge. | +| DEAD-004/005 | Maintenance, test surface, caller descriptors и unused facade | not measured | Session/access invalidation/refetch/cached envelope; periodic current-resource read и cleanup. Bundle/render выгода не заявлена. | +| DEAD-006/007 | Возможная будущая экономия compatibility maintenance | UNVERIFIED / not measured | Нужны consumer/retirement proof; shared identity/FK сохраняются. | +| DEAD-008–011 | Предотвращение ошибочного cleanup и регрессий | not measured | Сохранить финансовые interval/decimal/transaction/generation/lifecycle invariants. | + +**Ponytail-audit shortlist, от крупной closure к малой:** + +- `delete:` manual screen writer/reservation/normalization. Замена: ничего; + hourly source owners уже обслуживают текущий сценарий. + [`backend/src/modules/investments/tbank-sync/tbank-sync.service.ts:54–311`] +- `delete:` persisted discovery producer и exclusive publish. Замена: существующие + online paths; compatibility reader сохранить. + [`backend/src/modules/investments/instruments/application/catalog-discovery.service.ts:340–496`] +- `delete:` frontend only-test observe callback/key rewrite. Замена: существующий + ordinary refetch. + [`frontend/src/shared/api/resource-query.ts:69–127`] +- `shrink:` screen scope/returned facade. Замена: оставить только используемый + periodic GET effect. + [`frontend/src/features/tbank-connection/model/screen-sync.ts:15–61`] +- `delete:` orphan Accepted DTO. Замена: ничего. + [`backend/src/modules/investments/tbank-sync/tbank-sync.dto.ts:212–218`] + +**net:** сокращение строк **not measured**, dependencies **not assessed** +(направление 12). Оценка прежнего аудита `~550` не повторяется; ни процент, +ни скорость, ни CI minutes из inspection не выводятся. + +## 7. Top actions + +Максимум подтверждённых code cleanup actions — пять, а не искусственно десять. +Последовательность учитывает dependency closures и цену ошибки. + +### Quick wins + +1. **DEAD-003:** удалить одну orphan class; проверить Status schema. +2. **DEAD-004:** убрать only-test frontend observe path с сохранением живых + resource/auth guarantees и meaningful race boundary. +3. **DEAD-005:** удалить unused scope и facade, не удаляя periodic effect; + обновить 8 callers и проверить timer current-resource/unmount. + +### Более крупные closures с тестовым подтверждением + +4. **DEAD-001:** удалить manual writer плюс normalization/repo writer closure; + оставить status compatibility и live DI/source owners. +5. **DEAD-002:** после DEAD-001 удалить discovery producer и только exclusive + repository publish; перепривязать нужные online/shared-source assertions. + +**Решения, требующие compatibility/ADR или test proof:** DEAD-006/007 не являются +подтверждёнными шагами DELETE. API owner должен решить external support до +withdrawal route/query semantics. Отдельное ADR оправдано, только если меняется +сохранённый public contract/support policy; внутренние dead methods нового ADR +сами по себе не требуют. Любая data migration отдельно доказывает FK/restore. + +## 8. Cross-audit signals + +| Тема | Evidence | Рекомендуемый audit ID | +| --- | --- | --- | +| Timer/refetch policy и disabled queries — живой путь, не DELETE | `frontend/src/features/tbank-connection/model/screen-sync.ts:28–31`; `src/pages/bonds/ui/BondsPage/BondsPage.vm.ts:10–13` | 02 / 07 | +| Old observe convention при новых consumers и documented legacy support | `frontend/.ai/conventions/data.md:61–63`; `docs/architecture/http-api.md:50–52` | 15 | +| Income read сохранил request-driven membership branch; это достижимое поведение | `backend/src/modules/investments/operations/application/brokerage-operations.service.ts:408–435` | 07 / 13 | +| Проверка scheduler failure/time lifecycle нужна независимо от cleanup | `backend/src/modules/investments/tbank-sync/tbank-hourly.scheduler.ts:93–110,178–218` | 08 / 13 | +| Producer-only tests должны быть пересмотрены по текущему entrypoint | `backend/test/tbank-catalog-discovery.e2e-spec.ts:85–118,179–196`; `frontend/src/shared/api/resource-query.test.tsx:139–173` | 08 | + +Это сигналы владельцам, не отдельные findings этих направлений. Дублирование +живых mappers, dependencies, generic VM complexity и архивные docs не аудировались. + +## 9. Open questions / verification plan + +| Вопрос, меняющий решение | Текущее состояние | Самый дешёвый proof | +| --- | --- | --- | +| Есть ли поддерживаемый consumer GET `/tbank-sync/{id}`? | Local SPA consumers 0; actual external use UNVERIFIED. | Owner API/deployment отвечает о clients; существующая обезличенная access-log выборка по route, без запросов к real API. | +| Нужны ли старые `screen:` work после перехода? | Writer недостижим, reader публичен; сохранённые DB rows не проверялись. | Aggregate count/age/scope на disposable копии SQLite + agreed support horizon. Не печатать intent/token/account payloads. | +| Кто пользуется `sync=observe` именно у справочных routes? | Source/DTO публикуют режим; потребитель может быть вне SPA. | Endpoint-specific consumer list/log sample и контрактное решение; не выводить removal всех modes из удаления frontend callback. | +| Можно ли удалить persisted reference storage целиком? | Нет: есть positive live source/metadata reads и cascade candle FK. | Отдельная source/FK карта нужных identities и backup/restore proof; сначала code-only producer cleanup. | +| Какие variants planner/candle force останутся после manual deletion? | Owners живы; unused variant removal отдельно UNVERIFIED. | После узкого diff пересчитать callers конкретных variants; current Ledger/candle behavior comparison, не whole-file purge. | +| Пройдут ли cleanup DI/schema/regressions? | Изменений приложения нет, tests/build не запускались. | В отдельной implementation task сначала targeted owner/HTTP/isolated persistence checks, затем локальные обязательные gates по affected repos. | + +### 9.1. Проверка полноты и сохранности результата + +- Проверены 9 общих разделов, полная finding schema для всех 11 IDs, + 12 candidate chains, explicit DI/dynamic/public/external checks, + Reachability Ledger и Deletion Closure Plan. +- Финальная consolidation table содержит по одной строке на finding; + C01/C02 не удваивают actionable count. KEEP/INVESTIGATE не смешаны с DELETE. +- Source pointers просмотрены по локальным файлам и строкам; отрицательный + поиск дополнен положительными traces/counterexamples, ограничения сохранены. +- После записи отчёта повторно проверены SHA/status всех пяти checkout и + `git diff --check`; source/config четырёх репозиториев и root tracking чисты. + Отчёт проверен отдельно, потому что root `.gitignore` его игнорирует. +- Semantic/Boy Scout review ограничен самим report: не добавлены новые helpers, + scripts, обязательные workflows или неподтверждённые измерения; source edits + отсутствуют. Existing dated audits, archives и references не менялись. + +## Final machine-readable consolidation table + +| ID | Decision | Severity | Confidence | Repo | Files | One-line impact | Effort | Related IDs | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | +| DEAD-001 | DELETE | P2 | 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-screen.spec.ts; src/modules/investments/tbank-sync/tbank-sync.repository.ts | Недоступная manual orchestration и exclusive writer/test closure | M | DEAD-002,DEAD-006,DEAD-008,DEAD-009,DEAD-011 | +| DEAD-002 | DELETE | P2 | High | backend | src/modules/investments/instruments/application/catalog-discovery.service.ts; src/modules/investments/instruments/infrastructure/catalog-discovery.repository.ts; test/tbank-catalog-discovery.e2e-spec.ts | Producer старого persisted discovery недоступен production; shared readers/writers живы | M | DEAD-001,DEAD-007,DEAD-010,DEAD-011 | +| DEAD-003 | DELETE | P3 | High | backend | src/modules/investments/tbank-sync/tbank-sync.dto.ts | Orphan Accepted DTO без wire consumer | S | DEAD-001,DEAD-006 | +| DEAD-004 | DELETE | P3 | High | frontend | src/shared/api/resource-query.ts; src/shared/api/resource-query.test.tsx | Observe callback и key rewrite используются только direct tests | S | DEAD-005,DEAD-007,DEAD-011 | +| DEAD-005 | SIMPLIFY | P3 | High | frontend | src/features/tbank-connection/model/screen-sync.ts; src/features/tbank-connection/model/screen-sync.test.tsx; src/pages/accounts/ui/AccountsPage/AccountsPage.vm.ts; src/pages/account/ui/AccountPage/AccountPage.vm.ts; src/pages/account-operations/ui/AccountOperationsPage/AccountOperationsPage.vm.ts; src/pages/account-events/ui/AccountEventsPage/AccountEventsPage.vm.ts; src/pages/account-income/ui/AccountIncomePage/AccountIncomePage.vm.ts; src/pages/bonds/ui/BondsPage/BondsPage.vm.ts; src/pages/stock/ui/StockPage/StockPage.vm.ts; src/pages/bond/ui/BondPage/BondPage.vm.ts | Scope/returned facade unused; live periodic GET effect сохранить | S | DEAD-004,DEAD-009 | +| DEAD-006 | INVESTIGATE | P2 | High | backend | src/modules/investments/tbank-sync/tbank-sync.controller.ts; src/modules/investments/tbank-sync/tbank-sync.service.ts; src/modules/investments/tbank-sync/tbank-sync.dto.ts | Public status GET и old screen rows требуют external compatibility proof | M | DEAD-001,DEAD-003,DEAD-007 | +| DEAD-007 | INVESTIGATE | P2 | High | backend | src/modules/investments/instruments/application/catalog-discovery.service.ts; src/modules/investments/investment-resource.dto.ts; src/modules/investments/instruments/stocks/presentation/stock.controller.ts; src/modules/investments/instruments/bonds/presentation/bond.controller.ts | Backend observe readers externally reachable; retirement UNVERIFIED | M | DEAD-002,DEAD-004,DEAD-006,DEAD-010,DEAD-011 | +| DEAD-008 | KEEP | P2 | High | backend | src/modules/investments/investment-sync-plan.ts; src/modules/investments/operations/application/brokerage-operations.service.ts; src/modules/investments/operations/application/brokerage-operations-sync.service.ts | Ledger использует planner/history/interval helpers | S | DEAD-001,DEAD-009 | +| DEAD-009 | KEEP | P2 | High | backend | src/modules/investments/tbank-sync/tbank-hourly.scheduler.ts; src/modules/investments/tbank-sync/tbank-sync.module.ts; src/modules/investments/tbank-sync/tbank-sync.repository.ts | Nest lifecycle запускает живой hourly owner без frontend | S | DEAD-001,DEAD-005,DEAD-008,DEAD-011 | +| DEAD-010 | KEEP | P2 | High | backend | src/modules/investments/instruments/application/candle.service.ts; src/modules/investments/instruments/infrastructure/candle.repository.ts | Ordinary candle read требует first-range loader/staging/coverage | S | DEAD-001,DEAD-002,DEAD-007,DEAD-011 | +| DEAD-011 | KEEP | P1 | High | backend | src/modules/investments/instruments/infrastructure/catalog-discovery.repository.ts; src/modules/investments/instruments/application/catalog-enrichment.service.ts; prisma/schema.prisma | Shared source CAS/identity live; blanket deletion рискует isolation и candle data | S | DEAD-001,DEAD-002,DEAD-004,DEAD-007,DEAD-009,DEAD-010 | diff --git a/audits/2026-10-11/reports/05-frontend-architecture-report.md b/audits/2026-10-11/reports/05-frontend-architecture-report.md new file mode 100644 index 0000000..4546f0b --- /dev/null +++ b/audits/2026-10-11/reports/05-frontend-architecture-report.md @@ -0,0 +1,1038 @@ +# Аудит 05 — Frontend Architecture + +## 1. Audit metadata + +| Поле | Значение | +| --- | --- | +| Audit ID / name | 05 / Frontend Architecture | +| Дата | 2026-10-11 | +| Исполнитель | Один независимый агент; дополнительные сабагенты не запускались | +| Workspace | `/Users/ksv741/Project/vibe/WealthLedger` | +| Режим | Исходники/config четырёх repo и root read-only; единственная запись — этот новый отчёт | +| Scope | React/TypeScript UI architecture, ownership, VM/view, Query/state, формы, presentation/recovery, routing/accessibility | +| Источник задания | `audit-prompts/05-frontend-architecture.md`, прочитан целиком | +| Предыдущий аудит | `audit-results/technical-audit-2026-10-11.md` прочитан как реестр гипотез; каждый использованный механизм проверен заново | + +### Репозитории и ревизии + +В каждом checkout самостоятельно выполнены `git rev-parse HEAD` и +`git status --short --untracked-files=all` до исследования. + +| Repo | Абсолютный путь | Точный HEAD | Начальный status | +| --- | --- | --- | --- | +| root | `/Users/ksv741/Project/vibe/WealthLedger` | `dc030f84c979cedb2628ef4bd97c62d6c63a9256` | Пустой | +| frontend | `/Users/ksv741/Project/vibe/WealthLedger/frontend` | `c8821200ce699dab1ce65360d1968d5877a2e78b` | Пустой | +| backend | `/Users/ksv741/Project/vibe/WealthLedger/backend` | `0ad84ff4f0b4712abc7e69dc299856f14af6eadd` | Пустой | +| specs | `/Users/ksv741/Project/vibe/WealthLedger/specs` | `fcdc805f6c1b3be6a20a7e858e3e80cbd83a9121` | Пустой | +| docs | `/Users/ksv741/Project/vibe/WealthLedger/docs` | `e6fd4f64b81dd73b33959b845a1babcfac1891c8` | Пустой | + +Перед чтением каждого nested repo прочитан его `AGENTS.md`. Для frontend +прочитаны engineering, architecture, typescript-react, ui, data, testing; +для backend — engineering и HTTP API, поскольку использовался только явно +предоставленный публичный artifact. Specs выбраны через `AI_INDEX.md` и +точечный поиск основных behavioral specs; docs — через README и тематические +страницы. Backend application internals здесь не исследовались. + +**Skills:** первым загружен и применён установленный `ponytail-audit`. +Его complexity-pass отделён от проверки correctness: абстракция сама по себе +не является дефектом. Также применены `feature-sliced-design` с reference +layer-structure, `vercel-react-best-practices`, `web-design-guidelines`, +`graphify` и `verification-before-completion`. +В root и frontend не найден существующий `graphify-out/graph.json`. +Graphify pipeline, генерация графа и вспомогательные artifacts не выполнялись; +владельцы и связи прослежены по исходникам. + +### Фактически выполненные проверки + +| Команда / действие | Результат и граница доказательства | +| --- | --- | +| `git rev-parse HEAD && git status --short --untracked-files=all`, cwd каждого из пяти repo | SHA выше; все начальные деревья чистые | +| `git diff --no-index --stat -- "/Users/ksv741/Project/vibe/WealthLedger/backend/openapi.json" "/Users/ksv741/Project/vibe/WealthLedger/frontend/src/shared/api/spec/openapi.json"` | Exit 0, вывода нет: два имеющихся artifact совпадают; свежесть относительно runtime не проверена | +| `nvm use --silent && ./node_modules/.bin/depcruise src packages --config dependency-cruiser.cjs --output-type err-long`, cwd frontend | Первая попытка остановилась до Node: `nvm: command not found` | +| `source "/Users/ksv741/.nvm/nvm.sh" && nvm use --silent && ./node_modules/.bin/depcruise src packages --config dependency-cruiser.cjs --output-type err-long`, cwd frontend | Exit 0: `no dependency violations found (420 modules, 1773 dependencies cruised)`; прямой статический analyzer, без npm wrapper, запускающего build | +| Read / Glob / Grep | Source, callers, выбранные tests, schemas/config и behavioral requirements; абсолютные repo paths | +| Context7 `/tanstack/query` | Проверены manual `refetch` при `enabled: false`, stale/focus semantics и встроенное polling | +| Context7 `/websites/mantine_dev` | Проверена граница Fieldset disabled и Button, отображаемого ссылкой | +| WebFetch официальных Web Interface Guidelines | Использованы как checklist, без автоматического превращения каждого guideline в finding | + +Официальные источники библиотечных фактов: [TanStack disabled queries](https://tanstack.com/query/latest/docs/framework/react/guides/disabling-queries), +[TanStack window focus](https://tanstack.com/query/latest/docs/framework/react/guides/window-focus-refetching), +[Mantine Fieldset](https://mantine.dev/core/fieldset/), +[Mantine Button: disabled links](https://mantine.dev/core/button/). +Context7 вернул документацию текущей ветки, а не гарантированный snapshot +именно установленных minor versions; runtime-проверка этих versions не +выполнялась. Guideline checklist получен из +[официального источника](https://raw.githubusercontent.com/vercel-labs/web-interface-guidelines/main/command.md). + +Application tests не запускались: для выводов достаточно детерминированных +source branches и прочитанных existing scenarios. Ссылки на тесты ниже +означают просмотр сценария, **не свежий зелёный test run**. +Install, build, codegen, migrations, full-suite/verify, запуск приложения, +финансовые API, benchmark и profiler не выполнялись. Отчёты аудитов 06/07 +не читались. `.gitignore`, index, commits и PR не изменялись. + +Финальный контроль: отчёт перечитан; `git diff --no-index --check /dev/null +"audit-results/05-frontend-architecture-report.md"` и `git diff --check` +выполнены; HEAD/status всех пяти repo повторно сверены. Отчёт находится в +ignored `audit-results`, поэтому чистый root status сам по себе не доказывает +содержимое отчёта: новый файл проверен отдельно. + +Ссылки на code evidence далее имеют формат `repo/relative-path:lines` и +относятся строго к указанным SHA. `High` — высокая уверенность в механизме +по коду, не заявление о runtime-воспроизведении. Runtime impact и частота +сценариев не измерены. + +## 2. Executive verdict + +1. **Факт: FSD-граф работоспособен, масштабный перенос слоёв не нужен.** + Проверенный analyzer не выявил циклов, запрещённых направлений и обходов + public API. Реальные неудобства сосредоточены в потерях данных на границе + VM/view, а не в количестве slices. Полезные owners защищены FE-008–FE-011. +2. **Факт: основной портфель теряет source metadata, analytics не включает + её в loaded API.** Время и статус результата оказываются недоступны + пользователю при сохранённых денежных значениях — FE-001. +3. **Факт: сохранение snapshot реализовано лучше, чем сообщение о его + неуспешном перечитывании.** Investment VMs игнорируют feedback общего + screen hook; section view скрывает ошибку при наличии data. Пользователь + не получает предупреждение/retry — FE-002. Банковский список при cold + error может вообще вернуть пустой вывод — FE-004. +4. **Подтверждённый риск исполнения: ручной timer обходит query readiness.** + После его срока возможен поиск короче минимума либо запрос дочернего + ресурса с пустым UID. Focus policy также противоречит сохранённому + portfolio requirement. Это FE-003; частота и стоимость запросов не измерены. +5. **Факт: Fieldset не блокирует auth-ссылку.** Во время submit остаётся + переход на другую auth-форму, что нарушает local pending contract и + позволяет потерять контекст отправки — FE-005. Утечки identity этим + наблюдением не доказаны: session guards сохраняются. +6. **Непроверенные решения отделены от дефектов.** `CURRENCY` намеренно + скрывается в UI, хотя основной spec требует доступности иных позиций: + FE-006 — INVESTIGATE, пока не разрешён конфликт продуктового решения. + Длинные банковские списки/графики — potential bottleneck, FE-012, + **UNVERIFIED**, а не установленная performance regression. + +**Counts:** actionable `FIX`/`SIMPLIFY` — **6**: P0 **0**, P1 **0**, +P2 **5**, P3 **1**. Отдельно **KEEP 4**, **INVESTIGATE 2**. +FE-006 и FE-012 не включены в подтверждённые дефекты. FE-007 — необязательное +упрощение P3, не пользовательский сбой. P1 не присвоен: серьёзный эксплуатационный +или дорогой системный сценарий в этом audit scope не подтверждён. + +## 3. Inventory & coverage + +### 3.1. Фактическое устройство и метод подсчёта + +- SPA: React `19.3.0`, TypeScript `5.9.3`, Vite `8.3.0`, React Router `8.3.1`. + TanStack Query `5.102.8`, Mantine `9.6.1`, RHF `7.88.0`, Zod `4.6.4`, + openapi-fetch `0.17.0`, openapi-react-query `0.5.4` — direct versions из + `frontend/package.json:39–62`, не результат проверки runtime deployment. +- React Compiler включён в `frontend/vite.config.ts:9–11`; Vitest имеет + отдельный React/JSDOM profile (`frontend/vitest.config.ts:11–34`). + Это не доказательство оптимальности каждого render. +- По непосредственным directory entries `frontend/src`: **15 page slices, + 8 entity slices, 4 feature slices, 1 widget slice**; по `frontend/packages` + — **3 private packages** (`icons`, `tokens`, `ui`). Files/LOC этим методом + не считались. Assets/test не интерпретировались как FSD slices. +- **420 modules / 1773 dependencies** — точный вывод единственного успешного + dependency-cruiser run по `src packages` и действующему config. Это размер + его графа, не число production components и не семантическая оценка ownership. +- **8 production callers `useScreenSync`** установлены поиском символа по + `frontend/src` и чтением VM: accounts, account, operations, events, income, + bonds, bond, stock. Tests/index исключены из этого счёта. +- Публичные query keys образуются generated `queryOptions`; banking types + выводятся из `paths` (`frontend/src/entities/bank-card/api/types.ts:1–34`). + API snapshot совпал с предоставленным backend artifact, но генерация + `schema.d.ts` заново не выполнялась. + +### 3.2. Шесть end-to-end use cases + +Эти трассы включают UI route, фактических owners и shared transport. Видимые +состояния установлены по source и выбранным RTL/MSW сценариям; browser +render/keyboard interaction в этой сессии не воспроизводились. + +| Use case | Route → page → widget/feature → entity → shared API | Success / loading / error / stale / empty | +| --- | --- | --- | +| U1. Accounts и account/portfolio, включая `all` | `AppRoutes.tsx:194–210` → AccountsPage / AccountPage VM → PortfolioStructure, useScreenSync → brokerage-account, portfolio, broker-operation, brokerage-event, income → useResourceQuery → useAuthenticatedQuery → generated client | Structural skeleton; complete empty без вымышленных нулей; loaded группирует позиции и preview sections; access failure локален. Query snapshot сохраняется. Main source props теряются, transport feedback отсутствует: FE-001/002. Позиции `CURRENCY` скрываются: FE-006 | +| U2. Instrument search | `AppRoutes.tsx:158–165` → BondsPage → instrument-search (`q`, debounce, results/card), useScreenSync → bond/useBondSearch → useResourceQuery → generated GET `/bonds` | URL хранит q, trim + debounce 300 ms, автоматический query только с двух символов. Пустой успешный массив различим от отсутствующего result; labels и ссылки семантические. Cached error тихий; initial error ведёт даже при temporary failure в settings, без retry. Timer нарушает минимальную длину: FE-002/003 | +| U3. Bonds/stocks: detail и скринер | `AppRoutes.tsx:167–192` → BondPage / StockPage VM → instrument-details sections/chart/history + useScreenSync → bond/stock, broker-operation history → resource query/client. `/stocks` → StocksPage VM → generated POST search / GET search page напрямую через shared apiClient | UID/ticker canonical navigation сохраняет search; child queries ждут UID; sections независимы. Position без accounts скрыта; quote time и stale position видимы; complete empty не равен missing. Sections теряют cached-error warning/retry. Screener хранит единственный локальный snapshot результата и requestId, показывает asOf/error/empty и блокирует native controls; это не второй Query cache | +| U4. Banking: journal + create/edit/refund | `AppRoutes.tsx:140–156` → CardsPage / CardPage VM → page-owned OperationForm/DeleteOperation и entity BankCardForm/CategoryManager → bank-card queries/mutations/money/date → authenticated generated client | Card/history/summary независимы; URL owns period/page/limit; placeholder предыдущей страницы только в той же card/range/limit, edit на ней запрещён. Form validation, pending, conflict draft и intent replay сохранены. Cold list error может дать blank area; retry отсутствует у части read states, cached failures тихие: FE-004 | +| U5. Login/session/connection | `AppRoutes.tsx:109–138`, RouteGuards → LoginPage/RegisterPage → auth form/provider VM; settings → tbank-connection → shared client/session-aware query | Neutral bootstrap skeleton; один сериализованный refresh, один auth replay; logout очищает cache даже до ответа сети. Ошибки формы сохраняют ввод; email normalized, password untouched. Fieldset блокирует поля, но не Link: FE-005. Успешная connection mutation сбрасывает investment subtree; failed mutation сохраняет его | +| U6. Income analytics как сложный URL/form/data flow | `AppRoutes.tsx:221–228` → AccountIncomePage VM → page-local IncomePeriodForm/summary/comparisons/payments + useScreenSync → income query → resource/authenticated/generated API | Applied period в URL, draft в RHF; validation по календарю/Москве; resetRevision отдельно сбрасывает calendar UI. Новый uncached period не переименовывает старые строки, показывает skeleton; partial hides totals; previous/year dependency notices независимы. Source metadata и transport warning/retry в loaded API отсутствуют: FE-001/002 | + +В U3 намеренная dependency `identifier → UID → child queries` нужна для +точного инструмента; её нельзя назвать лишней waterfall без изменения +contract. Детальные request counts и server fan-out относятся к 07. + +### 3.3. UI Ownership Matrix + +| UI / responsibility | Фактический owner | Проверенные consumers / public API | Оценка | +| --- | --- | --- | --- | +| Providers, lazy routes, auth gates, route error reset | `app/ui/App.tsx`, `app/routes` | App → page slice index; ProtectedLayout → app/ui public API | KEEP: глобальная навигация не спрятана в shared | +| Portfolio state selection и позиции/previews | `pages/account` | AccountPage → VM → entity APIs → loaded/empty views; `AccountPage.vm.ts:66–136` | KEEP owner; FIX потери source и recovery, не перенос fetching во view | +| Структура портфеля/валюта/unknown/negative | `widgets/portfolio-structure` | Loaded и Empty account views; `PortfolioStructure.tsx:9–12` | Понятная существующая граница; перенос из-за общего FSD discouragement widgets не оправдан | +| Query keys, DTO projection, access groups | `entities/*/api` + `shared/api` | portfolio key используется тестами/consumers; generated option identity | KEEP; HTTP infrastructure не дублируется per page | +| Account breadcrumb scope | `app/ui/AppBreadcrumbs/AccountScopeSwitch` | `useBrokerageAccounts`, route section/search preservation; VM `:8–36` | KEEP; повторный observer использует тот же key, не собственный cache | +| Instrument search input/results | `features/instrument-search` | Текущий production consumer BondsPage; `useInstrumentSearch.ts:8–22` | Небольшой coherent owner; single consumer сам по себе не основание переносить slice | +| Общие chart/position/history sections | `features/instrument-details` | BondPage и StockPage; narrow structural VM types `model/types.ts:7–39` | KEEP: реальный reuse двух screens; section recovery расширяется здесь | +| Stocks criteria/search snapshot | `pages/stocks` | StocksPage VM → view; public shared apiClient | KEEP local ownership; запрос не скрыт в presentation hook; URL persistence — открытый product question | +| Banking queries/types/feedback/money/card form | `entities/bank-card` | CardsPage, CardPage, OperationForm, CategoryManager, BankCardForm; index `:1–30` | KEEP для согласованных money, card и conflict responsibilities | +| Transfer/refund/edit orchestration | `pages/card/ui/OperationForm` | CardPage `:21–27`; RHF context fields и entity APIs | KEEP page-local feature; не дробить по числу props | +| Analytics period, draft, grouping/comparisons | `pages/account-income` | VM → own form/views → income API | KEEP: бизнес-проекция отличается от wire DTO | +| Periodic investment reads | `features/tbank-connection/model/screen-sync.ts` | 8 production page VMs; scope descriptors сейчас игнорируются | FE-003: readiness/policy у query-owner, а interval обходной | +| Source text + busy/entry primitives | `shared/ui/SourceSyncStatus`, `packages/ui` | Accounts/Account/Operations/Events; bank/history, skeletons | KEEP semantic wrappers; ответственность шире нового имени для Mantine | +| Pagination labels stocks | `StocksPagePresentation.vm.ts` | Единственный StocksPage.view | FE-007: необязательная дополнительная navigation hop | + +FSD checks: `frontend/dependency-cruiser.cjs:30–75` задаёт public API, +same-layer и upward restrictions. Успешный graph gate подтверждает эти +настроенные проверки; не доказывает правильность любого бизнес-owner. +Не обнаружено actual propagation-of-change, требующего общего god slice, +нового state manager, обязательного `@x` или удаления entities слоя. + +### 3.4. State Ownership Matrix + +| State | Source of truth | Secondary copy | Consequence | +| --- | --- | --- | --- | +| Investment data/meta/query error/fetch status | TanStack Query envelope | `useResourceQuery` возвращает projection, VM вычисляет presentation | Второго data store нет; FE-001/002 возникают при выбрасывании полей projection | +| 30-minute refresh schedule | Сейчас собственный setInterval | Query отдельно owns stale/focus/reconnect + HTTP retry | Два policy owners; readiness не соблюдается ручным refetch — FE-003 | +| Accounts для breadcrumb и page | Один generated Query key | Второй observer, не копия data | KEEP; не превращать в custom global context | +| Instrument `q`, period, event filter | Router search params | Debounced trim q; computed categories | Обоснованная временная debounce projection; parameters переживают canonical route | +| Ledger applied filters | URL | Draft range/categories в React state | Обоснованное Apply поведение; scope change сбрасывает draft и cursors | +| Brokerage cursors | `useBrokerageOperationHistory` scope-bound local pagination | Нет копии rows | Оpaque cursor остаётся API-owned; invalid cursor сбрасывается к первой странице | +| Income applied period | URL normalized period | RHF draft DD.MM.YYYY, resetRevision/calendar state | Draft и committed state различаются по смыслу; form не должен refetch при каждой правке | +| Screener result/page/asOf | Один page-local `page` snapshot | Нет второго Query result | Императивный search owner допустим; requestId препятствует stale completion; перенос в Query сам по себе не нужен | +| Screener criteria | Local React draft | Результат от предыдущего search сохраняется отдельно | Не меняет старый список до Search; reload/back persistence не обещана отдельной spec | +| Bank card/history/summary | TanStack Query | History placeholder предыдущей страницы в том же scope | Не новый cache; readonly editing guard защищает placeholder identity | +| Bank operation/card draft | RHF | SubmittedValues, intent, adopted version, latest record | Это replay/conflict workflow, не ненужная копия server state; FE-010 KEEP | +| Modal open, purchase page, trigger focus | Ближайший component/presentation hook | Refs для trigger/scroll, не data cache | Связный UI lifecycle; `CardPagePresentation.vm.ts:18–53` выполняет layout read в effect, не в render | +| Auth identity/token | AuthProvider VM; token только ref в памяти | Shared transport session snapshot содержит readiness/handlers; userRef нужен current closure | KEEP single-flight/generation/cache clearing; не переносить token в storage | +| Connection status | Query + successful mutation response | Нет локальной копии финансовых datasets | Boundary commit/reset только при успехе; error сохраняет cache | +| PortfolioStructure currency | Local выбранная валюта | Selected derived из доступного structure | Fallback к первой валюте вычисляемый; неполная/negative оценка остаётся явной | + +### 3.5. Query Policy Inventory + +Общее наследование: `frontend/src/app/ui/App.tsx:13–20` задаёт +`refetchOnWindowFocus: true`, `refetchOnReconnect: true`, `retry: false`. +Если строка ниже не переопределяет policy, действует это наследование. +`staleTime` — время stale eligibility, **не rate limiter**. +Не-auth GET дополнительно имеет transport recovery: +`frontend/src/shared/api/client.ts:127–151,172–194,205–249`: deadline +15 000 ms, задержки 1000/2000 ms, permanent code exclusions и AbortSignal. +Эти числа прочитаны из constants/branches; это не измеренный request budget. +Auth endpoints и non-GET исключены из этого GET retry. + +| Use case / query-owner | enabled / key inputs | staleTime | Interval / refresh | Invalidation / recovery | +| --- | --- | --- | --- | --- | +| Accounts / `useBrokerageAccounts.ts:3–18` | Has session; generated GET accounts key | 1 800 000 ms | Screen timer; ещё observers в breadcrumb/account | Connection subtree reset, auth clear, accessGroup brokerage; cached transport feedback теряется | +| Portfolio / `usePortfolio.ts:11–25` | Session AND accountId; path accountId | 30 000 ms | Screen timer; ручной query.refetch доступен инфраструктуре, page result hook игнорируется | Access-group cancellation/clear; сохраняется data при temporary failure | +| Account preview operations / `useBrokerageOperations.ts:26–65` | Session AND accountId; normalized categories, cursor, limit/from/to/instrument | 30 000 ms | Account/ledger/detail screen timers | Connection/access reset; history scope/cursor recovery у entity | +| Events / `useBrokerageEvents.ts:10–23` | Session AND accountId, from/to | 30 000 ms | Account/events screen timers | Access-group reset; partial/empty видимы, cached transport error не передан | +| Summary/analytics / `useIncome.ts:14–45` | Session AND accountId; analytics from/to | 30 000 ms | Account/income screen timers | Same financial boundary; comparison dependencies из envelope, не отдельный client validation | +| Bond search / `bond/api/queries.ts:97–104` | Session AND query.length >= 2; trimmed debounced query | 30 000 ms | Bonds screen timer вызывает refetch даже disabled query | Resource без brokerage accessGroup; общая connection mutation всё равно reset bonds subtree | +| Bond/stock detail and child resources / соответствующие `api/queries.ts` | Session AND identifier; child session AND UID; candles also period | 30 000 ms | Detail screen timer over all resources; UID-empty child всё ещё включён в массив | Personal position/coupons/dividends и operations помечены accessGroup; connection reset охватывает весь stocks/bonds GET subtree | +| Stocks screener / `StocksPage.vm.ts:29–101` | Handler Search/page; apiClient, не useQuery | Не применяется | Новый POST только по действию; GET page императивный, requestId fence | Локальный snapshot/error/pending; GET transport и session generation guards остаются; local state исчезает при unmount | +| Bank card/cards/categories/expenses/operation / `bank-card/api/queries.ts:28–73` | Session; item/expenses дополнительно id; cards includeArchived в key | Не задано: Query default 0 | Focus/reconnect/mount; собственного screen timer нет | Bank mutation invalidates банковское GET subtree; session clear отдельно | +| Bank journal / `bank-card/api/queries.ts:76–145` | Session AND id/from/to; key range/limit/page; all-mode отдельный suffix | Не задано: 0 | All-mode последовательное чтение страниц с одним signal; page-mode conditional placeholder | Refetch/invalidation inherited; нет UI retry у read failure; FE-004 | +| Bank summary / `bank-card/api/queries.ts:147–156` | Session; cardId/from/to | Не задано: 0 | Query select сортирует exact cents; busy передан в view | Mutation invalidation и session clear; error только без data, без retry | +| Connection / `TbankConnectionSection.vm.ts:5–22` | Session, generated status key | Не задано: 0 | Cached progress/error/retry явно переданы | Успех PUT/DELETE коммитит status и reset investment subtree; неуспех не reset | +| Auth / `auth.vm.ts:81–151` | Cold-start refresh; serial auth requests; memory session | Не Query cache | Single-flight refresh + serialized cookie-writing operations | clearSession → generation increment/queryClient.clear; смена identity reset active/remove inactive | + +Тестовый QueryClient выключает Query retries +(`frontend/src/test/query.tsx:15–20`), но не отражает автоматически все +production focus/timer/transport policies. `observe` не вызывается production +UI по найденным callers; вопрос удаления совместимости передан 04, не +раскрывался здесь как самостоятельный dead-code audit. + +### 3.6. Render, forms, accessibility и contracts: результат покрытия + +- **Rendering:** позиции вычисляются в page model, chart data/table — в + presentation VM. Child VM types принимают narrow structural contracts, + даже если caller передаёт весь vm object. Оснований заменить их universal + component или добавить `memo` каждому компоненту не установлено. + Большие all-mode/candle arrays отмечены только FE-012. +- **Keys:** проверены account id, instrument UID, operation id/account pair, + candle time, category id, group key. Есть occurrence keys у income entries + (`model/display.ts:101–117`), что защищает повторяющиеся identifiers; + logoName входит в некоторые keys. Remount cost/регрессия не измерены, + массовое изменение keys не рекомендовано. +- **Effects:** screen timer имеет cleanup, resourcesRef обновляется effect; + его `_scope` и resource queryKey не управляют timer. Initial availability + имеет budget 10 000 ms и identity (`shared/lib/initial-availability.ts:18–46`), + но `isFetching` input не используется. Это не основание удалять bounded + initial-wait UX: hook не является polling source. URL/form reset effects + и cursor recovery выполняют внешнюю синхронизацию, а не второй data cache. +- **Forms:** RHF + resolver; factories учитывают актуальные cards/categories, + refund limit и explicit adopted version. Нормализация amount/email/date + имеет разные владельцы и разные инварианты; общий universal normalizer + не нужен. Серверная validation/authorization остаётся обязательной. +- **Accessibility:** семантические Link/Button, именованные action icons, + labels/field errors, busy skeleton, pagination labels, modal focus return, + keyboard-focusable price table, reduced motion theme проверены по source. + Подтверждённый pending-link gap — FE-005. Наличие main landmark не доказывает + route focus/skip-navigation experience: screen reader, contrast, responsive + overflow и реальный keyboard path **UNVERIFIED** без browser session. +- **Contract:** типы data/request выводятся из generated paths; unknown money + отличается от zero, formatMoney сохраняет decimal строку + (`shared/lib/display.ts:21–61`). Chart `Number` используется для визуальной + шкалы, точная таблица сохраняет original close. ApiErrorCode — subset открытого + wire code с fallback (`shared/api/problem-details.ts:19–28`), не runtime + validator всей schema. Malformed runtime payload не обещает гарантированной + локальной recovery: protected route ErrorBoundary — последний safe fallback. + +**Ограничения репрезентативности:** изучены все route definitions и выбранные +end-to-end owners, не каждый JSX/CSS/test во frontend. Невыполненные browser +и measurement checks не скрыты за успешным dependency gate. Backend execution, +DRY cross-repo, systemic dead code, качество всех assertions, CI/tooling и +load efficiency находятся вне scope; их сигналы перечислены в разделе 8. + +## 4. Findings + +### FE-001 — Source metadata теряется в API финансового view + +- **Decision:** FIX. +- **Severity:** P2. +- **Confidence:** High. +- **Evidence:** `frontend/src/pages/account/ui/AccountPage/AccountPage.vm.ts:46–50,125–135`; + `frontend/src/pages/account/ui/AccountPage/AccountPage.tsx:13–34`; + `frontend/src/pages/account/ui/AccountPage/components/AccountPageLoaded.view.tsx:28–38,55–59`; + `frontend/src/pages/account-income/ui/AccountIncomePage/AccountIncomePage.vm.ts:24–39,110–119`; + `frontend/src/pages/account-income/ui/AccountIncomePage/components/AccountIncomePageLoaded.view.tsx:36–70`. +- **Claim:** у account container уже вычисленный `source` не передаётся view; + у analytics loaded model source отсутствует целиком. Source warning/time + нельзя восстановить из сохранённых денежных rows без envelope. +- **Current mechanism:** route вызывает page VM → resource query отдаёт data/meta + → VM account возвращает source → container явно перечисляет props без source. + Empty account branch также не несёт metadata. Income VM использует meta для + partial/comparison notices, но выбрасывает freshness/time/attempt/availability. + `Portfolio.isStale` на основном экране не читается отдельным consumer. +- **Impact / actual cost:** old complete snapshot после failed hourly attempt + выглядит как обычный результат; отсутствует контекст происхождения сумм. + **Impact unmeasured**: частота старых snapshots и пользовательские ошибки + интерпретации не измерены. Прямой propagation defect, а не новый API requirement. +- **Why now:** информация уже опубликована и частично отображается на соседних + screens. Specs `frontend-data-resilience/spec.md:11–28` и + `portfolio-view/spec.md:121–158` сохраняют явный source context; полезность + правила здесь конкретна — не выдавать старые суммы за новое наблюдение. +- **Minimum change:** передать account source; включить источник в loaded/empty + финансовое presentation API; income использует тот же существующий status + renderer с page-owned completeness/availability wording. Не вводить client + timestamps вместо server `lastSuccessfulAt`. +- **Before / after API:** + + ```tsx + // До: source вычислен, но потерян. + + // После: остальные props сохраняются, source проходит boundary. + + // Income loaded VM: добавить source: analytics.meta; + // view получает source, а не useIncomeAnalytics внутри view. + ``` + +- **Guarantees at risk:** нельзя путать server stale, failed hourly attempt и + client transport failure; data/null/complete empty, деньги/валюты и исходное + время сохраняются. Для существующих responses meta гарантирована contract, + optional compatibility props можно оставить там, где нужны fixtures. +- **Counterarguments / trade-offs:** тихий экран может быть намеренной UX + политикой: `AccountPage.test.tsx:803–855` явно ожидает отсутствие source time. + Это доказательство действующего поведения, но не его полезности. Соседний + Accounts view уже показывает SourceSyncStatus; время не требует нового слоя. +- **Verification or rollback:** RTL/MSW current account route с full data и + failed/partial attempt; source timestamp неизменен, sums остаются; empty data + не становится missing. Изменить silent expectations в существующем test, + добавить income source presentation case. Проверять view props и route, + не только helper. Rollback только presentation diff, Query policies не трогать. +- **Effort:** M (account wiring S; полный scope включает income/empty states). +- **Related finding IDs:** FE-002, FE-008, FE-009, FE-011. + +### FE-002 — Investment view contract теряет transport recovery + +- **Decision:** FIX. +- **Severity:** P2. +- **Confidence:** High. +- **Evidence:** `frontend/src/features/tbank-connection/model/screen-sync.ts:37–61`; + `frontend/src/pages/account/ui/AccountPage/AccountPage.vm.ts:78–84`; + `frontend/src/pages/accounts/ui/AccountsPage/AccountsPage.vm.ts:21–58`; + `frontend/src/pages/account-income/ui/AccountIncomePage/AccountIncomePage.vm.ts:66–69,110–119`; + `frontend/src/features/instrument-details/model/section.ts:14–27`; + `frontend/src/features/instrument-details/ui/InstrumentSection.view.tsx:22–28,76–84`; + `frontend/src/pages/bonds/ui/BondsPage/BondsPage.vm.ts:17–20,27–39`. +- **Claim:** data сохранена, но cached transport error/progress/retry не доходит + до пользовательской boundary. InstrumentSection даже полученный error + скрывает при `hasData`; нет локального retry API. +- **Current mechanism:** page VMs вызывают `useScreenSync`, не сохраняя его + результат. Loaded unions не содержат client feedback. `section` сводит HTTP + failure и source failure в boolean; view Alert имеет guard `!hasData`. + Search boolean error тоже требует отсутствия data. Operations/events передают + source meta, но не client failed-read state. Initial generic Account/Accounts + error (`AccountPageError.view.tsx:46–50`, `AccountsPageError.view.tsx:41–45`) + не имеет retry; income error view также только StateView. +- **Impact / actual cost:** неуспешное перечитывание неразличимо от штатного + snapshot; восстановление требует внешней navigation/focus/timer вместо + действия затронутой section. **Impact unmeasured**. Наличие data предотвращает + разрушение экрана, но не выполняет recovery contract. +- **Why now:** существующий status UI и Query.refetch уже достаточны. + `frontend-data-resilience/spec.md:18–24`, UI convention `:133–146` и docs + `architecture/cross-cutting/ui-resilience.md:19–35` требуют двух различимых + состояний именно для сохранённых финансовых результатов. +- **Minimum change:** page VM отдаёт client feedback (`refreshing`, + `refreshError`, `retry`), view показывает неблокирующее предупреждение и + read-only retry. SectionState раздельно несёт source и transport status; + retry относится к failed query/section. Access failures сохраняют отдельный + navigation-only путь. Использовать Mantine Alert/Button и существующую + presentation семантику, не universal global error store. +- **Before / after API:** + + ```ts + // До: + type SectionState = { error: boolean; hasData: boolean; loading: boolean; partial?: boolean }; + // После: оставшиеся поля сохраняются. + type SectionFeedback = { + refreshError: boolean; refreshing: boolean; + sourceFailed: boolean; retry?: () => void; + }; + // Page/section VM собирает feedback из query; view получает named props. + // section error с hasData => warning + data, без data => local error + retry. + ``` + +- **Guarantees at risk:** не скрывать data при transient error; не показывать + чужой session/connection snapshot; retry — только HTTP read, не manual import. + Не перемещать query hook во view. Invalid cursor сначала восстанавливает scope + через existing history owner, а не повторяет бесконечно тот же cursor. +- **Counterarguments / trade-offs:** отсутствие кнопки manual source sync + оправданно; это не запрет повторить HTTP. Quiet behavior действительно + закреплён `AccountPage.test.tsx:751–855`, `AccountsPage` quiet tests и + `StockPage.test.tsx:392–407,449–471`; tests не заменяют behavioral решение. + Source warning из FE-001 не объясняет transport error, поскольку HTTP мог + не доставить новое meta вовсе. Не следует показывать дублирующиеся alerts. +- **Verification or rollback:** existing route RTL/MSW: initial temporary fail, + cached fail, slow refetch, successful retry, independent failed company section, + invalid cursor и confirmed access loss. Проверять завершение failed response, + сохранённые суммы/draft и доступную кнопку, а не private callback count. + Изменяются ожидаемые visible outcomes, не весь test stack. +- **Effort:** M. +- **Related finding IDs:** FE-001, FE-003, FE-004, FE-008, FE-011. + +### FE-003 — Screen timer обходит `enabled`, policy разбита между owners + +- **Decision:** FIX (с native simplification после behavior proof). +- **Severity:** P2. +- **Confidence:** High. +- **Evidence:** `frontend/src/features/tbank-connection/model/screen-sync.ts:7–35,39–50`; + `frontend/src/pages/bonds/ui/BondsPage/BondsPage.vm.ts:9–13`; + `frontend/src/entities/bond/api/queries.ts:97–104`; + `frontend/src/pages/stock/ui/StockPage/StockPage.vm.ts:23–55`; + `frontend/src/pages/bond/ui/BondPage/BondPage.vm.ts:23–55`; + `frontend/src/shared/api/authenticated-query.ts:42–50`; + `frontend/src/app/ui/App.tsx:13–20`. +- **Claim:** timer вызывает manual refetch каждого resource, не проверяя + `isEnabled`/`isFetching`. Disabled Query защищает автоматическое fetching, + но не ручной `refetch`. Это подтверждено текущей TanStack documentation. +- **Current mechanism:** 30-minute setInterval, scope descriptor игнорируется, + latest resources лежат в ref. `ScreenResource` не содержит isEnabled, хотя + useResourceQuery возвращает его (`resource-query.ts:132–146`). При пустом + bond query endpoint всё равно получает request; при неразрешённом UID все + dependent observers существуют с `uid: ""`, timer их принудительно читает. + Отдельно stale portfolio после 30 секунд допускает focus refetch из App, + хотя `portfolio-view/spec.md:121–124` запрещает focus-triggered refetch. +- **Impact / actual cost:** invalid-input GET, ложные local errors при ожидании + prerequisite, чтение в другой частоте, чем ожидает policy owner. + **Impact unmeasured**: traffic/latency/API load не считались. StaleTime не + превращает ручной timer в readiness-aware schedule. +- **Why now:** root-cause fix в одном общем владельце защищает все восемь + callers. Потерянные scope inputs создают необходимость поддерживать + descriptors, не влияющие на execution. +- **Minimum change:** сначала фильтровать enabled и уже fetching resources + во всех manual refresh/retry/timer ветках. Focus policy выбрать явно у + investment query owner, не менять banking только ради одного правила. + Следующий компактный вариант — встроенный `refetchInterval` у query owner, + с polling только у нужного screen observer; убрать собственный timer и + неиспользуемый scope. Проверить observers Accounts/breadcrumb, прежде чем + включать interval во всех экземплярах одного hook. +- **Before / after API:** + + ```ts + // Минимальная коррекция existing API: + type ScreenResource = { isEnabled: boolean; isFetching: boolean; refetch: () => Promise /* existing feedback fields */ }; + // resources.filter(r => r.isEnabled && !r.isFetching) перед manual refetch. + // Native destination, policy находится в entity query options: + useResourceQuery({ ...options, enabled: ready, refetchInterval: READ_INTERVAL_MS, + refetchIntervalInBackground: false, refetchOnWindowFocus: false }); + // Вместо useScreenSync({ unused scope fields }, [resources]). + ``` + +- **Guarantees at risk:** session-aware enabled, AbortSignal, canonical keys, + cached snapshot, interval stop on unmount и read-only backend semantics. + Native background/focus defaults меняют нынешнюю работу hidden tab; нельзя + выдать такое изменение за чисто механический refactor. +- **Counterarguments / trade-offs:** текущий timer объединяет ресурсы одного + экрана и может быть осознанной политикой hidden tab. Query polling без + observer ownership способен размножить timers для одного key. Поэтому + immediate readiness guard подтверждён, а полное native replacement требует + узкого proof; новый scheduler framework не нужен. Reconnect policy не + объявляется дефектом автоматически: product meaning minimum interval надо + согласовать. +- **Verification or rollback:** fake timers + existing screen-sync test boundary: + пустой/однобуквенный поиск, unresolved ticker, позднее получение UID, + unmount, cached refetch failure, два observers одного key, focus/reconnect. + MSW должен подтвердить отсутствие invalid request, а не только call helper. + Existing `screen-sync.test.tsx:21–80` проверяет ручные reads, но не disabled + interval. Rollback native phase к исправленному enabled-aware timer. +- **Effort:** M (immediate shared guard S). +- **Related finding IDs:** FE-002, FE-008, FE-011, FE-012. + +### FE-004 — Banking read-state API не обеспечивает локальный recovery + +- **Decision:** FIX. +- **Severity:** P2. +- **Confidence:** High. +- **Evidence:** `frontend/src/pages/cards/ui/CardsPage/CardsPage.vm.ts:15–45`; + `frontend/src/pages/cards/ui/CardsPage/CardsPage.view.tsx:31–77`; + `frontend/src/pages/card/ui/CardPage/CardPage.vm.ts:95–118,140–145`; + `frontend/src/pages/card/ui/CardPage/CardPage.view.tsx:52–83,133–142,177–186`; + `frontend/src/pages/card/ui/CardPage/CardHistory.view.tsx:38–123`; + `frontend/src/entities/bank-card/ui/BankSummary/BankSummary.view.tsx:17–23,84–125`. +- **Claim:** initial cards failure без data/loading возвращает `null`, не + error state. Card/history/summary не дают retry для соответствующего read; + cached card failure fields computed в VM не используются view, history и + summary warnings подавлены при наличии data. +- **Current mechanism:** CardsPageViewCards обрабатывает только items и + isLoading, затем null. CardPage initial error предлагает только `/cards`. + History возвращает rows раньше анализа failure; outer message имеет + `history.error && !rows`. Summary error также требует `!data` и имеет + только текст. VM summary type не передаёт error object/retry. Existing + BankFeedback уже принимает `cached`, `loadingError`, `retry`. +- **Impact / actual cost:** blank cards area после ошибки похожа на отсутствие + результата UI; пользователь не может адресно восстановить journal/summary. + Сохранённый баланс не становится нулём — это полезная гарантия, но ошибка + чтения остаётся скрытой. **Impact unmeasured**. +- **Why now:** `specs/openspec/specs/manual-bank-cards/spec.md:631–665` прямо + различает empty/error и требует repeat. Это необходимо для ручного учёта, + не generic preference к количеству Alert components. +- **Minimum change:** query owner остаётся bank-card; page VM отдаёт section + feedback и named retries. CardsPage добавляет error branch и cached warning; + CardPage/BankSummaryView используют BankFeedback либо простой локальный + Alert/Button. Ошибка одной section не убирает успешные card/history/summary. +- **Before / after API:** + + ```ts + // До: summary: { data, error: boolean, fetching, loading }. + // После, без смены query key/transport: + summary: { data: summary.data, error: summary.error, + fetching: summary.isFetching, loading: summary.isLoading, + retry: () => void summary.refetch() } + // Cards branch: !data && isError => BankFeedback(loadingError, retry). + // Cached branch: data + error => rows и BankFeedback(cached, retry). + ``` + +- **Guarantees at risk:** точный balance из backend, запрет optimistic success, + independence от T-Bank, pagination scope, disabled edit на placeholder rows, + drafts/version/intent сохраняются. NotFound не должен предлагать повтор + записи/создание дубля; recovery повторяет только GET. +- **Counterarguments / trade-offs:** summary failure уже отображает текст + без data, и prerequisites cardsFeedback существует. Finding не утверждает, + что все banking errors потеряны. Отсутствие alert при cached failure + закреплено `CardPage.test.tsx:315–341`; после исправления нужны новые + ожидаемые outcomes. Invalidation всего банковского subtree пока сохраняется. +- **Verification or rollback:** RTL/MSW по существующим CardsPage/CardPage и + BankSummary tests: cold cards failure, failure only summary/history, + cached failures, GET retry success, сохранение balance, draft и current URL. + Для placeholder rows оставить текущий edit guard. Rollback presentation + wiring, не write protections. Не создавать общий banking error manager. +- **Effort:** M. +- **Related finding IDs:** FE-002, FE-009, FE-010, FE-011. + +### FE-005 — Auth Link остаётся активным во время отправки + +- **Decision:** FIX. +- **Severity:** P2. +- **Confidence:** High. +- **Evidence:** `frontend/src/features/auth/ui/AuthForm/AuthForm.tsx:26–45,51–84`; + `frontend/src/features/auth/ui/LoginForm/LoginForm.tsx:6–9`; + `frontend/src/features/auth/ui/RegisterForm/RegisterForm.tsx:6–9`; + `frontend/src/features/auth/model/auth.vm.ts:121–133`; + `specs/openspec/specs/user-authentication/spec.md:109–122`. +- **Claim:** `
` не делает дочерний anchor + disabled. Mantine Button с `component={Link}` здесь не получает отдельного + interaction guard, поэтому переключение login/register остаётся доступно. +- **Current mechanism:** native fieldset блокирует form controls; React Router + Link отображается ссылкой. На `Button component={Link}` нет disabled data, + aria-disabled, tab exclusion или guarded click. Mantine docs явно требуют + для link separate `data-disabled` и предотвращения navigation. +- **Impact / actual cost:** пользователь меняет auth route во время request, + теряет старую форму/draft/error context; authentication всё ещё завершается + в общем provider. Нарушение pending UX подтверждено механизмом; account + compromise/утечка не доказаны. **Impact unmeasured**. +- **Why now:** малая локальная правка удовлетворяет уже принятому правилу + блокировки всех auth actions и не требует новой auth orchestration. +- **Minimum change:** guard navigation while pending, обозначить link + недоступным и исключить его из tab order; сохранить настоящую ссылку в + обычном состоянии. Handler находится у AuthForm owner. +- **Before / after API:** + + ```tsx + // До: +