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.
66 KiB
Аудит 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-рейтинга.
Приоритеты: высокий — существенный риск безопасности или искажения данных; средний — ограниченный дефект поведения, контракта или доказанная стоимость связности; низкий — локальное упрощение. Уверенность всех перечисленных находок высокая для описанного механизма; ограничения конкретного воспроизведения указаны отдельно.
План выполнения и границы
- Прочитать действующий backend AGENTS и активированные conventions; зафиксировать текущие HEAD/status. Сохранить существующие staged/unstaged/untracked изменения.
- Работать по находкам A01–A10 и cleanup C01–C07 ниже, в порядке пакетов из таблицы. Перед каждым исправлением перечитать владельца целиком, callers, связанные tests и 1–3 релевантных примера. Если факт больше не воспроизводится, отметить это с доказательством вместо механического применения рекомендации.
- Для дефекта сначала получить падающую проверку наблюдаемого результата на самой дешёвой надёжной boundary. Для concurrency/atomicity использовать реальную изолированную SQLite; mock race не доказывает DB-инвариант.
- Исправлять у существующего владельца минимальным связанным diff. Сохранять precision, ownership, public error codes, transaction/fencing, cache isolation, limiter и выполняющиеся promises. Предложения ниже задают результат, а не обязательную форму реализации.
- После каждого пакета выполнить targeted checks; в конце — актуальные обязательные
gates по backend conventions. Перед каждой Node-командой выполнить
nvm use --silent. - Сохранить исходный снимок аудита; результаты реализации записывать в таблицу «Статус исправлений». Для каждого 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.