diff --git a/.markdownlint-changelog.json b/.markdownlint-changelog.json new file mode 100644 index 0000000..9833cc3 --- /dev/null +++ b/.markdownlint-changelog.json @@ -0,0 +1,6 @@ +{ + "extends": ".markdownlint.json", + "MD024": { + "siblings_only": true + } +} diff --git a/.markdownlint-cli2.jsonc b/.markdownlint-cli2.jsonc deleted file mode 100644 index c4d9da3..0000000 --- a/.markdownlint-cli2.jsonc +++ /dev/null @@ -1,21 +0,0 @@ -{ - "config": { - "MD013": false - }, - "customRules": [ - "markdownlint-rule-relative-links" - ], - "overrides": [ - { - "filter": [ - "CHANGELOG.md" - ], - "config": { - "MD024": { - "siblings_only": true - } - }, - "combine": "merge" - } - ] -} diff --git a/.markdownlint-rule-relative-links.cjs b/.markdownlint-rule-relative-links.cjs new file mode 100644 index 0000000..d822720 --- /dev/null +++ b/.markdownlint-rule-relative-links.cjs @@ -0,0 +1 @@ +module.exports = require('markdownlint-rule-relative-links').default; diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 0000000..67d2ae5 --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,3 @@ +{ + "MD013": false +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 55990c6..0defadd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## [0.1.3](https://git.personal.ksv741.ru/WealthLedger/docs/compare/v0.1.2...v0.1.3) (2026-10-07) + +### Bug Fixes + +* **deps:** refresh docs security baseline ([256d49b](https://git.personal.ksv741.ru/WealthLedger/docs/commit/256d49b04591d55d980a22c49b413432c9bda746)) +* **deps:** remove vulnerable docs tooling ([9477e68](https://git.personal.ksv741.ru/WealthLedger/docs/commit/9477e688d180783881abc92f8835517715880279)) + +### Documentation + +* record code quality review and refactoring ([6fa62fb](https://git.personal.ksv741.ru/WealthLedger/docs/commit/6fa62fb8b51aba82766fb50b3e5871572af8be27)) +* record minimal runtime coordination ([0ee54ca](https://git.personal.ksv741.ru/WealthLedger/docs/commit/0ee54ca952db6ca7264c0d7fe8cf5759b3187c5b)) + ## [0.1.2](https://git.personal.ksv741.ru/WealthLedger/docs/compare/v0.1.1...v0.1.2) (2026-09-26) ### Documentation diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d1669fb..53f0693 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -177,9 +177,10 @@ Markdown-only правки не требуют тестов приложения `security.yml` при push, PR и ежедневно проверяет всю Git-историю через Gitleaks 8.30.1 с redacted-выводом и все npm dependencies через -`npm audit --include=dev --audit-level=low`. Override `smol-toml` исправляет -DoS в TOML parser markdownlint-cli2; его можно удалить, когда родительский -пакет примет исправленную версию. +`npm audit --include=dev --audit-level=low`. Overrides `smol-toml` и `js-yaml` +исправляют DoS в parsers markdownlint-cli; overrides для release-it обновляют +уязвимые basic-ftp и undici. Их можно удалить, когда родительские пакеты +примут исправленные версии. Renovate объединяет minor/patch updates; major требует согласования в Dependency Dashboard, automerge не включён. Required checks включаются diff --git a/adr/009-concurrent-refresh-rotation-policy.md b/adr/009-concurrent-refresh-rotation-policy.md index 21e85ae..2c303b4 100644 --- a/adr/009-concurrent-refresh-rotation-policy.md +++ b/adr/009-concurrent-refresh-rotation-policy.md @@ -2,6 +2,9 @@ **Status:** Accepted; supersedes the refresh-token reuse rule in ADR-006. +Cookie-clearing policy уточнена в [ADR 018](018-minimal-runtime-coordination.md): +неуспешный refresh больше не удаляет cookie. CAS и отказ superseded token сохранены. + ## Context Два одновременных запроса могут предъявить один действительный refresh token. Отзыв Session для проигравшего запроса делает недействительной сессию успешного запроса, а сохранение отдельной истории token или grace-window для различения replay добавляет непропорциональное состояние локальному MVP. diff --git a/adr/017-local-tbank-reads-and-background-sync.md b/adr/017-local-tbank-reads-and-background-sync.md index fa588b5..15eef69 100644 --- a/adr/017-local-tbank-reads-and-background-sync.md +++ b/adr/017-local-tbank-reads-and-background-sync.md @@ -2,6 +2,10 @@ **Status:** Accepted — архитектурное направление согласовано. +Prefetch policy ниже заменена [ADR 018](018-minimal-runtime-coordination.md): +свечи загружаются по запрошенному диапазону, без предварительной загрузки +всей истории бумаг портфеля. Остальные гарантии локального хранения сохранены. + Реализация `persist-tbank-data` добавлена в рабочие ветки приложения; performance/browser и canonical application gates пройдены. Это не отметка развёртывания в production. Ниже сохранён контекст diff --git a/adr/018-minimal-runtime-coordination.md b/adr/018-minimal-runtime-coordination.md new file mode 100644 index 0000000..40b8499 --- /dev/null +++ b/adr/018-minimal-runtime-coordination.md @@ -0,0 +1,45 @@ +# 018. Минимальная runtime-координация + +**Status:** Accepted. Уточняет cookie policy ADR 009 и prefetch policy ADR 017. + +## Context + +Изоляция владельцев данных, атомарные финансовые изменения и локальное чтение +не требуют отдельного протокола согласования каждого HTTP-ответа. Дополнительная +координация оправдана только наблюдаемым поведением, которое иначе нарушается. + +## Decision + +- Refresh сохраняет атомарную ротацию. Неуспешный refresh не меняет cookie: + поздний отказ не должен удалять cookie успешного запроса. Logout очищает cookie. +- Frontend объединяет refresh и упорядочивает cookie-writing requests только + внутри вкладки. Cross-tab auth synchronization не является требованием; + после изменения сессии в другой вкладке допускаются reload или re-login. +- Отменой, сбросом и публикацией query results владеет TanStack Query. + Смена session/connection очищает соответствующий cache; отдельные registries + HTTP-запросов, access epochs и URL-wide latest-read counters не используются. + Локальная auth boundary предотвращает применение старого auth result и + повтор mutation под другой identity. +- Status GET читает только состояние работы. Открытый экран перечитывает + resources через `sync=observe` во время работы и после завершения. + Сверка hashes между status и data GET, публичные progress counters и + невыдаваемые sync states не нужны. Полнота истории остаётся в resource metadata. +- Свечи загружаются для запрошенного диапазона. Предзагрузки всей истории + портфеля и автоматического расширения короткого периода до `all` нет. +- Backend формирует согласованные entries и aggregates из одних исходных + данных; frontend доверяет этому contract и не выполняет вторую сверку групп. +- Request context и HTTP completion принадлежат `nestjs-pino`/`pino-http`. + Проект сохраняет safe field filtering, redaction и route-template logging. +- Deployment всегда выполняет один stop → backup → migration → readiness + цикл. No-op и frontend-only оптимизации не обещаются; короткий простой принят. + +## Consequences + +Первое открытие нового диапазона свечей может ждать загрузку. Наблюдение активной +работы может перечитать неизменившиеся локальные данные, зато не требует +checksum protocol. Повтор deployment создаёт новый backup и может остановить API. + +DB ownership подключения, conditional publication, coverage, staging, +банковские intent fingerprints и версии записей сохраняются. Отмена HTTP +не считается отменой уже выполняющейся server-side transaction. +Сохранённый полный forecast не заменяется candidate с изменившимися dependencies. diff --git a/adr/README.md b/adr/README.md index 3e49b5d..efe8b86 100644 --- a/adr/README.md +++ b/adr/README.md @@ -22,3 +22,4 @@ - [015 — SQLite в ограниченном single-server deployment](015-single-server-sqlite-deployment.md) - [016 — Версионируемый комплект развёртывания](016-versioned-deployment-bundle.md) - [017 — Локальное чтение данных T-Bank и фоновая синхронизация](017-local-tbank-reads-and-background-sync.md) +- [018 — Минимальная runtime-координация](018-minimal-runtime-coordination.md) diff --git a/architecture/containers.md b/architecture/containers.md index 5884216..d471fc9 100644 --- a/architecture/containers.md +++ b/architecture/containers.md @@ -195,9 +195,9 @@ access token и вращаемый refresh token. [Политика query-кэш Авторизация доступа, недопущение token в публичный ответ и атомарность ротации сессии — отдельные свойства; наличие типизированного клиента -не доказывает их. Известные расхождения регистрации и деталей валидации -отмечены в [аутентификации](../domains/authentication/overview.md#регистрация-и-вход) -и [HTTP API](http-api.md#error-contract). +не доказывает их. Известное расхождение регистрации отмечено в +[аутентификации](../domains/authentication/overview.md#регистрация-и-вход), +а действующий формат ошибок описан в [HTTP API](http-api.md#error-contract). Поддерживается локальный Compose и ограниченная single-server topology с SQLite. Origin браузера, адрес API и cookie policy должны быть согласованы. diff --git a/architecture/cross-cutting/data-freshness.md b/architecture/cross-cutting/data-freshness.md index 64c262e..27da9b0 100644 --- a/architecture/cross-cutting/data-freshness.md +++ b/architecture/cross-cutting/data-freshness.md @@ -43,6 +43,10 @@ Status GET и data GET с `sync=observe` не запускают работу. наблюдение прекращается. Поздний ответ не заменяет уже принятый новый. [HTTP API](../http-api.md) объясняет envelope и экранный refresh. +Отменой query lifecycle владеет TanStack Query. Status не пересобирает +финансовые ответы и не передаёт hash notifications. Свечи нового диапазона +загружаются при его открытии; предзагрузка всей истории портфеля не выполняется. + ## Смена подключения Каждое чтение проверяет локальную сессию, текущее подключение, известный отказ diff --git a/architecture/deployment.md b/architecture/deployment.md index 7dadddc..e73dc7b 100644 --- a/architecture/deployment.md +++ b/architecture/deployment.md @@ -142,20 +142,15 @@ backups находятся рядом с data directory, вне checkout, и а не удаляются. Незавершённая копия помечается `.partial-` и не подходит для restore. Ключ шифрования хранится отдельно. Локальная копия не защищает от потери диска. -## Независимое обновление frontend +## Единый цикл обновления -Если изменились только image или конфигурация frontend, backend не -пересоздаётся, backup и migration не выполняются. Возможен короткий перерыв -на внешней точке входа; старым browser tabs может потребоваться reload. -При неизменных исправных контейнерах повтор команды не пересоздаёт сервисы. - -## Независимое обновление backend - -Изменение image или конфигурации backend запускает stop → backup → migration → -readiness. Неизменный frontend не пересоздаётся. Во время простоя возможен 502. +Каждое обновление, включая повтор того же тега и изменение только frontend, +выполняет stop → backup → migration → readiness. Сравнение hashes контейнеров +и конфигурации не выполняется. Приняты короткий простой API и дополнительный +backup; во время простоя возможен 502, вкладкам может потребоваться reload. Ошибка migration блокирует старт, ошибка готовности останавливает новый backend; ошибка frontend не останавливает исправный backend. Успех фиксируется только -после проверок запуска затронутых компонентов. +после проверок запуска обоих компонентов. Одна file lock защищает установку от параллельных команд. Маркер незавершённого обновления блокирует повтор до разбора оператором. Он содержит этап, версии diff --git a/architecture/http-api.md b/architecture/http-api.md index da471d3..75aa35a 100644 --- a/architecture/http-api.md +++ b/architecture/http-api.md @@ -26,8 +26,8 @@ T-Bank data GET возвращают envelope `{ data, meta }`. `data` може при missing или неполном финансовом агрегате; partial ledger/candles могут содержать подтверждённые строки. `meta` разделяет availability, freshness, lastSuccessfulAt, sync, coverage и состояния зависимостей. `dataRevision` -описывает набор данных, `stateVersion` — наблюдаемое состояние, включая -пустое завершённое покрытие. Ни одно поле не раскрывает connection revision. +описывает набор данных; пустое завершённое покрытие явно представлено в coverage. +Публичной hash-версии состояния нет. Ни одно поле не раскрывает connection revision. Обычный GET планирует missing/stale зависимости после проверки доступа. GET с `sync=observe` только читает. `POST /api/v1/tbank-sync` принимает @@ -35,6 +35,11 @@ GET с `sync=observe` только читает. `POST /api/v1/tbank-sync` пр scope. `GET /api/v1/tbank-sync/:id` наблюдает разрешённую работу; чужой или прежний handle возвращает `404`. Приём работы не означает её успех. +Status возвращает state/errorCode без данных ресурсов и численных progress +counters. Клиент перечитывает показанные resources через `sync=observe` во время +работы и после завершения, сохраняя generated query identity. Sync states ресурса: +`idle`, `running`, `failed`; отдельного публичного `queued` нет. + Source failure принятой работы передаётся безопасным `meta.sync.errorCode`; HTTP transport/access/input errors сохраняют Problem Details. Старые данные при transient failure сохраняют исходное успешное время. Детали поведения — @@ -65,16 +70,11 @@ message/stack. Типичный ответ: Для input validation контракт требует `400`, `VALIDATION_ERROR` и `errors` как массив `{ pointer, detail }`, где pointer адресует поле запроса. Это требование -не считается проверенным одним описанием OpenAPI: нужна проверка реального -HTTP-ответа после всех стадий обработки запроса. - -**Известное расхождение:** согласованная -[спецификация аутентификации](https://git.personal.ksv741.ru/WealthLedger/specs/src/commit/9e227a563dda67620f0ad78228aa8a4c18bf35a0/openspec/specs/user-authentication/spec.md#requirement-стандартные-ответы-об-ошибках) -требует `pointer` и `detail` для невалидных полей. В текущей конфигурации -валидация формирует строковые сообщения, а фильтр ожидает структурированные -ошибки полей; поэтому корректность деталей `errors` не подтверждена. -Исправление требует отдельного изменения реализации и проверки полного -HTTP-потока. OpenAPI сам по себе этого расхождения не устраняет. +проверяется реальным HTTP-ответом после всех стадий обработки запроса. Глобальная +валидация передаёт фильтру структурированные ошибки без исходных значений и DTO, +а фильтр формирует JSON Pointer, включая вложенные поля. HTTP suites подтверждают +детали ошибок регистрации, банковской карты и синхронизации; OpenAPI отдельно +фиксирует форму ответа. Ответ не включает автоматически все поля RFC (`type`, `title`, `instance`). Добавление обязательного поля является изменением публичного контракта. diff --git a/code-quality-review.md b/code-quality-review.md new file mode 100644 index 0000000..40abbd7 --- /dev/null +++ b/code-quality-review.md @@ -0,0 +1,1001 @@ +# Читаемость и сопровождаемость WealthLedger + +## 1. Область исследования + +Отчёт от 27 сентября 2026 года. Результат — семь подтверждённых возможностей +упрощения и небольшие порции работы. Наиболее полезный безопасный старт — +отделить чистое преобразование будущих выплат от сервисной оркестрации. +Затронутые области рекомендаций: backend investments/T-Bank, frontend +страницы инструментов, адаптер ресурсных запросов и несколько тестов. +Изменений реализации в рамках исследования нет. + +Это ревью стоимости понимания и изменения существующего кода. Бизнес-баги, +безопасность, производительность и полнота продукта не оценивались. +Количество файлов, строк и тестов использовалось для навигации, а не как +оценка качества. Ни одно удаление production-кода не предлагается на основании +одного поиска по имени. + +### Репозитории и глубина чтения + +Все доступные first-party пакеты с production-кодом включены в обследование. +Root, frontend, backend, docs и specs — самостоятельные Git-репозитории. +В начале исследования рабочие деревья были чистыми. Пути в отчёте указаны +от корня workspace; диапазоны строк относятся к следующим исходным HEAD: + +| Репозиторий | HEAD | Обследованная область | +| --- | --- | --- | +| frontend | `f6db28defa51bd9d8fe932ceae4c3c720aba7eb3` | Приложение, три workspace-пакета, публичные экспорты, статический граф, тестовые группы | +| backend | `4796d9d3f0160d96004a982f6c2526f199de4b9f` | Nest-модули, integrations, Prisma-границы, DTO, unit-тесты и выбранные HTTP/SQLite-сценарии | +| docs | `e4838e14855e9e696683a310efbc6afaaae51164` | Правила документации, архитектура, свежесть, доход, будущие выплаты и относящиеся к ним ADR | +| root, specs | Навигация и правила локальных checkout | Manifest/workspace-конфигурация и требования как контекст; самостоятельного runtime-пакета здесь нет | + +Прочитаны локальные AGENTS и применимые архитектурные, инженерные и тестовые +правила соответствующих репозиториев. Общие правила root не переносились на +приложения. Размещение этого отчёта в docs — прямо запрошенное пользователем +исключение из местного запрета на хранение исследовательских отчётов; правила +репозитория и навигационные индексы не изменялись. + +В frontend прослежены маршруты и сессия → API/query → entity → page VM → view; +особенно подробно — resource lifecycle, синхронизация экрана, акции/облигации, +портфель, фильтры операций/дохода и банковские формы. Прочитаны реализации +и экспорты tokens/ui/icons. В backend прослежены auth, банковские расчёты и +транзакции, инвестиционные ресурсы, операции/доход/прогноз, источники каталога +и свечей, T-Bank/MOEX/НКР. Для находок прочитаны конкретные реализации, +вызывающий код и относящиеся к ним тесты. + +**Ограничения:** это не построчное ревью каждого файла. Крупные наборы +AccountIncomePage, банковских UI/HTTP-тестов, persistence-тестов и отдельных +view/story-файлов читались выборочно; перечни остальных сценариев использованы +для карты покрытия. Не исследованы полностью миграции, эксплуатационные +скрипты, контейнерный runtime и все ветви каждого scheduler. Архивы, +`references/**`, generated-код, vendor, сборки и lockfile не оценивались как +авторский код. Источники OpenAPI-генерации и граница передачи схемы учитывались. +Graphify-графа в checkout нет; он не создавался, чтобы не оставлять дополнительные +артефакты. Реальные внешние API, production и потребители за пределами доступных +checkout не проверялись. Признак `private` в manifest не доказывает отсутствие +неизвестного внешнего использования. + +### Фактический стек + +Версии ниже взяты из manifest и сверены с установленными пакетами для основных +runtime-библиотек. Это фиксация контекста, а не предложение обновить зависимости. + +| Область | Используемые пакеты и версии | +| --- | --- | +| Frontend workspace | `wealth-ledger-frontend` 0.4.0; `@wealthledger/tokens`, `@wealthledger/ui`, `@wealthledger/icons` — 0.3.0; все private | +| UI и состояние | React/React DOM 19.3.0; React Router 8.3.1; Mantine core/hooks/dates/charts 9.6.1; TanStack Query 5.102.8 | +| Формы и API | React Hook Form 7.88.0; resolvers 5.9.1; Zod 4.6.4; openapi-fetch 0.17.0; openapi-react-query 0.5.4 | +| Представление | i18next 26.4.2; react-i18next 17.0.14; motion 13.2.0; dayjs 1.11.23; recharts 3.10.1 | +| Frontend tooling | TypeScript 5.9.3; Vitest 5.0.0; MSW 2.15.0; Vite 8.3.0 и Storybook 10.6.0 по manifest | +| Backend | `wealth-ledger-backend` 0.2.3; один пакет без npm workspaces | +| Сервер и хранение | Nest common/core/platform-express/swagger/jwt 12.0.1; config/passport 12.0.0; Prisma/client/SQLite adapter 7.10.0; better-sqlite3 13.0.3 | +| Внешние границы | grpc-js 1.14.4; proto-loader 0.8.1; class-validator 0.15.1; class-transformer 0.5.1; bcrypt 6.0.0; Cheerio 1.2.0; lru-cache 11.5.2 | +| Серверное окружение | nestjs-pino 5.1.0; pino-http 11.0.0; RxJS 7.8.2; TypeScript 6.0.3; Jest 30.5.1 | +| Документация и root | docs 0.1.2, root 0.2.0 — tooling; markdownlint-cli2 0.23.2, relative-links 5.1.2; Node 24.21.0 из локальных `.nvmrc` | + +### Выполненные проверки + +Перед Node-командами активирована локальная версия через +`source ~/.nvm/nvm.sh && nvm use --silent`. Использованы уже установленные +зависимости, без auto-fix и установки инструментов. + +| Где | Команда | Результат | +| --- | --- | --- | +| frontend | `./node_modules/.bin/depcruise src packages --config dependency-cruiser.cjs --output-type err-long` | 322 модуля, 1381 зависимость; нарушений настроенных правил нет | +| frontend | `./node_modules/.bin/vitest run --no-cache` | 54 файла, 357 тестов прошли | +| frontend | `./node_modules/.bin/tsc -p tsconfig.app.json --noEmit --incremental false` | Успешно; это проверка app-конфигурации, не отдельная сборка каждого пакета | +| frontend | `./node_modules/.bin/knip --no-progress` | Exit 0; неиспользуемые элементы не заявлены; configuration hint: исключённые `.mdx` не обходятся | +| backend | `npm test -- --runInBand --no-cache` | 36 suites, 209 тестов прошли; snapshots: 0 | +| backend | `npm run typecheck:app && npm run typecheck:test` | Обе проверки прошли | +| docs | `npm run verify` | Успешно: Markdown lint проверил 47 файлов, замечаний нет; `git diff --check` прошёл | + +Существующие backend HTTP/SQLite/e2e, durability/container gates и OpenAPI parity +в этой сессии не запускались. Не выполнялись браузерная/визуальная проверка, +полный application verify, сборка и генерация артефактов. Успех unit-тестов +не подменяет эти проверки и не доказывает поведенческую эквивалентность будущих +изменений. Для самого отчёта достаточно docs gate и смысловой сверки; в порциях +ниже указана дополнительная проверка будущего рефакторинга. + +## 2. Краткий вывод + +Основная сложность возникает на стыках уже полезных механизмов: +сервисный граф загружается ради чистого mapper; одинаковый cursor-протокол +поддерживается в двух VM; смысл периода передаётся через callback и индекс; +результат предметного ресурса маскируется типом нативного Query result. +Это локальные проблемы ответственности и явности контрактов. Перестройка +всей архитектуры или смена библиотек не обоснована. + +Предлагаемый порядок: маленький mapper-пилот, единое распознавание ошибки +T-Bank, общая пагинация двух инструментальных страниц; затем отдельно +типизация resource-result и более рискованная граница income/operations. +Два замечания к тестам имеют меньший приоритет: неточное описание проверок +и зависимость layout-теста от внутреннего CSS Mantine. + +**Оставить без переписывания:** + +- FSD-направление импортов, публичные package exports и backend `*.api.ts`. + В обследованном frontend-графе не обнаружена утечка application/domain в + общие пакеты. Переэкспорты здесь обозначают реальные границы. +- `EntryCard`, `StateView`, `LoadingSkeleton`: общая структура, доступность + и состояния имеют собственную ответственность. Это не повод заменить + каждый компонент прямым вызовом Mantine. +- Tokens/theme adapter и три SVG-компонента icons: локальные преобразования + и явные компоненты проще фабрики или нового runtime-валидатора trusted JSON. +- React.lazy-адаптеры именованных экспортов: они сохраняют lazy-loading. + Роутер не нуждается в универсальном конфигурационном языке. +- Состояние/refs сессии, single-flight, очередь/Web Locks, generation/access + epochs и очистку кэша. Похожее состояние обслуживает разные временные + границы; простое удаление разрушает защиту поздних ответов. +- Разделение transport error, failed source, missing/partial/complete и + сохранённого snapshot. TanStack Query не знает этого предметного протокола. +- Draft/applied фильтры и стабилизацию периода в income VM: эффект reset формы + зависит от идентичности значения; механическое удаление memo не обосновано. +- Точную decimal/bigint-арифметику, банковский `BankErrorCode`, транзакции, + проверки версий, idempotency и возвраты. Не заменять их float-арифметикой, + новым money-пакетом или generic repository над Prisma. +- Валидацию MOEX/НКР/T-Bank, ограниченные retry/admission и fencing публикации. + Типы TypeScript не проверяют внешний ответ. Scheduler свечей, операций и + каталога различаются обязанностями; единый job engine не предлагается. + +## 3. Рабочие критерии рефакторинга + +1. Правка должна уменьшать число мест, которые нужно открыть для изменения + конкретного правила. Новое имя/файл оправдано понятной ответственностью, + а не ограничением длины функции. +2. Использовать существующего владельца: UI — packages/ui, API транспорта — + shared/api, история инструмента — instrument-details, особенности провайдера — + integrations/tbank, периоды дохода — income. Не складывать предметные правила + в общий `utils` и не заводить новые пакеты для этих находок. +3. Объединять совпадающее поведение, а не похожий синтаксис. У страницы всех + операций иной scope и фильтры, у купона и дивиденда разные даты/единицы. +4. Сохранять локальный OpenAPI input и wire values. Generated-файлы вручную + не править; независимый литерал в контрактном тесте может быть полезен. +5. Сначала искать определение предметного значения. Существующий `as const` + не дефект. Во frontend `erasableSyntaxOnly` исключает обычный строковый enum; + предпочтение enum из запроса не применяется вопреки этому ограничению. +6. Тип должен описывать фактический результат. Удаление assertion полезно, + если после него виден настоящий контракт; замена на `any`/`unknown as` не + является упрощением. +7. Не удалять редкие ветви без доказательства эквивалентной защиты. Сохранить + отмену, доступ, late responses, атомарность, точность и accessibility. +8. Сначала получить тот же наблюдаемый результат существующим тестом. + Добавлять точечную проверку только там, где затрагиваемый контракт не + защищён; не тестировать библиотеку ради формального покрытия. +9. Проверять affected area после diff: не появился ли второй способ решать + задачу, не потеряны ли публичный экспорт или уникальное утверждение теста. + Размер diff и зелёный lint сами по себе этого не доказывают. + +## 4. Карта ответственности и зависимостей + +| Владелец | Ответственность и публичная граница | Потребители и направление | Решение | +| --- | --- | --- | --- | +| frontend app | Маршруты, auth composition, страницы; `app → pages → features → entities → shared` | Браузерное приложение; page собирает сценарий из нижних слоёв | Сохранить; не создавать общий page engine | +| `@wealthledger/tokens` | Значения дизайна, export `./tokens.json` | UI theme adapter | Сохранить trusted data boundary | +| `@wealthledger/ui` | Общие визуальные компоненты и тема; exports `.`, `./theme`, `./styles.css` | App/Storybook; зависит от tokens, peer React/Mantine | Доменной логики в прочитанной реализации не найдено | +| `@wealthledger/icons` | SVG и доступные props; export `.` | App/UI stories, peer React | Не добавлять зависимости на domain, UI или motion | +| frontend shared/api | OpenAPI client, session guards, адаптация resource lifecycle | Entities и features | Полезная граница; уточнить тип результата по R-04 | +| frontend instrument-details | Представление секций и истории инструмента | StockPage/BondPage | Сюда подходит общее поведение пагинации R-03 | +| backend auth/banking | Локальная сессия; ручная финансовая история и её транзакции | HTTP controllers, собственные services/Prisma | Не насаждать ports/adapters на каждый CRUD | +| backend investments | Счета, портфель, операции, доход, прогноз, инструменты, screen sync | Контроллеры → application; domain для чистых правил, infrastructure для хранения/адаптации | R-01 и R-05 уточняют локальные границы | +| backend integrations | T-Bank gRPC, MOEX HTTP, НКР HTML; публичные `*.api.ts` | Investment-модули; интеграции не зависят от бизнес-модулей | R-02 оставляет классификацию upstream-ошибки у интеграции | +| `InstrumentSourcesModule` | Каталог, enrichment, свечи и repositories | PortfolioModule и InstrumentsModule | Сохранить отдельный модуль источников | +| `InstrumentsModule` | Stock/bond business services и HTTP controllers | Использует sources и PortfolioModule | Не сливать с sources: портфелю не нужен обратный импорт всего InstrumentsModule | +| Backend OpenAPI → frontend snapshot | DTO/Nest metadata — источник wire-контракта; frontend получает артефакт явно | Нет обязательного чтения соседнего checkout при CI frontend | Сохранить независимость репозиториев | +| Root/docs/specs | Навигация/tooling; объяснение системы; поведенческие требования | Не runtime-зависимости приложений | Не переносить сюда код или общий реестр констант | + +Существенная подтверждённая проблема зависимостей — инфраструктурный runtime-import +ради чистых преобразований (R-01). Две другие связи скрыты в протоколах, а не +в циклическом импорте: income ↔ порядок массивов (R-05), resource ↔ обещания типа +Query result (R-04). Depcruise не сообщил нарушений настроенного frontend-графа; +это не доказательство отсутствия всех возможных динамических связей. Полная +автоматическая проверка циклов backend не проводилась, утверждения «циклов нигде +нет» здесь нет. Knip не даёт оснований для массового удаления экспортов. + +Карта согласуется с [контейнерами](architecture/containers.md), +[моделью свежести](architecture/cross-cutting/data-freshness.md), +[границей OpenAPI](adr/014-trusted-openapi-input.md) и +[решением о локальном чтении](adr/017-local-tbank-reads-and-background-sync.md). +Эти ссылки объясняют существующее устройство, не вводят новые требования. + +## 5. Подтверждённые находки + +Приоритет означает выигрыш в сопровождаемости с учётом риска, а не серьёзность +предполагаемого бизнес-бага. R-01 — первый пилот; R-02/R-03 — следующие локальные +упрощения; R-04/R-05 требуют более осторожной проверки; R-06/R-07 — тестовый долг. + +### R-01. Чистые проекции импортируются через сервисный граф + +**Места и повторения.** Backend: + +- `src/modules/investments/upcoming-events/upcoming-events.service.ts:205–248,267–278` — + `dividendEvent`, `bondEvent`, `projectedMoney`; вызовы в `get`, строки 149–160. +- `src/modules/investments/upcoming-events/upcoming-events.service.spec.ts:1–5,14–54` — + тестирует эти чистые функции, но подменяет `PrismaService` для загрузки imports. +- `src/modules/investments/instruments/stocks/application/stock.spec.ts:1–7` — + тот же setup для `metrics`, `stockDividends`, `stockQuote`; определения: + `get-stock-company.use-case.ts:39`, `stock-income.service.ts:101`, + `stock-market.service.ts:62` в том же каталоге. +- `src/modules/investments/instruments/bonds/application/bond.spec.ts:1–7` — + такой же Prisma mock для `bondCoupons`, `bondMetrics`, `bondQuote`; + определения в `bond-income.service.ts:128,186`, `bond-market.service.ts:66`. + +**Стоимость.** Чтобы понять и проверить умножение количества на выплату, +приходится учитывать Nest/Prisma import graph. Тестовый setup маскирует лишнюю +связь с инфраструктурой. Причина не в длине сервиса и не в наличии функций +рядом с классом, а в том, что самостоятельный расчёт не загружается независимо. + +**Минимальная правка.** В пилоте перенести только две upcoming-event проекции +и их общий `projectedMoney` в один соседний mapper-модуль. Сервис и существующий +тест импортируют его, тест перестаёт мокать Prisma. DTO и provider records нужны +только как type imports. Это преобразование границы провайдера, а не новый +универсальный domain layer. + +До: `test → upcoming-events.service → services/repositories → Prisma`. +После: `test/service → upcoming-event mapper → exact decimal`. + +Новая ответственность ясна: «provider event + holding → event DTO». +Не добавлять Injectable mapper, интерфейс с одной реализацией, отдельный файл +для каждой функции или общий движок stock/bond. Остальные helpers сервиса +не переносить автоматически. Stock/bond рассматривать после оценки пилота. + +**Сохранить.** Registry/event date kind, точные знаковые дробные количества, +нормализацию валюты, `null` при неизвестной сумме/валюте, оферту и погашение. +Фильтрация отменённых дивидендов, окно дат, сортировка, totals, account scope +и атомарная публикация остаются в текущем сервисном потоке. + +**Зависимости, риск, проверка.** Независимый небольшой шаг, риск низкий при +механическом переносе. Существующие три теста проекций сохраняются со всеми +assertions; app/test typecheck проверяют imports. Для связанного runtime +сохраняется `test/upcoming-events.e2e-spec.ts:255–284`: HTTP-проекция, суммы, +account/all и отсутствие private holdings в общем источнике. Этот e2e здесь +прочитан, но не запускался. Порция B-01. + +### R-02. Распознавание ошибки токена повторяет протокол интеграции + +**Места.** Источник ошибки — +`backend/src/integrations/tbank/tbank.client.ts:520–524`: gRPC auth-коды +нормализуются в `Error('INVALID_TBANK_TOKEN')`. Прочитанные потребители: + +- `src/integrations/tbank/tbank-token.service.ts:24–31` — перевод в HTTP exception; +- `src/integrations/tbank/tbank-membership.service.ts:61–65` — аргумент `failMembership`; +- `src/integrations/tbank/instrument-metadata.service.ts:128–136,181–186` — запрет + fallback UID→FIGI при token error и отклонение membership; +- `src/modules/investments/portfolio/application/portfolio.service.ts:372–378` — + token rejection вместо обычного `repository.fail`; +- `src/modules/investments/operations/application/brokerage-operations-sync.service.ts:286–291` — + признак ошибки для repository. + +Тот же предикат найден и прочитан в +`src/modules/investments/instruments/application/catalog-discovery.service.ts:348–356`, +`catalog-enrichment.service.ts:500–509`, `candle.service.ts:443–450`. +В каждом случае повторяется именно `error instanceof Error && error.message === ...`. +Записи `errorCode` в repositories — отдельная часть persisted/wire-контракта; +их не следует заменять без разбора только ради устранения всех строк. + +**Стоимость.** Знание о форме upstream-ошибки распространено по orchestration. +При изменении распознавания приходится сверять несколько обработчиков и +отделять его от разных действий восстановления. + +**Минимальная правка.** Один integration-owned `isInvalidTbankToken(error)` с +нынешним точным предикатом, доступный через существующий `tbank.api.ts`. +До: каждый consumer знает `instanceof Error` и текст сообщения. +После: consumer вызывает predicate, но сам выбирает `fail`, rejection или HTTP. +Это единая семантика распознавания, а не глобальный каталог всех ошибок. +Не менять класс исключения, публичную строку, правила retry и значения хранения; +не вводить второй enum ошибок frontend поверх generated API. + +**Сохранить.** Не считать произвольный объект `{message: ...}` подходящим Error; +не расширять совпадение по подстроке. Сохранить немедленный отказ auth RPC, +разные side effects и fencing каждого attempt. `BankErrorCode` уже существует +у banking owner и не относится к этой миграции. + +**Зависимости, риск, проверка.** Владелец найден; от B-01 не зависит. Риск низкий +для предиката, средний при широкой замене catch-ветвей. TbankClient unit-сценарии +строк 267–277 проверяют auth-коды и отсутствие retry; `tbank-token.service.spec.ts` +проверяет перевод и сохранение несвязанных ошибок. При переносе достаточно +точечной проверки точного Error/non-Error распознавания и этих тестов; +не создавать тесты наличия константы. Проверить соответствующие existing +persistence-сценарии отказа/смены token, если затронута оркестрация. Порция B-02. + +### R-03. Одинаковая пагинация истории инструмента живёт в двух VM + +**Места.** Frontend: + +- `src/pages/stock/ui/StockPage/StockPage.vm.ts:41–61,78–86,158–166`; +- `src/pages/bond/ui/BondPage/BondPage.vm.ts:41–61,78–86,151–159`. + +Прочитаны scope `${uid}:${filter}`, стек cursor, запрос `all` с instrumentUid +и limit 20, recovery `INVALID_OPERATIONS_CURSOR`, exact invalidation первой +страницы и переходы назад/вперёд. Вызывающее представление истории уже общее +в `features/instrument-details`. + +**Стоимость.** Одно изменение cursor/recovery-протокола требует синхронно +редактировать два несоседних page controller. Его трудно отделить взглядом +от котировок, свечей и специфичных доходов инструмента. + +**Минимальная правка.** Один hook поведения истории инструмента у существующего +`features/instrument-details`, принимающий UID/scope и категории, возвращающий +operations и действия пагинации. Invalid-cursor recovery принадлежит этому +поведению; обычный screen retry остаётся callback страницы. Страница продолжает +владеть URL, различием coupons/dividends и составом `useScreenSync`. + +До: StockPage и BondPage содержат одинаковые state/query/recovery блоки. +После: каждая собирает свои ресурсы и один общий `useInstrumentOperations(...)`. +Не создавать параметризованную фабрику страниц, generic pagination framework +или переносить всё в shared. + +**Сохранить.** UID, account=`all`, limit=20, canonical query key, disable до +появления UID, точную invalidation, историю cursor и URL replace. Особенно +сохранить нынешнюю семантику A→B→A: scope mismatch временно скрывает старый стек, +но сам по себе не обнуляет state. «Очистить в эффекте» — уже другое поведение. +`AccountOperationsPage.vm.ts` имеет иные account/date/category scope, limit 100 +и reset; его не включать по внешнему сходству. + +**Зависимости, риск, проверка.** Независимо от R-04; риск средний из-за state +lifecycle. `StockPage.test.tsx:377–408` и `BondPage.test.tsx:378–399` проверяют +видимую следующую страницу и запрос без cursor после смены фильтра. Сохранить +оба page-level сценария: они проверяют подключение разных страниц. В прочитанных +сценариях нет отдельного доказательства invalid-cursor recovery и A→B→A; +при извлечении добавить только эту адресную проверку наблюдаемого запроса/результата, +а не тестировать внутренний массив state. Порция B-03. + +### R-04. Resource-result обещает нативный Query result после изменения его смысла + +**Места.** `frontend/src/shared/api/resource-query.ts:21–44`, +`unwrapResourceResult`; использование в `useResourceQuery:154–158` и напрямую +в entity stocks. Потребители — portfolio/accounts/income/operations/events, +stocks/bonds; `features/tbank-connection/model/screen-sync.ts` использует +отдельный `transport.status`, а `features/instrument-details/model/section.ts` +преобразует состояния в представление. + +**Стоимость.** Wrapper раскрывает envelope в `data`, меняет `status`, +`isPending`, `isSuccess`, но приводит весь spread к +`UseQueryResult, E>`. Непереопределённый `refetch` +по-прежнему возвращает исходный envelope; `isLoading` остаётся нативным, +хотя successful HTTP с `data:null` объявляется resource-pending. +Читатель должен знать исключения из обещанного типа. Это подтверждённая +неясность контракта, а не утверждение о найденном пользовательском сбое. + +**Минимальная правка.** Убрать assertion «это полный нативный результат с новым +T»; вывести фактический тип spread + overrides либо явно описать только +переопределяемую часть, сохранив реальные типы методов и остальных полей. +Разделение transport/resource уже есть — использовать его при именовании +и чтении статусов. Исправить только потребителей, опирающихся на ложное +типовое обещание; runtime-поля не удалять под видом сокращения интерфейса. + +До: envelope query преобразован и объявлен другим `UseQueryResult`. +После: явно типизированный resource result с исходным transport/refetch и +предметным `data/meta/status`. Не строить второй query cache/state machine, +не добавлять runtime schema и новые FSD-пакеты. + +**Сохранить.** `null` payload → отсутствие готового ресурса, last-known data, +transport/source distinction, auth/access isolation, canonical cache key, +`observe` без нового import и проверку notified stateVersion. Отдельное +`as unknown as K` в построении observational query (`126–139`) связано с +адаптацией generated query key; R-04 не даёт основания просто удалить механизм. + +**Зависимости, риск, проверка.** Риск средний: широко используемая граница. +Сначала typecheck всех затронутых callers, затем существующие +`resource-lifecycle.test.tsx`, `authenticated-query.test.tsx`, screen-sync, +section и page suites. Проверять разные transport/resource состояния и результат +refetch; не заменять проверки кэша тестом нового type alias. Минимум — текущие +контракты в T-02 ниже; весь frontend unit-набор достаточно дешёв для финального +прогона. Порция B-04, отдельно от извлечения пагинации. + +### R-05. Период дохода передаётся побочным присваиванием и позицией в массиве + +**Места.** Backend: + +- `src/modules/investments/income/income-analytics.service.ts:49–68,79–110` — + `summary`/`analytics` меняют внешнюю `let plan` внутри callback; затем + распаковывают `[current, prior, year]`. +- `src/modules/investments/operations/application/brokerage-operations.service.ts:439–483,493–501,557–559` — + `collectRanges` передаёт callback в несколько чтений, а имя ресурса выводит + через `['selected', 'previous', 'yearAgo'][index]`. +- `src/modules/investments/operations/infrastructure/brokerage-operations.repository.ts:55–68,97–111,202–205` — + `readPage` получает openedAt и вызывает planner внутри Prisma-транзакции. +- `src/modules/investments/income/income-analytics.service.spec.ts:35,58,192` — + mock также должен воспроизводить вызов callback. Production callers + `collectRanges` найдены только в этих двух методах income. + +**Стоимость.** Чтобы понять, какой plan соответствует итоговому snapshot, +нужно проследить побочное присваивание через repository и повторные чтения. +Чтобы изменить именование периода, нужно знать позиционный договор двух +модулей. При этом наружу уже передаются явные роли; внутренняя неявность не +необходима публичному контракту. + +**Минимальная правка.** Сделать callback чистым и вернуть из `collectRanges` +данные планирования, относящиеся именно к возвращаемому snapshot, вместе с +именованными диапазонами. Достаточно конкретного результата: openedAt/нужные +метаданные snapshot и periods. Income может восстановить свой чистый plan +из аргументов и возвращённого openedAt; метку диапазона задаёт income, а +operations переносит её при формировании resourceKey, не выводит из индекса. +Для отсутствующего snapshot явно сохранить планирование с `undefined`. + +До: `let plan; await collectRanges(openedAt => { plan = ...; return ranges; })`. +После: чистый planner + возвращаемые `snapshot context / labeled periods`; +income собирает ответ из данных одного результата. +Не нужен универсальный planner framework, реестр периодов, новый слой или +generic callback, переносящий произвольный application object через Prisma. + +**Сохранить.** Нельзя сначала отдельно прочитать openedAt, затем получить +операции: это разорвёт существующий snapshot. Сохранить transaction boundary, +passive observe, scheduling/fencing, Moscow/date-only границы, историю открытия, +недоступный leap-day/year comparison, `null` для неполного сравнения, точные +суммы, hashes и внешние resourceKey. Порядок legacy dependencies и роли +`selected`, `previous`, `year` также остаются прежними, несмотря на внутреннюю +метку `yearAgo`. + +**Зависимости, риск, проверка.** От других находок не зависит, риск выше среднего +из-за snapshot/coverage. Обязательны income-period и income-analytics unit, +`test/tbank-income-local.e2e-spec.ts` и соответствующая OpenAPI contract-проверка. +Прочитанные e2e строки 124–203 отдельно защищают exact totals, повторные ID разных +счетов, reopen без RPC, missing/partial/confirmed empty и независимые comparison. +Это не гарантирует все пути concurrent scheduling: их необходимо сохранить +и адресно проверить при изменении финального snapshot. Порция B-05. + +### R-06. Layout-тест зависит от внутренней генерации CSS Mantine + +**Места.** `frontend/src/pages/account/ui/AccountPage/AccountPage.test.tsx:663–681` +ищет во всех `