Переносит архив `audits/2026-10-11/` (19 файлов) из codex/audit-remediation-2026-10-11 в develop и добавляет точечный allowlist в .gitignore.
348 lines
49 KiB
Markdown
348 lines
49 KiB
Markdown
# Технический аудит WealthLedger
|
||
|
||
Дата: 11 октября 2026 года.
|
||
|
||
# Executive Summary
|
||
|
||
**Главная проблема проекта — незавершённый переход между моделями получения данных, а не недостаток архитектурных слоёв.** Часовой обход, прежняя screen-driven синхронизация, online lookup и локальные snapshots местами сосуществуют с разными правилами обновления. Добавление новых сервисов и абстракций это усложнит; наибольшую пользу даст удаление старых путей и уточнение ответственности существующих владельцев.
|
||
|
||
Наиболее существенные системные проблемы:
|
||
|
||
1. **Выбор сохранённого прогноза может блокировать публикацию нового полного результата.**
|
||
2. **Ошибки часового координатора могут обозначаться успехом; отдельные сбои persistence способны оставить обход незавершённым.**
|
||
3. **Часовой обход не обновляет уже сохранённые календари выплат из-за политики `stable`.**
|
||
4. **Frontend получает информацию о свежести и ошибках, но основной экран портфеля теряет её при передаче props.**
|
||
5. **Часть тестов защищает старые сценарии или проверяет гонку без фактического запуска конкурирующей работы.**
|
||
|
||
При этом основа проекта обоснованна: модульный монолит, конкретные providers, точная денежная арифметика, SQLite-транзакции, изоляция подключения и generated OpenAPI. **Масштабный архитектурный рефакторинг не нужен.**
|
||
|
||
## Основание и ограничения аудита
|
||
|
||
Проверены текущие checkout:
|
||
|
||
| Репозиторий | Commit |
|
||
|---|---|
|
||
| `frontend` | `c882120` |
|
||
| `backend` | `0ad84ff` |
|
||
| `specs` | `fcdc805` |
|
||
| `docs` | `e6fd4f6` |
|
||
|
||
Аудит выполнен read-only с использованием `ponytail-audit`. Изучены архитектура, владельцы поведения, вызывающие пути, рискованные реализации и связанные тесты/specs.
|
||
|
||
Проведены две лёгкие проверки:
|
||
|
||
- После `nvm use --silent` запущен существующий dependency-cruiser: **420 модулей, 1773 зависимости, нарушений настроенных границ и циклов не обнаружено**.
|
||
- `git diff --no-index --stat` между существующим `backend/openapi.json` и frontend snapshot: **различий нет**. Это подтверждает совпадение файлов на диске, но не свежую генерацию относительно runtime.
|
||
|
||
Рабочие деревья четырёх репозиториев остались чистыми. Build, полные `verify`, SQLite/E2E и нагрузочные проверки не запускались. Ниже отдельно обозначены выводы по коду, архитектурные риски и предпочтения; runtime-воспроизведение дефектов в рамках аудита не выполнялось.
|
||
|
||
# Findings
|
||
|
||
## F1. Выбор forecast смешивает последний результат и запрошенное окно
|
||
|
||
**Priority: High · Confidence: High · Тип: дефект по коду**
|
||
|
||
- **Evidence:** `backend/src/modules/investments/portfolio/infrastructure/forecast.repository.ts:50–79`. При отсутствии точного `scopeKey` выбирается первый пересекающийся forecast без сортировки по времени. Затем `if (!previous && fallback) return fallback` прекращает обработку даже при наличии нового полного `candidate`. Окно входит в ключ: `upcoming-events.service.ts:160–173`.
|
||
- **Problem:** успешный расчёт для следующего дня может уступить старому пересекающемуся окну. Для непересекающегося запроса fallback вообще не выбирается, хотя действующая spec требует успешный пустой результат сохранённого прогноза.
|
||
- **Impact:** после успешного обхода пользователь может продолжать видеть прежние рассчитанные суммы и старое окно. При ежедневном сдвиге окна это способно продолжаться до исчезновения пересечения.
|
||
- **Root cause:** идентичность сохраняемого результата и параметры HTTP-фильтра объединены в строковом `scopeKey`.
|
||
- **Recommendation:** локально разделить выбор последнего полного forecast и фильтрацию запроса. Fallback должен сохраняться при неуспехе нового результата, а не блокировать новый полный результат только из-за другого окна. Сохранить существующие проверки зависимостей и атомарной публикации.
|
||
- **Trade-off:** потребуется уточнить read/write-семантику внутри существующих service/repository и пересмотреть несколько тестов. Новые слои не нужны.
|
||
|
||
## F2. Часовой координатор не гарантирует корректное завершение ошибки
|
||
|
||
**Priority: High · Confidence: High для ветвей ошибок · Тип: дефект и архитектурный риск**
|
||
|
||
- **Evidence:** `backend/src/modules/investments/tbank-sync/tbank-hourly.scheduler.ts:93–110,178–218`. Общий `catch` вызывает `settle` без добавления проблемы; при пустом `failures` результат — `succeeded`. Ошибка `settleHourly` подавляется. `settledWorks` ждёт `running` без конечной границы. В `catalog-enrichment.service.ts:582–590` аварийный выход очищает process-state, но не завершает durable work rows.
|
||
- **Problem:** исключение вне локальных обработчиков может превратиться в успешный обход. Незавершённая запись дочерней работы может удерживать polling бесконечно.
|
||
- **Impact:** статус обхода вводит в заблуждение. Если hourly row остаётся `running`, `reserveHourly` не разрешает следующий обход в том же процессе — `tbank-sync.repository.ts:190–200`.
|
||
- **Root cause:** завершение определяется количеством накопленных ошибок, а не фактом прохождения всех обязательных шагов; подавление persistence errors скрывает нарушение lifecycle.
|
||
- **Recommendation:** считать незавершённый сценарий ошибкой, явно фиксировать причину общего `catch`, ограничить ожидание дочерней работы и обеспечить диагностируемое восстановление терминального состояния. Использовать текущие durable rows и logger.
|
||
- **Trade-off:** больше явных error branches. Это необходимая защита lifecycle, а не избыточный defensive programming.
|
||
|
||
## F3. Часовой обход повторно использует календари без обновления источника
|
||
|
||
**Priority: Medium · Confidence: High · Тип: дефект политики обновления**
|
||
|
||
- **Evidence:** scheduler вызывает `events.refresh` — `tbank-hourly.scheduler.ts:166–175`. Далее `readSources(..., false)` приводит к `requestMany(..., false, ...)` — `upcoming-events.service.ts:182–239`, `catalog-enrichment.service.ts:113–125`. В `catalog-enrichment.service.ts:189–193,679–692` повторное обновление разрешено для `quote`, но `dividends` и `bond-events` относятся к `stable`.
|
||
- **Problem:** успешно сохранённый календарь не обновляется при последующих обходах. Для `bond-events` ключ не меняется вместе с датой.
|
||
- **Impact:** новые или изменённые выплаты могут отсутствовать в прогнозе, хотя часовой обход завершился успешно.
|
||
- **Root cause:** календари выплат получили политику практически неизменяемого справочника.
|
||
- **Recommendation:** в существующем hourly refresh принудительно обновлять именно календарные источники через уже имеющийся `force`, сохранив локальность обычных GET и reuse стабильных metadata.
|
||
- **Trade-off:** дополнительные календарные запросы во время обхода. Их фактический бюджет следует проверить на representative портфеле.
|
||
|
||
## F4. Длительный обход не соблюдает правило пропуска часового срока
|
||
|
||
**Priority: Medium · Confidence: High · Тип: несоответствие текущей spec**
|
||
|
||
- **Evidence:** `backend/src/modules/investments/tbank-sync/tbank-sync.repository.ts:189–201` проверяет только `now - createdAt >= hourMs`. `specs/openspec/specs/hourly-investment-sync/spec.md:19–25` требует: обход 10:00, завершившийся после 11:00, допускает следующий запуск не ранее 12:00.
|
||
- **Problem:** после завершения в 11:10 текущая проверка разрешит запуск уже на ближайшем tick.
|
||
- **Impact:** пропущенный срок фактически превращается в немедленный повтор после завершения длинного обхода.
|
||
- **Root cause:** проверяется прошедшая длительность, а не следующий допустимый срок относительно начала попытки.
|
||
- **Recommendation:** исправить вычисление eligibility в `reserveHourly`; проверить контролируемым временем короткий обход, пропущенный срок и восстановление после простоя.
|
||
- **Trade-off:** следующий запуск иногда будет позже текущего поведения — именно это требуется сохранённой политикой.
|
||
|
||
## F5. Основной экран портфеля теряет metadata и скрывает ошибку перечитывания
|
||
|
||
**Priority: Medium · Confidence: High · Тип: дефект передачи данных**
|
||
|
||
- **Evidence:** VM возвращает `source: portfolio.meta` — `frontend/src/pages/account/ui/AccountPage/AccountPage.vm.ts:125–135`, но container не передаёт `source` — `AccountPage.tsx:24–34`. Loaded view уже умеет показывать её — `components/AccountPageLoaded.view.tsx:55–59`. Тест `AccountPage.test.tsx:751–855` закрепляет отсутствие времени, предупреждения и retry после ошибки.
|
||
- **Problem:** подготовленная информация теряется на последнем переходе VM → view. Состояние transport error также не представлено в loaded-модели.
|
||
- **Impact:** пользователь видит сохранённые суммы без необходимого контекста свежести и неудачного обновления.
|
||
- **Root cause:** рассогласование props между container и view; тесты проверяют «тихое сохранение», а не требуемое восстановление.
|
||
- **Recommendation:** передать существующий `source`; представить ошибку перечитывания и retry существующими UI-компонентами, сохранив cached snapshot.
|
||
- **Trade-off:** экран покажет дополнительные статусы. Новая архитектура представления не требуется.
|
||
|
||
## F6. Income GET всё ещё может запускать личную синхронизацию
|
||
|
||
**Priority: Medium · Confidence: High · Тип: нарушение заявленной границы read**
|
||
|
||
- **Evidence:** `backend/src/modules/investments/operations/application/brokerage-operations.service.ts:408–435`: обычный `collectRanges` вызывает `membership.request` и `sync.readPlan`. Controller передаёт `observe=false`, если `sync=observe` отсутствует — `income-analytics.controller.ts:24–79`. Действующая граница запрещает импорт из обычных personal reads — `specs/openspec/specs/tbank-data-sync/spec.md:9–14`.
|
||
- **Problem:** endpoint аналитики одновременно читает и инициирует обновление membership/операций.
|
||
- **Impact:** лишние RPC и два владельца обновления одного источника; поведение обычного GET отличается от преимущественно тестируемого observe-пути.
|
||
- **Root cause:** в `collectRanges` сохранился прежний request-driven сценарий.
|
||
- **Recommendation:** убрать обновление membership из income read и оставить получение отсутствующих интервалов предусмотренному владельцу Ledger. До изменения уточнить cold-start поведение исторической аналитики.
|
||
- **Trade-off:** неполная историческая аналитика не сможет незаметно инициировать загрузку. Этот UX-вопрос необходимо решить явно.
|
||
|
||
## F7. Политика перечитывания распределена между Query и собственным таймером
|
||
|
||
**Priority: Medium · Confidence: High · Тип: риск лишних запросов и несогласованная политика**
|
||
|
||
- **Evidence:** `frontend/src/app/ui/App.tsx:13–19` включает focus refetch, тогда как `.ai/conventions/data.md:70–74` описывает выключенный. Portfolio/operations/events/stock используют `staleTime: 30_000`; accounts — `1_800_000`. `features/tbank-connection/model/screen-sync.ts:22–35` напрямую вызывает `refetch` всех resources, не проверяя `isEnabled`. Например, `BondsPage.vm.ts:10–13` передаёт туда query, отключённый для короткого поиска.
|
||
- **Problem:** несколько механизмов задают частоту GET. Ручной `refetch` обходит автоматическое отключение query через `enabled`.
|
||
- **Impact:** возврат фокуса и повторное открытие создают запросы раньше описанного интервала; таймер может отправить запрос с неподготовленным identifier/query.
|
||
- **Root cause:** периодическое чтение вынесено из владельца query, а разные поколения политики остались одновременно.
|
||
- **Recommendation:** согласовать `staleTime`/focus policy; использовать существующий `refetchInterval` у query-owner. Если таймер сохраняется, фильтровать disabled resources. Удалить неиспользуемый `_scope`.
|
||
- **Trade-off:** необходимо выбрать поведение фоновой вкладки и reconnect. Native Query policy уменьшит собственную координацию.
|
||
|
||
Семантика `enabled`, ручного `refetch` и `refetchInterval` сверена с актуальной документацией TanStack Query.
|
||
|
||
## F8. Часть race-тестов допускает ложноположительный результат
|
||
|
||
**Priority: Medium · Confidence: High для backend-примера, Medium для frontend-примера · Тип: дефект теста и риск**
|
||
|
||
- **Evidence:** `backend/test/tbank-local-reads.e2e-spec.ts:489–515` создаёт deferred portfolio, но запускает только GET, который теперь локален. `portfolioService.request` не вызывается. Во frontend `StocksPage.test.tsx:175–237` освобождает старый ответ и сразу проверяет уже существующий новый результат, не дожидаясь завершения старого GET.
|
||
- **Problem:** backend-тест не создаёт позднюю публикацию; frontend-тест не гарантирует, что проверка произошла после неё.
|
||
- **Impact:** тест может оставаться зелёным при удалении защиты, которую якобы проверяет.
|
||
- **Root cause:** backend-сценарий не обновлён после изменения data flow; frontend-сценарий не имеет барьера завершения проигравшего запроса.
|
||
- **Recommendation:** заменить backend-сценарий запуском реального source-owner с подтверждением начала RPC и завершения после смены подключения. Во frontend ждать завершения старого запроса/обработки его результата.
|
||
- **Trade-off:** тесты станут немного явнее, но будут проверять реальный regression risk.
|
||
|
||
Дополнительно **не найдены прямые проверки `tick`/`reserveHourly` для нового координатора**. В ряде SQLite suites scheduler подменён stub — например, `backend/test/upcoming-events.e2e-spec.ts:140–147`.
|
||
|
||
## F9. Неиспользуемая manual screen-sync orchestration продолжает поддерживаться
|
||
|
||
**Priority: Medium · Confidence: High · Тип: лишняя сложность**
|
||
|
||
- **Evidence:** `backend/src/modules/investments/tbank-sync/tbank-sync.service.ts:54–311` содержит `request` и `dispatch`. Production controller предоставляет только status GET — `tbank-sync.controller.ts:31–41`; production callers `request` не найдены. `tbank-screen.ts` имеет единственного production-потребителя в этом пути; `tbank-screen.spec.ts` проверяет manual sync boundary.
|
||
- **Problem:** недоступный сценарий сохраняет планирование, DI-зависимости, validation и тесты.
|
||
- **Impact:** сотни строк для понимания и сопровождения модели, которая уже не запускается пользователем.
|
||
- **Root cause:** прежний путь был отключён на HTTP-границе, но не удалён внутри.
|
||
- **Recommendation:** удалить `request`/`dispatch`, их исключительно используемые helpers и тесты. Сохранить status/read compatibility, если она ещё нужна.
|
||
- **Trade-off:** нельзя будет вернуть manual sync простым подключением старого метода; возвращать его сейчас оснований нет.
|
||
|
||
**Важно:** `planInvestmentScreen` нельзя удалять целиком — его использует текущий `BrokerageOperationsService`.
|
||
|
||
## F10. Online search создаёт fan-out и лишнюю waterfall-зависимость
|
||
|
||
**Priority: Medium · Confidence: High для числа вызовов · Тип: архитектурный риск эффективности**
|
||
|
||
- **Evidence:** `backend/src/modules/investments/instruments/application/catalog-discovery.service.ts:295–337`: один search RPC, до 30 detail RPC, затем batch prices. Лимит результатов — `catalog-selection.ts:5`.
|
||
- **Problem:** на один поиск получается до **32 upstream-вызовов**; цены начинают загружаться только после всех details.
|
||
- **Impact:** расход лимита T-Bank и рост времени ответа. Ошибка одного detail отклоняет весь `Promise.all`.
|
||
- **Root cause:** online-путь сохранил полное enrichment каждого результата, хотя список и detail имеют разные потребности.
|
||
- **Recommendation:** сразу запускать независимый prices request параллельно details. Отдельно проверить, какие detail-поля действительно нужны компактному списку, прежде чем убирать N запросов.
|
||
- **Trade-off:** сокращение detail fan-out может уменьшить набор полей списка; без сверки потребностей UI это изменение не рекомендую выполнять вслепую.
|
||
|
||
Новый агрегирующий endpoint для решения этой проблемы не нужен.
|
||
|
||
## F11. Банковская история использует N+1 DB reads
|
||
|
||
**Priority: Medium · Confidence: High для структуры запросов, Medium для значимости затрат · Тип: риск эффективности**
|
||
|
||
- **Evidence:** `backend/src/modules/banking/bank-cards/application/bank-operations.service.ts:41–68,333–365`. Каждая строка вызывает `operationView`, повторно читающий карту, связанные операции, категорию и возвраты.
|
||
- **Problem:** число reads растёт вместе с размером страницы; одинаковые связи загружаются многократно.
|
||
- **Impact:** лишняя работа Prisma/SQLite внутри transaction. Это embedded DB, поэтому нельзя приписывать ей стоимость сетевой БД, но повторные queries остаются.
|
||
- **Root cause:** mapper одной операции повторно используется как loader списка.
|
||
- **Recommendation:** получить нужные связи и refund totals для всей страницы одним набором запросов и передать их в преобразование. Сохранить transaction и ownership checks.
|
||
- **Trade-off:** list path станет немного отличаться от single-operation path. Реальный выигрыш в миллисекундах не измерялся.
|
||
|
||
## F12. Минимальная длина пароля имеет разные единицы
|
||
|
||
**Priority: Medium · Confidence: High · Тип: подтверждённое несоответствие**
|
||
|
||
- **Evidence:** backend — `src/modules/auth/presentation/auth.dto.ts:43–54,66–74`: минимум через `MinLength(12)`, максимум через UTF-8 bytes. Frontend — `src/features/auth/model/auth.schema.ts:36–42`: обе границы в байтах. Spec — `specs/openspec/specs/user-authentication/spec.md:10`.
|
||
- **Problem:** например, шесть кириллических букв проходят frontend byte-minimum, но отклоняются backend character-minimum.
|
||
- **Impact:** форма принимает пароль, который API не принимает.
|
||
- **Root cause:** смешаны два способа измерения одной policy.
|
||
- **Recommendation:** проверить обе границы в существующем byte-validator и убрать конфликтующий `MinLength`.
|
||
- **Trade-off:** некоторые не-ASCII пароли с меньшим числом символов станут допустимы согласно текущему требованию.
|
||
|
||
Расхождение уже отмечено в docs, но от этого не перестаёт быть дефектом.
|
||
|
||
## F13. Online-пути не используют существующий перевод ошибки брокерского токена
|
||
|
||
**Priority: Medium · Confidence: High · Тип: дефект error mapping**
|
||
|
||
- **Evidence:** `backend/src/integrations/tbank/tbank.client.ts:553–557` бросает обычный `Error(INVALID_TBANK_TOKEN)`. Online calls в `catalog-discovery.service.ts:149–165,227–249,295–317` и `catalog-enrichment.service.ts:241–267` не переводят его. Общий filter отдаёт для обычного Error `500` — `common/filters/http-exception.filter.ts:19–31`. Translator уже существует: `tbank-token.service.ts:26–35`.
|
||
- **Problem:** известная постоянная ошибка подключения выглядит как internal server error.
|
||
- **Impact:** frontend GET transport может повторить заведомо неуспешный запрос; UI теряет корректный recovery code.
|
||
- **Root cause:** новый online-путь обошёл прежнюю integration error boundary.
|
||
- **Recommendation:** переиспользовать existing translator на online application boundary, сохранив обработку durable attempts.
|
||
- **Trade-off:** появится явная связь online-пути с существующей token-error policy. Новый error registry не нужен.
|
||
|
||
## F14. Тесты MOEX закрепляют поведение, противоречащее актуальному требованию
|
||
|
||
**Priority: Medium · Confidence: High · Тип: конфликт контракта**
|
||
|
||
- **Evidence:** `backend/src/integrations/moex/moex.client.ts:182–185` считает отсутствующий блок ошибкой. `moex.client.spec.ts:121–191` и `test/upcoming-events.e2e-spec.ts:540–599` закрепляют это. `specs/openspec/specs/brokerage-account-upcoming-events/spec.md:82–84` требует трактовать отсутствие блока как отсутствие данных.
|
||
- **Problem:** код и тесты согласованы между собой, но не с сохранённым поведением.
|
||
- **Impact:** зелёные тесты не доказывают выполнение требования; прогноз может считаться неуспешным в сценарии, описанном как нормальное отсутствие событий.
|
||
- **Root cause:** отсутствие блока объединено с повреждённым форматом ответа.
|
||
- **Recommendation:** сначала согласовать смысл отсутствующего блока MOEX. Разделить отсутствие и malformed payload; не удалять проверки повреждённых строк/cursor.
|
||
- **Trade-off:** неверная трактовка отсутствия блока способна превратить неполный календарь в подтверждённый пустой. Поэтому здесь требуется решение о семантике, а не механическое сокращение guards.
|
||
|
||
## F15. Дублируется whitelist-преобразование instrument
|
||
|
||
**Priority: Low · Confidence: High · Тип: возможность упрощения**
|
||
|
||
- **Evidence:** одинаковые `publicInstrument` в `backend/src/modules/investments/instruments/application/catalog-discovery.service.ts:499–530` и `catalog-enrichment.service.ts:646–677`.
|
||
- **Problem:** одна boundary-модель имеет две поддерживаемые копии.
|
||
- **Impact:** новое поле или изменение безопасной проекции необходимо повторять; возможен drift.
|
||
- **Root cause:** online и persisted пути развивались отдельно.
|
||
- **Recommendation:** оставить одну функцию у текущего instrument boundary owner.
|
||
- **Trade-off:** один локальный import вместо независимых копий. Сам whitelist сохраняется.
|
||
|
||
## F16. Маршрут чтения документации ведёт к разным поколениям архитектуры
|
||
|
||
**Priority: Medium · Confidence: High · Тип: documentation drift**
|
||
|
||
- **Evidence:** `docs/architecture/containers.md:143–148` описывает запуск работы запросом и request-driven восстановление. `docs/adr/019-hourly-reads-and-online-instrument-reference.md:9–16` описывает нового владельца — hourly walk. В `specs/AI_INDEX.md:15–31` отсутствуют `hourly-investment-sync`, `tbank-data-sync` и `online-instrument-information`.
|
||
- **Problem:** актуальные правила существуют, но основной маршрут чтения не приводит к ним последовательно.
|
||
- **Impact:** разработчик может восстановить удаляемую модель или принять старое поведение за текущий invariant.
|
||
- **Root cause:** обновлены отдельные требования/ADR, но не все текущие объясняющие страницы и индекс.
|
||
- **Recommendation:** обновить существующий индекс и текущие архитектурные объяснения; подробную текущую механику держать в одной тематической странице, из остальных ссылаться на неё.
|
||
- **Trade-off:** часть страниц станет менее самодостаточной, зато уменьшится число расходящихся копий.
|
||
|
||
Исторические ADR и архивы сами по себе дефектом не являются.
|
||
|
||
## F17. Отдельный presentation VM не имеет самостоятельной ответственности
|
||
|
||
**Priority: Low · Confidence: Medium · Тип: предпочтение сопровождаемости**
|
||
|
||
- **Evidence:** `frontend/src/pages/stocks/ui/StocksPage/StocksPagePresentation.vm.ts:1–20` содержит только два callback для доступных названий Pagination; единственный consumer — `StocksPage.view.tsx:208`. Общая convention требует выносить callbacks — `.ai/conventions/ui.md:72–89`.
|
||
- **Problem:** для двух локальных presentation-функций добавлен отдельный маршрут навигации по коду.
|
||
- **Impact:** небольшая дополнительная cognitive cost без установленного выигрыша в поведении или производительности.
|
||
- **Root cause:** вероятно, формальное применение общего VM-паттерна.
|
||
- **Recommendation:** при ближайшем изменении держать callbacks рядом с Pagination, сохранив доступные названия. Самостоятельный VM оставлять для формы, fetching или связного lifecycle.
|
||
- **Trade-off:** меньше единообразия структуры. **Это необязательный cleanup, не архитектурный дефект.**
|
||
|
||
# Simplification Opportunities
|
||
|
||
Ниже — отдельный complexity-pass в формате `ponytail-audit`, от большего сокращения к меньшему:
|
||
|
||
- **`delete:`** удалить недоступные `TbankSyncService.request/dispatch`, исключительно используемую нормализацию и её тесты; оставить необходимый status reader. [`backend/src/modules/investments/tbank-sync/`]
|
||
- **`delete:`** удалить frontend `observe`/`observationKey`, если подтверждено отсутствие внешних consumers: в production callers не найдены, вызовы есть только в тестах. Обычный `refetch` уже обслуживает текущий сценарий. [`frontend/src/shared/api/resource-query.ts:69–127`]
|
||
- **`native:`** передать периодическое чтение TanStack Query `refetchInterval`; убрать собственный timer/ref coordination и неиспользуемые screen descriptors. [`frontend/src/features/tbank-connection/model/screen-sync.ts`]
|
||
- **`shrink:`** объединить две одинаковые `publicInstrument`; сохранить одну безопасную проекцию. [`backend/src/modules/investments/instruments/application/`]
|
||
- **`delete:`** удалить прямой passthrough-тест controller; identity/guard/query проверять существующей configured HTTP suite. [`backend/src/modules/investments/upcoming-events/upcoming-events.controller.spec.ts:1–20`]
|
||
- **`yagni:`** объединить двухфункциональный presentation VM с его единственным view при ближайшем изменении. [`frontend/src/pages/stocks/ui/StocksPage/`]
|
||
|
||
**net: -~550 lines, -0 deps possible.**
|
||
|
||
Это приблизительная оценка наиболее очевидных сокращений, не размер подготовленного diff. Подтверждённых кандидатов на удаление dependency не установлено.
|
||
|
||
# Testing Assessment
|
||
|
||
## Что действительно полезно
|
||
|
||
| Проверки | Почему стоит сохранить |
|
||
|---|---|
|
||
| `backend/test/app.e2e-spec.ts:161–213` | Конкурентная refresh rotation проходит через реальный SQLite CAS; проверяется дальнейшая работоспособность победителя |
|
||
| `backend/test/bank-cards.e2e-spec.ts`, особенно refund/concurrency/replay cases | Проверяются финансовые ограничения, версии, rollback и недопущение повторного создания |
|
||
| Decimal, RUB-money, portfolio calculations | Различают точные суммы, валюты, неизвестные значения и ноль |
|
||
| Operations persistence/coverage и forecast publication tests | Проверяют реальные инварианты хранения и согласованности, которые mock Prisma не доказывает |
|
||
| Frontend RTL/MSW формы и auth transport | Проверяют видимый результат, отсутствие invalid requests, сохранение ввода и session boundary |
|
||
| Browser layout tests | Проверяют свойства реального layout, недоступные JSDOM |
|
||
| Отдельный performance benchmark | Измеряет конкретный persisted-read budget; уже исключён из обычного набора functional suites |
|
||
|
||
## Что удалить, заменить или перенести
|
||
|
||
| Проблема | Действие |
|
||
|---|---|
|
||
| Passthrough controller unit test | **Удалить**: сейчас проверяет вызов mock; заменить недостающее поведение assertion в существующем HTTP-тесте |
|
||
| Late portfolio test без запуска source work | **Заменить** реальной управляемой гонкой — F8 |
|
||
| Frontend race test без ожидания старого ответа | **Усилить** барьером завершения; probe сам по себе не проблема |
|
||
| Тесты «тихого» cached error на AccountPage | **Пересмотреть ожидаемое поведение**: оставить сохранение данных, проверять предупреждение и retry — F5 |
|
||
| Manual screen normalization tests | **Удалить вместе с недоступным production-сценарием**, не переносить в новую abstraction |
|
||
| MOEX missing-block tests | **Согласовать ожидание с контрактом**, сохранить malformed/cursor checks — F14 |
|
||
|
||
## Самые важные недостающие сценарии
|
||
|
||
1. **Hourly lifecycle:** первый запуск, повтор в срок, длинный обход с пропуском срока, restart и persistence failure.
|
||
2. **Два последовательных успешных календарных обновления:** изменившийся upstream действительно меняет сохранённый календарь.
|
||
3. **Forecast следующего дня:** новый полный результат заменяет прежний; узкий GET фильтрует сохранённый результат; непересекающееся окно возвращает предусмотренную пустоту.
|
||
4. **Обычный income GET без `sync=observe`:** не создаёт personal work.
|
||
5. **Disabled query на timer/focus boundary:** неподготовленный identifier не вызывает request.
|
||
6. **Online invalid broker token:** публичный code постоянной ошибки без транспортных повторов.
|
||
7. **Пароль на UTF-8 границах:** frontend и configured backend принимают одинаковые значения.
|
||
|
||
Достаточно существующих Jest, RTL/MSW и SQLite runner. Общий процент покрытия здесь ничего не решает: основной недостаток — **несоответствие некоторых тестовых сценариев текущему data flow**.
|
||
|
||
# API & Data Flow: прослеженные сценарии
|
||
|
||
| Сценарий | Фактическая цепочка | Вывод |
|
||
|---|---|---|
|
||
| Открытие счёта | Page VM → несколько resource GET → локальные repositories → snapshots → VM/view | Локальная read-модель полезна; metadata теряется на последнем переходе |
|
||
| Часовое обновление | Native timer → durable reservation → membership → portfolios → operations → calendars → forecast | Есть основания сохранить coordinator; ошибки lifecycle и stable calendars требуют исправления |
|
||
| Поиск облигаций / online lookup | Debounced Query → backend discovery → search + N details + prices → DTO → карточки | Есть fan-out и ненужная последовательность prices после details |
|
||
| Скринер акций | POST → полный Shares → отбор → ограниченный snapshot → GET страниц | Snapshot оправдан устойчивостью страниц; повторное получение каталога соответствует текущему требованию |
|
||
| Создание банковской операции | Form intent → mutation → validation/transaction → fingerprint/version → DTO → invalidation | Защиты обоснованны; list reads можно сделать дешевле без изменения write-модели |
|
||
|
||
# Top 10 Improvements
|
||
|
||
**S** — локальная правка и небольшая проверка. **M** — несколько связанных владельцев/сценариев и integration proof.
|
||
|
||
| № | Изменение | Ожидаемый эффект | Сложность |
|
||
|---:|---|---|---|
|
||
| 1 | Передать `source` и восстановить error/retry presentation AccountPage — F5 | Пользователь получает контекст показанных сумм; очень маленький production diff | S |
|
||
| 2 | Удалить недоступный manual screen-sync путь — F9 | Наибольшее сокращение кода, тестов и DI-навигации | S–M |
|
||
| 3 | Свести периодическое чтение к query-owner — F7 | Меньше собственных timers и запросов с неподготовленными inputs | S–M |
|
||
| 4 | Исправить vacuous race tests и добавить proof hourly lifecycle — F8 | Тесты начинают ловить реальные регрессии нового data flow | M |
|
||
| 5 | Исправить выбор/публикацию последнего forecast — F1 | Устраняет удержание старого результата после успешного обновления | M |
|
||
| 6 | Сделать завершение hourly ошибок явным — F2 | Убирает ложный успех и риск остановки последующих обновлений | M |
|
||
| 7 | Обновлять календарные источники из hourly owner — F3 | Прогноз получает изменения выплат, а не только новые количества бумаг | S–M |
|
||
| 8 | Убрать request-driven membership из income read — F6 | Один владелец personal refresh, меньше скрытых RPC | S–M, после ответа о cold history |
|
||
| 9 | Согласовать byte-validation пароля — F12 | Убирает подтверждённое frontend/backend расхождение | S |
|
||
| 10 | Переиспользовать invalid-token translator в online-путях — F13 | Корректный recovery code и отсутствие бесполезных повторов | S–M |
|
||
|
||
N+1 банковской истории и сокращение search detail fan-out разумны следующим этапом: структура затрат доказана, но фактический выигрыш и допустимый набор полей требуют уточнения.
|
||
|
||
# Do Not Change
|
||
|
||
1. **Модульный NestJS-монолит с конкретными providers.** Не обнаружена необходимость в обязательных ports/interfaces, CQRS или отдельном сервисе на каждый use case.
|
||
2. **SQLite и single-process coordination.** Для текущей топологии это существенно проще распределённой системы. Ошибки lifecycle следует исправить внутри существующей модели.
|
||
3. **Decimal strings и bigint-копейки.** Они сохраняют точность и смысл неизвестной суммы; замена на обычный `number` будет ухудшением.
|
||
4. **Connection revision, CAS и conditional publication.** Они защищают от публикации результата под другим подключением. Это необходимая сложность.
|
||
5. **Bank intent fingerprints, версии, tombstones и refund transactions.** Проверенные сценарии lost response, replay и конкурирующих изменений оправдывают эти механизмы.
|
||
6. **Generated OpenAPI и производные frontend types.** Это не бесполезное дублирование моделей, а граница независимых репозиториев.
|
||
7. **Snapshots скринера.** Они обеспечивают устойчивые страницы одного результата. Пересчитывать каталог при каждом GET страницы проще лишь внешне.
|
||
8. **Mantine controls, React Hook Form и необходимые page VMs.** В формах и data-driven screens у них есть конкретная ответственность. Оснований дробить большие views по числу строк не найдено.
|
||
9. **Общая тема для приложения и Storybook.** Два реальных consumers оправдывают visual foundation packages.
|
||
10. **Исторические ADR и OpenSpec archives.** Их следует отличать от актуальных требований, а не удалять ради уменьшения количества файлов.
|
||
|
||
Признаки возможной AI-assisted сложности здесь объективны: недоступная orchestration, тесты старого сценария, дублирующие boundary mappers и формальная декомпозиция. **Происхождение кода по этим признакам не устанавливается.**
|
||
|
||
# Open Questions
|
||
|
||
1. **Forecast:** должен ли hourly walk сохранять новый полный результат для каждого нового дня независимо от HTTP-запросов? Как должен агрегироваться `all`, если сохранены отдельные account forecasts?
|
||
2. **Исторический income:** кто загружает отсутствующие периоды после отказа от request-driven импорта? Должен ли пользователь предварительно открывать Ledger?
|
||
3. **Клиентская свежесть:** правило «не чаще 30 минут» относится только к timer или также к focus, reconnect и повторному mount?
|
||
4. **MOEX:** отсутствие запрошенного блока действительно подтверждает отсутствие событий или может означать неполный ответ? Сейчас spec и тесты отвечают по-разному.
|
||
5. **Совместимость:** есть ли внешние consumers `GET /tbank-sync/:id` и observe-mode instrument endpoints? Это определяет объём удаления старого read compatibility.
|
||
6. **Search UI:** какие поля требуют detail RPC для каждого результата, а какие уже присутствуют в search response?
|
||
7. **Налоговая формулировка:** подтверждён ли смысл `dividend_net` для подписи «до налогов»? В docs это уже отмечено как неразрешённая интерпретация.
|
||
8. **Рабочий масштаб:** сколько подключений и инструментов ожидается одновременно? Это нужно для оценки бюджета hourly calendar refresh и значимости N+1.
|
||
|
||
**Итог:** сначала устранить рассогласования нового data flow и сделать его тесты достоверными; затем удалить неиспользуемую координацию. Это даст больше пользы, чем добавление новых архитектурных практик или инструментов.
|