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

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

674 lines
66 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Аудит 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`.