From 6fa62fb8b51aba82766fb50b3e5871572af8be27 Mon Sep 17 00:00:00 2001 From: codex-bot Date: Sun, 27 Sep 2026 18:26:55 +0300 Subject: [PATCH] docs: record code quality review and refactoring --- code-quality-review.md | 1001 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1001 insertions(+) create mode 100644 code-quality-review.md diff --git a/code-quality-review.md b/code-quality-review.md new file mode 100644 index 0000000..40abbd7 --- /dev/null +++ b/code-quality-review.md @@ -0,0 +1,1001 @@ +# Читаемость и сопровождаемость WealthLedger + +## 1. Область исследования + +Отчёт от 27 сентября 2026 года. Результат — семь подтверждённых возможностей +упрощения и небольшие порции работы. Наиболее полезный безопасный старт — +отделить чистое преобразование будущих выплат от сервисной оркестрации. +Затронутые области рекомендаций: backend investments/T-Bank, frontend +страницы инструментов, адаптер ресурсных запросов и несколько тестов. +Изменений реализации в рамках исследования нет. + +Это ревью стоимости понимания и изменения существующего кода. Бизнес-баги, +безопасность, производительность и полнота продукта не оценивались. +Количество файлов, строк и тестов использовалось для навигации, а не как +оценка качества. Ни одно удаление production-кода не предлагается на основании +одного поиска по имени. + +### Репозитории и глубина чтения + +Все доступные first-party пакеты с production-кодом включены в обследование. +Root, frontend, backend, docs и specs — самостоятельные Git-репозитории. +В начале исследования рабочие деревья были чистыми. Пути в отчёте указаны +от корня workspace; диапазоны строк относятся к следующим исходным HEAD: + +| Репозиторий | HEAD | Обследованная область | +| --- | --- | --- | +| frontend | `f6db28defa51bd9d8fe932ceae4c3c720aba7eb3` | Приложение, три workspace-пакета, публичные экспорты, статический граф, тестовые группы | +| backend | `4796d9d3f0160d96004a982f6c2526f199de4b9f` | Nest-модули, integrations, Prisma-границы, DTO, unit-тесты и выбранные HTTP/SQLite-сценарии | +| docs | `e4838e14855e9e696683a310efbc6afaaae51164` | Правила документации, архитектура, свежесть, доход, будущие выплаты и относящиеся к ним ADR | +| root, specs | Навигация и правила локальных checkout | Manifest/workspace-конфигурация и требования как контекст; самостоятельного runtime-пакета здесь нет | + +Прочитаны локальные AGENTS и применимые архитектурные, инженерные и тестовые +правила соответствующих репозиториев. Общие правила root не переносились на +приложения. Размещение этого отчёта в docs — прямо запрошенное пользователем +исключение из местного запрета на хранение исследовательских отчётов; правила +репозитория и навигационные индексы не изменялись. + +В frontend прослежены маршруты и сессия → API/query → entity → page VM → view; +особенно подробно — resource lifecycle, синхронизация экрана, акции/облигации, +портфель, фильтры операций/дохода и банковские формы. Прочитаны реализации +и экспорты tokens/ui/icons. В backend прослежены auth, банковские расчёты и +транзакции, инвестиционные ресурсы, операции/доход/прогноз, источники каталога +и свечей, T-Bank/MOEX/НКР. Для находок прочитаны конкретные реализации, +вызывающий код и относящиеся к ним тесты. + +**Ограничения:** это не построчное ревью каждого файла. Крупные наборы +AccountIncomePage, банковских UI/HTTP-тестов, persistence-тестов и отдельных +view/story-файлов читались выборочно; перечни остальных сценариев использованы +для карты покрытия. Не исследованы полностью миграции, эксплуатационные +скрипты, контейнерный runtime и все ветви каждого scheduler. Архивы, +`references/**`, generated-код, vendor, сборки и lockfile не оценивались как +авторский код. Источники OpenAPI-генерации и граница передачи схемы учитывались. +Graphify-графа в checkout нет; он не создавался, чтобы не оставлять дополнительные +артефакты. Реальные внешние API, production и потребители за пределами доступных +checkout не проверялись. Признак `private` в manifest не доказывает отсутствие +неизвестного внешнего использования. + +### Фактический стек + +Версии ниже взяты из manifest и сверены с установленными пакетами для основных +runtime-библиотек. Это фиксация контекста, а не предложение обновить зависимости. + +| Область | Используемые пакеты и версии | +| --- | --- | +| Frontend workspace | `wealth-ledger-frontend` 0.4.0; `@wealthledger/tokens`, `@wealthledger/ui`, `@wealthledger/icons` — 0.3.0; все private | +| UI и состояние | React/React DOM 19.3.0; React Router 8.3.1; Mantine core/hooks/dates/charts 9.6.1; TanStack Query 5.102.8 | +| Формы и API | React Hook Form 7.88.0; resolvers 5.9.1; Zod 4.6.4; openapi-fetch 0.17.0; openapi-react-query 0.5.4 | +| Представление | i18next 26.4.2; react-i18next 17.0.14; motion 13.2.0; dayjs 1.11.23; recharts 3.10.1 | +| Frontend tooling | TypeScript 5.9.3; Vitest 5.0.0; MSW 2.15.0; Vite 8.3.0 и Storybook 10.6.0 по manifest | +| Backend | `wealth-ledger-backend` 0.2.3; один пакет без npm workspaces | +| Сервер и хранение | Nest common/core/platform-express/swagger/jwt 12.0.1; config/passport 12.0.0; Prisma/client/SQLite adapter 7.10.0; better-sqlite3 13.0.3 | +| Внешние границы | grpc-js 1.14.4; proto-loader 0.8.1; class-validator 0.15.1; class-transformer 0.5.1; bcrypt 6.0.0; Cheerio 1.2.0; lru-cache 11.5.2 | +| Серверное окружение | nestjs-pino 5.1.0; pino-http 11.0.0; RxJS 7.8.2; TypeScript 6.0.3; Jest 30.5.1 | +| Документация и root | docs 0.1.2, root 0.2.0 — tooling; markdownlint-cli2 0.23.2, relative-links 5.1.2; Node 24.21.0 из локальных `.nvmrc` | + +### Выполненные проверки + +Перед Node-командами активирована локальная версия через +`source ~/.nvm/nvm.sh && nvm use --silent`. Использованы уже установленные +зависимости, без auto-fix и установки инструментов. + +| Где | Команда | Результат | +| --- | --- | --- | +| frontend | `./node_modules/.bin/depcruise src packages --config dependency-cruiser.cjs --output-type err-long` | 322 модуля, 1381 зависимость; нарушений настроенных правил нет | +| frontend | `./node_modules/.bin/vitest run --no-cache` | 54 файла, 357 тестов прошли | +| frontend | `./node_modules/.bin/tsc -p tsconfig.app.json --noEmit --incremental false` | Успешно; это проверка app-конфигурации, не отдельная сборка каждого пакета | +| frontend | `./node_modules/.bin/knip --no-progress` | Exit 0; неиспользуемые элементы не заявлены; configuration hint: исключённые `.mdx` не обходятся | +| backend | `npm test -- --runInBand --no-cache` | 36 suites, 209 тестов прошли; snapshots: 0 | +| backend | `npm run typecheck:app && npm run typecheck:test` | Обе проверки прошли | +| docs | `npm run verify` | Успешно: Markdown lint проверил 47 файлов, замечаний нет; `git diff --check` прошёл | + +Существующие backend HTTP/SQLite/e2e, durability/container gates и OpenAPI parity +в этой сессии не запускались. Не выполнялись браузерная/визуальная проверка, +полный application verify, сборка и генерация артефактов. Успех unit-тестов +не подменяет эти проверки и не доказывает поведенческую эквивалентность будущих +изменений. Для самого отчёта достаточно docs gate и смысловой сверки; в порциях +ниже указана дополнительная проверка будущего рефакторинга. + +## 2. Краткий вывод + +Основная сложность возникает на стыках уже полезных механизмов: +сервисный граф загружается ради чистого mapper; одинаковый cursor-протокол +поддерживается в двух VM; смысл периода передаётся через callback и индекс; +результат предметного ресурса маскируется типом нативного Query result. +Это локальные проблемы ответственности и явности контрактов. Перестройка +всей архитектуры или смена библиотек не обоснована. + +Предлагаемый порядок: маленький mapper-пилот, единое распознавание ошибки +T-Bank, общая пагинация двух инструментальных страниц; затем отдельно +типизация resource-result и более рискованная граница income/operations. +Два замечания к тестам имеют меньший приоритет: неточное описание проверок +и зависимость layout-теста от внутреннего CSS Mantine. + +**Оставить без переписывания:** + +- FSD-направление импортов, публичные package exports и backend `*.api.ts`. + В обследованном frontend-графе не обнаружена утечка application/domain в + общие пакеты. Переэкспорты здесь обозначают реальные границы. +- `EntryCard`, `StateView`, `LoadingSkeleton`: общая структура, доступность + и состояния имеют собственную ответственность. Это не повод заменить + каждый компонент прямым вызовом Mantine. +- Tokens/theme adapter и три SVG-компонента icons: локальные преобразования + и явные компоненты проще фабрики или нового runtime-валидатора trusted JSON. +- React.lazy-адаптеры именованных экспортов: они сохраняют lazy-loading. + Роутер не нуждается в универсальном конфигурационном языке. +- Состояние/refs сессии, single-flight, очередь/Web Locks, generation/access + epochs и очистку кэша. Похожее состояние обслуживает разные временные + границы; простое удаление разрушает защиту поздних ответов. +- Разделение transport error, failed source, missing/partial/complete и + сохранённого snapshot. TanStack Query не знает этого предметного протокола. +- Draft/applied фильтры и стабилизацию периода в income VM: эффект reset формы + зависит от идентичности значения; механическое удаление memo не обосновано. +- Точную decimal/bigint-арифметику, банковский `BankErrorCode`, транзакции, + проверки версий, idempotency и возвраты. Не заменять их float-арифметикой, + новым money-пакетом или generic repository над Prisma. +- Валидацию MOEX/НКР/T-Bank, ограниченные retry/admission и fencing публикации. + Типы TypeScript не проверяют внешний ответ. Scheduler свечей, операций и + каталога различаются обязанностями; единый job engine не предлагается. + +## 3. Рабочие критерии рефакторинга + +1. Правка должна уменьшать число мест, которые нужно открыть для изменения + конкретного правила. Новое имя/файл оправдано понятной ответственностью, + а не ограничением длины функции. +2. Использовать существующего владельца: UI — packages/ui, API транспорта — + shared/api, история инструмента — instrument-details, особенности провайдера — + integrations/tbank, периоды дохода — income. Не складывать предметные правила + в общий `utils` и не заводить новые пакеты для этих находок. +3. Объединять совпадающее поведение, а не похожий синтаксис. У страницы всех + операций иной scope и фильтры, у купона и дивиденда разные даты/единицы. +4. Сохранять локальный OpenAPI input и wire values. Generated-файлы вручную + не править; независимый литерал в контрактном тесте может быть полезен. +5. Сначала искать определение предметного значения. Существующий `as const` + не дефект. Во frontend `erasableSyntaxOnly` исключает обычный строковый enum; + предпочтение enum из запроса не применяется вопреки этому ограничению. +6. Тип должен описывать фактический результат. Удаление assertion полезно, + если после него виден настоящий контракт; замена на `any`/`unknown as` не + является упрощением. +7. Не удалять редкие ветви без доказательства эквивалентной защиты. Сохранить + отмену, доступ, late responses, атомарность, точность и accessibility. +8. Сначала получить тот же наблюдаемый результат существующим тестом. + Добавлять точечную проверку только там, где затрагиваемый контракт не + защищён; не тестировать библиотеку ради формального покрытия. +9. Проверять affected area после diff: не появился ли второй способ решать + задачу, не потеряны ли публичный экспорт или уникальное утверждение теста. + Размер diff и зелёный lint сами по себе этого не доказывают. + +## 4. Карта ответственности и зависимостей + +| Владелец | Ответственность и публичная граница | Потребители и направление | Решение | +| --- | --- | --- | --- | +| frontend app | Маршруты, auth composition, страницы; `app → pages → features → entities → shared` | Браузерное приложение; page собирает сценарий из нижних слоёв | Сохранить; не создавать общий page engine | +| `@wealthledger/tokens` | Значения дизайна, export `./tokens.json` | UI theme adapter | Сохранить trusted data boundary | +| `@wealthledger/ui` | Общие визуальные компоненты и тема; exports `.`, `./theme`, `./styles.css` | App/Storybook; зависит от tokens, peer React/Mantine | Доменной логики в прочитанной реализации не найдено | +| `@wealthledger/icons` | SVG и доступные props; export `.` | App/UI stories, peer React | Не добавлять зависимости на domain, UI или motion | +| frontend shared/api | OpenAPI client, session guards, адаптация resource lifecycle | Entities и features | Полезная граница; уточнить тип результата по R-04 | +| frontend instrument-details | Представление секций и истории инструмента | StockPage/BondPage | Сюда подходит общее поведение пагинации R-03 | +| backend auth/banking | Локальная сессия; ручная финансовая история и её транзакции | HTTP controllers, собственные services/Prisma | Не насаждать ports/adapters на каждый CRUD | +| backend investments | Счета, портфель, операции, доход, прогноз, инструменты, screen sync | Контроллеры → application; domain для чистых правил, infrastructure для хранения/адаптации | R-01 и R-05 уточняют локальные границы | +| backend integrations | T-Bank gRPC, MOEX HTTP, НКР HTML; публичные `*.api.ts` | Investment-модули; интеграции не зависят от бизнес-модулей | R-02 оставляет классификацию upstream-ошибки у интеграции | +| `InstrumentSourcesModule` | Каталог, enrichment, свечи и repositories | PortfolioModule и InstrumentsModule | Сохранить отдельный модуль источников | +| `InstrumentsModule` | Stock/bond business services и HTTP controllers | Использует sources и PortfolioModule | Не сливать с sources: портфелю не нужен обратный импорт всего InstrumentsModule | +| Backend OpenAPI → frontend snapshot | DTO/Nest metadata — источник wire-контракта; frontend получает артефакт явно | Нет обязательного чтения соседнего checkout при CI frontend | Сохранить независимость репозиториев | +| Root/docs/specs | Навигация/tooling; объяснение системы; поведенческие требования | Не runtime-зависимости приложений | Не переносить сюда код или общий реестр констант | + +Существенная подтверждённая проблема зависимостей — инфраструктурный runtime-import +ради чистых преобразований (R-01). Две другие связи скрыты в протоколах, а не +в циклическом импорте: income ↔ порядок массивов (R-05), resource ↔ обещания типа +Query result (R-04). Depcruise не сообщил нарушений настроенного frontend-графа; +это не доказательство отсутствия всех возможных динамических связей. Полная +автоматическая проверка циклов backend не проводилась, утверждения «циклов нигде +нет» здесь нет. Knip не даёт оснований для массового удаления экспортов. + +Карта согласуется с [контейнерами](architecture/containers.md), +[моделью свежести](architecture/cross-cutting/data-freshness.md), +[границей OpenAPI](adr/014-trusted-openapi-input.md) и +[решением о локальном чтении](adr/017-local-tbank-reads-and-background-sync.md). +Эти ссылки объясняют существующее устройство, не вводят новые требования. + +## 5. Подтверждённые находки + +Приоритет означает выигрыш в сопровождаемости с учётом риска, а не серьёзность +предполагаемого бизнес-бага. R-01 — первый пилот; R-02/R-03 — следующие локальные +упрощения; R-04/R-05 требуют более осторожной проверки; R-06/R-07 — тестовый долг. + +### R-01. Чистые проекции импортируются через сервисный граф + +**Места и повторения.** Backend: + +- `src/modules/investments/upcoming-events/upcoming-events.service.ts:205–248,267–278` — + `dividendEvent`, `bondEvent`, `projectedMoney`; вызовы в `get`, строки 149–160. +- `src/modules/investments/upcoming-events/upcoming-events.service.spec.ts:1–5,14–54` — + тестирует эти чистые функции, но подменяет `PrismaService` для загрузки imports. +- `src/modules/investments/instruments/stocks/application/stock.spec.ts:1–7` — + тот же setup для `metrics`, `stockDividends`, `stockQuote`; определения: + `get-stock-company.use-case.ts:39`, `stock-income.service.ts:101`, + `stock-market.service.ts:62` в том же каталоге. +- `src/modules/investments/instruments/bonds/application/bond.spec.ts:1–7` — + такой же Prisma mock для `bondCoupons`, `bondMetrics`, `bondQuote`; + определения в `bond-income.service.ts:128,186`, `bond-market.service.ts:66`. + +**Стоимость.** Чтобы понять и проверить умножение количества на выплату, +приходится учитывать Nest/Prisma import graph. Тестовый setup маскирует лишнюю +связь с инфраструктурой. Причина не в длине сервиса и не в наличии функций +рядом с классом, а в том, что самостоятельный расчёт не загружается независимо. + +**Минимальная правка.** В пилоте перенести только две upcoming-event проекции +и их общий `projectedMoney` в один соседний mapper-модуль. Сервис и существующий +тест импортируют его, тест перестаёт мокать Prisma. DTO и provider records нужны +только как type imports. Это преобразование границы провайдера, а не новый +универсальный domain layer. + +До: `test → upcoming-events.service → services/repositories → Prisma`. +После: `test/service → upcoming-event mapper → exact decimal`. + +Новая ответственность ясна: «provider event + holding → event DTO». +Не добавлять Injectable mapper, интерфейс с одной реализацией, отдельный файл +для каждой функции или общий движок stock/bond. Остальные helpers сервиса +не переносить автоматически. Stock/bond рассматривать после оценки пилота. + +**Сохранить.** Registry/event date kind, точные знаковые дробные количества, +нормализацию валюты, `null` при неизвестной сумме/валюте, оферту и погашение. +Фильтрация отменённых дивидендов, окно дат, сортировка, totals, account scope +и атомарная публикация остаются в текущем сервисном потоке. + +**Зависимости, риск, проверка.** Независимый небольшой шаг, риск низкий при +механическом переносе. Существующие три теста проекций сохраняются со всеми +assertions; app/test typecheck проверяют imports. Для связанного runtime +сохраняется `test/upcoming-events.e2e-spec.ts:255–284`: HTTP-проекция, суммы, +account/all и отсутствие private holdings в общем источнике. Этот e2e здесь +прочитан, но не запускался. Порция B-01. + +### R-02. Распознавание ошибки токена повторяет протокол интеграции + +**Места.** Источник ошибки — +`backend/src/integrations/tbank/tbank.client.ts:520–524`: gRPC auth-коды +нормализуются в `Error('INVALID_TBANK_TOKEN')`. Прочитанные потребители: + +- `src/integrations/tbank/tbank-token.service.ts:24–31` — перевод в HTTP exception; +- `src/integrations/tbank/tbank-membership.service.ts:61–65` — аргумент `failMembership`; +- `src/integrations/tbank/instrument-metadata.service.ts:128–136,181–186` — запрет + fallback UID→FIGI при token error и отклонение membership; +- `src/modules/investments/portfolio/application/portfolio.service.ts:372–378` — + token rejection вместо обычного `repository.fail`; +- `src/modules/investments/operations/application/brokerage-operations-sync.service.ts:286–291` — + признак ошибки для repository. + +Тот же предикат найден и прочитан в +`src/modules/investments/instruments/application/catalog-discovery.service.ts:348–356`, +`catalog-enrichment.service.ts:500–509`, `candle.service.ts:443–450`. +В каждом случае повторяется именно `error instanceof Error && error.message === ...`. +Записи `errorCode` в repositories — отдельная часть persisted/wire-контракта; +их не следует заменять без разбора только ради устранения всех строк. + +**Стоимость.** Знание о форме upstream-ошибки распространено по orchestration. +При изменении распознавания приходится сверять несколько обработчиков и +отделять его от разных действий восстановления. + +**Минимальная правка.** Один integration-owned `isInvalidTbankToken(error)` с +нынешним точным предикатом, доступный через существующий `tbank.api.ts`. +До: каждый consumer знает `instanceof Error` и текст сообщения. +После: consumer вызывает predicate, но сам выбирает `fail`, rejection или HTTP. +Это единая семантика распознавания, а не глобальный каталог всех ошибок. +Не менять класс исключения, публичную строку, правила retry и значения хранения; +не вводить второй enum ошибок frontend поверх generated API. + +**Сохранить.** Не считать произвольный объект `{message: ...}` подходящим Error; +не расширять совпадение по подстроке. Сохранить немедленный отказ auth RPC, +разные side effects и fencing каждого attempt. `BankErrorCode` уже существует +у banking owner и не относится к этой миграции. + +**Зависимости, риск, проверка.** Владелец найден; от B-01 не зависит. Риск низкий +для предиката, средний при широкой замене catch-ветвей. TbankClient unit-сценарии +строк 267–277 проверяют auth-коды и отсутствие retry; `tbank-token.service.spec.ts` +проверяет перевод и сохранение несвязанных ошибок. При переносе достаточно +точечной проверки точного Error/non-Error распознавания и этих тестов; +не создавать тесты наличия константы. Проверить соответствующие existing +persistence-сценарии отказа/смены token, если затронута оркестрация. Порция B-02. + +### R-03. Одинаковая пагинация истории инструмента живёт в двух VM + +**Места.** Frontend: + +- `src/pages/stock/ui/StockPage/StockPage.vm.ts:41–61,78–86,158–166`; +- `src/pages/bond/ui/BondPage/BondPage.vm.ts:41–61,78–86,151–159`. + +Прочитаны scope `${uid}:${filter}`, стек cursor, запрос `all` с instrumentUid +и limit 20, recovery `INVALID_OPERATIONS_CURSOR`, exact invalidation первой +страницы и переходы назад/вперёд. Вызывающее представление истории уже общее +в `features/instrument-details`. + +**Стоимость.** Одно изменение cursor/recovery-протокола требует синхронно +редактировать два несоседних page controller. Его трудно отделить взглядом +от котировок, свечей и специфичных доходов инструмента. + +**Минимальная правка.** Один hook поведения истории инструмента у существующего +`features/instrument-details`, принимающий UID/scope и категории, возвращающий +operations и действия пагинации. Invalid-cursor recovery принадлежит этому +поведению; обычный screen retry остаётся callback страницы. Страница продолжает +владеть URL, различием coupons/dividends и составом `useScreenSync`. + +До: StockPage и BondPage содержат одинаковые state/query/recovery блоки. +После: каждая собирает свои ресурсы и один общий `useInstrumentOperations(...)`. +Не создавать параметризованную фабрику страниц, generic pagination framework +или переносить всё в shared. + +**Сохранить.** UID, account=`all`, limit=20, canonical query key, disable до +появления UID, точную invalidation, историю cursor и URL replace. Особенно +сохранить нынешнюю семантику A→B→A: scope mismatch временно скрывает старый стек, +но сам по себе не обнуляет state. «Очистить в эффекте» — уже другое поведение. +`AccountOperationsPage.vm.ts` имеет иные account/date/category scope, limit 100 +и reset; его не включать по внешнему сходству. + +**Зависимости, риск, проверка.** Независимо от R-04; риск средний из-за state +lifecycle. `StockPage.test.tsx:377–408` и `BondPage.test.tsx:378–399` проверяют +видимую следующую страницу и запрос без cursor после смены фильтра. Сохранить +оба page-level сценария: они проверяют подключение разных страниц. В прочитанных +сценариях нет отдельного доказательства invalid-cursor recovery и A→B→A; +при извлечении добавить только эту адресную проверку наблюдаемого запроса/результата, +а не тестировать внутренний массив state. Порция B-03. + +### R-04. Resource-result обещает нативный Query result после изменения его смысла + +**Места.** `frontend/src/shared/api/resource-query.ts:21–44`, +`unwrapResourceResult`; использование в `useResourceQuery:154–158` и напрямую +в entity stocks. Потребители — portfolio/accounts/income/operations/events, +stocks/bonds; `features/tbank-connection/model/screen-sync.ts` использует +отдельный `transport.status`, а `features/instrument-details/model/section.ts` +преобразует состояния в представление. + +**Стоимость.** Wrapper раскрывает envelope в `data`, меняет `status`, +`isPending`, `isSuccess`, но приводит весь spread к +`UseQueryResult, E>`. Непереопределённый `refetch` +по-прежнему возвращает исходный envelope; `isLoading` остаётся нативным, +хотя successful HTTP с `data:null` объявляется resource-pending. +Читатель должен знать исключения из обещанного типа. Это подтверждённая +неясность контракта, а не утверждение о найденном пользовательском сбое. + +**Минимальная правка.** Убрать assertion «это полный нативный результат с новым +T»; вывести фактический тип spread + overrides либо явно описать только +переопределяемую часть, сохранив реальные типы методов и остальных полей. +Разделение transport/resource уже есть — использовать его при именовании +и чтении статусов. Исправить только потребителей, опирающихся на ложное +типовое обещание; runtime-поля не удалять под видом сокращения интерфейса. + +До: envelope query преобразован и объявлен другим `UseQueryResult`. +После: явно типизированный resource result с исходным transport/refetch и +предметным `data/meta/status`. Не строить второй query cache/state machine, +не добавлять runtime schema и новые FSD-пакеты. + +**Сохранить.** `null` payload → отсутствие готового ресурса, last-known data, +transport/source distinction, auth/access isolation, canonical cache key, +`observe` без нового import и проверку notified stateVersion. Отдельное +`as unknown as K` в построении observational query (`126–139`) связано с +адаптацией generated query key; R-04 не даёт основания просто удалить механизм. + +**Зависимости, риск, проверка.** Риск средний: широко используемая граница. +Сначала typecheck всех затронутых callers, затем существующие +`resource-lifecycle.test.tsx`, `authenticated-query.test.tsx`, screen-sync, +section и page suites. Проверять разные transport/resource состояния и результат +refetch; не заменять проверки кэша тестом нового type alias. Минимум — текущие +контракты в T-02 ниже; весь frontend unit-набор достаточно дешёв для финального +прогона. Порция B-04, отдельно от извлечения пагинации. + +### R-05. Период дохода передаётся побочным присваиванием и позицией в массиве + +**Места.** Backend: + +- `src/modules/investments/income/income-analytics.service.ts:49–68,79–110` — + `summary`/`analytics` меняют внешнюю `let plan` внутри callback; затем + распаковывают `[current, prior, year]`. +- `src/modules/investments/operations/application/brokerage-operations.service.ts:439–483,493–501,557–559` — + `collectRanges` передаёт callback в несколько чтений, а имя ресурса выводит + через `['selected', 'previous', 'yearAgo'][index]`. +- `src/modules/investments/operations/infrastructure/brokerage-operations.repository.ts:55–68,97–111,202–205` — + `readPage` получает openedAt и вызывает planner внутри Prisma-транзакции. +- `src/modules/investments/income/income-analytics.service.spec.ts:35,58,192` — + mock также должен воспроизводить вызов callback. Production callers + `collectRanges` найдены только в этих двух методах income. + +**Стоимость.** Чтобы понять, какой plan соответствует итоговому snapshot, +нужно проследить побочное присваивание через repository и повторные чтения. +Чтобы изменить именование периода, нужно знать позиционный договор двух +модулей. При этом наружу уже передаются явные роли; внутренняя неявность не +необходима публичному контракту. + +**Минимальная правка.** Сделать callback чистым и вернуть из `collectRanges` +данные планирования, относящиеся именно к возвращаемому snapshot, вместе с +именованными диапазонами. Достаточно конкретного результата: openedAt/нужные +метаданные snapshot и periods. Income может восстановить свой чистый plan +из аргументов и возвращённого openedAt; метку диапазона задаёт income, а +operations переносит её при формировании resourceKey, не выводит из индекса. +Для отсутствующего snapshot явно сохранить планирование с `undefined`. + +До: `let plan; await collectRanges(openedAt => { plan = ...; return ranges; })`. +После: чистый planner + возвращаемые `snapshot context / labeled periods`; +income собирает ответ из данных одного результата. +Не нужен универсальный planner framework, реестр периодов, новый слой или +generic callback, переносящий произвольный application object через Prisma. + +**Сохранить.** Нельзя сначала отдельно прочитать openedAt, затем получить +операции: это разорвёт существующий snapshot. Сохранить transaction boundary, +passive observe, scheduling/fencing, Moscow/date-only границы, историю открытия, +недоступный leap-day/year comparison, `null` для неполного сравнения, точные +суммы, hashes и внешние resourceKey. Порядок legacy dependencies и роли +`selected`, `previous`, `year` также остаются прежними, несмотря на внутреннюю +метку `yearAgo`. + +**Зависимости, риск, проверка.** От других находок не зависит, риск выше среднего +из-за snapshot/coverage. Обязательны income-period и income-analytics unit, +`test/tbank-income-local.e2e-spec.ts` и соответствующая OpenAPI contract-проверка. +Прочитанные e2e строки 124–203 отдельно защищают exact totals, повторные ID разных +счетов, reopen без RPC, missing/partial/confirmed empty и независимые comparison. +Это не гарантирует все пути concurrent scheduling: их необходимо сохранить +и адресно проверить при изменении финального snapshot. Порция B-05. + +### R-06. Layout-тест зависит от внутренней генерации CSS Mantine + +**Места.** `frontend/src/pages/account/ui/AccountPage/AccountPage.test.tsx:663–681` +ищет во всех `