Files
backend/BACKEND_AUDIT.md
T
ksv741 4796d9d3f0
Backend security / security (push) Successful in 8s
Backend verify / verify (push) Successful in 11m9s
Conventional commits / commits (pull_request) Successful in 28s
Backend security / security (pull_request) Successful in 7s
Backend verify / verify (pull_request) Successful in 10m38s
fix: harden auth and investment synchronization
Resolve A01-A10 and C01-C07 from the backend audit.
Preserve auth failures, forced candle reconciliation and forecast freshness.
Validate MOEX responses and nested HTTP errors; remove unused legacy paths.

Verified: 209 unit tests, 262 SQLite e2e tests on Linux, OpenAPI and durability.
Record the remaining macOS native SQLite abort in the audit.
2026-09-27 14:09:05 +03:00

66 KiB
Raw Blame History

Аудит backend WealthLedger и план исправлений

Дата аудита: 2026-09-27. Репозиторий: /Users/ksv741/Project/vibe/WealthLedger/backend. База: 516e43203eb3fee8e6a0c7018be2fee11f1b900c, ветка codex/investment-contracts. При аудите рабочее дерево было чистым; production-код не изменялся. Этот документ сохраняет результаты аудита и задаёт проверяемый план исправлений. Само сохранение документа не означает выполнения исправлений.

Номера строк относятся к указанному commit. Перед изменением повторно найти symbol и проверить актуальный flow: после предыдущих исправлений строки сдвинутся. Все относительные пути и ссылки относятся только к backend.

Общая оценка

Backend имеет понятных владельцев ответственности: контроллеры тонкие, денежные расчёты преимущественно точные, транзакции и очереди сосредоточены у своих владельцев. Массовое дробление файлов не требуется. Наиболее проблемные области — обработка ошибок auth, проверка ответов MOEX, согласование состояния фоновой синхронизации и временных признаков кешированных данных.

Изучены локальные AGENTS.md, порядок работы, архитектура и активированные темы HTTP, errors, data, testing, security, integrations и observability. Соседние репозитории не использовались.

Инвентаризированы 135 production-файлов TypeScript, 16 004 строки, 17 Nest module-файлов. Generated Prisma, vendor protocols, миграции и тесты отделены от production-рейтинга.

Приоритеты: высокий — существенный риск безопасности или искажения данных; средний — ограниченный дефект поведения, контракта или доказанная стоимость связности; низкий — локальное упрощение. Уверенность всех перечисленных находок высокая для описанного механизма; ограничения конкретного воспроизведения указаны отдельно.

План выполнения и границы

  1. Прочитать действующий backend AGENTS и активированные conventions; зафиксировать текущие HEAD/status. Сохранить существующие staged/unstaged/untracked изменения.
  2. Работать по находкам A01–A10 и cleanup C01–C07 ниже, в порядке пакетов из таблицы. Перед каждым исправлением перечитать владельца целиком, callers, связанные tests и 1–3 релевантных примера. Если факт больше не воспроизводится, отметить это с доказательством вместо механического применения рекомендации.
  3. Для дефекта сначала получить падающую проверку наблюдаемого результата на самой дешёвой надёжной boundary. Для concurrency/atomicity использовать реальную изолированную SQLite; mock race не доказывает DB-инвариант.
  4. Исправлять у существующего владельца минимальным связанным diff. Сохранять precision, ownership, public error codes, transaction/fencing, cache isolation, limiter и выполняющиеся promises. Предложения ниже задают результат, а не обязательную форму реализации.
  5. После каждого пакета выполнить targeted checks; в конце — актуальные обязательные gates по backend conventions. Перед каждой Node-командой выполнить nvm use --silent.
  6. Сохранить исходный снимок аудита; результаты реализации записывать в таблицу «Статус исправлений». Для каждого ID указать diff/решение, команды и результат, остаточный риск либо точную причину невыполнения.
Пакет Пункты Условие завершения
1. Auth и безопасные ошибки A01, A02 Отказы хранилища не маскируются; секреты не попадают в completion log
2. MOEX trust boundary A03 Malformed response не публикуется как полный результат; pagination завершается
3. Статус синхронизации A04 Screen учитывает состояние successor через владельца operations
4. Свечи A05 Force не теряется при coalescing; forced publication остаётся атомарной
5. Свежесть и время A06, A07 Stale-флаг актуален; wire timestamp совпадает с используемым при фильтрации
6. HTTP validation A08 Реальный pipeline возвращает структурированные field errors
7. Связность и удаление legacy A09, A10, C01–C07 Живые boundaries сохранены; лишние реализации и exports удалены

Пакеты независимы по цели, но могут пересекаться файлами и общими fixtures. Параллелить только области с непересекающимся владением файлами, затем выполнить общую проверку. A02 и A08 изменяют один HTTP filter и должны быть согласованы.

В scope входят исправления подтверждённых находок и их regression tests. Гипотезы в конце документа не являются задачами на изменение кода. Новые зависимости, generic repositories, scheduler framework, интерфейсы ради паттерна, schema migrations и size-only splits для этих исправлений не требуются. Отдельную необходимость такого расширения следует доказать до реализации. Коммиты, push и PR выполняются только по отдельному запросу пользователя.

Подтверждённые находки

A01. Auth lifecycle смешивает invalid credentials и operational failure

Приоритет: высокий. Уверенность: высокая.

Места и потребители: AuthService.logout, строки 104–126, AuthController.logout, 95–104; refresh, 85–91.

Общий catch в logout подавляет JWT verification errors и отказ Prisma при записи revokedAt. Controller затем очищает cookie и возвращает 204, хотя серверная сессия осталась действительной. Сохранённая копия refresh token продолжает работать. Смежное проявление: refresh controller удаляет cookie при любой ошибке, включая временный отказ БД, без доказательства недействительности токена.

Доказательство: inline fault injection на настоящих AuthService, JwtService и AuthController с синтетической сессией подтвердил:

logoutResolved=true
cookieCleared=true
sessionNotRevoked=true
sameRefreshTokenStillUsable=true
unexpectedRefreshErrorPropagated=true
validCookieRemoved=true

Это доказательство обработки injected boundary failure, а не реального отказа БД. Auth HTTP suite проверяет обычный logout, но не этот отказ. Нарушен контракт отзыва сессии: идемпотентность не превращает несостоявшуюся запись в успешную.

Минимальное действие: ограничить подавление ошибок logout недействительным JWT; ошибки хранилища пробрасывать. При refresh очищать cookie только при установленном auth failure. Существующий AuthService.refresh уже отделяет verification от DB reads; BankCardsRepository.write также пробрасывает неизвестные ошибки.

Приёмка:

  • Отсутствующий/недействительный/уже отозванный refresh сохраняет идемпотентный logout.
  • Ошибка записи revocation не возвращает фиктивный 204.
  • Успешный logout делает последующий refresh невозможным.
  • Временный отказ refresh не удаляет действующую cookie, повтор после восстановления возможен.
  • Проверены status/code/cookie через configured HTTP; успешный отзыв — с SQLite.

Риск: сохранить существующую семантику недействительных токенов. Изменение access-JWT lifetime или введение replay history не входят в этот пункт.

A02. Многострочное сообщение исключения попадает в completion log

Приоритет: высокий. Уверенность: высокая.

Места: HttpExceptionFilter, 30–37. Потребители: RequestLoggingMiddleware, 43–57, StructuredLogger, 47–63.

Фильтр удаляет только первую строку exception.stack. Остальные строки многострочного Error.message сохраняются. Поле stack разрешено logger allowlist; redaction HTTP body к нему не применяется.

Воспроизведение: configured HTTP app; AuthService.login выбрасывает синтетический Error('operation failed\npassword: SYNTHETIC_AUDIT_SECRET'). Ответ остаётся 500 / INTERNAL_SERVER_ERROR, но completion log содержит вторую строку сообщения. Реальные секреты не использовались. Существующий filter spec проверяет однострочное сообщение. Нарушены observability и security rules.

Минимальное действие: убрать передачу сырого stack через общую HTTP boundary, сохранив code/request context. Если stack необходим, нужна проверенная форма кадров без произвольного текста исключения; удаление только первой строки недостаточно.

Приёмка: multiline synthetic secrets отсутствуют в response и в конечном payload logger, а status/code/correlation сохраняются. Проверить настоящий filter → middleware → logger, не только mock вызова filter.

Риск: диагностическая подробность может уменьшиться; raw message возвращать нельзя.

A03. MOEX malformed response превращается в полный пустой прогноз

Приоритет: высокий. Уверенность: высокая.

Место: MoexClient, 122–142 и 163–165. Отсутствующий запрошенный ISS block преобразуется в []. Неполный cursor может завершить чтение с частичными данными; cursor с нулевым pageSize может зациклить его.

Реальная цепочка: CatalogEnrichmentService, 444 и 509–548 публикует bondEvents: []; readMany считает существующий row complete; UpcomingEventsService, 137–197 строит пустые events/totals; ForecastRepository, 124–150 может заменить прежний непустой forecast. Владелец protocol validation — integration client.

Воспроизведение: настоящий MoexClient с mock fetch HTTP 200 без coupons/offers вернул []. Cursor INDEX=0,TOTAL=1,PAGESIZE=0 дал пять запросов start=0; проба остановлена искусственной ошибкой. Timeout каждого fetch не ограничивает весь цикл.

Расхождение тестов: moex.client.spec, 97–107 считает отсутствующий block пустым результатом; upcoming-events.e2e, 439–451 обещает обратное, но подменяет весь клиент готовым rejection и не проверяет parser.

Минимальное действие: различать отсутствующий/повреждённый block и корректный data: []; проверять структуру строк и cursor, положительный pageSize и строгое продвижение start. Нарушение протокола → MOEX_UNAVAILABLE, без частичного success.

Приёмка:

  • Malformed block/cursor не меняет сохранённый полный календарь/forecast.
  • Корректный пустой block остаётся успешным пустым результатом.
  • Непродвигающаяся pagination завершается ошибкой, без бесконечных fetch.
  • Семантика HTTP 404 проверена отдельно и не изменена случайно.
  • Downstream regression использует настоящий MoexClient с mock fetch, а не mock rejection клиента.

Риск: перепутать законное отсутствие данных с ошибкой protocol shape.

A04. Screen sync сообщает succeeded до завершения successor

Приоритет: средний. Уверенность: высокая.

Места: TbankSyncService.status, 330–345, TbankSyncRepository.read, 70–80. Operations repository, 441–445 помечает объединяемую группу superseded и сохраняет successorId. Владелец операций умеет разрешать цепочку в observe, строки 564–590, но screen читает прямого child.

Сценарий: две активные screen-группы на части января; третья объединяет интервалы. Screen вытесненной группы сообщает succeeded, хотя successor ещё running; его последующая ошибка также теряется.

Воспроизведение настоящего status с mocked persistence:

screenState=succeeded
resource.availability=missing
resource.sync.state=running
actualOperationState=running

SQLite bridge покрыт operations-sync e2e, 455–474, но существующая проверка наблюдает operations.observe, а не screen status.

Минимальное действие: разрешать operations successors через существующего владельца перед агрегацией screen state/progress. Не копировать второй алгоритм разрешения цепочки в screen service.

Приёмка: configured HTTP + SQLite bridge; задержанный successor → running, затем успех → succeeded или отказ → failed; progress согласован, чужая revision не видна. Риск: fencing, цепочки successors и корректный подсчёт progress.

A05. Force обновления свечей теряется при coalescing

Приоритет: средний. Уверенность: высокая.

Места: CandleService.request/load, 326–365, CandleRepository.reserve, 76–82. Публичный caller: TbankSyncService, 288–306 вызывает candles.request с force=true; candleRange необязателен, ключ может совпасть с ordinary GET/prefetch.

При launch=false service возвращает существующий id без сохранения новых force/priority. При присоединении cross-user observer требования также не повышаются.

Воспроизведение настоящего класса с mocked repository/upstream: январь покрыт, ordinary period=all заблокирован перед загрузкой detail, затем приходит force all. После release единственный RPC идёт от 2026-02-01T00:00:00.000Z до 2026-02-02T12:00:00.000Z; январь не перечитан. Исправленные исторические значения остаются прежними. Existing candle suite отдельно проверяет force и coalescing.

Минимальное действие: сохранить forced требование и выполнить полную reconciliation. Если gap-план уже начат, безопасно перепланировать или выполнить следующую forced попытку. Простого force ||= true после публикации первых окон недостаточно. Аналог повышения требований есть в CatalogDiscoveryService, но его алгоритм нельзя механически переносить на staged свечи.

Приёмка: ordinary load + force того же scope действительно обновляют уже закрытую свечу. Возвращённый work ID завершается только после выполнения forced требования с актуальным диапазоном запроса. Проверить join до запуска и после публикации первого обычного окна, включая observer другого пользователя; экранный запрос получает экранный приоритет. Ошибка следующего forced окна сохраняет прежнюю историю и timestamps; user/revision fencing сохраняется. Proof атомарности — существующая SQLite boundary.

Риск: высокий риск неверного локального исправления из-за staged/atomic publication.

A06. Payout snapshot замораживает positionIsStale

Приоритет: средний. Уверенность: высокая.

Место: ForecastRepository.resolve, 132–137. Потребители: StockIncomeService, 67–87 и 161–163, BondIncomeService, 180–182.

Истечение 60-секундного portfolio TTL меняет isStale, но не source revision/timestamps. Repository возвращает прежний payload вместо нового candidate. Возможны одновременно meta.freshness: stale и data.positionIsStale: false.

Воспроизведение: настоящий repository с mocked transaction; candidate false, затем true при тех же версиях → повторный результат false, всего одна запись. Pure payout tests принимают stale input, но не проверяют aging persisted forecast.

Минимальное действие: вычислять возраст сохранённой позиции при выдаче по её positionAt и действующей freshness policy. Учитывать именно позицию сохранённого fallback, а не случайно подставлять свежесть более нового несовместимого портфеля. Не сравнивать весь candidate без разбора: текущий calculatedAt у events вызовет запись на каждом чтении. Не менять financial revision только из-за хода времени.

Приёмка: controllable clock, TTL истёк без изменения источников, sync=observe: stale=true, суммы и source timestamps прежние, новых RPC нет; fallback остаётся согласованным. Риск: смешение возраста сохранённой и текущей позиции.

A07. Timestamp операций расходится между persistence и wire

Приоритет: средний. Уверенность: высокая.

Место: brokerage-operation.mapper, 85–90 игнорирует nanos; repository, 653–657 и publish учитывает миллисекунды. Consumers mapper: BrokerageOperationsService, обычный ledger и income reads (строки 359 и 619 на базе аудита).

Воспроизведение: seconds=1768000000, nanos=600000000 → persistence 2026-01-09T23:06:40.600Z, wire 2026-01-09T23:06:40.000Z. Запрос from=…40.500Z возвращает строку с видимым временем раньше from. Existing mapper specs проверяют money/quantity, но не это соответствие.

Минимальное действие: согласовать конвертацию protobuf timestamp; при возможности использовать уже сохранённый occurredAt для wire. Предотвратить дальнейшее расхождение реализаций без создания универсальной библиотеки дат.

Приёмка: subsecond mapper case и HTTP boundary внутри секунды; порядок/cursor, обычный ledger и income используют согласованное время. Риск: исправляется публичное значение timestamp; формат и точность Money сохраняются.

A08. Configured validation response теряет ошибки полей

Приоритет: средний. Уверенность: высокая.

Места: configureApp, 29, HttpExceptionFilter, 44–48 и 70–78. ValidationPipe выдаёт message:string[], filter приводит его к ValidationError[] и ищет constraints. children вложенных ошибок также не обходятся.

Configured HTTP reproduction: invalid email и отсутствующий password:

{"status":400,"code":"VALIDATION_ERROR","errors":[]}

Ожидаемый HTTP contract требует pointer/detail. Conventions уже фиксируют это известное расхождение. OpenAPI parity проверяет schema, но не наблюдаемый response pipeline.

Минимальное действие: согласовать exceptionFactory и filter, сформировать структурированные ошибки, включая вложенные поля, без target/value исходного DTO.

Приёмка: реальные requests credentials и nested Money дают непустые корректные pointer/detail; status/code/content-type сохраняются; неизвестные исключения по-прежнему безопасны. Проверить согласование с A02 и публичной metadata. Риск: public error shape и корректность вложенных путей.

A09. Planner зависит от service barrel и образует runtime import cycle

Приоритет: средний. Уверенность: высокая.

Места: investment-sync-plan, 3–6 → operations.api → operations services → investment-sync-plan.

Алгоритму нужны interval rules, но barrel загружает services, TbankClient и Prisma. Planner spec, строка 1 уже вынужден mock Prisma. Это доказанная стоимость связности; production crash из-за цикла не обнаружен. Type-only цикл T-Bank records — отдельный безвредный случай.

Минимальное действие: предоставить interval rules чистый публичный вход, аналогичный разрешённому integrations/tbank/decimal.ts; service API оставить отдельно. Не заменять cycle на forwardRef и не вводить новый DI слой.

Приёмка: planner импортируется/тестируется без Prisma mock; cycle устранён, interval semantics и DI initialization сохранены. Риск: низкий, import/export surface.

A10. Неиспользуемый legacy T-Bank access flow

Приоритет: низкий. Уверенность: высокая.

Место: TbankAccessService, 255–330. listSupportedAccounts/withSupportedAccounts/withConnection/withAccount/ withAccountForConnection/private connection вызываются только внутри собственной цепочки и tests. Проверены production consumers, DI и decorators.

Legacy flow поддерживает generation=hash(ciphertext), тогда как живой persisted flow использует DB revision, local и tokenForAttempt. Поддерживаются две модели без production-потребности; около 76 строк, context types и TbankClient dependency.

Минимальное действие: повторно проверить callers на актуальном HEAD, удалить legacy-цепочку, неиспользуемые context exports и зависимость; отдельно проверить оставшихся consumers accessibleTbankAccount до его удаления.

Приёмка: нет мёртвой цепочки, живые access/invalid-token/revision/fencing cases сохранены. Перенести полезные security assertions, если они были только в удаляемых specs. Риск: удалить общее правило или полезную проверку вместе с legacy.

Локальные упрощения

Все пункты: низкий приоритет, высокая уверенность. Перед удалением повторить поиск callers; отсутствие production callers в базе аудита не гарантирует его на новом HEAD.

ID Место и доказательство Минимальное действие и приёмка
C01 portfolio.service, 515–522: локальный decimalFromString повторяет common parser без validation; callers position/value Импортировать существующий common owner; удалить дубликат. Exact-money tests и typecheck. Текущая ошибка сумм не доказана; проверить допустимые входы.
C02 tbank.api: TbankSyncAttempt импортируют напрямую семь внешних файлов sync/instruments; enrichment также обходит API для market types Экспортировать используемые типы, перенаправить imports через API. TbankCoupon уже экспортируется. Typecheck и поиск обходов; runtime-риск минимален.
C03 auth.service, 190–192: normalizeEmail возвращает аргумент; register/login вызывают его, реальная нормализация в CredentialsDto Удалить no-op, сохранить DTO transform. Existing HTTP case/whitespace tests; дополнительную нормализацию без необходимости не вводить.
C04 bank-card.calculations, 56–76: string period используется только tests; production summary передаёт from/to Сузить API до рабочего диапазона; перевести полезные unit cases. Проверить границы месяца/range.
C05 instrument-market, 39–51: optional без consumers; stock-position wrapper нужен только своему spec Удалить optional, перенаправить полезные wrapper tests на instrumentPosition. Сохранить fractional/mixed/offset holdings cases.
C06 portfolio.api, 2: totalsByCurrency не имеет внешнего production consumer; внутренний использует mapper напрямую Удалить только лишний export, mapper сохранить; typecheck.
C07 auth.controller.spec, 9–20: проверяет HTTP_CODE_METADATA через prototype; те же 200 проверяются configured HTTP suite Удалить дублирующий wiring spec только при сохранении реальных HTTP status/cookie assertions.

Карта ответственности и проверенные сценарии

Область / владелец Интерфейс и consumers Состояние, зависимости, инварианты и эффекты
App/config/common/Prisma Composition root, configureApp, Money/decimal, logging Env validation, HTTP pipeline, DB lifecycle, request context; общая арифметика без Nest/Prisma
Auth Auth HTTP; guard/AccessPayload API для protected controllers User/Session, bcrypt/JWT, refresh hash/CAS; cookies у controller
Bank cards/operations/categories BankCardsController → профильные services Ownership, archive/version/replay/tombstones; refund budget и перевод в общей transaction boundary
Bank summary Controller → use case → repository → pure calculation Согласованное чтение и BigInt-сводка по периоду
Brokerage accounts HTTP/read API для screen → service → PortfolioService Собственной persistence нет; missing membership отличается от confirmed empty
Portfolio/forecast HTTP и API для instruments/events/accounts Private user/revision/account snapshots; mapping/decimal; aggregate completeness; atomic dependency version checks
Operations HTTP/API для income/screen Local journal, signed cursor binding; durable work/staging, intervals, recovery, atomic reconciliation
Income analytics HTTP/read API → Operations API Period roles/completeness, grouping и точные signed totals по валютам; своей DB нет
Upcoming events HTTP/read API → portfolio/calendars → forecast Events по positions; missing calendar не равен пустому; redemption исключён из income totals
Catalog discovery/enrichment Stock/bond services и public source APIs Shared identity/aliases/source rows; private work, TTL, batching, coalescing и generation-fenced publication
Candles Market services/screen → CandleService/Repository Shared candles/coverage; mutable tail, gaps, windows, очередь и staged force replacement
Stock/bond position/income Controllers → application services UID/type matching, exact weighted cost, mixed currencies, payout estimates; Portfolio/Catalog/Forecast APIs
T-Bank connection Guarded GET/PUT/DELETE Проверка токена до replacement, AES-GCM, новая revision/cascade; failed validation сохраняет connection
T-Bank client/access/membership/metadata Integration API для business modules gRPC/TLS, единые limiter/retry/deadline; credentials, account access, membership и publication fencing
Screen sync POST descriptor / GET status Orchestration через APIs; durable children/attempts; passive observation
MOEX/NCR Integration APIs → enrichment ISS parser/pagination; NCR strict registry parser/exact ISIN/shared cache

Прослежены auth/session flows; создание/изменение карт, операций, переводов, возвратов и summary; ledger→sync→staging→publication; portfolio→mapping→snapshot; payouts/events→forecast; catalog/candles и screen orchestration.

Повторной регистрации владельцев caches/queues и циклов Nest modules не обнаружено. InstrumentSourcesModule позволяет portfolio пользоваться источниками без DI-цикла с InstrumentsModule. Type-only цикл T-Bank records не является finding.

Размеры production-файлов

Количество строк включает comments/blank lines. Размер — основание для чтения, а не самостоятельный дефект. Обязательные splits не обоснованы.

Файл Строк Решение и причина
brokerage-operations.repository 924 Оставить: overlap groups, staging, recovery и publication держат общий transactional invariant
catalog-enrichment.service 665 Упростить локально: protocol boundary/imports; сохранить producers, batching, completions вместе
brokerage-operations.service 636 Оставить: ledger/income имеют различную completeness; generic builder добавит скрытые режимы
tbank-sync.service 595 Упростить: разрешение successors вернуть operations owner, generic job framework не нужен
tbank.client 560 Оставить: records/RPC methods и единый limiter/retry owner
portfolio.service 522 Упростить: удалить duplicate parser, scheduling state не разрывать
candle.service 481 Оставить: исправить force join, сохранить queue/publication invariant
catalog-discovery.repository 480 Оставить: identity/aliases/source publication и CAS связны
catalog-discovery.service 390 Оставить: search/detail и producer lifecycle имеют одного owner
bank-operations.service 343 Оставить: разрыв validation/refund/transfer writes повышает риск
tbank-access.service 331 Упростить удалением legacy flow
investment-sync-plan 327 Упростить зависимость: убрать service barrel из алгоритма
instrument-metadata.repository 320 Оставить: identity, batching и publication согласованы
income-analytics.service 318 Оставить: связные projections, выгода дополнительных моделей не доказана
candle.repository 306 Оставить: reconciliation, coverage, fencing и stage cleanup
upcoming-events.service 297 Оставить: один сценарий, основной дефект у MOEX boundary
brokerage-operations-sync.service 294 Оставить: связный владелец durable work execution

Отдельные категории: 37 unit specs / 3319 строк; 21 HTTP/SQLite specs / 7878 строк; 10 test support/fixtures / 1993 строки; 6 vendor proto / 3588 строк; 5 SQL migrations / 345 строк. Generated Prisma и build artifacts не ранжировались.

Удачные решения, которые требуется сохранить

  • BigInt/decimal и string Money; разные валюты не складываются неявно.
  • Refresh CAS, ownership queries, версии операций, replay fingerprints/tombstones.
  • Network calls отделены от DB transactions.
  • Revision/attempt fencing, late-result discard, staging перед force reconciliation.
  • Missing/partial/confirmed empty/saved complete различаются явно.
  • Concrete providers, компактные плоские модули и разрешённый прямой Prisma в связных CRUD/transaction services не являются автоматическими дефектами.
  • SQLite behavior tests проверяют реальные денежные границы, races и rollback.

Выполненные проверки и пределы доказательства

Перед успешными Node-командами загружалась ~/.nvm/nvm.sh, затем nvm use --silent. Первый вызов nvm без загрузки shell function завершился command not found; после загрузки проверок это ограничение не осталось.

Команда / проверка Результат аудита
git status, git rev-parse HEAD, git diff --check Чистое дерево, неизменный commit, diff errors отсутствуют
npm run lint Успешно
npm run typecheck:app; npm run typecheck:test Успешно
npm test -- --runInBand 37 suites / 200 tests passed
Build в npm run test:e2e Prisma generation и Nest build успешны
Полный e2e в sandbox listen EPERM на localhost; не засчитан
LOG_LEVEL=error node test/sqlite.cjs --all, с разрешением localhost all-accounts: 18 passed; auth-suite затем завершилась native SIGABRT
node test/sqlite.cjs tbank-screen-sync.e2e-spec.ts tbank-operations-persistence.e2e-spec.ts tbank-candles-persistence.e2e-spec.ts tbank-metadata-persistence.e2e-spec.ts tbank-local-reads.e2e-spec.ts openapi-contract.e2e-spec.ts Первые три: 27 + 5 + 9 passed; metadata native abort; последующие не достигнуты
node test/sqlite.cjs openapi-contract.e2e-spec.ts, отдельно 3 tests passed
Inline reproductions Подтверждены описанные случаи; validation/logging через configureApp, остальные с указанными mocked boundaries

Native abort произошёл в node::RemoveEnvironmentCleanupHook / better_sqlite3.node на Node 24.21.0. Причина не установлена; связывать его с конкретной source-находкой нельзя. Полный e2e gate не подтверждён. При реализации сначала повторить в штатном окружении; если проблема остаётся, диагностировать отдельно и явно сообщить, не отключая падающие suites и не выдавая неполную проверку за зелёную.

Логи текущей сессии (временные файлы, могут исчезнуть): /tmp/wealthledger-audit-e2e.log, /tmp/wealthledger-audit-targeted-e2e.log, /tmp/wealthledger-audit-openapi.log. Документ не зависит от сохранности этих файлов.

Проверено: все production-модули, composition/API, основные consumers, значимые flows, связанные tests кандидатов. Все production auth/banking, integrations/sync/operations и остальные investment slices прочитаны.

Частично проверено: весь набор HTTP fixtures и integration suites не проходил одинаково глубокий построчный аудит; реальное выполнение e2e ограничено указанным native abort. Schema прочитана; история миграций не проходила полный compatibility audit.

Не проверено: clean npm ci, полный verify с coverage как единая команда, durability/restart/backup, container/deployment, production performance, реальные внешние API и полная сверка vendor proto с upstream.

Финальная проверка реализации

Использовать актуальные scripts/conventions backend. На базе аудита для code changes требуются npm ci, npm run verify, relevant SQLite/HTTP checks; для этих HTTP изменений также OpenAPI generation/parity. Полный npm run test:e2e нужен для итоговой проверки совокупных изменений. Перед Node — nvm use --silent.

Автоисправления lint/format не являются доказательством. Не запускать миграции, reset/drop или tests на пользовательской БД. Штатный SQLite runner создаёт временные базы и защищает от использования внешнего DATABASE_URL.

По завершении повторить routing по staged/unstaged/new files, выполнить Boy Scout и semantic review: callers, owners, money/access/atomicity/error contracts, generation/cache/async lifecycle. Указать точные команды, outcomes и ограничения.

Неподтверждённые гипотезы — вне scope исправлений

  • Logout/refresh interleaving и browser cookie overwrite требуют явных ordering semantics и отдельного HTTP/browser proof.
  • N+1 в карточных views виден, но измеренная latency/timeout проблема не установлена.
  • JSON casts внутренних сохранённых payload сами по себе не доказывают небезопасную boundary.
  • Cash/accrued-interest valuation: локального контракта недостаточно для обвинения в ошибке.
  • Native SQLite abort требует отдельной диагностики runtime/dependency lifecycle.

Статус исправлений

Начало исправлений: HEAD 516e43203eb3fee8e6a0c7018be2fee11f1b900c, ветка codex/investment-contracts; git status --short: только ?? BACKEND_AUDIT.md, staged/unstaged tracked changes отсутствовали. Исходные разделы аудита сохранены. npm ci успешно: 1065 packages, 44s; manifest/lockfile не менялись. Перед Node-командами загружается ~/.nvm/nvm.sh и выполняется nvm use --silent.

Все A01–A10 и C01–C07 повторно подтверждены и реализованы в порядке пакетов. Статусы ниже разделяют готовность исправления и полноту проверки: native abort не засчитывается как успешный behavioral test. Неподтвердившихся и уже исправленных на исходном HEAD пунктов не обнаружено.

ID Статус Изменение / доказательство Проверки и ограничения
A01 Исправлено и проверено Logout подавляет только JWT verification failure; refresh удаляет cookie только при UnauthorizedException. Configured HTTP red: logout 204 вместо 500, refresh удалил cookie при отказе БД. node test/sqlite.cjs app.e2e-spec.ts: regression воспроизведена на временной SQLite; macOS green повтор и первый полный e2e столкнулись с native SIGABRT. Итоговый Linux e2e: auth 12/12 passed, включая revocation failure и сохранение cookie для retry.
A02 Исправлено и проверено Удалены raw stack и его неиспользуемый transport/allowlist; regression проверяет multiline synthetic secret в конечном Pino payload и correlation. Targeted filter/logging: 3 suites / 4 tests passed, unit red → green; macOS configured HTTP runner завершился native SIGABRT; итоговый Linux auth 12/12 passed, включая настоящий filter → middleware → Pino payload.
A03 Исправлено и проверено MoexClient отклоняет отсутствующий/повреждённый block, неверную ширину row, malformed/nonprogressing cursor. Пустые блоки и 404 сохранены. Unit red 12 failures → 21 passed; node test/sqlite.cjs upcoming-events.e2e-spec.ts: downstream red 2 failures → 18 passed, настоящий client + mock fetch сохраняют calendar/forecast payload/revisions/timestamps. Typecheck и scoped lint passed.
A04 Исправлено и проверено Screen status разрешает superseded child через operations.observe перед агрегацией state/progress; attempt/revision fencing сохранён. node test/sqlite.cjs tbank-screen-sync.e2e-spec.ts: HTTP/SQLite red succeeded вместо running → 29 passed; successor success/failure, progress, чужой user и удалённая revision.
A05 Исправлено и проверено Каждый join повышает force/priority при более сильном требовании; идентичный join не меняет updatedAt. Load фиксирует force/now на проход, при новой forced потребности выполняет полную reconciliation. Финальная публикация и observers сериализованы с reservations; screen priority=0. Service unit red → green (полный исторический диапазон и более поздний now); node test/sqlite.cjs tbank-candles-persistence.e2e-spec.ts: 14 passed на финальном коде, same/cross-user join до загрузки и после обычного окна, rollback второго forced окна, timestamps и fencing. Native abort в первом финальном запуске; единственный повтор без изменений прошёл.
A06 Исправлено и проверено ForecastService вычисляет возвращаемый positionIsStale из сохранённого positionAt через существующую portfolio TTL policy; не меняет финансовый snapshot. node test/sqlite.cjs upcoming-events.e2e-spec.ts: red stock/bond false на TTL → 20 passed; TTL−1/TTL, observe без RPC, прежние суммы/revisions/timestamps, fallback при несовместимом свежем candidate. Review дополнительно воспроизвёл meta.freshness=fresh для старого fallback (2 red); freshForMs теперь учитывает сохранённый stale flag, repeat 20 passed, stateVersion рассчитывается штатно.
A07 Исправлено и проверено Одна operationTimestamp conversion у mapper для stage/publish; ledger/income wire используют сохранённый occurredAt. Missing seconds по-прежнему отклоняется. Unit red .600→.000 → 9 passed; node test/sqlite.cjs tbank-ledger-local.e2e-spec.ts: HTTP red → 11 passed (from=.500, cursor .800/.600, income timestamp/exact total). Оба typecheck/lint passed. На macOS operations-persistence завершилась native SIGABRT и не засчитана. Итоговый Linux полный e2e: ledger 11/11 и operations-persistence 5/5 passed.
A08 Исправлено и проверено Global/screen ValidationPipe сохраняют structured errors без target/value; filter обходит children и экранирует JSON pointers. Удалён descriptorErrors; OpenAPI errors.items согласован с object shape. Nested Money HTTP red errors:[] → bank22 / screen29 passed; escaped pointer и отсутствие дублей/secret проверены. Unit7 и scoped lint passed. Metadata red items:string → финальный OpenAPI parity 3 passed. На macOS auth suite native SIGABRT; итоговый Linux auth 12/12 passed, включая credential field errors без input leak.
A09 Исправлено и проверено Planner импортирует существующий чистый operations/domain/operations-sync-plan как явно документированный публичный вход; service barrel больше не экспортирует runtime interval rules. Planner и CandleService tests выполняются без Prisma mock; targeted пакет 7 suites / 49 tests passed. DI подтверждается configured HTTP и итоговым build.
A10 Исправлено и проверено Удалены шесть legacy methods, context types, TbankClient constructor dependency и accessibleTbankAccount без живых callers; legacy-only spec удалён. Поиск src/test не находит legacy symbols; 5 unit suites / 11 tests, оба typecheck, scoped lint/build; connection HTTP/SQLite4 passed: missing/invalid/unsupported/cross-user, replacement/deletion, revision/fencing. Token error/crypto assertions сохранены. MacOS metadata-persistence15 passed, operations-sync/catalog-discovery native SIGABRT. Итоговый Linux e2e: metadata15, operations-sync17 и catalog-discovery10 passed.
C01 Исправлено и проверено PortfolioService использует common decimalFromString; локальный parser удалён. Входы из money/formatDecimal уже canonical; текущая ошибка сумм не заявляется. Exact-money portfolio tests в targeted49 passed; итоговые types/verify ниже.
C02 Исправлено и проверено TbankSyncAttempt/TbankLastPrice экспортируются type-only через tbank.api; семь production consumers используют API, TbankCoupon переиспользован. Поиск прямых imports tbank-access.service/tbank-market.types в src/modules пуст; оба typecheck и scoped lint passed.
C03 Исправлено и проверено Удалён no-op normalizeEmail; CredentialsDto transform trim/lowercase сохранён. Auth unit passed; реальные case/whitespace HTTP assertions сохранены. После macOS native SIGABRT полный Linux auth gate прошёл: 12/12.
C04 Исправлено и проверено bankSummary принимает только рабочий диапазон from/to; строковый month overload удалён, полезные unit cases переведены на диапазоны. Targeted49 passed: month start/end, outside range, signed64/BigInt суммы и refunds.
C05 Исправлено и проверено Удалены optional без callers и stockPosition wrapper; spec перенесён к instrumentPosition, проверяет напрямую владельца. Targeted49 passed: fractional/large quantity, weighted costs, mixed/unknown currencies, no position и offset long/short.
C06 Исправлено и проверено Удалён неиспользуемый внешний totalsByCurrency export; живой mapper и внутренний caller сохранены. Поиск imports подтверждает единственный внутренний caller; exact-money targeted tests passed, итоговый typecheck ниже.
C07 Исправлено и проверено Удалён HTTP_CODE_METADATA wiring spec; реальные register/login/refresh status200 и logout/cookie assertions сохранены в app.e2e. Проверены callers/оставшиеся HTTP assertions; полный Linux auth gate прошёл: 12/12. Предыдущий macOS native SIGABRT остаётся отдельным ограничением среды.

Gates первого прогона (macOS)

Все команды выполнены из backend на Node v24.21.0 после nvm use --silent. Проверки данных использовали штатный runner, fresh temporary SQLite и штатные migrations; рабочая БД не использовалась. Для localhost/Prisma cache запуски получили разрешение вне sandbox; native abort повторялся и там.

Команда Итог
npm ci PASS; manifest и lockfile без изменений.
npm run verify PASS: lint, app/test typecheck, 36 suites / 209 unit tests с coverage, build.
npm run openapi:generate PASS; актуальный игнорируемый openapi.json сформирован.
npm run test:openapi-contract PASS: 1 suite / 3 tests, включая object shape validation errors.
npx prisma validate PASS. Schema и migrations не менялись.
npm run test:durability -- /tmp/wealthledger-fix-baseline-current-runtime-fjftdF PASS. Baseline 0862d56, до последней T-Bank migration, отдельно установлен и собран на текущем Node; результат /var/folders/j_/h2ft23gd3qq35ps3v5bcftfm0000gn/T/wealthledger-bank-durability-SIeCq0.
LOG_LEVEL=error npm run test:e2e INCOMPLETE, exit 1: all-accounts 18/18 passed, затем app.e2e native SIGABRT до Jest summary. Остальные suites общий gate не выполнил.
node test/sqlite.cjs tbank-candles-persistence.e2e-spec.ts PASS на финальном коде: 14/14 после одного native abort и повтора без edits.
node test/sqlite.cjs tbank-metadata-persistence.e2e-spec.ts PASS на финальном коде: 15/15.
node test/sqlite.cjs tbank-operations-sync.e2e-spec.ts INCOMPLETE: native SIGABRT до summary.
node test/sqlite.cjs tbank-operations-persistence.e2e-spec.ts INCOMPLETE: native SIGABRT до summary.
node test/sqlite.cjs tbank-catalog-discovery.e2e-spec.ts INCOMPLETE: native SIGABRT до summary.
git diff --check PASS.

Логи итоговых gates: /tmp/wealthledger-fix-verify.log, /tmp/wealthledger-fix-openapi-generate.log, /tmp/wealthledger-fix-openapi-contract.log, /tmp/wealthledger-fix-prisma-validate.log, /tmp/wealthledger-fix-durability.log, /tmp/wealthledger-fix-final-e2e.log. Дополнительные SQLite логи: /tmp/wealthledger-fix-a05-latest-sqlite-retry.log, /tmp/wealthledger-fix-final-metadata-persistence.log, /tmp/wealthledger-final-operations-sync.log, /tmp/wealthledger-final-operations-persistence.log, /tmp/wealthledger-fix-final-catalog-discovery.log.

Native failure: assertion (env) != nullptr в node::RemoveEnvironmentCleanupHook, stack содержит destructor из better_sqlite3.node. Это конкретное ограничение текущего native runtime, уже наблюдавшееся при исходном аудите; его первопричина в рамках исправлений не установлена. Тесты не отключались, runner/dependencies не изменялись. Эти macOS запуски остаются INCOMPLETE. Полный Linux прогон ниже закрыл e2e gate и auth/operations/catalog acceptance; локальный native abort на macOS при этом не исправлен.

Повторная полная проверка (Linux, 2026-09-27)

PASS: 21 e2e suites / 262 tests, exit 0, ни одного skipped/disabled test. Выполнена вся штатная npm run test:e2e: build и node test/sqlite.cjs --all. Каждая suite получила отдельную temporary SQLite со всеми пятью migrations. Закрыты прежде неполные auth12, operations-persistence5, operations-sync17, catalog-discovery10; также прошли candles14, forecast/upcoming20, screen29, OpenAPI3 и большой read-baseline с 300 000 операций / 1 000 000 свечей.

Перед повтором зафиксирован тот же HEAD и hashes всех tracked/untracked files; до завершения тестов ни один исходник, тест, config или audit не менялся. Поэтому предыдущие verify (209 unit tests), OpenAPI и durability сохраняют актуальность. В этом повторе изменён только статус данного документа.

MacOS LOG_LEVEL=error node test/sqlite.cjs app.e2e-spec.ts снова завершился native SIGABRT. Для Linux использован существующий Dockerfile target development, Node v24.21.0 (проверен против .nvmrc) и неизменённый lockfile: SHA-256 f986979b7922d684fead3a4d32532d11f1e469cec0e35669509343be41cbb2cd. Image wealthledger-backend:audit-verification-20260927, ID sha256:69264cf3dba23566d99fded450104bbe56f92755604e88232beb97825155ae73. Сборка выполнила npm ci; зависимости и runner не подменялись. Test/scripts подключались read-only, внешняя сеть отключена, host ports не публиковались; одноразовый контейнер удалён после завершения, рабочая БД не подключалась.

Команды воспроизведения из backend (на host предварительно source ~/.nvm/nvm.sh и nvm use --silent; внутри image закреплён тот же Node):

docker build --target development --tag wealthledger-backend:audit-verification-20260927 .
docker run --rm --network none \
  --mount type=bind,source=/Users/ksv741/Project/vibe/WealthLedger/backend/test,target=/app/test,readonly \
  --mount type=bind,source=/Users/ksv741/Project/vibe/WealthLedger/backend/scripts,target=/app/scripts,readonly \
  -e LOG_LEVEL=error wealthledger-backend:audit-verification-20260927 npm run test:e2e

Логи: /tmp/wealthledger-recheck-auth.log (macOS abort), /tmp/wealthledger-recheck-linux-build.log (Linux image build), /tmp/wealthledger-recheck-linux-e2e.log (полный PASS).

Read-only диагностика локализовала macOS crash в GC finalizer Statement/Database вложенного better-sqlite3 12.11.1, используемого Prisma adapter 7.10.0. Node 24.21 ObjectWrap destructor вызывает RemoveEnvironmentCleanupHook, который падает на env != nullptr. Связь с Jest VM context и происхождением native binary остаётся гипотезой, а не доказанной первопричиной. Ошибка application lifecycle не обнаружена: close/disconnect ожидаются. Linux PASS доказывает поведение backend в этом окружении, но не устранение native crash на macOS.

Semantic / Boy Scout review и состояние дерева

Повторный routing по staged/unstaged/new files активировал Architecture, HTTP/errors, Data, Testing, Security, Integrations и Observability. Проверены owners/callers и связанная документация; независимый review охватил auth/logging, candle coalescing/publication, forecast fallback и cleanup. После устранения найденной при review stale metadata у старого forecast fallback оставшихся semantic замечаний нет. Проверены точная денежная арифметика, ownership, transactions, generation fencing, сохранение cache/pending promises и отсутствие новых дублирующих helpers.

На момент завершения проверки, до запроса публикации, HEAD оставался 516e43203eb3fee8e6a0c7018be2fee11f1b900c, ветка codex/investment-contracts; staged files отсутствуют. Все изменения локальные, без commit/push/PR. Новые untracked files: исходный BACKEND_AUDIT.md, src/modules/investments/instruments/application/candle.service.spec.ts и src/modules/investments/instruments/application/instrument-position.spec.ts. Остальные изменения — tracked source/tests и три связанных convention-файла. Новые зависимости, scripts, migrations и разделение production-файлов не добавлены. Исходный текст до раздела «Статус исправлений» побайтово совпадает со снимком /tmp/wealthledger-backend-audit-before-fixes.md.

Подготовка публикации

После успешной полной проверки пользователь разрешил commit, PR и merge. Ветка публикации — codex/backend-audit-fixes от актуального origin/develop 5d3835e; его tree совпадает с исходным HEAD проверки, поэтому исходники и результаты gates не изменились. Pre-commit review не обнаружил посторонних файлов, секретов или незакрытых semantic findings. PR направляется в develop; merge выполняется после обязательных verify, security, commits.