fix: harden auth and investment synchronization
Backend security / security (push) Successful in 8s
Backend verify / verify (push) Successful in 11m9s
Conventional commits / commits (pull_request) Successful in 28s
Backend security / security (pull_request) Successful in 7s
Backend verify / verify (pull_request) Successful in 10m38s

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.
This commit is contained in:
2026-09-27 14:09:05 +03:00
parent 5d3835e796
commit 4796d9d3f0
55 changed files with 1954 additions and 569 deletions
+673
View File
@@ -0,0 +1,673 @@
# Аудит 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`.