docs: archive technical audit 2026-10-11 in develop (#36)
Conventional commits / commits (pull_request) Successful in 13s

This commit is contained in:
2026-10-17 19:30:38 +03:00
parent 0bdc409ed0
commit 2b63bde934
20 changed files with 10749 additions and 0 deletions
@@ -0,0 +1,765 @@
# WealthLedger — Complexity, Redundancy & Engineering Overhead Audit
Дата: 11 октября 2026 года.
Метод: `ponytail-audit`, статический анализ owners, consumers, проверок и последствий удаления.
## Основание и ограничения
Изучены root, `frontend`, `backend`, `docs`, `specs` и обнаруженный отдельный репозиторий `deploy`. Первый отчёт `technical-audit-2026-10-11.md` прочитан. Его функциональные дефекты и уже перечисленные simplification opportunities не включены в новые findings.
Основные checkout совпадают с первым аудитом:
| Репозиторий | Commit |
|---|---|
| frontend | `c882120` |
| backend | `0ad84ff` |
| docs | `e6fd4f6` |
| specs | `fcdc805` |
Выполнены чтение, поиск, Git inspection и подсчёты через `rg`. Единственная дополнительная проверка инструментом:
```sh
source "$HOME/.nvm/nvm.sh"
nvm use --silent
node node_modules/openapi-typescript/bin/cli.js \
src/shared/api/spec/openapi.json \
-o src/shared/api/generated/schema.d.ts \
--check
```
**Результат: exit 0, установленный `openapi-typescript 7.13.0`.** Проверка не записывает output: сравнение и выход происходят до `mkdirSync`/`writeFileSync` — `frontend/node_modules/openapi-typescript/bin/cli.js:125–134,280–291`.
Build, полные verify, тестовые наборы, установки, Docker и deployment не запускались. Финальные `git status --short` всех шести обследованных рабочих деревьев не показали изменений.
Время, стоимость CI, bundle size и расход токенов не измерялись.
# A. Executive Summary
## Главные источники избыточности
1. **Обязательный полный локальный workflow для любого code change.**
Frontend и backend требуют `npm ci` и полный `verify` перед завершением, независимо от изменённой области. Дополнительные conventions отдельно направляют к typecheck/lint/risk checks. Это расширяет небольшую задачу до полного bootstrap и проверки всего приложения.
Evidence: `frontend/.ai/conventions/engineering.md:58–66`, `backend/.ai/conventions/engineering.md:58–66`, `frontend/.ai/conventions/ui.md:165–170`.
2. **Повторная работа внутри одной границы проверки.**
- Frontend `verify` вызывает TypeScript дважды.
- Backend отдельно typecheck’ит application, хотя test typecheck включает те же application sources.
- Backend CI собирает приложение в `verify`, затем повторно в `test:e2e`.
- SQLite runner regression повторяет весь functional suite пять раз ради свойств самого runner.
Evidence: `frontend/package.json:26`, `backend/package.json:22–34`, `backend/tsconfig.typecheck.test.json:7–8`, `backend/.gitea/workflows/verify.yml:84–89`, `backend/test/sqlite-regression.cjs:20–22,77–91`.
3. **Самописная проверка, уже реализованная установленным инструментом.**
`check-openapi.mjs` — 45 строк temp-directory/subprocess/cleanup orchestration для возможности `openapi-typescript --check`. Это наиболее уверенный tooling-cut с сохранением той же гарантии.
4. **Production API, поддерживаемые тестовыми consumers.**
- `useAuthenticatedQueries`: production callers отсутствуют.
- `InstrumentMetadataService.read`: используется metadata persistence suite; production использует `request`/`savedMany`.
Evidence: `frontend/src/shared/api/authenticated-query.ts:15–32,61`, `frontend/src/shared/api/authenticated-query.test.tsx:9–23`, `backend/src/integrations/tbank/instrument-metadata.service.ts:41–63`, `backend/test/tbank-metadata-persistence.e2e-spec.ts:87–382`.
5. **Тесты иногда обходят дешёвую существующую boundary.**
Чистые преобразования проверяются через fake Prisma transactions или background orchestration; page tests поддерживают fake query-result shapes параллельно существующей MSW boundary.
6. **Противоречивая процессная authority.**
Локальные инструкции задают opt-in OpenSpec и запрещают audit artifacts в docs. Действующие process specs требуют registry, snapshots и canonical workflow для audit-derived работы. Это создаёт дополнительные artifacts и решения о том, какую инструкцию исполнять.
Evidence: `specs/AGENTS.md:3–6`, `docs/AGENTS.md:12–14`, `specs/openspec/specs/project-foundation/spec.md:9–19`, `specs/openspec/specs/audit-remediation-workflow/spec.md:9–49`.
## Общая оценка
**Масштабный архитектурный рефакторинг не нужен.** Не подтверждена потребность вводить новые interfaces, generic workers, fixture frameworks, архитектурные validators или quality tools.
Самые полезные изменения сокращают **число путей, запусков и обязательных artifacts**, сохраняя существующие owners и реальные behavioral proofs.
# B. Complexity Inventory
## B1. Количественные показатели
| Метрика | Результат | Метод и ограничения |
|---|---:|---|
| Package script entries | **67** | Root 3; frontend 25; backend 27; docs 5; specs 5; UI/icons workspace builds 2. Это entrypoints и aliases, не 67 самописных программ |
| Custom script/adapter/harness/plugin files | **21** | Frontend 6; backend 10; docs 2; specs 1; deploy 2. Git hooks и декларативные configs отдельно |
| Custom Git hooks | **3** | Frontend pre-commit/commit-msg; specs commit-msg. Generated Husky files и Git samples исключены |
| Custom release hook declarations | **4** | Frontend/backend/docs/specs `after:git:init`; root такого helper не имеет |
| Frontend `verify` command stages | **12** | Разделение команды на последовательные стадии; вложенные workspace builds отдельно не разворачиваются |
| Backend `verify` command stages | **6** | Lint, lint-config tests, два typecheck, unit coverage, build |
| Operational verification families | **29** | Группы в разделе C; не количество отдельных ESLint rules или test assertions |
| Участки повторной/малоинформативной проверки | **7** | Frontend TS; backend TS subset; backend rebuild; full-suite runner repetition; preset-parity meta-tests; docs/specs clean-checkout diff checks |
| Test files frontend/backend | **63 / 59** | Backend: 39 colocated specs + 20 integration suites. Browser files входят в frontend 63 |
| Test files вместе с deploy harness | **123** | 122 TS/TSX test files + `deploy/tests/deploy.sh`; CJS инфраструктурные проверки здесь отдельно |
| Production TS/TSX | **34 465 строк** | Frontend 16 967; backend 17 498. Generated, stories, fixtures/test support исключены |
| Test TS/TSX | **23 679 строк** | Frontend 11 537; backend 12 142 |
| Test/production ratio | **≈ 0,69** | Физические строки, включая comments/blank lines. Не критерий качества архитектуры |
| Frontend stories / support | **18 файлов / 6 файлов** | 1 855 / 263 строки соответственно; не включены в test LOC |
| Классифицированные factory/polling definitions | **не менее 18** | Frontend 3 общие factories; backend 4 service factories, 4 deferred helpers, 7 polling helpers. Локальные render/helpers не полностью включены |
| Frontend module mocks | **4 в 3 файлах** | `vi.mock`; не учитывает spies и MSW handlers |
| Backend module mocks | **18 в 12 unit files** | `jest.mock`; сам count не является finding |
| Snapshot assertions | **0 найдено** | Поиск `toMatchSnapshot`, `toMatchInlineSnapshot`, `toMatchFileSnapshot`, `__snapshots__` в обследованных test sources |
| Backend runtime SHA/HMAC mechanisms | **8** | 7 `createHash` emission sites + один signed-cursor mechanism с encode/decode HMAC. Password bcrypt отдельно |
| Gitleaks archive checksum steps | **4** | Одна download-integrity policy в четырёх независимых repos |
| OCI digest publication paths | **2** | Frontend/backend image workflows |
| Подтверждённые точки удаления | **18** | Перечень в G; считаются удаляемые элементы/стадии, не файлы |
| Обследованные single-production-consumer abstractions | **6 примеров** | RouteTransition, InstrumentSearch, useInstrumentSearch, metadata repository `work`, `queueSnapshot`, `TBANK_ACCESS`. Это выборка, не полный census |
LOC и количества используются для описания масштаба. Они **не доказывают** избыточность.
## B2. Все custom scripts и исполняемые adapters
| Файл | Назначение и consumers; автоматизация | Пересечение / последствия удаления / решение |
|---|---|---|
| `frontend/scripts/check-openapi.mjs:1–45` | Freshness generated types; вызывается `openapi:check` и `verify` | Полностью покрывается native `--check`. **Удалить реализацию, сохранить проверку** |
| `frontend/scripts/check-release.cjs:1–24` | Manifest version ↔ reachable stable tag; release-it hook | Не эквивалентен `requireCommits`. **KEEP**: предотвращает повторную подготовку версии |
| `frontend/scripts/test-eslint.mjs:23–395` | Config/rule/fixer proofs; автоматически из `test:lint`/`verify` | Смешивает проектные checks и exhaustive preset parity. **SIMPLIFY**, сохранять probes реальных exceptions/custom fixers |
| `frontend/scripts/test-pre-commit.mjs:17–79` | Staged-index scanning, redaction, fail-closed; security CI и ручная команда | Full-history scan не доказывает index behavior. **KEEP** |
| `frontend/scripts/check-image-publication.cjs:15–88` | Модель retry после post-push failure, exact digest/revision/smoke | Автоматических callers не найдено. Не считать dead только из-за этого: самостоятельный workflow proof. **KEEP как targeted check**, не обязательный gate каждой правки |
| `frontend/eslint-rules.mjs:1–178` | Два custom ESLint rules; загружается lint config | Часть правил — formatting/обязательная форма кода. **SIMPLIFY**, не заменять новым analyzer |
| `backend/scripts/check-release.cjs:1–11` | Version/tag guard; release-it hook | Штатный release guard не покрывает эту policy. **KEEP** |
| `backend/scripts/generate-openapi.cjs:1–41` | Swagger generation из built app без deployment config | Package command и contract integration suite. TypeScript сам контракт не генерирует. **KEEP** |
| `backend/test/sqlite.cjs:1–95` | Per-suite temporary DB, migrations, subprocesses, signal cleanup | Все SQLite entrypoints. Обычный Jest не гарантирует DB isolation. **KEEP** |
| `backend/test/sqlite-regression.cjs:1–96` | Runner isolation/failure/signal proof; ручной README entrypoint | Пять полных успешных suite runs избыточны для runner properties. **SIMPLIFY область прогонов** |
| `backend/test/e2e-env.cjs:1–17` | Отказ от не-изолированной DB, synthetic credentials | Jest setup и crash fixture. **KEEP**: защита данных developer |
| `backend/test/fixtures/tbank-sync-crash.cjs:1–86` | Управляемый kill процесса на durable boundaries | Consumer — operations persistence suite. In-process mocks не заменяют crash proof. **KEEP** |
| `backend/test/eslint-config.cjs:9–146` | Profile scope, generated ignores, import exceptions | Автоматически `test:lint`. **SIMPLIFY**: project scope/exception checks полезны; поведение каждой library rule не нужно доказывать заново |
| `backend/test/runtime-check.cjs:1–8` | Проверяет строку start command, наличие dist и require generated client | Runtime callers не найдены; config test использует имя как fixture path. Real image smoke сильнее. **DELETE** |
| `backend/test/container-smoke.cjs:1–77` | Native modules/assets, migration, non-root/read-only runtime, auth/restart | Image publication и manual package entrypoint. Host build это не доказывает. **KEEP** |
| `backend/test/bank-cards-durability.cjs:1–219` | Historical build → migration → restart → rollback → restore | Risk-triggered manual entrypoint. Fresh DB suites не заменяют historical compatibility. **KEEP** |
| `docs/scripts/check-release.cjs:1–10` | Clean status + version/tag guard | Release hook. Porcelain видит untracked files; native clean guard не полностью эквивалентен. **KEEP** |
| `docs/.markdownlint-rule-relative-links.cjs:1` | Adapter `.default` установленного link rule | Оба markdownlint запуска. Не собственный validator. **KEEP** |
| `specs/scripts/check-release.cjs:1–10` | Clean status + version/tag guard | Release hook. **KEEP** |
| `deploy/deploy.sh:1–192` | Tagged snapshot, preflight, lock/pending, backup, migration/readiness | Ручной запуск, затем self-handoff к tagged script. **KEEP** |
| `deploy/tests/deploy.sh:1–251` | Real Git/files/locks + Docker double; optional bind-restore proof | Ручной entrypoint. **KEEP**: orchestration и bind semantics не доказываются application tests |
**Не установлена необходимость централизовать десятистрочные release helpers между независимыми repos.** Общий tooling package добавит dependency boundary и release ownership ради небольшого копирования.
## B3. Package scripts: роль и оправданность
Определения: `package.json:8–12`, `frontend/package.json:9–34`, `backend/package.json:11–38`, `docs/package.json:8–14`, `specs/package.json:8–14`.
| Группа entrypoints | Consumers и оценка |
|---|---|
| Root `release:preview`, `release:prepare`, `commitlint` | Release/operator и commits CI. Штатные CLI wrappers; оставить |
| Frontend `dev`, `dev:catalog`, `build:catalog`, `preview:catalog`, `preview` | Developer/catalog lifecycle. Разные outputs; не дублируют одну проверку |
| Frontend `build`, `build:packages`, `typecheck` | Application/workspace compilation. Packages нужны из-за dist-based exports; проблема — частота повторного запуска |
| Frontend `lint`, `lint:eslint`, `lint:fix` | Alias, check, explicit edit. `lint` alias сам по себе не требует удаления |
| Frontend `test`, `test:run`, `test:coverage`, `test:lint` | Watch, one-shot, coverage, tooling tests. Различимые режимы; уменьшать обязательность, не механически удалять entrypoints |
| Frontend `openapi:generate`, `openapi:check` | Explicit generation и read-only freshness. Оставить оба; check реализовать native CLI |
| Frontend `knip`, `deps`, `deps:storybook` | Dead usage, production boundaries, transitive catalog isolation — разные свойства |
| Frontend `verify`, `prepare`, release pair, `commitlint` | Aggregate gate, Husky bootstrap, release и commit policy. `verify` упростить |
| Backend `start`, `start:dev`, `start:debug`, `start:prod` | Различные runtime/debug modes; production также задаётся Docker |
| Backend `build`, `openapi:generate` | Prisma/Nest output и public artifact; оставить standalone completeness |
| Backend `format`, `lint`, `lint:check`, `lint:fix` | Aliases и явный check/edit. Небольшая стоимость, нет причины строить новые wrappers |
| Backend `typecheck:app`, `typecheck:test` | Оставить targeted entrypoints; в полном gate достаточно test superset |
| Backend `test`, `test:watch`, `test:cov`, `test:debug` | Runner modes; текущая необходимость debug не опровергнута отсутствием CI caller |
| Backend `test:e2e`, `test:sqlite`, `test:performance`, `test:openapi-contract` | Full functional, auth/cards subset, отдельный benchmark, отдельный contract proof. Сокращать repeated build при orchestration, не смешивать наборы |
| Backend `test:container`, `test:durability` | Image/historical-data boundaries; risk-triggered |
| Backend `verify`, `test:lint`, release pair, `commitlint` | Aggregate/tooling/release policy; упростить aggregate |
| Docs `lint`, `verify`, release pair, `commitlint` | Содержательный Markdown gate. Два lint запуска обрабатывают разные наборы с разными changelog rules |
| Specs `verify`, release pair, `commitlint`, `prepare` | `verify` полезен только как local diff check в нынешнем виде; `prepare` незаметно меняет hooksPath |
| UI/icons `build` | Vite bundle + TypeScript declarations для двух реально используемых packages |
## B4. Hash, SHA, freshness и integrity
| Группа / механизм | Реальный failure mode | Альтернативные защиты и риск удаления | Решение |
|---|---|---|---|
| **Business idempotency:** bank intent fingerprint | Повтор запроса после lost response; тот же ID с другим содержимым | Unique ID предотвращает вторую запись, но не различает повтор и конфликтующий intent. `backend/src/modules/banking/bank-cards/application/bank-card-input.ts:74–81`, `bank-cards.service.ts:76–81`, `bank-operations.service.ts:216–223` | **KEEP** |
| **Security:** refresh-token SHA и password bcrypt | Чтение БД не должно раскрывать reusable token/password; rotation CAS | CAS не заменяет безопасное хранение. `backend/src/modules/auth/application/auth.service.ts:52,110–133,180,204–205` | **KEEP** |
| **Cursor integrity:** HMAC + revision/filter binding | Подмена cursor или применение к изменённому dataset/filter | Ownership проверяет доступ, но не подписанность и согласованность cursor. `backend/src/modules/investments/operations/application/brokerage-operations.service.ts:347–388`, `backend/src/modules/investments/operations/infrastructure/brokerage-operations.repository.ts:128–143` | **KEEP** |
| **Work identity:** hash canonical operations intervals | Разные диапазоны не должны случайно объединяться; одинаковые intents coalesce | Возможна строковая canonical identity, но существующий механизм короткий и связан с durable keys. `backend/src/modules/investments/operations/infrastructure/brokerage-operations.repository.ts:254–269` | **KEEP**, нет доказанного выигрыша от замены |
| **Resource identity/revisions:** accounts, candles, operations | Согласованность metadata и identity текущей read-model | Это public/runtime metadata, не development gate. `backend/src/modules/investments/brokerage-accounts/brokerage-accounts.service.ts:42–54`, `backend/src/modules/investments/instruments/infrastructure/candle.repository.ts:41–52`, `backend/src/modules/investments/operations/application/brokerage-operations.service.ts:238–239` | **KEEP до установленного contract-safe cut** |
| **Freshness metadata/TTL** | Отсутствие данных, stale и confirmed completeness нельзя смешивать | Coverage/revisions защищают другие свойства. `backend/src/modules/investments/investment-resource.mapper.ts:31–64,78–88`, `backend/src/integrations/tbank/tbank-source.policy.ts:1–21` | **KEEP**; удалить только неиспользуемый `candleTail` key |
| **Data integrity:** SQLite integrity/FK checks | Повреждение/невалидные связи после migration/restore | Unit mocks и successful startup недостаточны. `backend/test/bank-cards-durability.cjs:109–118,190–205`, `backend/test/container-smoke.cjs:58–64` | **KEEP на соответствующих boundaries** |
| **Generated artifacts:** local OpenAPI → types | Committed types расходятся с предоставленным snapshot | TS не сравнивает types с JSON. Native `--check` покрывает custom script | **SIMPLIFY implementation, KEEP guarantee** |
| **Runtime OpenAPI generator parity** | Generator отличается от configured app schema | Frontend freshness этого не доказывает. `backend/test/openapi-contract.e2e-spec.ts:108–156` | **KEEP** |
| **Download integrity:** Gitleaks tarball SHA-256 | Исполнение не тех bytes скачанного binary | HTTPS/version URL не эквивалентны pinned checksum. Frontend/backend verify `52–58`, docs/specs security `24–30` | **KEEP** |
| **Image content identity:** OCI digest/revision | Smoke проверяет другой image, tag изменён, retry скрывает несовпадение | Host tests не доказывают bytes опубликованного image. `frontend/.gitea/workflows/image.yml:46–62`, `backend/.gitea/workflows/image.yml:42–55` | **KEEP** |
| **Process identity:** release manifest/tag, deployment Git commit | Повторная подготовка версии; deployment не из выбранного tagged snapshot | Native release-it покрывает лишь часть; рабочий checkout не immutable input. `deploy/deploy.sh:26–39` | **KEEP** |
| **Process visual SHA tables** | Approved export изменён отдельно от approval input | Git commit binding может покрывать Git-only delivery; отдельно передаваемые exports — другой риск. `specs/openspec/schemas/wealth-ledger-sdd-v2/schema.yaml:322–330`, `specs/openspec/schemas/wealth-ledger-sdd-v2/templates/design.md:38–61` | **Условный SIMPLIFY**, не безусловное удаление |
| **npm lockfile integrity** | Package bytes не соответствуют lockfile | Стандартный механизм npm, не custom framework | **KEEP** |
**Самописных self-hash/fingerprint validators для planning Markdown или hash-gated reuse CI результатов не обнаружено.** Требования в prose не равны действующей автоматизации.
# C. Redundancy Matrix
## Фактическая цепочка
### Frontend
```text
Developer / AI:
npm ci
targeted typecheck/lint/tests
full verify
pre-commit:
staged Gitleaks → lint-staged ESLint → deps → build:packages
CI:
commitlint → history Gitleaks / audit / hook proof
→ full verify
full verify:
packages → OpenAPI freshness → TS → ESLint → lint-config tests
→ Vitest coverage → Knip → production graph → story graph
→ TS again → application build → Storybook build
```
Источники: `frontend/.ai/conventions/engineering.md:58–66`, `frontend/.husky/pre-commit:1–3`, `frontend/.gitea/workflows/verify.yml:17–100`, `frontend/package.json:26`.
### Backend
```text
Developer / AI:
npm ci → targeted checks → full verify → risk-specific suites
CI:
commitlint → history Gitleaks / audits
→ Prisma generate → verify
→ test:e2e → build again → isolated SQLite suites
```
Источники: `backend/.ai/conventions/engineering.md:58–66`, `backend/.ai/conventions/testing.md:38–57`, `backend/.gitea/workflows/verify.yml:84–89`, `backend/package.json:28–34`.
### Docs / specs / deploy
- Docs: Markdown/link lint + local diff check; CI content/security/commit checks.
- Specs: local `git diff --check`; CI устанавливает Node dependencies и повторяет эту команду на чистом checkout. Native OpenSpec validation документирована отдельно, но не вызывается этим job.
- Deploy: отдельный ручной lifecycle и shell harness, вне application CI.
## Матрица
Частота ниже — **сценарная**, не измеренная: один local final verify, один commit и один CI event. Для checks outside этой цепочки указан их фактический trigger.
| Mechanism | Purpose / consumers | Overlap и частота | Cost | Recommendation | Confidence |
|---|---|---|---|---|---|
| 1. TypeScript | Source/tooling diagnostics; local/CI | FE: 4 calls в двух verify. BE: по 2 app/test calls; test включает app | Повтор compiler invocation; incremental может удешевлять | Убрать FE второй call; BE app из aggregate | High |
| 2. ESLint | Syntax, async, security/style policies | FE: 2 full + staged hook; scopes различаются | Typed lint всего проекта | KEEP full CI; local scoped; staged сохранить | High |
| 3. Lint-config tests | Scope/exceptions/custom fixers | 2 full executions; часть заново доказывает presets | Synthetic lintText/config calculations | Сократить generic parity; project probes по tooling trigger | High |
| 4. Knip | Dead exports/dependencies/entries | 2 executions; TS не эквивалентен | Whole-project analysis | KEEP CI; local по соответствующему изменению | High |
| 5. Production dependency graph | FSD/package/test boundaries, cycles | FE: local + hook + CI = 3 | Hook дополнительно builds packages | Перенести из каждого commit в scoped local/CI | High |
| 6. Story reachable graph | Catalog не достигает application data | 2 executions; base config включён, но graph semantics отличаются | Второй graph traversal | KEEP: не объединять вслепую | High |
| 7. Generated OpenAPI freshness | Snapshot ↔ types bytes | 2 executions; TS не заменяет | Custom temp/subprocess machinery | Native `--check` | High |
| 8. Unit/RTL/MSW tests | Own behavior, transport/UI results | Обычно 2 full runs; local/CI разные trust boundaries | Whole suite local | Targeted local, full CI | High |
| 9. Browser layout | Реальная геометрия narrow/wide | Два viewport instances на run | Chromium/runtime setup | KEEP для geometry changes и CI | High |
| 10. Coverage | FE floor; BE report | По одному сбору на full test run | Instrumentation/reporting | CI report; local targeted без coverage | High |
| 11. Application build | Emission/bundling/assets | BE CI два builds подряд; Docker — отдельная boundary | Prisma/Nest/Vite work | Убрать same-job rebuild; Docker оставить | High |
| 12. Storybook build | Catalog bundle | Local/CI по одному; app build не заменяет | Отдельный bundle | CI/catalog-risk scope | High |
| 13. SQLite HTTP/integration | DB constraints, guards, contract | CI 19 functional suites; standalone 20-я performance отдельно | Per-suite migration + Jest | KEEP independent DB isolation | High |
| 14. Runner regression | Isolation, cleanup, signals | Пять успешных `--all` + failure/signal paths вручную | 95 successful suite executions | Representative two-suite runs | High facts / Medium cut |
| 15. Historical durability | Migration/rollback/restore | Schema/data-risk trigger | Separate historical build/app runs | KEEP risk-triggered | High |
| 16. Image smoke | Actual image runtime/assets/persistence | Publication/manual; host checks не эквивалентны | Docker lifecycle | KEEP | High |
| 17. Publication model test | Retry/digest failure branches | Только explicit invocation найден | Stateful command double, YAML block extraction | KEEP focused, не every-change gate | Medium |
| 18. Staged secret scan | Не допустить secret в commit | Каждый FE commit | Index scan | KEEP; history scan позднее | High |
| 19. History secret scan | Secrets во всей Git history | PR/push/schedule | Full-history scan/download | KEEP; оптимизировать только по измерениям | High |
| 20. Hook security proof | Index vs worktree, redaction/fail-closed | FE security CI | Temp Git repo/real scanner | KEEP; разные гарантии с №19 | High |
| 21. Dependency audit | Known advisories | CI; BE runtime/dev thresholds различаются | Network/advisory processing | KEEP; overlap не доказан одинаковыми properties | High |
| 22. Commitlint | PR title и retained commits | CI 2 invocations; local commit hook дополнительно | App repos устанавливают полный manifest для job | KEEP policy; не добавлять custom parser | High |
| 23. Release preparation guard | Branch/tree/version lifecycle | Explicit release prepare | Несколько Git queries | KEEP; preview не считать proof custom hook | High |
| 24. Image version/digest binding | Tag/package/lock/revision/published bytes | Release-tag workflow | Metadata/registry checks | KEEP release boundary | High |
| 25. Scanner checksum | Downloaded scanner bytes | 4 repo-local security paths | Один hash command | KEEP | High |
| 26. Markdown/link checks | Docs syntax/local targets | 2 disjoint lint commands/run | 50 Markdown files | KEEP; не второй custom link checker | High |
| 27. `git diff --check` | Working diff whitespace/conflict markers | Local useful; docs/specs CI clean checkout малоинформативен | Specs CI ещё устанавливает deps | KEEP local; удалить пустой CI work | High |
| 28. Native OpenSpec validation | Schema/change/spec structure | Manual; v2 instructions требуют после 7 stages | Repeated CLI/context work | Только explicitly selected SDD; proportional scope | High facts / Medium policy |
| 29. Deploy preflight/lifecycle tests | Lock, backup, ordering, failure cleanup | Manual deploy/harness | Docker doubles/real locks/files | KEEP | High |
### Важные разграничения
- **Local и CI не объявлены дубликатами только потому, что запускают одинаковую команду.** Сокращается обязательный local scope, CI остаётся независимой проверкой.
- Production и Storybook dependency-cruiser отличаются `tsPreCompilationDeps: true/false`: `frontend/dependency-cruiser.cjs:78–87`, `frontend/.storybook/dependency-cruiser.cjs:22`. Один простой запуск не доказан эквивалентным двум.
- OpenAPI generator parity и consumer type freshness проверяют разные отношения.
- Coverage, browser layout и JSDOM behavior не взаимозаменяемы.
# D. SOLID / DRY / KISS / YAGNI Findings
## SRP
### D1. Test-only orchestration внутри production metadata service
`InstrumentMetadataService.read` совмещает access, saved lookup, demand start и work lookup, но production callers используют отдельные existing methods.
- Owner: `backend/src/integrations/tbank/instrument-metadata.service.ts:41–63`.
- Production примеры: `backend/src/modules/investments/portfolio/application/portfolio.service.ts:122–123,275–283,309–315`, `backend/src/modules/investments/operations/application/brokerage-operations.service.ts:213–216`, `backend/src/modules/investments/upcoming-events/upcoming-events.service.ts:200–206`.
- Test consumer: `backend/test/tbank-metadata-persistence.e2e-spec.ts:87–382`, 38 call sites.
**Вывод:** убрать совмещённый API после перевода тестов на production-used boundary. Не удалять persistence suite и её concurrency/restart proofs.
### D2. Portfolio имеет ложную DI-зависимость и два имени одной операции
- Неиспользуемый `TbankMembershipService` constructor dependency: `backend/src/modules/investments/portfolio/application/portfolio.service.ts:9–18,45–53`.
- Public `request` → private `queueSnapshot` без дополнительной policy: тот же файл, `234–243`.
- Тест обходит public operation и читает private repository: `backend/test/tbank-value-history.e2e-spec.ts:99–111,247–256,307–313`.
**Вывод:** один public `request`, без unused DI; тест получает существующий repository provider напрямую. Не дробить queue/publication lifecycle.
## OCP / YAGNI
### D3. Plural authenticated-query boundary существует ради теста
`frontend/src/shared/api/authenticated-query.ts:15–32,61`; единственный consumer — `frontend/src/shared/api/authenticated-query.test.tsx:9–23`.
Тестовая plural branch содержит один query и берёт `[0]`. Приложение потребляет singular hook.
**Вывод:** удалить plural implementation и вариант test matrix. Singular auth gating оставить.
### D4. InstrumentSearch сохраняет stock-вариант без product consumer
- Union contract: `frontend/src/features/instrument-search/ui/InstrumentSearch/InstrumentSearch.view.tsx:18–31`.
- Единственный production renderer consumer — BondsPage: `frontend/src/pages/bonds/ui/BondsPage/BondsPage.view.tsx:26–113`.
- Stock-конфигурация используется тестом: `frontend/src/features/instrument-search/ui/InstrumentSearch/InstrumentSearch.test.tsx:21–29`.
**Вывод:** специализировать current renderer под bond, сохранив debounce, URL/search states и presentation cases. Не объединять bond search со stock screener.
### D5. Малые невостребованные элементы поверхности
| Элемент | Evidence | Минимальное действие |
|---|---|---|
| `history.retry` | `frontend/src/entities/broker-operation/model/useBrokerageOperationHistory.ts:52–61,76`; все три VMs его не читают | Удалить action; automatic invalid-cursor recovery `37–50` оставить |
| `isFetching` input | `frontend/src/shared/lib/initial-availability.ts:10–16,18–46`; не читается | Удалить из type, пяти callers и fixtures |
| `membershipWork()` | `backend/src/integrations/tbank/tbank-access.service.ts:90–97`; callers не найдены | Удалить метод, не одноимённое repository result field |
| `candleTail` TTL key | `backend/src/integrations/tbank/tbank-source.policy.ts:3–8`; production references отсутствуют | Удалить key; настоящую candle coverage/tail policy оставить |
| `HourlySyncSectionDto` | `backend/src/modules/investments/tbank-sync/tbank-sync.dto.ts:232–250`; current problems DTO другой, `271–272` | Удалить непривязанный class |
| Export `totalsByCurrency` | `backend/src/modules/investments/portfolio/infrastructure/tbank-portfolio.mapper.ts:56,109–141`; используется внутри файла | Удалить export, не calculation |
## ISP / DIP
### D6. String DI alias для единственной реализации
- Structural contract/string token: `backend/src/modules/investments/instruments/stocks/application/stock-screener.service.ts:49–63`.
- Единственный `useExisting` alias: `backend/src/modules/investments/instruments/instruments.module.ts:23–25`.
- Alternative implementations/overrides token не найдены.
**Вывод:** injection по существующему concrete class token. Узкий type можно сохранить через `Pick`, как уже сделано в `backend/src/modules/investments/tbank-connection/tbank-connection.service.ts:11–19`.
Это небольшой cleanup, не повод создавать interface.
### D7. FormProvider без context consumers
`frontend/src/features/auth/ui/AuthForm/AuthForm.tsx:50–86`: поля используют локальный `methods.register`, descendants не используют form context.
Но `frontend/.ai/conventions/ui.md:128–129` предписывает `FormProvider`.
**Вывод:** сначала сделать existing rule условным по наличию context consumers, затем убрать provider здесь. Не создавать consumers ради оправдания wrapper.
## LSP
Для основной business architecture **практически неприменим**: нет собственной иерархии взаимозаменяемых domain implementations. Concrete providers/repositories — не нарушение LSP или DIP.
DTO inheritance может сохранять Swagger metadata/schema identity — например `backend/src/modules/investments/instruments/bonds/presentation/bond.dto.ts:5–16`. Удалять пустые DTO subclasses по визуальному сходству нельзя.
## DRY
Подтверждены узкие дубли knowledge/assertions:
- Anonymous `/accounts` redirect повторён table case и standalone case: `frontend/src/app/routes/AppRoutes/AppRoutes.test.tsx:35–61`.
- Failed-image behavior повторяется на page level, хотя сильнее проверен EntryCard: `frontend/src/pages/account-events/ui/AccountEventsPage/AccountEventsPage.test.tsx:166–169`, `frontend/packages/ui/src/EntryCard/EntryCard.test.tsx:24–63`.
- Process authority повторяет approval/cascade/context policies в canonical specs, schemas и config: `specs/openspec/config.yaml:55–58`, `specs/openspec/specs/openspec-authority-and-lifecycle/spec.md:53–80`, `specs/openspec/schemas/wealth-ledger-sdd-v2/schema.yaml:554–566,625–642`.
Не рекомендовано объединять:
- короткие local money fixtures;
- worker implementations с разной publication semantics;
- repo-local release configs;
- похожие UI/domain states с разными responsibilities.
## KISS
### D8. Motion dependency ради одного opacity transition
- Единственный source consumer: `frontend/src/app/routes/RouteTransition.tsx:3–30`.
- Единственный caller: `frontend/src/app/routes/RouteFallback.tsx:70–81`.
- Dependency: `frontend/package.json:52`.
**Минимум:** локальный CSS fade-in с `prefers-reduced-motion`; сохранить `key={pathname}` и current mount boundary.
Потенциально удаляется **одна runtime dependency**. Bundle savings не измерены. Это replacement opportunity, а не unused package в текущем коде.
# E. Test Suite Complexity
## E1. Setup architecture
### Frontend
Общая инфраструктура небольшая:
- `frontend/src/test/query.tsx:5–24`: две Query factories, consumers — 7 и 20 test files.
- `frontend/src/test/resource.ts:6–25`: одна resource-envelope factory, 20 test consumers.
- `frontend/src/test/setup.ts:13–48`: MSW lifecycle, RTL cleanup, auth reset, browser substitutes.
- `frontend/src/test/server.ts:1–7`: прямой MSW server.
- `frontend/src/test/storybook.test.tsx:19–57`: isolation checks для девяти story modules.
**Общего самописного test framework не обнаружено.** Размножение локальных render/provider setups не требует автоматически создавать универсальный render helper.
### Backend
- 20 integration suites: 14 AppModule, 5 direct repository/service boundaries, одна migration suite.
- 14 Nest bootstrap sites, 23 overrideProvider call sites.
- 4 service factories, 4 deferred definitions, 7 polling helpers.
- 8 CJS инфраструктурных/helper файлов, перечисленных в B2.
Fresh DB на suite, synthetic credentials и signal cleanup — необходимая стоимость. Не рекомендовано объединять все suites в общую DB ради меньшего setup.
## E2. Участки для сокращения
В таблице ниже сокращённые frontend filenames принадлежат указанным page/component directories; backend filenames — `src/modules` или `test` по полным путям в разделах D и H.
| Набор | Что защищает | Лишняя подготовка / overlap | Что теряется и решение |
|---|---|---|---|
| `frontend/src/pages/account-events/ui/AccountEventsPage/AccountEventsPage.test.tsx:13–28,40–61,83–205` | Route scope, loading/busy, events/empty/error presentation | Два module mocks и пять fake query shapes через `as never`; соседняя `AccountEventsPage.filters.test.tsx:50–108` уже использует MSW | **Упростить**, не удалять outcomes. Network cases — existing MSW; presentation-only — existing view |
| `backend/src/modules/banking/bank-cards/application/get-bank-summary.use-case.spec.ts:11–48` | Category mapping, точное `1.23`, inconsistent category rejection | Fake Prisma tx + настоящий repository, хотя use case вызывает только `summaryData()` | Mock ближайшую repository boundary. DB proof не теряется: fake tx его не обеспечивал |
| `backend/src/modules/investments/portfolio/application/portfolio.service.spec.ts:48–165` | Signed nanos, unknown type, cash/quantity/currency mapping | Background request, polling и `publish.mock.calls` для чистого mapper result | Первые два cases напрямую через existing `mapPortfolio`; orchestration/integration proof оставить |
| `backend/src/modules/investments/operations/presentation/brokerage-operations.controller.spec.ts:1–18` | Передача args в mock service | Prisma mock, casts, assertion прямого вызова; configured HTTP cases покрывают account/query/access | **Удалить**. HTTP proof: `backend/test/tbank-ledger-local.e2e-spec.ts:113–137,194–227,336–366`, `backend/test/all-accounts.e2e-spec.ts:63–104` |
| `frontend/src/app/routes/AppRoutes/AppRoutes.test.tsx:55–61` | Anonymous redirect | Более сильный table case уже на `35–53` | **Удалить standalone case** |
| `frontend/src/pages/account-events/ui/AccountEventsPage/AccountEventsPage.test.tsx:166–169` | Image error fallback | EntryCard test проверяет также same-source rerender/new-source reset | Удалить только error exercise; event→logo/initial assertions оставить |
| `backend/test/tbank-metadata-persistence.e2e-spec.ts:87–382` | Reuse, coalescing, privacy, late publication, reopen, batch lookup | Сценарии завязаны на production-unused `read()` | **Перевести boundary**, не удалить suite |
| `backend/test/bank-cards.e2e-spec.ts:371–389,710–753` | Rename/default role, durability, FK/unique | Три declarations зависят от предыдущих tests и их category state | Сделать три cases самодостаточными существующим `createCard`; не fixture framework |
| `backend/test/sqlite-regression.cjs:20–22,77–91` | Runner isolation/cleanup/signals | Пять повторов всех 19 functional suites | По две representative suites/run; canonical full functional CI оставить |
| `frontend/scripts/test-eslint.mjs:148–202` | Все recommended plugin rules включены | Exhaustive parity каждой library preset | Удалить blanket parity; оставить project exceptions/scopes/custom behavior probes |
## E3. Assertions против стоимости setup
Наиболее явные несоразмерности:
- **18 строк controller unit** поддерживают исключительно mock delegation.
- **Две mock layers + пять query shapes** конкурируют с уже существующей HTTP boundary в одном page area.
- **Пять полных SQLite runs** защищают свойства runner, не пять разных domain scenarios.
- **Fake Prisma transaction** требуется для двух assertions чистого summary use case.
Большие financial fixtures или длинный crash setup сами по себе не плохи: они могут защищать boundary, недоступную дешёвому test level.
## E4. Что оставить
- Денежные unit cases: signed nanos, значения выше `2^53`, currencies, unknown≠zero.
- Real SQLite CAS/transactions/replay/refund constraints.
- Session transport/generation fencing.
- Metadata/candle/operations late-publication и restart/crash cases.
- Configured HTTP guards/pipes/cookie/status tests.
- Browser geometry.
- Historical migration/rollback/restore.
- Story isolation, когда она доказывает отсутствие application network/storage access.
Примеры: `backend/src/modules/banking/bank-cards/domain/bank-card.calculations.spec.ts:46–159`, `backend/test/bank-cards.e2e-spec.ts:462–523,622–683`, `backend/test/app.e2e-spec.ts:161–213`, `backend/test/tbank-operations-persistence.e2e-spec.ts:74–100`.
**Не установлено, какие tests были созданы исключительно ради coverage.** Frontend floors (`frontend/vitest.config.ts:19–24`) создают constraint, но не доказывают происхождение тестов. Snapshot bloat не найден.
# F. AI Agent & Developer Workflow
## F1. Bootstrap и full gate обязательны слишком широко
Одинаковое требование в обоих application repos:
> Code/dependency change: `npm ci`, полный `npm run verify`…
`frontend/.ai/conventions/engineering.md:60–62`, `backend/.ai/conventions/engineering.md:60–62`.
Для code-only изменения в уже подготовленном checkout повторная установка не устанавливает новый инвариант. Full frontend verify также запускает browser tests, Knip, оба dependency graphs и Storybook build.
**Упростить:** bootstrap при новом checkout/изменении lockfile или toolchain; local checks по affected behavior; полный независимый CI gate сохранить.
Потеря — более поздний feedback по незатронутым областям, а не отсутствие merge-time проверки.
## F2. Pre-commit собирает packages и проверяет весь graph на каждом commit
`frontend/.husky/pre-commit:1–3`, `frontend/package.json:25,33`.
Даже Markdown commit достигает `npm run deps`, который сначала builds workspaces. UI/icons builds включают Vite и declaration compilation: `frontend/packages/ui/package.json:23–25`, `frontend/packages/icons/package.json:19–21`.
**Перенести graph check из hook в scoped local/CI.** Staged secret scan и staged ESLint оставить.
## F3. Mechanical lint policy создаёт искусственную форму кода
- Вложенные component helpers запрещены custom rule: `frontend/eslint-rules.mjs:63–85`.
- Локальные Zod declarations/refinements требуют filename suffix: тот же файл, `20–36`.
- JSX sibling spacing имеет собственный AST traversal/fixer: тот же файл, `104–175`.
- Любой inline JSX callback/conditional prop запрещён: `frontend/eslint.config.mjs:273–283`.
- Backend magic-number profile требует имена даже для `0`, `1` и bigint literals: `backend/test/eslint-config.cjs:78–92`.
Часть этих правил защищает readability preferences, а не data flow или runtime correctness. Они увеличивают число именованных declarations, файлов и exceptions.
**Упростить blanket restrictions**, оставив semantic boundaries: no data fetching во view, безопасные promises, stable query client, доступность, named business policy values. Не заменять custom policy ещё одним инструментом.
## F4. Generic skills конфликтуют с локальным соразмерным workflow
- `writing-plans` автоматически применим к multi-step requirements: `.agents/skills/workflow/writing-plans/SKILL.md:3`.
- Предписывает файл в `docs/superpowers/plans`: тот же файл, `16–17`.
- Добавляет fixed test/commit structure и Review Focus cases: тот же файл, `43–50,77–87,108–137`.
- TDD применяется к refactoring и требует failing test до любого production code: `.agents/skills/workflow/test-driven-development/SKILL.md:18–45`.
- Локальный Test Intent не требует отдельного artifact, исключает trivial wrapper/declaration testing: `frontend/.ai/conventions/engineering.md:49–55`, `backend/.ai/conventions/engineering.md:49–55`.
**Рекомендация:** ограничить применение этих workflows явно выбранной planning/TDD задачей и реальным regression risk. Сохранить reproduction для bug fixes и meaningful proof; не требовать новый тест для удаления unused field/export.
Стандартные OpenSpec skills изменять не требуется.
## F5. Opt-in OpenSpec против mandatory audit remediation
Противоречие:
- Opt-in: `specs/AGENTS.md:3–6`, `frontend/.ai/conventions/engineering.md:11–13`, `backend/.ai/conventions/engineering.md:11–13`.
- Mandatory audit-derived registry/workflow: `specs/openspec/specs/project-foundation/spec.md:9–19`.
- Обязательные snapshots/registry: `specs/openspec/specs/audit-remediation-workflow/spec.md:9–49`.
- Docs запрещает размещение audit/research reports: `docs/AGENTS.md:12–14`.
**Упростить действующую process policy**, согласовав область действия и место artifacts. Не создавать registry/SDD change автоматически для реализации этого аудита.
## F6. Custom SDD workflow остаётся тяжёлым даже без behavioral delta
- V2: семь planning artifact groups.
- При `skip_specs` остаются шесть.
- Schemas: 885 строк; templates: 335 строк.
- Семь stage instructions требуют native validation после записи.
- Apply preconditions перечисляют семь explicit approvals: `specs/openspec/schemas/wealth-ledger-sdd-v2/schema.yaml:587–589`.
Это не доказательство бесполезности анализа/QA. Однако small docs/tooling change не всегда требует отдельных System Analysis, QA Analysis, Docs Impact и sessions.
**Сначала соблюдать opt-in режим.** При отдельном пересмотре custom schema — соразмерный набор artifacts по реальному риску. Не удалять approval/data-safety boundaries вслепую.
## F7. Specs CI даёт мало гарантии за полноценный install
`specs/.gitea/workflows/verify.yml:17–22` → `npm ci` → `git diff --check`.
На чистом checkout команда не проверяет committed PR diff, semantic structure или OpenSpec artifacts. Зато npm `prepare` настраивает hooksPath: `specs/package.json:13`.
**Удалить нынешний пустой job**, предварительно установив required-check settings. Существующие native validations запускать для соответствующего изменения; этот аудит не требует нового verifier.
## F8. Что не является проблемой
- AGENTS routers уже ограничивают чтение affected topics.
- Root и nested conventions не наследуются друг в друга — разные enum/style choices не конфликт.
- Read-only archives/history не нужно удалять.
- Повторение краткого stage-specific rule допустимо, если оно нужно executor текущей стадии.
- Транзитивно избыточные schema edges нельзя удалять только по graph reachability: они могут влиять на CLI context inputs.
## Необходимые измерения
Без новых tools достаточно existing CI logs и session usage:
1. Duration каждой verify stage, cold/warm отдельно.
2. Время workspace builds в local commands/hook.
3. Частота local full verify и reinstall.
4. CI critical path и full-history scan duration.
5. Token/context volume на обычную задачу против explicitly selected SDD change.
6. Какие gates действительно находят regressions и на какой boundary.
Сейчас денежная/временная экономия не оценена численно.
# G. DELETE / SIMPLIFY / KEEP
## DELETE — 18 подтверждённых точек
Удаление подразумевает сохранение указанного более сильного или эквивалентного механизма.
1. Custom implementation `check-openapi.mjs` — заменить native check.
2. Второй TypeScript call в frontend verify.
3. `typecheck:app` stage из backend full verify — test typecheck superset оставить.
4. Повторный backend build перед SQLite в том же CI job.
5. Невызываемый `runtime-check.cjs`.
6. `useAuthenticatedQueries` и исключительно его test branch.
7. Неиспользуемый `history.retry`.
8. Неиспользуемый `isFetching` input.
9. Неиспользуемая Portfolio membership DI dependency.
10. String alias `TBANK_ACCESS` — concrete provider оставить.
11. Невызываемый `TbankAccessService.membershipWork`.
12. Неиспользуемый `candleTail` policy key.
13. Непривязанный `HourlySyncSectionDto`.
14. Неиспользуемый внешний export `totalsByCurrency`.
15. Delegation-only `brokerage-operations.controller.spec.ts`.
16. Повторный standalone anonymous `/accounts` route test.
17. Повторный image-error exercise в AccountEvents page test.
18. Пустой specs CI verify job — после проверки branch requirements.
Evidence и consequences перечислены в B–E. Это **18 элементов/стадий**, а не 18 файлов.
## SIMPLIFY
- Local gate scope и bootstrap frequency.
- Dependency graph invocation в pre-commit.
- SQLite runner regression scope.
- Exhaustive preset parity и mechanical custom lint policy.
- Metadata tests через production-used methods.
- Portfolio `request`/private test access.
- AccountEvents fake-query setup.
- Pure mapper/use-case tests.
- Три order-dependent bank cases.
- Bond-only InstrumentSearch contract.
- Motion → CSS.
- FormProvider rule по наличию consumers.
- Process specs/SDD activation.
- Specs hook activation через явное подключение вместо npm lifecycle.
- Visual SHA tables только там, где exports реально передаются отдельно от Git.
## KEEP
- Финансовая точность и business validation.
- Intent fingerprint, unique IDs, versions, tombstones, refund transaction.
- Refresh hashing/CAS, cursor signature/binding.
- Connection revision и conditional publication.
- Coverage/completeness/freshness как разные свойства.
- Independent local/CI trust boundaries.
- Per-suite DB isolation и crash/durability proofs.
- Runtime OpenAPI parity и consumer artifact freshness.
- Scanner checksum, exact image digest/revision checks.
- Deployment lock, pending marker, backup staging, explicit recovery.
- Repo-local небольшие configs/helpers вместо нового shared tooling package.
- Исторические ADR/archives.
- Реальные browser layout и Storybook isolation guarantees.
**Условный кандидат, не DELETE:** `InvestmentRefreshState` имеет ноль production callers, но четыре stories и catalog isolation consumer — `frontend/src/features/tbank-connection/ui/InvestmentRefreshState/InvestmentRefreshState.view.tsx:1–61`, `frontend/src/features/tbank-connection/ui/InvestmentRefreshState/InvestmentRefreshState.stories.tsx:1–52`, `frontend/src/test/storybook.test.tsx:11,38`. Его удаление уберёт catalog artifact; намерение owner не установлено.
# H. Top 20 Complexity Reduction Opportunities
Порядок — по ожидаемому сокращению engineering overhead. Экономия указана в механизмах/запусках, не в выдуманных секундах.
Сокращённые filenames в таблице однозначно относятся к полным путям, указанным в B–F.
| № | Файлы и доказательства | Текущий механизм → минимальное изменение | Что перестанет работать / ожидаемая экономия | Риск · Confidence |
|---:|---|---|---|---|
| **1** | FE/BE `.ai/conventions/engineering.md:58–66` | `npm ci` + full verify для любого code change → bootstrap по изменению inputs, local affected checks, full CI | Исчезнет обязательный ранний full-project proof; меньше reinstall/full suites на малую задачу | Medium · High |
| **2** | `backend/test/sqlite-regression.cjs:20–22,77–91`; `backend/test/sqlite.cjs:10–17` | Пять successful `--all` runs → по две representative suites | Теряются повторные business runs внутри runner proof; **95 → 10** successful suite executions, canonical full CI остаётся | Medium · High facts |
| **3** | `frontend/package.json:26`; `frontend/tsconfig.app.json:17`, `frontend/tsconfig.node.json:12` | Два TS calls с noEmit configs → один | Второй call не выполняется; минус один compiler invocation/full verify | Low · High |
| **4** | `backend/package.json:22–34`; typecheck configs app `7–8`, test `7–8` | App subset + test superset → test superset в aggregate | Отдельная маркировка app-stage failure; минус один nonincremental TS pass | Low · High |
| **5** | `backend/.gitea/workflows/verify.yml:84–89`; package `28,34` | Verify build, затем E2E build → после verify existing `node test/sqlite.cjs --all` | E2E stage перестанет пересобирать уже готовый output; standalone entrypoint сохранить | Low · High |
| **6** | `frontend/.husky/pre-commit:1–3`; package `25,33`; UI/icons package builds | Whole graph + workspace build на каждый commit → local/CI graph | Потеря hook-time graph feedback; минус один workspace build/graph traversal на commit | Low–Medium · High |
| **7** | `frontend/scripts/check-openapi.mjs:1–45`; CLI `125–134` | Custom temp/subprocess compare → `openapi-typescript … -o … --check` | Ничего из freshness guarantee; **45 строк custom orchestration** заменяются CLI option | Low · High; native check executed |
| **8** | `specs/package.json:9,13`; `specs/.gitea/workflows/verify.yml:17–22` | Install + clean working diff check → удалить пустой job | Теряется малоинформативный check status; минус job/install/hooks side effect | Low после branch-setting review · High |
| **9** | FE `eslint-rules.mjs:1–178`; `test-eslint.mjs:148–202,290–339` | Mechanical AST policy + exhaustive preset parity → оставить semantic boundaries и custom-exception probes | Меньше формальных style/shape guarantees; меньше custom rule/test maintenance | Medium · High facts |
| **10** | `frontend/src/app/routes/RouteTransition.tsx:3–30`; package `52`; caller `RouteFallback.tsx:70–81` | Motion для fade-in → CSS с reduced motion | Library-driven fade-in заменяется; сохранить key/mount semantics; **−1 runtime dependency** | Low–Medium · High |
| **11** | Metadata service `45–61`; persistence tests `87–382`; repository `159–170` | Test-only read/start API → tests через `request`/`savedMany`, затем удалить API/work accessor | Исчезнет unused combined operation; реальные reuse/race/restart proofs сохранить | Medium · High |
| **12** | `frontend/src/shared/api/authenticated-query.ts:15–32,61`; test `9–23` | Unused plural generic hook → удалить и test-only variant | Multi-query convenience без product consumers; меньше generic surface/test matrix | Low · High |
| **13** | Portfolio service `45–53,234–243`; value-history tests `99–111,247–256` | Unused DI + public/private alias → один `request`, прямой provider access в tests | Private test route исчезнет; queue/publication не меняется | Low · High |
| **14** | History hook `52–61,76`; initial availability `10–46` | Невостребованное action и непрочитанный input → удалить | Никакой used behavior; automatic cursor recovery/wait budget остаются | Low · High |
| **15** | InstrumentSearch view `18–31`; BondsPage view `26–113`; test `21–29` | Stock/bond renderer при одном bond consumer → bond-specific current contract | Исчезнет test-only stock mode; current product search не меняется | Low–Medium · High |
| **16** | AccountEvents test `13–28,40–205`; filters test `50–108` | Fake hooks/query shapes → existing MSW/view boundaries | Исчезнут internal-hook assertions и пять fake shapes; visible outcomes сохранить | Medium · High |
| **17** | Summary spec `11–48`; portfolio spec `48–165`; mapper `33–138` | Fake tx/background work ради чистого result → ближайший existing owner | Unit не будет exercise’ить orchestration; configured DB/publication proofs остаются | Low–Medium · High |
| **18** | Controller spec `1–18`; route test `35–61`; page image test `166–169` | Delegation-only и одинаковые assertions → удалить узкие дубли | Только internal-call/repeated fallback assertions; более сильные HTTP/route/EntryCard proofs остаются | Low · High |
| **19** | `backend/test/bank-cards.e2e-spec.ts:371–389,710–753`; setup `86–105` | Три tests с implicit prerequisites → собственный arrange через `createCard` | Теряется зависимость от suite history, не business guarantee; targeted tests дешевле понимать | Low–Medium · High |
| **20** | Specs AGENTS `3–6`; foundation `9–19`; remediation `9–49`; writing-plans `16–17,77–87`; TDD `18–45` | Mandatory artifacts/SDD/TDD rules конфликтуют с opt-in/direct work → уточнить existing activation/scope | Не будет автоматических planning/registry artifacts для каждой audit-derived правки; меньше context/approval loops | Medium · High |
Отдельные дешёвые dead-surface cuts из G можно делать при связанном изменении owner. Ради удаления одного export не нужен самостоятельный архитектурный проект.
# I. Minimal Engineering Baseline
Это предложение сократить существующий workflow, **не новая convention или набор инструментов**.
## Local development
Оставить:
- Активировать local Node version.
- Bootstrap при новом checkout или изменении dependency/toolchain inputs.
- Targeted test на cheapest reliable boundary.
- Relevant typecheck/lint.
- Semantic review affected behavior и `git diff --check`.
- Risk-triggered DB/browser/contract/durability proof.
Убрать обязательный `npm ci` и весь full-project gate после каждого code-only изменения.
Full verify остаётся доступным локально, но его обязательная независимая граница — CI.
## Pre-commit
Оставить frontend:
1. Staged Gitleaks с redaction/fail-closed.
2. Staged ESLint.
3. Commit-msg policy при подключённом hook.
Перенести whole dependency graph и workspace builds из pre-commit.
Specs `commit-msg` можно оставить; подключение hooks сделать явным вместо автоматического `prepare`. Это не отменит уже записанный hooksPath само по себе.
## CI
### Frontend
Оставить:
- Native OpenAPI freshness.
- Один TypeScript pass.
- Один ESLint run.
- Behavioral tests + coverage.
- Knip.
- Production и Storybook graph checks с сохранением различающихся semantics.
- Application и catalog builds.
- Security и commit policy.
Lint-config tests сократить до project-specific scopes/exceptions/custom behavior; изменение tooling требует соответствующего focused proof.
### Backend
Оставить:
- Generated Prisma client до checks.
- Один full-source/test typecheck.
- ESLint.
- Unit suite.
- Один build.
- Изолированные functional SQLite suites, включая runtime OpenAPI contract.
- Security и commit policy.
После уже выполненного build использовать existing runner без повторного build. Performance и historical durability остаются отдельными risk-triggered командами.
### Docs / specs
- Docs Markdown/link lint сохранить.
- Working-diff check оставить локально.
- Specs пустой verify job удалить после проверки branch requirements.
- Native OpenSpec validation использовать по изменению schema/spec/change и только в выбранном SDD scope.
- Security/release/commit mechanisms сохранить.
## Testing layers
Минимально достаточные существующие layers:
1. Pure domain/unit — деньги, normalizers, mappers.
2. RTL/MSW — UI, forms, route/session/transport behavior.
3. Configured HTTP + real SQLite — pipes, guards, transactions, CAS, persistence.
4. Browser — geometry/lifecycle, которые JSDOM не доказывает.
5. Image/historical/deploy — соответствующие packaging/data-recovery risks.
Удалять одинаковый proof, а не целый layer.
## Architecture constraints
Оставить:
- Production не зависит от test code.
- Package не зависит от app.
- FSD public API/layer/cycle boundaries.
- Presentation не выполняет application fetching.
- Story graph не достигает application data.
- Pure domain не зависит от persistence/transport.
Не добавлять interface перед каждым concrete provider, общий worker framework или новые архитектурные checks.
## AI instructions
Оставить:
- Local AGENTS router.
- Owner/callers/invariants до изменения.
- Сохранение чужих изменений.
- Context по affected topics.
- Meaningful verification evidence.
- Opt-in OpenSpec.
Сократить:
- Universal plan-file creation.
- Новый failing test для каждого mechanical refactor.
- Mandatory full reinstall/full verify на любую правку.
- Blanket FormProvider/VM/helper/style mechanics без current responsibility.
- Повторные process authority descriptions и обязательные artifacts с конфликтующим размещением.
---
## Итог
**Наиболее выгодный путь: сократить обязательную частоту и scope проверок → удалить доказанную невостребованную поверхность → упростить test boundaries → согласовать процессную authority.**
Не требуется менять финансовую архитектуру, добавлять инструменты или объединять независимые repositories.
**Net:** точный line delta не подготовлен; подтверждена замена 45-строчного OpenAPI checker native CLI, 18 точек удаления и возможность убрать одну runtime dependency после CSS-замены. Экономия времени и токенов требует измерений.
@@ -0,0 +1,347 @@
# Технический аудит WealthLedger
Дата: 11 октября 2026 года.
# Executive Summary
**Главная проблема проекта — незавершённый переход между моделями получения данных, а не недостаток архитектурных слоёв.** Часовой обход, прежняя screen-driven синхронизация, online lookup и локальные snapshots местами сосуществуют с разными правилами обновления. Добавление новых сервисов и абстракций это усложнит; наибольшую пользу даст удаление старых путей и уточнение ответственности существующих владельцев.
Наиболее существенные системные проблемы:
1. **Выбор сохранённого прогноза может блокировать публикацию нового полного результата.**
2. **Ошибки часового координатора могут обозначаться успехом; отдельные сбои persistence способны оставить обход незавершённым.**
3. **Часовой обход не обновляет уже сохранённые календари выплат из-за политики `stable`.**
4. **Frontend получает информацию о свежести и ошибках, но основной экран портфеля теряет её при передаче props.**
5. **Часть тестов защищает старые сценарии или проверяет гонку без фактического запуска конкурирующей работы.**
При этом основа проекта обоснованна: модульный монолит, конкретные providers, точная денежная арифметика, SQLite-транзакции, изоляция подключения и generated OpenAPI. **Масштабный архитектурный рефакторинг не нужен.**
## Основание и ограничения аудита
Проверены текущие checkout:
| Репозиторий | Commit |
|---|---|
| `frontend` | `c882120` |
| `backend` | `0ad84ff` |
| `specs` | `fcdc805` |
| `docs` | `e6fd4f6` |
Аудит выполнен read-only с использованием `ponytail-audit`. Изучены архитектура, владельцы поведения, вызывающие пути, рискованные реализации и связанные тесты/specs.
Проведены две лёгкие проверки:
- После `nvm use --silent` запущен существующий dependency-cruiser: **420 модулей, 1773 зависимости, нарушений настроенных границ и циклов не обнаружено**.
- `git diff --no-index --stat` между существующим `backend/openapi.json` и frontend snapshot: **различий нет**. Это подтверждает совпадение файлов на диске, но не свежую генерацию относительно runtime.
Рабочие деревья четырёх репозиториев остались чистыми. Build, полные `verify`, SQLite/E2E и нагрузочные проверки не запускались. Ниже отдельно обозначены выводы по коду, архитектурные риски и предпочтения; runtime-воспроизведение дефектов в рамках аудита не выполнялось.
# Findings
## F1. Выбор forecast смешивает последний результат и запрошенное окно
**Priority: High · Confidence: High · Тип: дефект по коду**
- **Evidence:** `backend/src/modules/investments/portfolio/infrastructure/forecast.repository.ts:50–79`. При отсутствии точного `scopeKey` выбирается первый пересекающийся forecast без сортировки по времени. Затем `if (!previous && fallback) return fallback` прекращает обработку даже при наличии нового полного `candidate`. Окно входит в ключ: `upcoming-events.service.ts:160–173`.
- **Problem:** успешный расчёт для следующего дня может уступить старому пересекающемуся окну. Для непересекающегося запроса fallback вообще не выбирается, хотя действующая spec требует успешный пустой результат сохранённого прогноза.
- **Impact:** после успешного обхода пользователь может продолжать видеть прежние рассчитанные суммы и старое окно. При ежедневном сдвиге окна это способно продолжаться до исчезновения пересечения.
- **Root cause:** идентичность сохраняемого результата и параметры HTTP-фильтра объединены в строковом `scopeKey`.
- **Recommendation:** локально разделить выбор последнего полного forecast и фильтрацию запроса. Fallback должен сохраняться при неуспехе нового результата, а не блокировать новый полный результат только из-за другого окна. Сохранить существующие проверки зависимостей и атомарной публикации.
- **Trade-off:** потребуется уточнить read/write-семантику внутри существующих service/repository и пересмотреть несколько тестов. Новые слои не нужны.
## F2. Часовой координатор не гарантирует корректное завершение ошибки
**Priority: High · Confidence: High для ветвей ошибок · Тип: дефект и архитектурный риск**
- **Evidence:** `backend/src/modules/investments/tbank-sync/tbank-hourly.scheduler.ts:93–110,178–218`. Общий `catch` вызывает `settle` без добавления проблемы; при пустом `failures` результат — `succeeded`. Ошибка `settleHourly` подавляется. `settledWorks` ждёт `running` без конечной границы. В `catalog-enrichment.service.ts:582–590` аварийный выход очищает process-state, но не завершает durable work rows.
- **Problem:** исключение вне локальных обработчиков может превратиться в успешный обход. Незавершённая запись дочерней работы может удерживать polling бесконечно.
- **Impact:** статус обхода вводит в заблуждение. Если hourly row остаётся `running`, `reserveHourly` не разрешает следующий обход в том же процессе — `tbank-sync.repository.ts:190–200`.
- **Root cause:** завершение определяется количеством накопленных ошибок, а не фактом прохождения всех обязательных шагов; подавление persistence errors скрывает нарушение lifecycle.
- **Recommendation:** считать незавершённый сценарий ошибкой, явно фиксировать причину общего `catch`, ограничить ожидание дочерней работы и обеспечить диагностируемое восстановление терминального состояния. Использовать текущие durable rows и logger.
- **Trade-off:** больше явных error branches. Это необходимая защита lifecycle, а не избыточный defensive programming.
## F3. Часовой обход повторно использует календари без обновления источника
**Priority: Medium · Confidence: High · Тип: дефект политики обновления**
- **Evidence:** scheduler вызывает `events.refresh` — `tbank-hourly.scheduler.ts:166–175`. Далее `readSources(..., false)` приводит к `requestMany(..., false, ...)` — `upcoming-events.service.ts:182–239`, `catalog-enrichment.service.ts:113–125`. В `catalog-enrichment.service.ts:189–193,679–692` повторное обновление разрешено для `quote`, но `dividends` и `bond-events` относятся к `stable`.
- **Problem:** успешно сохранённый календарь не обновляется при последующих обходах. Для `bond-events` ключ не меняется вместе с датой.
- **Impact:** новые или изменённые выплаты могут отсутствовать в прогнозе, хотя часовой обход завершился успешно.
- **Root cause:** календари выплат получили политику практически неизменяемого справочника.
- **Recommendation:** в существующем hourly refresh принудительно обновлять именно календарные источники через уже имеющийся `force`, сохранив локальность обычных GET и reuse стабильных metadata.
- **Trade-off:** дополнительные календарные запросы во время обхода. Их фактический бюджет следует проверить на representative портфеле.
## F4. Длительный обход не соблюдает правило пропуска часового срока
**Priority: Medium · Confidence: High · Тип: несоответствие текущей spec**
- **Evidence:** `backend/src/modules/investments/tbank-sync/tbank-sync.repository.ts:189–201` проверяет только `now - createdAt >= hourMs`. `specs/openspec/specs/hourly-investment-sync/spec.md:19–25` требует: обход 10:00, завершившийся после 11:00, допускает следующий запуск не ранее 12:00.
- **Problem:** после завершения в 11:10 текущая проверка разрешит запуск уже на ближайшем tick.
- **Impact:** пропущенный срок фактически превращается в немедленный повтор после завершения длинного обхода.
- **Root cause:** проверяется прошедшая длительность, а не следующий допустимый срок относительно начала попытки.
- **Recommendation:** исправить вычисление eligibility в `reserveHourly`; проверить контролируемым временем короткий обход, пропущенный срок и восстановление после простоя.
- **Trade-off:** следующий запуск иногда будет позже текущего поведения — именно это требуется сохранённой политикой.
## F5. Основной экран портфеля теряет metadata и скрывает ошибку перечитывания
**Priority: Medium · Confidence: High · Тип: дефект передачи данных**
- **Evidence:** VM возвращает `source: portfolio.meta` — `frontend/src/pages/account/ui/AccountPage/AccountPage.vm.ts:125–135`, но container не передаёт `source` — `AccountPage.tsx:24–34`. Loaded view уже умеет показывать её — `components/AccountPageLoaded.view.tsx:55–59`. Тест `AccountPage.test.tsx:751–855` закрепляет отсутствие времени, предупреждения и retry после ошибки.
- **Problem:** подготовленная информация теряется на последнем переходе VM → view. Состояние transport error также не представлено в loaded-модели.
- **Impact:** пользователь видит сохранённые суммы без необходимого контекста свежести и неудачного обновления.
- **Root cause:** рассогласование props между container и view; тесты проверяют «тихое сохранение», а не требуемое восстановление.
- **Recommendation:** передать существующий `source`; представить ошибку перечитывания и retry существующими UI-компонентами, сохранив cached snapshot.
- **Trade-off:** экран покажет дополнительные статусы. Новая архитектура представления не требуется.
## F6. Income GET всё ещё может запускать личную синхронизацию
**Priority: Medium · Confidence: High · Тип: нарушение заявленной границы read**
- **Evidence:** `backend/src/modules/investments/operations/application/brokerage-operations.service.ts:408–435`: обычный `collectRanges` вызывает `membership.request` и `sync.readPlan`. Controller передаёт `observe=false`, если `sync=observe` отсутствует — `income-analytics.controller.ts:24–79`. Действующая граница запрещает импорт из обычных personal reads — `specs/openspec/specs/tbank-data-sync/spec.md:9–14`.
- **Problem:** endpoint аналитики одновременно читает и инициирует обновление membership/операций.
- **Impact:** лишние RPC и два владельца обновления одного источника; поведение обычного GET отличается от преимущественно тестируемого observe-пути.
- **Root cause:** в `collectRanges` сохранился прежний request-driven сценарий.
- **Recommendation:** убрать обновление membership из income read и оставить получение отсутствующих интервалов предусмотренному владельцу Ledger. До изменения уточнить cold-start поведение исторической аналитики.
- **Trade-off:** неполная историческая аналитика не сможет незаметно инициировать загрузку. Этот UX-вопрос необходимо решить явно.
## F7. Политика перечитывания распределена между Query и собственным таймером
**Priority: Medium · Confidence: High · Тип: риск лишних запросов и несогласованная политика**
- **Evidence:** `frontend/src/app/ui/App.tsx:13–19` включает focus refetch, тогда как `.ai/conventions/data.md:70–74` описывает выключенный. Portfolio/operations/events/stock используют `staleTime: 30_000`; accounts — `1_800_000`. `features/tbank-connection/model/screen-sync.ts:22–35` напрямую вызывает `refetch` всех resources, не проверяя `isEnabled`. Например, `BondsPage.vm.ts:10–13` передаёт туда query, отключённый для короткого поиска.
- **Problem:** несколько механизмов задают частоту GET. Ручной `refetch` обходит автоматическое отключение query через `enabled`.
- **Impact:** возврат фокуса и повторное открытие создают запросы раньше описанного интервала; таймер может отправить запрос с неподготовленным identifier/query.
- **Root cause:** периодическое чтение вынесено из владельца query, а разные поколения политики остались одновременно.
- **Recommendation:** согласовать `staleTime`/focus policy; использовать существующий `refetchInterval` у query-owner. Если таймер сохраняется, фильтровать disabled resources. Удалить неиспользуемый `_scope`.
- **Trade-off:** необходимо выбрать поведение фоновой вкладки и reconnect. Native Query policy уменьшит собственную координацию.
Семантика `enabled`, ручного `refetch` и `refetchInterval` сверена с актуальной документацией TanStack Query.
## F8. Часть race-тестов допускает ложноположительный результат
**Priority: Medium · Confidence: High для backend-примера, Medium для frontend-примера · Тип: дефект теста и риск**
- **Evidence:** `backend/test/tbank-local-reads.e2e-spec.ts:489–515` создаёт deferred portfolio, но запускает только GET, который теперь локален. `portfolioService.request` не вызывается. Во frontend `StocksPage.test.tsx:175–237` освобождает старый ответ и сразу проверяет уже существующий новый результат, не дожидаясь завершения старого GET.
- **Problem:** backend-тест не создаёт позднюю публикацию; frontend-тест не гарантирует, что проверка произошла после неё.
- **Impact:** тест может оставаться зелёным при удалении защиты, которую якобы проверяет.
- **Root cause:** backend-сценарий не обновлён после изменения data flow; frontend-сценарий не имеет барьера завершения проигравшего запроса.
- **Recommendation:** заменить backend-сценарий запуском реального source-owner с подтверждением начала RPC и завершения после смены подключения. Во frontend ждать завершения старого запроса/обработки его результата.
- **Trade-off:** тесты станут немного явнее, но будут проверять реальный regression risk.
Дополнительно **не найдены прямые проверки `tick`/`reserveHourly` для нового координатора**. В ряде SQLite suites scheduler подменён stub — например, `backend/test/upcoming-events.e2e-spec.ts:140–147`.
## F9. Неиспользуемая manual screen-sync orchestration продолжает поддерживаться
**Priority: Medium · Confidence: High · Тип: лишняя сложность**
- **Evidence:** `backend/src/modules/investments/tbank-sync/tbank-sync.service.ts:54–311` содержит `request` и `dispatch`. Production controller предоставляет только status GET — `tbank-sync.controller.ts:31–41`; production callers `request` не найдены. `tbank-screen.ts` имеет единственного production-потребителя в этом пути; `tbank-screen.spec.ts` проверяет manual sync boundary.
- **Problem:** недоступный сценарий сохраняет планирование, DI-зависимости, validation и тесты.
- **Impact:** сотни строк для понимания и сопровождения модели, которая уже не запускается пользователем.
- **Root cause:** прежний путь был отключён на HTTP-границе, но не удалён внутри.
- **Recommendation:** удалить `request`/`dispatch`, их исключительно используемые helpers и тесты. Сохранить status/read compatibility, если она ещё нужна.
- **Trade-off:** нельзя будет вернуть manual sync простым подключением старого метода; возвращать его сейчас оснований нет.
**Важно:** `planInvestmentScreen` нельзя удалять целиком — его использует текущий `BrokerageOperationsService`.
## F10. Online search создаёт fan-out и лишнюю waterfall-зависимость
**Priority: Medium · Confidence: High для числа вызовов · Тип: архитектурный риск эффективности**
- **Evidence:** `backend/src/modules/investments/instruments/application/catalog-discovery.service.ts:295–337`: один search RPC, до 30 detail RPC, затем batch prices. Лимит результатов — `catalog-selection.ts:5`.
- **Problem:** на один поиск получается до **32 upstream-вызовов**; цены начинают загружаться только после всех details.
- **Impact:** расход лимита T-Bank и рост времени ответа. Ошибка одного detail отклоняет весь `Promise.all`.
- **Root cause:** online-путь сохранил полное enrichment каждого результата, хотя список и detail имеют разные потребности.
- **Recommendation:** сразу запускать независимый prices request параллельно details. Отдельно проверить, какие detail-поля действительно нужны компактному списку, прежде чем убирать N запросов.
- **Trade-off:** сокращение detail fan-out может уменьшить набор полей списка; без сверки потребностей UI это изменение не рекомендую выполнять вслепую.
Новый агрегирующий endpoint для решения этой проблемы не нужен.
## F11. Банковская история использует N+1 DB reads
**Priority: Medium · Confidence: High для структуры запросов, Medium для значимости затрат · Тип: риск эффективности**
- **Evidence:** `backend/src/modules/banking/bank-cards/application/bank-operations.service.ts:41–68,333–365`. Каждая строка вызывает `operationView`, повторно читающий карту, связанные операции, категорию и возвраты.
- **Problem:** число reads растёт вместе с размером страницы; одинаковые связи загружаются многократно.
- **Impact:** лишняя работа Prisma/SQLite внутри transaction. Это embedded DB, поэтому нельзя приписывать ей стоимость сетевой БД, но повторные queries остаются.
- **Root cause:** mapper одной операции повторно используется как loader списка.
- **Recommendation:** получить нужные связи и refund totals для всей страницы одним набором запросов и передать их в преобразование. Сохранить transaction и ownership checks.
- **Trade-off:** list path станет немного отличаться от single-operation path. Реальный выигрыш в миллисекундах не измерялся.
## F12. Минимальная длина пароля имеет разные единицы
**Priority: Medium · Confidence: High · Тип: подтверждённое несоответствие**
- **Evidence:** backend — `src/modules/auth/presentation/auth.dto.ts:43–54,66–74`: минимум через `MinLength(12)`, максимум через UTF-8 bytes. Frontend — `src/features/auth/model/auth.schema.ts:36–42`: обе границы в байтах. Spec — `specs/openspec/specs/user-authentication/spec.md:10`.
- **Problem:** например, шесть кириллических букв проходят frontend byte-minimum, но отклоняются backend character-minimum.
- **Impact:** форма принимает пароль, который API не принимает.
- **Root cause:** смешаны два способа измерения одной policy.
- **Recommendation:** проверить обе границы в существующем byte-validator и убрать конфликтующий `MinLength`.
- **Trade-off:** некоторые не-ASCII пароли с меньшим числом символов станут допустимы согласно текущему требованию.
Расхождение уже отмечено в docs, но от этого не перестаёт быть дефектом.
## F13. Online-пути не используют существующий перевод ошибки брокерского токена
**Priority: Medium · Confidence: High · Тип: дефект error mapping**
- **Evidence:** `backend/src/integrations/tbank/tbank.client.ts:553–557` бросает обычный `Error(INVALID_TBANK_TOKEN)`. Online calls в `catalog-discovery.service.ts:149–165,227–249,295–317` и `catalog-enrichment.service.ts:241–267` не переводят его. Общий filter отдаёт для обычного Error `500` — `common/filters/http-exception.filter.ts:19–31`. Translator уже существует: `tbank-token.service.ts:26–35`.
- **Problem:** известная постоянная ошибка подключения выглядит как internal server error.
- **Impact:** frontend GET transport может повторить заведомо неуспешный запрос; UI теряет корректный recovery code.
- **Root cause:** новый online-путь обошёл прежнюю integration error boundary.
- **Recommendation:** переиспользовать existing translator на online application boundary, сохранив обработку durable attempts.
- **Trade-off:** появится явная связь online-пути с существующей token-error policy. Новый error registry не нужен.
## F14. Тесты MOEX закрепляют поведение, противоречащее актуальному требованию
**Priority: Medium · Confidence: High · Тип: конфликт контракта**
- **Evidence:** `backend/src/integrations/moex/moex.client.ts:182–185` считает отсутствующий блок ошибкой. `moex.client.spec.ts:121–191` и `test/upcoming-events.e2e-spec.ts:540–599` закрепляют это. `specs/openspec/specs/brokerage-account-upcoming-events/spec.md:82–84` требует трактовать отсутствие блока как отсутствие данных.
- **Problem:** код и тесты согласованы между собой, но не с сохранённым поведением.
- **Impact:** зелёные тесты не доказывают выполнение требования; прогноз может считаться неуспешным в сценарии, описанном как нормальное отсутствие событий.
- **Root cause:** отсутствие блока объединено с повреждённым форматом ответа.
- **Recommendation:** сначала согласовать смысл отсутствующего блока MOEX. Разделить отсутствие и malformed payload; не удалять проверки повреждённых строк/cursor.
- **Trade-off:** неверная трактовка отсутствия блока способна превратить неполный календарь в подтверждённый пустой. Поэтому здесь требуется решение о семантике, а не механическое сокращение guards.
## F15. Дублируется whitelist-преобразование instrument
**Priority: Low · Confidence: High · Тип: возможность упрощения**
- **Evidence:** одинаковые `publicInstrument` в `backend/src/modules/investments/instruments/application/catalog-discovery.service.ts:499–530` и `catalog-enrichment.service.ts:646–677`.
- **Problem:** одна boundary-модель имеет две поддерживаемые копии.
- **Impact:** новое поле или изменение безопасной проекции необходимо повторять; возможен drift.
- **Root cause:** online и persisted пути развивались отдельно.
- **Recommendation:** оставить одну функцию у текущего instrument boundary owner.
- **Trade-off:** один локальный import вместо независимых копий. Сам whitelist сохраняется.
## F16. Маршрут чтения документации ведёт к разным поколениям архитектуры
**Priority: Medium · Confidence: High · Тип: documentation drift**
- **Evidence:** `docs/architecture/containers.md:143–148` описывает запуск работы запросом и request-driven восстановление. `docs/adr/019-hourly-reads-and-online-instrument-reference.md:9–16` описывает нового владельца — hourly walk. В `specs/AI_INDEX.md:15–31` отсутствуют `hourly-investment-sync`, `tbank-data-sync` и `online-instrument-information`.
- **Problem:** актуальные правила существуют, но основной маршрут чтения не приводит к ним последовательно.
- **Impact:** разработчик может восстановить удаляемую модель или принять старое поведение за текущий invariant.
- **Root cause:** обновлены отдельные требования/ADR, но не все текущие объясняющие страницы и индекс.
- **Recommendation:** обновить существующий индекс и текущие архитектурные объяснения; подробную текущую механику держать в одной тематической странице, из остальных ссылаться на неё.
- **Trade-off:** часть страниц станет менее самодостаточной, зато уменьшится число расходящихся копий.
Исторические ADR и архивы сами по себе дефектом не являются.
## F17. Отдельный presentation VM не имеет самостоятельной ответственности
**Priority: Low · Confidence: Medium · Тип: предпочтение сопровождаемости**
- **Evidence:** `frontend/src/pages/stocks/ui/StocksPage/StocksPagePresentation.vm.ts:1–20` содержит только два callback для доступных названий Pagination; единственный consumer — `StocksPage.view.tsx:208`. Общая convention требует выносить callbacks — `.ai/conventions/ui.md:72–89`.
- **Problem:** для двух локальных presentation-функций добавлен отдельный маршрут навигации по коду.
- **Impact:** небольшая дополнительная cognitive cost без установленного выигрыша в поведении или производительности.
- **Root cause:** вероятно, формальное применение общего VM-паттерна.
- **Recommendation:** при ближайшем изменении держать callbacks рядом с Pagination, сохранив доступные названия. Самостоятельный VM оставлять для формы, fetching или связного lifecycle.
- **Trade-off:** меньше единообразия структуры. **Это необязательный cleanup, не архитектурный дефект.**
# Simplification Opportunities
Ниже — отдельный complexity-pass в формате `ponytail-audit`, от большего сокращения к меньшему:
- **`delete:`** удалить недоступные `TbankSyncService.request/dispatch`, исключительно используемую нормализацию и её тесты; оставить необходимый status reader. [`backend/src/modules/investments/tbank-sync/`]
- **`delete:`** удалить frontend `observe`/`observationKey`, если подтверждено отсутствие внешних consumers: в production callers не найдены, вызовы есть только в тестах. Обычный `refetch` уже обслуживает текущий сценарий. [`frontend/src/shared/api/resource-query.ts:69–127`]
- **`native:`** передать периодическое чтение TanStack Query `refetchInterval`; убрать собственный timer/ref coordination и неиспользуемые screen descriptors. [`frontend/src/features/tbank-connection/model/screen-sync.ts`]
- **`shrink:`** объединить две одинаковые `publicInstrument`; сохранить одну безопасную проекцию. [`backend/src/modules/investments/instruments/application/`]
- **`delete:`** удалить прямой passthrough-тест controller; identity/guard/query проверять существующей configured HTTP suite. [`backend/src/modules/investments/upcoming-events/upcoming-events.controller.spec.ts:1–20`]
- **`yagni:`** объединить двухфункциональный presentation VM с его единственным view при ближайшем изменении. [`frontend/src/pages/stocks/ui/StocksPage/`]
**net: -~550 lines, -0 deps possible.**
Это приблизительная оценка наиболее очевидных сокращений, не размер подготовленного diff. Подтверждённых кандидатов на удаление dependency не установлено.
# Testing Assessment
## Что действительно полезно
| Проверки | Почему стоит сохранить |
|---|---|
| `backend/test/app.e2e-spec.ts:161–213` | Конкурентная refresh rotation проходит через реальный SQLite CAS; проверяется дальнейшая работоспособность победителя |
| `backend/test/bank-cards.e2e-spec.ts`, особенно refund/concurrency/replay cases | Проверяются финансовые ограничения, версии, rollback и недопущение повторного создания |
| Decimal, RUB-money, portfolio calculations | Различают точные суммы, валюты, неизвестные значения и ноль |
| Operations persistence/coverage и forecast publication tests | Проверяют реальные инварианты хранения и согласованности, которые mock Prisma не доказывает |
| Frontend RTL/MSW формы и auth transport | Проверяют видимый результат, отсутствие invalid requests, сохранение ввода и session boundary |
| Browser layout tests | Проверяют свойства реального layout, недоступные JSDOM |
| Отдельный performance benchmark | Измеряет конкретный persisted-read budget; уже исключён из обычного набора functional suites |
## Что удалить, заменить или перенести
| Проблема | Действие |
|---|---|
| Passthrough controller unit test | **Удалить**: сейчас проверяет вызов mock; заменить недостающее поведение assertion в существующем HTTP-тесте |
| Late portfolio test без запуска source work | **Заменить** реальной управляемой гонкой — F8 |
| Frontend race test без ожидания старого ответа | **Усилить** барьером завершения; probe сам по себе не проблема |
| Тесты «тихого» cached error на AccountPage | **Пересмотреть ожидаемое поведение**: оставить сохранение данных, проверять предупреждение и retry — F5 |
| Manual screen normalization tests | **Удалить вместе с недоступным production-сценарием**, не переносить в новую abstraction |
| MOEX missing-block tests | **Согласовать ожидание с контрактом**, сохранить malformed/cursor checks — F14 |
## Самые важные недостающие сценарии
1. **Hourly lifecycle:** первый запуск, повтор в срок, длинный обход с пропуском срока, restart и persistence failure.
2. **Два последовательных успешных календарных обновления:** изменившийся upstream действительно меняет сохранённый календарь.
3. **Forecast следующего дня:** новый полный результат заменяет прежний; узкий GET фильтрует сохранённый результат; непересекающееся окно возвращает предусмотренную пустоту.
4. **Обычный income GET без `sync=observe`:** не создаёт personal work.
5. **Disabled query на timer/focus boundary:** неподготовленный identifier не вызывает request.
6. **Online invalid broker token:** публичный code постоянной ошибки без транспортных повторов.
7. **Пароль на UTF-8 границах:** frontend и configured backend принимают одинаковые значения.
Достаточно существующих Jest, RTL/MSW и SQLite runner. Общий процент покрытия здесь ничего не решает: основной недостаток — **несоответствие некоторых тестовых сценариев текущему data flow**.
# API & Data Flow: прослеженные сценарии
| Сценарий | Фактическая цепочка | Вывод |
|---|---|---|
| Открытие счёта | Page VM → несколько resource GET → локальные repositories → snapshots → VM/view | Локальная read-модель полезна; metadata теряется на последнем переходе |
| Часовое обновление | Native timer → durable reservation → membership → portfolios → operations → calendars → forecast | Есть основания сохранить coordinator; ошибки lifecycle и stable calendars требуют исправления |
| Поиск облигаций / online lookup | Debounced Query → backend discovery → search + N details + prices → DTO → карточки | Есть fan-out и ненужная последовательность prices после details |
| Скринер акций | POST → полный Shares → отбор → ограниченный snapshot → GET страниц | Snapshot оправдан устойчивостью страниц; повторное получение каталога соответствует текущему требованию |
| Создание банковской операции | Form intent → mutation → validation/transaction → fingerprint/version → DTO → invalidation | Защиты обоснованны; list reads можно сделать дешевле без изменения write-модели |
# Top 10 Improvements
**S** — локальная правка и небольшая проверка. **M** — несколько связанных владельцев/сценариев и integration proof.
| № | Изменение | Ожидаемый эффект | Сложность |
|---:|---|---|---|
| 1 | Передать `source` и восстановить error/retry presentation AccountPage — F5 | Пользователь получает контекст показанных сумм; очень маленький production diff | S |
| 2 | Удалить недоступный manual screen-sync путь — F9 | Наибольшее сокращение кода, тестов и DI-навигации | S–M |
| 3 | Свести периодическое чтение к query-owner — F7 | Меньше собственных timers и запросов с неподготовленными inputs | S–M |
| 4 | Исправить vacuous race tests и добавить proof hourly lifecycle — F8 | Тесты начинают ловить реальные регрессии нового data flow | M |
| 5 | Исправить выбор/публикацию последнего forecast — F1 | Устраняет удержание старого результата после успешного обновления | M |
| 6 | Сделать завершение hourly ошибок явным — F2 | Убирает ложный успех и риск остановки последующих обновлений | M |
| 7 | Обновлять календарные источники из hourly owner — F3 | Прогноз получает изменения выплат, а не только новые количества бумаг | S–M |
| 8 | Убрать request-driven membership из income read — F6 | Один владелец personal refresh, меньше скрытых RPC | S–M, после ответа о cold history |
| 9 | Согласовать byte-validation пароля — F12 | Убирает подтверждённое frontend/backend расхождение | S |
| 10 | Переиспользовать invalid-token translator в online-путях — F13 | Корректный recovery code и отсутствие бесполезных повторов | S–M |
N+1 банковской истории и сокращение search detail fan-out разумны следующим этапом: структура затрат доказана, но фактический выигрыш и допустимый набор полей требуют уточнения.
# Do Not Change
1. **Модульный NestJS-монолит с конкретными providers.** Не обнаружена необходимость в обязательных ports/interfaces, CQRS или отдельном сервисе на каждый use case.
2. **SQLite и single-process coordination.** Для текущей топологии это существенно проще распределённой системы. Ошибки lifecycle следует исправить внутри существующей модели.
3. **Decimal strings и bigint-копейки.** Они сохраняют точность и смысл неизвестной суммы; замена на обычный `number` будет ухудшением.
4. **Connection revision, CAS и conditional publication.** Они защищают от публикации результата под другим подключением. Это необходимая сложность.
5. **Bank intent fingerprints, версии, tombstones и refund transactions.** Проверенные сценарии lost response, replay и конкурирующих изменений оправдывают эти механизмы.
6. **Generated OpenAPI и производные frontend types.** Это не бесполезное дублирование моделей, а граница независимых репозиториев.
7. **Snapshots скринера.** Они обеспечивают устойчивые страницы одного результата. Пересчитывать каталог при каждом GET страницы проще лишь внешне.
8. **Mantine controls, React Hook Form и необходимые page VMs.** В формах и data-driven screens у них есть конкретная ответственность. Оснований дробить большие views по числу строк не найдено.
9. **Общая тема для приложения и Storybook.** Два реальных consumers оправдывают visual foundation packages.
10. **Исторические ADR и OpenSpec archives.** Их следует отличать от актуальных требований, а не удалять ради уменьшения количества файлов.
Признаки возможной AI-assisted сложности здесь объективны: недоступная orchestration, тесты старого сценария, дублирующие boundary mappers и формальная декомпозиция. **Происхождение кода по этим признакам не устанавливается.**
# Open Questions
1. **Forecast:** должен ли hourly walk сохранять новый полный результат для каждого нового дня независимо от HTTP-запросов? Как должен агрегироваться `all`, если сохранены отдельные account forecasts?
2. **Исторический income:** кто загружает отсутствующие периоды после отказа от request-driven импорта? Должен ли пользователь предварительно открывать Ledger?
3. **Клиентская свежесть:** правило «не чаще 30 минут» относится только к timer или также к focus, reconnect и повторному mount?
4. **MOEX:** отсутствие запрошенного блока действительно подтверждает отсутствие событий или может означать неполный ответ? Сейчас spec и тесты отвечают по-разному.
5. **Совместимость:** есть ли внешние consumers `GET /tbank-sync/:id` и observe-mode instrument endpoints? Это определяет объём удаления старого read compatibility.
6. **Search UI:** какие поля требуют detail RPC для каждого результата, а какие уже присутствуют в search response?
7. **Налоговая формулировка:** подтверждён ли смысл `dividend_net` для подписи «до налогов»? В docs это уже отмечено как неразрешённая интерпретация.
8. **Рабочий масштаб:** сколько подключений и инструментов ожидается одновременно? Это нужно для оценки бюджета hourly calendar refresh и значимости N+1.
**Итог:** сначала устранить рассогласования нового data flow и сделать его тесты достоверными; затем удалить неиспользуемую координацию. Это даст больше пользы, чем добавление новых архитектурных практик или инструментов.