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
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:
@@ -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`.
|
||||
Reference in New Issue
Block a user