Files
main/audits/2026-10-11/reports/previous/technical-audit-2026-10-11.md
codex-bot d07fbfabb1 docs: archive technical audit 2026-10-11 in develop (#36)
Переносит архив `audits/2026-10-11/` (19 файлов) из codex/audit-remediation-2026-10-11 в develop и добавляет точечный allowlist в .gitignore.
2026-10-17 19:38:03 +03:00

348 lines
49 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Технический аудит 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 и сделать его тесты достоверными; затем удалить неиспользуемую координацию. Это даст больше пользы, чем добавление новых архитектурных практик или инструментов.