Files
docs/code-quality-review.md
codex-bot 6fa62fb8b5
Docs security / security (push) Successful in 9s
Docs verify / verify (push) Successful in 14s
docs: record code quality review and refactoring
2026-09-27 18:26:55 +03:00

104 KiB
Raw Permalink Blame History

Читаемость и сопровождаемость 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 не даёт оснований для массового удаления экспортов.

Карта согласуется с контейнерами, моделью свежести, границей OpenAPI и решением о локальном чтении. Эти ссылки объясняют существующее устройство, не вводят новые требования.

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<NonNullable<T['data']>, E>. Непереопределённый refetch по-прежнему возвращает исходный envelope; isLoading остаётся нативным, хотя successful HTTP с data:null объявляется resource-pending. Читатель должен знать исключения из обещанного типа. Это подтверждённая неясность контракта, а не утверждение о найденном пользовательском сбое.

Минимальная правка. Убрать assertion «это полный нативный результат с новым T»; вывести фактический тип spread + overrides либо явно описать только переопределяемую часть, сохранив реальные типы методов и остальных полей. Разделение transport/resource уже есть — использовать его при именовании и чтении статусов. Исправить только потребителей, опирающихся на ложное типовое обещание; runtime-поля не удалять под видом сокращения интерфейса.

До: envelope query преобразован и объявлен другим UseQueryResult<T>. После: явно типизированный 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 ищет во всех <style> строку --sg-cols:1, отсутствие @media и других значений, затем ожидает ровно один найденный стиль. components/PositionSection.view.tsx:13–16 того же каталога задаёт реальный контракт: expanded sections имеют SimpleGrid cols={1}, остальные responsive.

Стоимость. Тест назван проверкой одной колонки на любой ширине, но не измеряет раскладку. Изменение способа генерации/дедупликации CSS Mantine может сломать его при той же геометрии. Совпадение стиля другого элемента также не является доказательством контракта нужной секции.

Минимальная правка. Заменить только это утверждение проверкой геометрии нужной секции в настоящем браузере на узкой и широкой ширине, когда есть согласованный воспроизводимый способ такого запуска. Контент/доступные роли проверять существующими DOM-тестами. Не заводить новый e2e framework или полную визуальную матрицу ради одного теста; mock props SimpleGrid тоже не является доказательством реальной раскладки.

До: число строк CSS с внутренней переменной. После: элементы нужной секции располагаются вертикально при двух контрольных ширинах. Это проверяет выбранное приложением поведение, а не устройство Mantine.

Сохранить. Одна колонка у expanded sections и responsive other section; логотипы, подписи и суммы не теряют своих существующих проверок.

Зависимости, риск, проверка. Сейчас равнозначная исполняемая layout-проверка не подтверждена, установленного project e2e gate для неё не выявлено. Следовательно, текущее решение — оставить до замены, а не удалить. Риск удаления сейчас — потеря единственной найденной проверки этого свойства. B-07 условная, после появления доказанной замены; ручной снимок сам по себе не заменяет постоянную регрессионную защиту.

R-07. Названия двух query-тестов обещают больше, чем assertions

Места. Frontend:

  • src/entities/portfolio/api/usePortfolio.test.tsx:11–24 — «30 second cache and no retry», но тело ждёт success и проверяет positions;
  • src/entities/broker-operation/api/useBrokerageOperations.test.tsx:11–30 — «without retries», но тело проверяет нормализацию категорий и успешную страницу.

Стоимость. Название создаёт ложную уверенность, что изменение staleTime или retry будет обнаружено. Успешный запрос этого не доказывает; общий test QueryClient к тому же отключает retry.

Минимальная правка. Переименовать тесты по существующим утверждениям: «загружает портфель счёта» и «нормализует категории и загружает страницу». Сохранить полезную проверку параметров/данных. Не добавлять fake-timer suite библиотечного кэширования только для оправдания старого заголовка.

Сохранить. Тела тестов, настройку клиента и background-refetch тест ниже в usePortfolio. Его проверка сохранённого snapshot имеет отдельный контракт.

Зависимости, риск, проверка. Самостоятельная правка двух названий, низкий риск; достаточно запуска двух существующих файлов. Если нужен пересмотр политики retry, это отдельная задача, не следствие этой находки. Порция B-06.

6. Решения по существенным группам тестов

«Оставить» означает, что полезный контракт найден либо доказательств для удаления недостаточно. Это не заявление об идеальности каждого assertion. Таблица охватывает существенные группы доступных пакетов; внутри крупных групп решение основано на перечисленных примерах и выборочном чтении, а не на построчном доказательстве всех тестов.

Frontend

ID / группа Наш контракт и обнаруживаемая регрессия Детали реализации и пересечение уровней Решение
T-01 auth/client/authenticated-query, AuthProvider, AuthForm, routes Восстановление сессии, очистка personal cache, корректная навигация; поздний ответ прежней сессии не становится данными новой Mocks транспорта/времени обслуживают нашу координацию. Маршрутный UI не заменяет concurrency-сценарий клиента, клиент не доказывает видимую навигацию Оставить; не удалять refs/generation вместе с «повторяющимися» проверками
T-02 resource-lifecycle, screen-sync, sync, section Каноническая cache identity, observe после уведомления, missing payload, отказ доступа, поздние ответы, transport/source error; регрессия — неверный scope или возвращение скрытого snapshot Это подключение Query к нашему протоколу, не тест реализации query cache. Чтение cache уместно для границы, но пользовательский результат предпочтителен в page-тесте Оставить; R-04 меняет тип, не сокращает эти сценарии
T-03 entity API hooks accounts/portfolio/events/operations Выбор endpoint/параметров, раскрытие ответа, сохранённые данные после фоновой ошибки Happy path не доказывает retry/staleTime. Два завышенных названия подтверждены R-07; usePortfolio background-refetch имеет отдельную ценность Упростить названия по R-07, assertions оставить
T-04 StockPage/BondPage, каталоги stocks/bonds UID/ticker routing, сохранение фильтров, отдельные состояния секций; percent облигации не превращается в деньги; pagination даёт видимый результат Stock 377–408 и Bond 378–399 похожи, но проверяют wiring двух страниц. Моки API здесь граница, не структура hook Оставить page wiring; после R-03 общий алгоритм проверять адресно, не размножать полную матрицу на обе страницы
T-05 AccountPage VM/view, position-groups, CurrencyCards, PortfolioStructure, display Группировка позиций/валют, суммы и доступные действия, неполные значения; регрессия — смешение групп или потеря подписанного показателя Pure grouping и показ пользователю — разные гарантии. Соседние проверки логотипов/сумм не доказывают число колонок Оставить, кроме условной замены единственного CSS assertion по R-06
T-06 account-operations/events/income pages и filters Draft против applied URL, reset/back, cursor, выбранный период, доступность сравнения, восстановление ошибки Большие файлы не доказывают дублирование. Прочитаны VM и выбранные сценарии, не все тела income suite Оставить; нет доказательства эквивалентного покрытия для удаления
T-07 банковские schema/period/summary Signed64, точные копейки, выбранные комбинации полей, реальные календарные даты/Moscow-период; регрессия — округление или принятие невозможной даты Это наши ограничения поверх Zod и Date, не повтор тестов string()/required(). UI happy path не покрывает арифметические границы Оставить
T-08 banking API, BankCardForm, OperationForm, DeleteOperation, CategoryManager, Card/CardsPage Сохранение intent, передача версии, принятие актуальных данных после конфликта, архив, связь refund/edit, видимое обновление результата Проверка правильных параметров/invalidations защищает наше wiring. Unit формы и backend транзакция не взаимозаменяемы; большие группы читались выборочно Оставить; не сокращать mocks без сохранения сценария конфликта/повтора
T-09 packages/ui и icons, theme/contrast, animation binding Общие роли/status, визуальная семантика theme adapter, SVG accessibility, lifecycle анимации; регрессия — потерянные props/cleanup/доступное имя EntryCard fallback отчасти использует Mantine, но проверяет выбранные нами props и интеграцию. Удаление требует отдельного доказательства ненужности Оставить; новые фабрики и snapshots не нужны
T-10 Storybook isolation Отобранные composeStories не обращаются к fetch/XHR/storage/cookie при render/действии Проверка вызовов — сам контракт изоляции fixture. Depcruise контролирует imports, не runtime side effects. Список stories явный, это не доказательство проверки всех возможных stories Оставить, не считать дублем dependency gate
T-11 shell: breadcrumbs, document title, error boundary, accounts list Наши подписи/навигация/восстановление при ошибке и доступные роли Render React Router сам по себе не цель; выбранное нами назначение title, ссылки и сообщения — отдельный контракт Оставить; массовые удаления по малому размеру теста не обоснованы

Backend

ID / группа Наш контракт и обнаруживаемая регрессия Детали реализации и пересечение уровней Решение
T-12 decimal, rub-money, bank calculations/summary Точная арифметика, границы значений, знак, состав итога; регрессия — округление/неправильный вклад операции Pure function здесь публичная граница правила. HTTP сценарии не перечисляют все арифметические случаи Оставить; не заменять только happy path формы
T-13 auth service и app.e2e-spec.ts Ротация/повторы сессии и HTTP cookie/guard boundary Mocked service concurrency и настоящий configured app проверяют разные обязанности. Это не повторное тестирование Nest Оставить; e2e в сессии не запускался
T-14 bank-cards HTTP и durability CAS, idempotency, атомарные transfer/refund, owner scope, restart; пример bank-cards.e2e-spec.ts:372–442 проверяет видимые остатки и replay после edit/delete Реальная SQLite транзакция не заменяется моками Prisma; SQL constraints относятся к выбранной модели. Durability отдельна от in-process результата Оставить; большие наборы прочитаны выборочно и не запускались
T-15 TbankClient, token/source/account policy, crypto Наши RPC arguments, нормализация, admission/retry/deadline, допустимые accounts, шифрование; регрессия — лишний retry auth или обход очереди Счёт RPC и fake timers обоснованы контрактом ограничений; не просто гарантия grpc-js. R-02 не должен ослабить проверки Оставить; при R-02 упростить распознавание в production, не assertions протокола
T-16 MOEX/НКР Exact ISIN/board, корректные источник/дата/единицы, malformed против not-found/empty, withdrawn против missing Cheerio/fetch mock отделяет внешнюю границу. TS тип не делает вход trusted, HTTP 200 не означает валидные данные Оставить
T-17 investment-resource mapper, sync-plan, tbank-screen, coverage/planner Availability/freshness/work, выбранный scope и интервалы; регрессия — partial становится complete или планирует другую область Проверять конкретное решение planner полезно. Не удалять как «внутренний helper»: это владелец предметной политики Оставить
T-18 portfolio/accounts, instrument-position/structure, operation mapper Объединение локальных источников, точные позиции, account attribution и внешняя нормализация Много зависимостей portfolio mock не доказывает бесполезность orchestration-теста. Полезно упрощать чистые mapper imports, но не подменять связь всех ресурсов happy path Оставить; массовая перепись fixtures не обоснована
T-19 stock/bond/upcoming projection unit Даты, цены против процентов, точные quantity/payment, неизвестная сумма/оферта; три upcoming примера прочитаны целиком Prisma mock нужен из-за import graph, а не вычислению. Это setup-долг R-01, не лишний контракт Упростить setup через R-01, оставить все финансовые утверждения
T-20 income-period/analytics и tbank-income-local Calendar/history availability, selected/previous/year, валюты и точность, IDs разных счетов, partial против confirmed empty Callback mock связан с реализацией R-05. HTTP/SQLite 124–203 дополнительно проверяет сохранение и отсутствие RPC, unit даёт адресные расчётные случаи Оставить контракты; упростить mock после явного результата R-05
T-21 persistence: metadata/catalog/candles/operations/local reads/ledger/connection/migration Stage→publish, coverage, старый attempt/revision, reopen, confirmed absence; регрессия — поздняя работа публикует старые данные или промежуточный результат становится полным Похожие гонки у разных owners не равнозначные тесты: меняются repositories и ключи. Полностью все тела не прочитаны Оставить; доказательств для удаления нет
T-22 screen sync, all-accounts, portfolio structure, stocks/bonds/upcoming HTTP Composition/DTO, account/all scopes, источник публичных/личных данных, passive observe; upcoming 255–284 дополняет pure mapper HTTP integration не заменяет точные signed/fractional примеры unit; unit controller не проверяет Nest pipes/guards Оставить
T-23 тонкие operations/upcoming controller unit Делегирование user/scope/query в нужный service Вызов метода напрямую не проверяет validation/auth. Для каждого утверждения эквивалентная configured HTTP-проверка не установлена; тест stocks другого endpoint не эквивалент Пока оставить. Замена допустима только после точного сопоставления сценария, не создавать новый batch на предположении
T-24 env/main/filter/logging/DTO и OpenAPI contract Выбранная конфигурация, безопасная форма ProblemDetails, поля/коды валидации и wire schema Это проектное подключение библиотек. Generated schema и UI типизация не доказывают конфигурацию реального HTTP app Оставить; contract e2e не запускался

Удалить: ни для одной существенной группы достаточного доказательства отсутствия уникального контракта не получено. Предложения удаления целых suites нет. Единственная адресная замена — R-06, причём действующий тест сохраняется до воспроизводимой проверки того же поведения. Переименование R-07 не ослабляет assertions; перенос R-01 не меняет предметные примеры.

7. Последовательность небольших порций

Это план будущей реализации. Ни одна порция в этой сессии не начата. Проверки ниже — минимум для локального доказательства конкретной правки; перед завершением будущей implementation-задачи также выполняются актуальные обязательные gates её репозитория. Команды запускать после местного nvm use. Новые проверочные скрипты, зависимости и governance-документы не требуются.

B-01. Пилот: независимая проекция upcoming event

  • Находка: R-01. Граница: upcoming-events service, один соседний mapper, существующий projection test; stock/bond и общий scheduler не трогать.
  • Зависимости: нет. Сохранить: все значения DTO/decimal/date/null, поведение вызывающего get и exports, которыми он пользуется.
  • Минимальные проверки: npm test -- --runInBand --runTestsByPath src/modules/investments/upcoming-events/upcoming-events.service.spec.ts, npm run typecheck:app, npm run typecheck:test; проверить HTTP forecast имеющимся SQLite runner при подтверждённом актуальном build.
  • Готово, когда: тест проекций проходит без Prisma mock; mapper не загружает Nest/repository runtime; assertions и сериализованные результаты сохранены. Оценить выигрыш по чтению import graph, не по количеству новых файлов.

Перенос stock/bond из R-01 — последующее локальное решение после пилота, только для функций с тем же подтверждённым препятствием независимой загрузке. Это не разрешение на массовое извлечение каждого helper.

B-02. Одно распознавание invalid token

  • Находка: R-02. Граница: integration-owned predicate, tbank.api.ts, перечисленные проверки catch; сами recovery-ветви и persisted codes прежние.
  • Зависимости: от пилота технически не зависит. Сохранить: точный Error/message критерий, wire value, разные последствия и attempt/revision fencing.
  • Минимальные проверки: TbankClient и token service unit, точный predicate на matching/nonmatching Error и non-Error; app/test typecheck. Если diff затронул больше predicate, соответствующий existing persistence test обязателен.
  • Готово, когда: consumers больше не знают форму message-протокола; нет глобального error registry и расширения того, что считается token error.

B-03. Общая история инструмента для двух страниц

  • Находка: R-03. Граница: StockPage/BondPage VM, instrument-details hook и его публичный экспорт, затронутые проверки. AccountOperations не включать.
  • Зависимости: независимо от R-04; не объединять два рефакторинга. Сохранить: scope/cursor/limit/UID, URL, ordinary retry и exact recovery, включая нынешний возврат A→B→A.
  • Минимальные проверки: обе page suites через Vitest; адресный сценарий invalid cursor и возврата scope с наблюдением запросов/строк; app typecheck и существующий dependency-cruiser gate.
  • Готово, когда: state/query/recovery истории определены в одном месте, специфичные источники страницы читаются прямо, новый hook не требует конфигурационного описания всей страницы.

B-04. Честный тип resource-result

  • Находка: R-04. Граница: shared/api/resource-query.ts и только реальные callers, которым требуется уточнение типа/смысла статуса.
  • Зависимости: нет; отдельно от нового hook. Сохранить: runtime API, тип возвращаемого envelope из refetch, canonical keys, transport/resource distinction и все access/observe guards.
  • Минимальные проверки: app typecheck; T-02 и затронутые pages, затем полный текущий Vitest-набор. Убедиться, что тип метода не исправлен очередным assertion.
  • Готово, когда: тип показывает фактические поля и методы, целого приведения к другому UseQueryResult нет, cache/request/visible-state поведение прежнее.

B-05. Явный результат чтения периодов дохода

  • Находка: R-05. Граница: income planner/analytics, collectRanges, данные возвращаемого repository snapshot и их тестовые fixtures.
  • Зависимости: не требует других batches; выполнять отдельно после менее рискованных шагов. Сохранить: единый snapshot, cold fallback, observe, scheduling, coverage, dates, roles/order/resourceKey/hashes и exact totals.
  • Минимальные проверки: income-period/analytics unit, app/test typecheck, tbank-income-local.e2e-spec.ts через штатный SQLite runner и OpenAPI contract. Использовать актуальную сборку по локальным правилам; не считать старый dist доказательством нового исходника. При изменении repeated-read пути адресно проверить выбор финального snapshot и старую revision.
  • Готово, когда: callback не меняет внешнюю переменную, роль диапазона не угадывается по индексу в operations, итоговый plan объясняется возвращёнными данными того же snapshot. Публичный ответ не меняется.

B-06. Точные названия двух тестов

  • Находка: R-07. Граница: только заголовки двух entity hook tests.
  • Зависимости: нет. Сохранить: assertions, fixtures, retry/client policy.
  • Минимальные проверки: Vitest по usePortfolio.test.tsx и useBrokerageOperations.test.tsx.
  • Готово, когда: каждое обещание в названии подтверждается существующим телом теста. Новых проверок библиотеки нет.

B-07. Условная замена проверки одной колонки

  • Находка: R-06. Граница: один layout assertion AccountPage и его равноценная проверка. Зависимость: сначала доступный воспроизводимый browser/layout способ без установки нового framework в рамках этого плана.
  • Сохранить: expanded/other layout и все content/accessibility assertions.
  • Минимальные проверки: геометрия конкретной секции на узкой и широкой ширине; действующий AccountPage suite. Доказать чувствительность замены к намеренному переходу expanded section в несколько колонок до удаления CSS assertion.
  • Готово, когда: регрессия колонок обнаруживается без чтения внутреннего CSS Mantine. Пока это не доказано, batch отложен и существующий тест остаётся.

Проверка самого отчёта

Смысловой review выполнен по affected areas: рекомендации разделены по причинам, публичные и persisted значения сохранены в ограничениях, для каждой находки есть owner, код и способ проверки. Не предложены новые зависимости, массовая перестройка, удаление защитных ветвей или tests только ради числа строк. Результаты запуска тестов отделены от чтения e2e и от требований к будущим изменениям. Финальная сверка staged/unstaged/new файлов пяти репозиториев показала только новый docs/code-quality-review.md; staged-изменений нет. Новое содержимое просмотрено отдельно, поскольку обычный git diff не показывает untracked-файл. Единственный создаваемый файл — этот отчёт.

8. Выполненные изменения

B-01 — независимая проекция upcoming event, 27 сентября 2026

Статус: выполнено; связанная находка R-01 закрыта в границах пилота. Записи разделов 1–7 сохраняют исходное состояние исследования. Другие порции и аналогичные преобразования stock/bond в этом запуске не выполнялись.

В backend добавлен соседний src/modules/investments/upcoming-events/upcoming-event.mapper.ts: dividendEvent, bondEvent и приватный projectedMoney перенесены дословно. Service вызывает mapper; DTO/provider imports нового файла — type-only. Расчёт можно прочитать и загрузить отдельно от orchestration, Nest и Prisma. Фильтрация, окно дат, сортировка, totals, access и публикация остались в сервисе. HTTP DTO, публичный upcoming-events-read.api.ts и сигнатуры сервиса не менялись.

Существующий upcoming-events.service.spec.ts переименован в upcoming-event.mapper.spec.ts и импортирует mapper. Сохранены все три сценария, fixtures и assertions: точные знаковые дробные выплаты, registry date/валюта, неизвестная сумма и оферта, сумма погашения. Удалён только Prisma mock, который чистому преобразованию больше не нужен. Новых тестовых сценариев не добавлено; сравнение с HEAD подтвердило дословное сохранение функций и тел тестов.

Фактические проверки (перед Node-командами — местный nvm use --silent):

Проверка До изменения После изменения
npm ci --offline --no-audit --no-fund Успешно по существующему lockfile; manifests/lockfile не изменены Повторная установка не требовалась
Backend npm run verify Lint, app/test typecheck и 36 suites / 209 тестов прошли; build упёрся в sandbox EPERM на кэше Prisma Полный gate прошёл: lint, оба typecheck, 36 suites / 209 тестов с coverage и build
Backend npm run build После предоставления доступа к кэшу Prisma прошёл Прошёл в составе verify
node test/sqlite.cjs upcoming-events.e2e-spec.ts 20 тестов прошли Те же 20 тестов прошли
Targeted Jest для upcoming-event.mapper.spec.ts Три исходных теста прошли в общем unit-наборе Отдельный запуск: 3 теста прошли без Prisma mock
Импорт собранного mapper через Node Отдельного mapper ещё не было Обе функции экспортируются; в require.cache нет Nest/Prisma runtime-модулей
Docs npm run verify и review diff Docs gate прошёл Docs gate и проверка diff выполнены после записи результата

Исходный отказ build был ограничением файлового sandbox, не падением кода; сборка и HTTP-тесты повторены с необходимым доступом к кэшу и локальному порту. SQLite runner создавал и удалял временную базу, upstream подменялся тестами; production-данные и реальные внешние API не использовались. При установке npm сообщил о deprecated packages и неразрешённых install scripts двух зависимостей; политика scripts и версии не менялись, проверкам это не помешало.

Boy Scout review: нет новой DI-прослойки, barrel, зависимостей или дублирующего расчёта; mapper остаётся у существующего владельца upcoming-events. Остальные модули не затронуты. Полный набор backend e2e, frontend/browser и отдельный OpenAPI parity не запускались: wire metadata и frontend не менялись; проверен существующий HTTP-сценарий затронутой области. Commit/push не выполнялись.

B-02 — единое распознавание invalid token, 27 сентября 2026

Статус: выполнено; R-02. Проверка текущего кода подтвердила завершённый пилот B-01 и девять одинаковых сравнений ошибки в восьми сервисах. Добавлен backend/src/integrations/tbank/tbank-error.policy.ts с чистым isInvalidTbankToken(error: unknown): boolean; функция экспортирована через существующий tbank.api.ts. Внутри интеграции используются прямые импорты, investment-сервисы используют публичную границу. Новый файл не имеет imports и не создаёт цикл или DI-provider.

Token translation, membership, metadata, portfolio, operations sync, catalog discovery/enrichment и candles теперь вызывают один предикат. Он дословно сохраняет прежнее условие instanceof Error и точное равенство message. Recovery, UID→FIGI fallback, fencing attempt/revision, действия repositories, retry и исходный Error('INVALID_TBANK_TOKEN') не изменены. Значения HTTP/storage codes сохранены. Теперь при чтении catch видно действие сервиса, а форма upstream-ошибки определена в одном месте. Реестр ошибок, новый класс исключения и зависимости не добавлялись.

Тесты. Все существующие проверки сохранены. В tbank-token.service.spec.ts добавлены четыре случая через существующую границу translateInvalidToken: объект с таким же message, строка, Error с префиксом и null должны выйти тем же значением, без перевода в token error. Они защищают от расширения распознавания до duck typing/substring и от ошибки при null. Сценарии сначала прошли на старой production-реализации. В итоговом тесте отказ внешней операции задаётся штатным mockRejectedValue, без подавления lint или TypeScript. Отдельная инфраструктура и файл тестов предиката не нужны.

Проверка До production-правки B-02 После
npm run lint, npm run typecheck:app, npm run typecheck:test Прошли Прошли в составе полного verify
Jest --runInBand --runTestsByPath для tbank-token.service.spec.ts и tbank.client.spec.ts 22 исходных теста; после добавления четырёх защитных случаев — 26 прошли на старой реализации Те же сценарии прошли в полном unit-наборе
Backend npm run verify Полный gate уже проходил после B-01; в этом запуске до правки выполнены проверки строк выше Прошёл целиком: lint, оба typecheck, 36 suites / 213 тестов с coverage, Prisma generate и build
SQLite tbank-connection.e2e-spec.ts 4 теста прошли при повторном запуске после native SIGABRT 4 теста прошли
SQLite tbank-metadata-persistence.e2e-spec.ts 15 тестов прошли 15 тестов прошли при повторном запуске после native SIGABRT
SQLite tbank-catalog-discovery.e2e-spec.ts 10 тестов прошли при повторном запуске после native SIGABRT 10 тестов прошли
SQLite tbank-operations-sync.e2e-spec.ts 17 тестов прошли 17 тестов прошли
Docs npm run verify, git diff --check и смысловой review Исходный отчёт и незакоммиченные изменения сохранены Проверены после записи результата

SQLite suites запускались штатным node test/sqlite.cjs <suite...> на временных базах; реальные upstream заменены mocks. Во всех Node-командах активирована местная .nvmrc. Использованы зависимости, восстановленные успешным npm ci в B-01; manifest/lockfile и установленные версии не менялись.

Исходные падения и ограничения. До изменения production-кода два запуска оборвались с SIGABRT, после — один. В сохранённом выводе catalog/metadata видны node::RemoveEnvironmentCleanupHook, assertion (env) != nullptr и destructor statement native better-sqlite3. Повторные запуски тех же suites прошли без изменения кода, тестов или окружения. Это не assertion теста; точная причина нестабильности в B-02 не исследовалась и не исправлялась. Успешные повторы не означают её устранения. Полный e2e-набор, browser/frontend и OpenAPI parity не запускались; публичный wire-контракт не менялся.

Boy Scout review подтвердил: в production осталось одно сравнение message для этой классификации; действия всех catch-ветвей сохранены, новый policy принадлежит T-Bank. Контрольные суммы файлов B-01 совпадают с началом запуска. Следующая порция B-03 не начата. Commit/push не выполнялись.

B-03 — общая пагинация истории инструмента, 27 сентября 2026

Статус: выполнено; R-03. В существующий frontend slice features/instrument-details перенесён useInstrumentOperations; StockPage и BondPage используют его через публичный index. Стек cursor, запрос истории и recovery теперь определены один раз. Страницы сохраняют URL replace, свои категории доходов, состав screen sync и отображение строк.

Сохранены account=all, limit=20, ожидание UID, canonical query key, exact invalidation первой страницы при INVALID_OPERATIONS_CURSOR и делегирование остальных ошибок screen retry. Scope mismatch по-прежнему только скрывает стек: возврат A→B→A восстанавливает cursor. Hook не импортирует соседнюю feature; callback screen retry передаётся при вызове действия. AccountOperations, resource-query, API и зависимости не менялись.

Тесты. Все существующие сценарии обеих страниц сохранены. Stock-сценарий пагинации дополнен возвратом фильтра и переходом назад. Два адресных recovery случая проверяют свежую сумму после ошибки: invalid cursor возвращает первую страницу, обычная ошибка повторяет текущую через sync=observe. Проверяются параметры HTTP и доступность кнопки «Назад», без доступа к внутреннему state. Новые assertions сначала прошли на старой production-реализации.

Проверка До переноса production-кода После
npm ci --offline --no-audit --no-fund Успешно по существующему lockfile Manifest/lockfile не изменены
npm run lint, npm run typecheck Прошли Прошли в полном verify
npm run test:run -- src/pages/stock/ui/StockPage/StockPage.test.tsx src/pages/bond/ui/BondPage/BondPage.test.tsx 19 исходных тестов; с дополнительными assertions — 21 прошёл Те же 21 прошли
Frontend npm run verify До edits выполнены проверки выше Полностью прошёл: OpenAPI check, TypeScript, Biome, 54 suites / 359 тестов, coverage, Knip, оба dependency-cruiser gate, app и Storybook build
Docs npm run verify, git diff --check Прошли Повторены после записи результата

Coverage: statements 92,26%, branches 82,58%, functions 87,92%, lines 93,27%; пороги не менялись. Первый финальный verify остановился на форматировании двух новых imports; после локального форматирования полный повтор прошёл. При установке npm сообщил о deprecated glob и install script MSW, при Storybook build — о размере chunks. Версии, script policy и build limits не менялись. Во всех Node-командах активирована местная .nvmrc.

Boy Scout review: один предметный hook для двух текущих потребителей, без новых эффектов, generic framework, конфигурации страниц или циклов. Изменения предыдущих порций в backend сохранены. Проверки UI выполнены через RTL/MSW; отдельный браузерный прогон не выполнялся, markup и стили не менялись. Backend suites повторно не запускались. B-04 не начата; commit/push не выполнялись.

B-04 — фактический тип resource-result, 27 сентября 2026

Статус: выполнено; R-04. В frontend/src/shared/api/resource-query.ts удалено приведение всего результата к UseQueryResult с распакованным payload. Теперь TypeScript выводит тип spread и переопределённых полей; локальные аннотации T["data"], T["meta"] и status сохраняют конкретный DTO и словарь статусов. Методы, включая refetch, сохраняют настоящий тип envelope. Это убирает скрытое исключение из контракта без нового wrapper или type alias.

Runtime-поля сохранены: успешный HTTP с data:null остаётся resource-pending, а transport — successful; нативный isLoading не переопределён. Query keys, observe/stateVersion, guards доступа и session isolation не изменены. Специализированная metadata дохода, включая dependency role, сохранена через T["meta"]; ни одному production-потребителю не потребовалась правка.

Тесты. Все существующие проверки оставлены. В resource-lifecycle.test.tsx уточнено различие transport/resource flags и добавлен один сценарий: refetch возвращает envelope сначала с null, затем со свежим портфелем, а hook раскрывает payload и meta. Сценарий сначала прошёл на старом коде; после исправления типа добавлено прямое чтение refetch().data.data и суммы из payload, проверяемое также TypeScript. Отдельная тестовая инфраструктура не добавлялась.

Проверка До изменения production После
npm ci --offline --no-audit --no-fund Прошла по существующему lockfile Зависимости и lockfile сохранены
npm run lint, npm run typecheck Прошли Прошли в полном verify
npm run test:run -- для resource-lifecycle, authenticated-query, tbank-connection/model, section, StockPage и BondPage 7 suites / 49 тестов; дополненный lifecycle отдельно — 5 тестов 7 suites / 50 тестов
Frontend npm run verify Выполнены адресные проверки выше Прошёл полностью: OpenAPI check, TypeScript, Biome, 54 suites / 360 тестов, coverage, Knip, оба dependency-cruiser gate, app и Storybook build
Docs npm run verify, git diff --check Прошли Повторены после записи результата

Coverage: statements 92,27%, branches 82,58%, functions 87,92%, lines 93,28%; пороги сохранены. Промежуточный typecheck выявил потерю конкретного metadata типа при выводе из generic constraint; исправлено у владельца через T["meta"], без assertions и изменений callers. Исходных падений проверок не было. Остались предупреждения npm о glob/install script MSW и Storybook о chunks; версии и настройки не менялись. Перед Node-командами активирована .nvmrc.

Boy Scout review: production diff ограничен одним преобразованием результата, новых cache/state machine, runtime schema и экспортов нет. Diff и checksum файлов B-03 совпадают с началом запуска; backend не редактировался. Браузерный прогон и backend suites не запускались: UI и wire-контракт не менялись. B-05 не начата. Commit/push не выполнялись.

B-05 — явный snapshot периодов дохода, 27 сентября 2026

Статус: выполнено; R-05. collectRanges теперь возвращает openedAt и именованные periods из итогового repository snapshot. В существующий operations domain добавлен конкретный тип OperationPeriod: date-only range с именем, которое задаёт income. Repository сохраняет исходные даты и имя внутри прежней read transaction; operations переносит имя в resourceKey. Позиционный массив selected/previous/yearAgo удалён из operations.

Income выбирает результаты по имени; callback больше не присваивает внешний plan. Summary использует прочитанный range, analytics восстанавливает чистый план по выбранным датам и openedAt того же snapshot. Даты уже выполненного чтения не заменяются новым default-периодом после await. Начальная валидация analytics до access/scheduling сохранена. При отсутствии snapshot используются периоды, построенные с undefined, и прежний cold response.

Transaction, повторные чтения, revision fence, observe и scheduling сохранены. Дополнительные name/range не передаются в scheduling targets. Сохранены resourceKey, binding/hash inputs, coverage и внешние dependency roles/order selected, previous, year; внутреннее имя yearAgo не меняет HTTP API. Расчёт денег, buckets, календарные правила и DTO не редактировались.

Тесты. Все 16 income unit-сценариев и их assertions сохранены; fixtures адаптированы к явному результату, повторные mocks сведены к существующему seed. В SQLite/HTTP suite добавлены три случая: изменение openedOn в обе стороны во время scheduling должно отразиться в итоговом year comparison и revision; замена подключения после первого snapshot не должна запускать старую revision или возвращать её данные, но должна сохранить cold period identity. Все три проверки сначала прошли на старой production-реализации.

Проверка До production-правки После
npm ci --offline --no-audit --no-fund Прошла по текущему lockfile Manifest/lockfile не изменены
npm run lint, npm run typecheck:app, npm run typecheck:test Прошли Прошли в полном verify
Jest --runInBand --runTestsByPath для income-period и income-analytics 2 suites / 16 тестов Те же 16 прошли
npm run build и node test/sqlite.cjs tbank-income-local.e2e-spec.ts openapi-contract.e2e-spec.ts После разрешения доступа к кэшу Prisma: 7 income + 3 OpenAPI теста; дополненная income suite — 10 тестов на старом коде После актуальной сборки: 10 income + 3 OpenAPI теста прошли
Backend npm run verify Выполнены адресные проверки выше Полностью прошёл: lint, оба typecheck, 36 suites / 213 unit-тестов с coverage, Prisma generate и build
Docs npm run verify, git diff --check Прошли Повторены после записи результата

Исходный build получил sandbox EPERM на кэше Prisma; повтор с доступом к кэшу и локальному HTTP-порту прошёл. Native SIGABRT в этом запуске не наблюдался. SQLite suites использовали временные базы и synthetic upstream; production данные и реальные внешние API не вызывались. npm сообщил о deprecated packages и install scripts двух зависимостей; версии и политика scripts не менялись. Перед Node-командами активирована местная .nvmrc.

Boy Scout review: новые файлы, DI-слои и generic planner не добавлены; планирование и snapshot остаются у прежних владельцев. Diff B-01/B-02 и staged index побайтово совпали с началом запуска; frontend не редактировался. Полный backend e2e-набор и browser/frontend проверки не запускались. B-06 не начата. Commit/push не выполнялись.

B-06 — точные названия query-тестов, 27 сентября 2026

Статус: выполнено; R-07. Изменены только два заголовка: usePortfolio.test.tsx — loads the account portfolio; useBrokerageOperations.test.tsx — normalizes categories, preserves the cursor and loads a page. Названия больше не обещают проверку staleTime/retry. Сравнение с HEAD подтвердило дословное сохранение тел тестов, fixtures и assertions, включая background refetch. Production-код, клиент и зависимости не менялись; новых тестов нет.

До edits npm run test:run -- src/entities/portfolio/api/usePortfolio.test.tsx src/entities/broker-operation/api/useBrokerageOperations.test.tsx прошёл: 2 файла / 3 теста. После — полный frontend npm run verify: 54 suites / 360 тестов, coverage, TypeScript, Biome, OpenAPI check, Knip, оба dependency-cruiser gate, app и Storybook build прошли. Использованы зависимости из успешного npm ci B-04; перед Node-командами активирована .nvmrc. Падений нет; предупреждение Storybook о размере chunks осталось без изменения настроек. Docs npm run verify и git diff --check выполнены до/после записи результата.

Boy Scout review подтвердил соответствие каждого заголовка assertions и scope в две строки тестов. Предыдущие изменения и staged-файлы сохранены. Backend/browser проверки не запускались. B-07 не начата; commit/push не выполнялись.

B-07 — условие замены layout-проверки, 27 сентября 2026

Статус: отложено; R-06 остаётся открытой. Повторная проверка подтвердила актуальность CSS assertion в AccountPage.test.tsx и cols={1} для expanded sections в PositionSection.view.tsx. Условие безопасной замены не выполнено: vitest.config.ts использует JSDOM, package scripts и Gitea verify не содержат browser/layout gate. Storybook build не исполняет play; существующий src/test/storybook.test.tsx также использует RTL/JSDOM. Ни story отдельной секции с геометрической проверкой, ни воспроизводимого browser runner в проекте не обнаружено. Доступный агенту браузерный инструмент сам по себе не становится регрессионной проверкой репозитория.

Согласно условию B-07, существующий тест оставлен целиком. Production-код, остальные assertions, зависимости и конфигурации не менялись. Геометрия при двух ширинах и чувствительность к намеренной регрессии не проверялись: без постоянно исполняемой замены это не даёт основания удалить текущую защиту. Введение нового browser runner выходит за согласованную границу этой порции.

npm run test:run -- src/pages/account/ui/AccountPage/AccountPage.test.tsx прошёл: 41 тест. Docs npm run verify и git diff --check проверены до/после записи; перед Node-командами активирована .nvmrc. Полный frontend verify повторно не запускался: после успешного B-06 изменена только эта запись. Boy Scout review и routing подтвердили отсутствие code diff этой порции; предыдущие staged/unstaged изменения сохранены. Commit/push не выполнялись.

Порции B-01–B-06 выполнены; B-07 требует отдельного решения о постоянном браузерном запуске, затем проверки геометрии и доказательства обнаружения регрессии до удаления CSS assertion.

B-07 — браузерная проверка геометрии, 27 сентября 2026

Статус: выполнено; R-06. Пользователь явно разрешил узкое исключение из запрета на новые библиотеки: браузерный провайдер текущего Vitest, Chromium в verify/CI и одну проверку геометрии на двух ширинах с намеренной регрессией. Добавлены devDependencies @vitest/browser-playwright@5.0.0 и playwright@1.63.0; версии существующих пакетов в lockfile не изменились.

vitest.config.ts разделяет JSDOM project с прежним setup и браузерный project с двумя Chromium instances: 375 и 1280 px. Существующий npm run verify исполняет оба вместе с coverage; пороги сохранены. Оба Gitea workflow, вызывающих verify, устанавливают Chromium через playwright install --with-deps. README описывает локальный запуск; .vitest с артефактами исключён из Git.

position-layout.browser.test.tsx использует production PositionSection, тему и CSS, три synthetic позиции и реальные bounding rectangles их видимых названий. Акции и облигации остаются вертикальными на обеих ширинах; other — вертикальным на узкой и в три колонки на широкой. Проверка не читает CSS-переменные, классы или style tags Mantine, не подменяет layout API. Удалён только прежний CSS-тест из AccountPage.test.tsx; остальные content/accessibility assertions и production-код не изменены.

До удаления старого теста новая проверка прошла, AccountPage suite прошёл 41 тест. Временное cols={2} вызвало ожидаемое падение на 375 px. После окончательной настройки instances оба браузерных запуска снова прошли; мутация только облигаций на responsive grid вызвала падение bonds at 1280px (left 501 вместо 69), узкий запуск остался зелёным. После каждого опыта исходный компонент восстановлен побайтово. Конфликт типов browser matchers с существующими JSDOM assertions устранён отказом от импорта vitest/browser в тест: ширины задаёт конфигурация. Существующие assertions не переписывались.

Выполнены npm ci, установка Chromium, целевой browser run и полный npm run verify: 56 запусков suites / 361 тест (359 JSDOM и два запуска одного browser test), coverage, TypeScript, Biome, OpenAPI, Knip, оба dependency-cruiser gate, app и Storybook build прошли. Coverage: statements 92,3%, branches 82,66%, functions 87,92%, lines 93,3%. Docs npm run verify и git diff --check прошли. Перед Node-командами активирована .nvmrc.

Ограничения: npm audit --include=dev --audit-level=low не выполнен успешно: настроенный Nexus вернул HTTP 400 с требованием настроить Lifecycle/Firewall. Отсутствие уязвимостей не подтверждено. Vitest сообщает о проигнорированном Vite hook configureServer; Storybook — о размере chunks. Оба предупреждения не остановили проверки. CI workflow изменены, но удалённый Linux runner и публикация образа не запускались; browser proof получен локально в Chromium на macOS. Другие браузеры и полная визуальная матрица вне scope.

Boy Scout review: проверка находится у AccountPage, отдельный e2e runner, новые production abstractions и изменения контрактов не добавлены. Сравнение diff подтвердило сохранность предыдущих staged/unstaged изменений. Порции B-01–B-07 завершены; commit/push не выполнялись.