Переносит архив `audits/2026-10-11/` (19 файлов) из codex/audit-remediation-2026-10-11 в develop и добавляет точечный allowlist в .gitignore.
49 KiB
Технический аудит WealthLedger
Дата: 11 октября 2026 года.
Executive Summary
Главная проблема проекта — незавершённый переход между моделями получения данных, а не недостаток архитектурных слоёв. Часовой обход, прежняя screen-driven синхронизация, online lookup и локальные snapshots местами сосуществуют с разными правилами обновления. Добавление новых сервисов и абстракций это усложнит; наибольшую пользу даст удаление старых путей и уточнение ответственности существующих владельцев.
Наиболее существенные системные проблемы:
- Выбор сохранённого прогноза может блокировать публикацию нового полного результата.
- Ошибки часового координатора могут обозначаться успехом; отдельные сбои persistence способны оставить обход незавершённым.
- Часовой обход не обновляет уже сохранённые календари выплат из-за политики
stable. - Frontend получает информацию о свежести и ошибках, но основной экран портфеля теряет её при передаче props.
- Часть тестов защищает старые сценарии или проверяет гонку без фактического запуска конкурирующей работы.
При этом основа проекта обоснованна: модульный монолит, конкретные 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не вызывается. Во frontendStocksPage.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 callersrequestне найдены.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 отдаёт для обычного Error500—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:удалить frontendobserve/observationKey, если подтверждено отсутствие внешних consumers: в production callers не найдены, вызовы есть только в тестах. Обычныйrefetchуже обслуживает текущий сценарий. [frontend/src/shared/api/resource-query.ts:69–127]native:передать периодическое чтение TanStack QueryrefetchInterval; убрать собственный 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 |
Самые важные недостающие сценарии
- Hourly lifecycle: первый запуск, повтор в срок, длинный обход с пропуском срока, restart и persistence failure.
- Два последовательных успешных календарных обновления: изменившийся upstream действительно меняет сохранённый календарь.
- Forecast следующего дня: новый полный результат заменяет прежний; узкий GET фильтрует сохранённый результат; непересекающееся окно возвращает предусмотренную пустоту.
- Обычный income GET без
sync=observe: не создаёт personal work. - Disabled query на timer/focus boundary: неподготовленный identifier не вызывает request.
- Online invalid broker token: публичный code постоянной ошибки без транспортных повторов.
- Пароль на 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
- Модульный NestJS-монолит с конкретными providers. Не обнаружена необходимость в обязательных ports/interfaces, CQRS или отдельном сервисе на каждый use case.
- SQLite и single-process coordination. Для текущей топологии это существенно проще распределённой системы. Ошибки lifecycle следует исправить внутри существующей модели.
- Decimal strings и bigint-копейки. Они сохраняют точность и смысл неизвестной суммы; замена на обычный
numberбудет ухудшением. - Connection revision, CAS и conditional publication. Они защищают от публикации результата под другим подключением. Это необходимая сложность.
- Bank intent fingerprints, версии, tombstones и refund transactions. Проверенные сценарии lost response, replay и конкурирующих изменений оправдывают эти механизмы.
- Generated OpenAPI и производные frontend types. Это не бесполезное дублирование моделей, а граница независимых репозиториев.
- Snapshots скринера. Они обеспечивают устойчивые страницы одного результата. Пересчитывать каталог при каждом GET страницы проще лишь внешне.
- Mantine controls, React Hook Form и необходимые page VMs. В формах и data-driven screens у них есть конкретная ответственность. Оснований дробить большие views по числу строк не найдено.
- Общая тема для приложения и Storybook. Два реальных consumers оправдывают visual foundation packages.
- Исторические ADR и OpenSpec archives. Их следует отличать от актуальных требований, а не удалять ради уменьшения количества файлов.
Признаки возможной AI-assisted сложности здесь объективны: недоступная orchestration, тесты старого сценария, дублирующие boundary mappers и формальная декомпозиция. Происхождение кода по этим признакам не устанавливается.
Open Questions
- Forecast: должен ли hourly walk сохранять новый полный результат для каждого нового дня независимо от HTTP-запросов? Как должен агрегироваться
all, если сохранены отдельные account forecasts? - Исторический income: кто загружает отсутствующие периоды после отказа от request-driven импорта? Должен ли пользователь предварительно открывать Ledger?
- Клиентская свежесть: правило «не чаще 30 минут» относится только к timer или также к focus, reconnect и повторному mount?
- MOEX: отсутствие запрошенного блока действительно подтверждает отсутствие событий или может означать неполный ответ? Сейчас spec и тесты отвечают по-разному.
- Совместимость: есть ли внешние consumers
GET /tbank-sync/:idи observe-mode instrument endpoints? Это определяет объём удаления старого read compatibility. - Search UI: какие поля требуют detail RPC для каждого результата, а какие уже присутствуют в search response?
- Налоговая формулировка: подтверждён ли смысл
dividend_netдля подписи «до налогов»? В docs это уже отмечено как неразрешённая интерпретация. - Рабочий масштаб: сколько подключений и инструментов ожидается одновременно? Это нужно для оценки бюджета hourly calendar refresh и значимости N+1.
Итог: сначала устранить рассогласования нового data flow и сделать его тесты достоверными; затем удалить неиспользуемую координацию. Это даст больше пользы, чем добавление новых архитектурных практик или инструментов.