# Аудит 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](AGENTS.md), [порядок работы](.ai/conventions/engineering.md), [архитектура](.ai/architecture.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](src/modules/auth/application/auth.service.ts), [AuthController.logout, 95–104; refresh, 85–91](src/modules/auth/presentation/auth.controller.ts). Общий catch в logout подавляет JWT verification errors и отказ Prisma при записи `revokedAt`. Controller затем очищает cookie и возвращает 204, хотя серверная сессия осталась действительной. Сохранённая копия refresh token продолжает работать. Смежное проявление: refresh controller удаляет cookie при любой ошибке, включая временный отказ БД, без доказательства недействительности токена. **Доказательство:** inline fault injection на настоящих AuthService, JwtService и AuthController с синтетической сессией подтвердил: ```text logoutResolved=true cookieCleared=true sessionNotRevoked=true sameRefreshTokenStillUsable=true unexpectedRefreshErrorPropagated=true validCookieRemoved=true ``` Это доказательство обработки injected boundary failure, а не реального отказа БД. [Auth HTTP suite](test/app.e2e-spec.ts) проверяет обычный logout, но не этот отказ. Нарушен [контракт отзыва сессии](.ai/conventions/security-auth.md): идемпотентность не превращает несостоявшуюся запись в успешную. **Минимальное действие:** ограничить подавление ошибок 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](src/common/filters/http-exception.filter.ts). **Потребители:** [RequestLoggingMiddleware, 43–57](src/common/logging/request-logging.middleware.ts), [StructuredLogger, 47–63](src/common/logging/structured-logger.service.ts). Фильтр удаляет только первую строку `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](.ai/conventions/observability.md) и 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](src/integrations/moex/moex.client.ts). Отсутствующий запрошенный ISS block преобразуется в `[]`. Неполный cursor может завершить чтение с частичными данными; cursor с нулевым pageSize может зациклить его. **Реальная цепочка:** [CatalogEnrichmentService, 444 и 509–548](src/modules/investments/instruments/application/catalog-enrichment.service.ts) публикует `bondEvents: []`; readMany считает существующий row complete; [UpcomingEventsService, 137–197](src/modules/investments/upcoming-events/upcoming-events.service.ts) строит пустые events/totals; [ForecastRepository, 124–150](src/modules/investments/portfolio/infrastructure/forecast.repository.ts) может заменить прежний непустой 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](src/integrations/moex/moex.client.spec.ts) считает отсутствующий block пустым результатом; [upcoming-events.e2e, 439–451](test/upcoming-events.e2e-spec.ts) обещает обратное, но подменяет весь клиент готовым 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](src/modules/investments/tbank-sync/tbank-sync.service.ts), [TbankSyncRepository.read, 70–80](src/modules/investments/tbank-sync/tbank-sync.repository.ts). [Operations repository, 441–445](src/modules/investments/operations/infrastructure/brokerage-operations.repository.ts) помечает объединяемую группу `superseded` и сохраняет successorId. Владелец операций умеет разрешать цепочку в observe, строки 564–590, но screen читает прямого child. **Сценарий:** две активные screen-группы на части января; третья объединяет интервалы. Screen вытесненной группы сообщает succeeded, хотя successor ещё running; его последующая ошибка также теряется. **Воспроизведение настоящего status с mocked persistence:** ```text screenState=succeeded resource.availability=missing resource.sync.state=running actualOperationState=running ``` SQLite bridge покрыт [operations-sync e2e, 455–474](test/tbank-operations-sync.e2e-spec.ts), но существующая проверка наблюдает 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](src/modules/investments/instruments/application/candle.service.ts), [CandleRepository.reserve, 76–82](src/modules/investments/instruments/infrastructure/candle.repository.ts). Публичный caller: [TbankSyncService, 288–306](src/modules/investments/tbank-sync/tbank-sync.service.ts) вызывает 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](src/modules/investments/portfolio/infrastructure/forecast.repository.ts). **Потребители:** [StockIncomeService, 67–87 и 161–163](src/modules/investments/instruments/stocks/application/stock-income.service.ts), [BondIncomeService, 180–182](src/modules/investments/instruments/bonds/application/bond-income.service.ts). Истечение 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](src/modules/investments/operations/infrastructure/brokerage-operation.mapper.ts) игнорирует nanos; [repository, 653–657 и publish](src/modules/investments/operations/infrastructure/brokerage-operations.repository.ts) учитывает миллисекунды. 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](src/main.ts), [HttpExceptionFilter, 44–48 и 70–78](src/common/filters/http-exception.filter.ts). ValidationPipe выдаёт message:string[], filter приводит его к ValidationError[] и ищет constraints. children вложенных ошибок также не обходятся. **Configured HTTP reproduction:** invalid email и отсутствующий password: ```json {"status":400,"code":"VALIDATION_ERROR","errors":[]} ``` Ожидаемый [HTTP contract](.ai/conventions/http-api.md) требует 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](src/modules/investments/investment-sync-plan.ts) → [operations.api](src/modules/investments/operations/operations.api.ts) → operations services → investment-sync-plan. Алгоритму нужны interval rules, но barrel загружает services, TbankClient и Prisma. [Planner spec, строка 1](src/modules/investments/investment-sync-plan.spec.ts) уже вынужден 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](src/integrations/tbank/tbank-access.service.ts). 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](src/modules/investments/portfolio/application/portfolio.service.ts): локальный decimalFromString повторяет common parser без validation; callers position/value | Импортировать существующий common owner; удалить дубликат. Exact-money tests и typecheck. Текущая ошибка сумм не доказана; проверить допустимые входы. | | C02 | [tbank.api](src/integrations/tbank/tbank.api.ts): TbankSyncAttempt импортируют напрямую семь внешних файлов sync/instruments; enrichment также обходит API для market types | Экспортировать используемые типы, перенаправить imports через API. TbankCoupon уже экспортируется. Typecheck и поиск обходов; runtime-риск минимален. | | C03 | [auth.service, 190–192](src/modules/auth/application/auth.service.ts): normalizeEmail возвращает аргумент; register/login вызывают его, реальная нормализация в CredentialsDto | Удалить no-op, сохранить DTO transform. Existing HTTP case/whitespace tests; дополнительную нормализацию без необходимости не вводить. | | C04 | [bank-card.calculations, 56–76](src/modules/banking/bank-cards/domain/bank-card.calculations.ts): string period используется только tests; production summary передаёт from/to | Сузить API до рабочего диапазона; перевести полезные unit cases. Проверить границы месяца/range. | | C05 | [instrument-market, 39–51](src/modules/investments/instruments/application/instrument-market.ts): optional без consumers; [stock-position wrapper](src/modules/investments/instruments/stocks/application/stock-position.ts) нужен только своему spec | Удалить optional, перенаправить полезные wrapper tests на instrumentPosition. Сохранить fractional/mixed/offset holdings cases. | | C06 | [portfolio.api, 2](src/modules/investments/portfolio/portfolio.api.ts): totalsByCurrency не имеет внешнего production consumer; внутренний использует mapper напрямую | Удалить только лишний export, mapper сохранить; typecheck. | | C07 | [auth.controller.spec, 9–20](src/modules/auth/presentation/auth.controller.spec.ts): проверяет 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](src/modules/investments/operations/infrastructure/brokerage-operations.repository.ts) | 924 | Оставить: overlap groups, staging, recovery и publication держат общий transactional invariant | | [catalog-enrichment.service](src/modules/investments/instruments/application/catalog-enrichment.service.ts) | 665 | Упростить локально: protocol boundary/imports; сохранить producers, batching, completions вместе | | [brokerage-operations.service](src/modules/investments/operations/application/brokerage-operations.service.ts) | 636 | Оставить: ledger/income имеют различную completeness; generic builder добавит скрытые режимы | | [tbank-sync.service](src/modules/investments/tbank-sync/tbank-sync.service.ts) | 595 | Упростить: разрешение successors вернуть operations owner, generic job framework не нужен | | [tbank.client](src/integrations/tbank/tbank.client.ts) | 560 | Оставить: records/RPC methods и единый limiter/retry owner | | [portfolio.service](src/modules/investments/portfolio/application/portfolio.service.ts) | 522 | Упростить: удалить duplicate parser, scheduling state не разрывать | | [candle.service](src/modules/investments/instruments/application/candle.service.ts) | 481 | Оставить: исправить force join, сохранить queue/publication invariant | | [catalog-discovery.repository](src/modules/investments/instruments/infrastructure/catalog-discovery.repository.ts) | 480 | Оставить: identity/aliases/source publication и CAS связны | | [catalog-discovery.service](src/modules/investments/instruments/application/catalog-discovery.service.ts) | 390 | Оставить: search/detail и producer lifecycle имеют одного owner | | [bank-operations.service](src/modules/banking/bank-cards/application/bank-operations.service.ts) | 343 | Оставить: разрыв validation/refund/transfer writes повышает риск | | [tbank-access.service](src/integrations/tbank/tbank-access.service.ts) | 331 | Упростить удалением legacy flow | | [investment-sync-plan](src/modules/investments/investment-sync-plan.ts) | 327 | Упростить зависимость: убрать service barrel из алгоритма | | [instrument-metadata.repository](src/integrations/tbank/instrument-metadata.repository.ts) | 320 | Оставить: identity, batching и publication согласованы | | [income-analytics.service](src/modules/investments/income/income-analytics.service.ts) | 318 | Оставить: связные projections, выгода дополнительных моделей не доказана | | [candle.repository](src/modules/investments/instruments/infrastructure/candle.repository.ts) | 306 | Оставить: reconciliation, coverage, fencing и stage cleanup | | [upcoming-events.service](src/modules/investments/upcoming-events/upcoming-events.service.ts) | 297 | Оставить: один сценарий, основной дефект у MOEX boundary | | [brokerage-operations-sync.service](src/modules/investments/operations/application/brokerage-operations-sync.service.ts) | 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): ```bash 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`.