# Читаемость и сопровождаемость 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` ищет во всех `