Files
main/audits/2026-10-11/reports/03-duplication-and-reusability-report.md
T

891 lines
88 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Аудит 03 — Duplication & Reusability
## 1. Audit metadata
- **Audit ID/name:** 03 / Duplication & Reusability.
- **Дата:** 2026-10-11.
- **Workspace:** `/Users/ksv741/Project/vibe/WealthLedger`.
- **Режим:** независимое read-only исследование; единственный записанный
артефакт этого исполнителя — настоящий отчёт вне четырёх checkout.
- **Протокол:** `audit-prompts/03-duplication-and-reusability.md`, прочитан
целиком. Другие новые отчёты не читались и не перезаписывались.
- **Skill:** установленный `ponytail-audit` загружен через Skill tool до
исследования. Его complexity-only подход применён к предложениям
`SIMPLIFY`; подтверждённое рассогласование validation policy разобрано
отдельно по контракту аудита, не приписано correctness-проверке skill.
Дополнительно использованы `feature-sliced-design` для ownership,
`systematic-debugging` для трассировки причины DRY-001 и
`verification-before-completion` для проверки результата исследования.
- **Graphify:** skill загружен. По известному абсолютному пути root
`graphify-out/graph.json` отсутствует; поиски этого artifact внутри четырёх
checkout также не дали результатов. Glob может пропускать ignored paths,
поэтому отсутствие любого скрытого графа не доказано. Доступного existing
graph для query не обнаружено; анализ продолжен статически. Генерация,
установка, индексация и создание graphify artifacts не выполнялись.
### Репозитории и самостоятельно снятое состояние
Все пути в таблице абсолютные. В остальных evidence путь начинается с имени
checkout и считается относительно workspace.
| Repo | Path | HEAD SHA | Начальный `git status --short` | После исследования и сохранения отчёта |
| --- | --- | --- | --- | --- |
| root | `/Users/ksv741/Project/vibe/WealthLedger` | `dc030f84c979cedb2628ef4bd97c62d6c63a9256` | пустой | пустой; отчёт ignored |
| frontend | `/Users/ksv741/Project/vibe/WealthLedger/frontend` | `c8821200ce699dab1ce65360d1968d5877a2e78b` | пустой | пустой |
| backend | `/Users/ksv741/Project/vibe/WealthLedger/backend` | `0ad84ff4f0b4712abc7e69dc299856f14af6eadd` | пустой | пустой |
| specs | `/Users/ksv741/Project/vibe/WealthLedger/specs` | `fcdc805f6c1b3be6a20a7e858e3e80cbd83a9121` | пустой | пустой |
| docs | `/Users/ksv741/Project/vibe/WealthLedger/docs` | `e6fd4f64b81dd73b33959b845a1babcfac1891c8` | пустой | пустой |
`AGENTS.md` каждого checkout прочитан до исследования его содержимого.
Прочитаны локальные architecture/engineering и активированные темы:
frontend data, TypeScript/React, UI, testing, Storybook; backend HTTP API,
data, errors, security/auth, integrations, testing. Specs выбирались через
`AI_INDEX.md`; docs — через README/CONTRIBUTING и тематические страницы.
Root conventions не применялись к соседним checkout. Локальный FSD profile
и его реальные import restrictions важнее общего совета о размещении CRUD.
### Выполненные проверки и границы исполнения
1. В каждом repo самостоятельно выполнены `git rev-parse HEAD` и
`git status --short`: в начале, после исследования и перед завершением.
2. Использованы Read/Glob/Grep по конкретным repo paths для поиска символов,
callers, требований и существующих тестовых сценариев.
3. Выполнен read-only
`git diff --no-index --stat backend/openapi.json frontend/src/shared/api/spec/openapi.json`
из root: различий нет. Это сравнение existing файлов, а не доказательство
свежести генерации относительно runtime.
4. Проверены `git diff --check` и отдельно whitespace нового ignored отчёта
через `git diff --no-index --check /dev/null audit-results/03-duplication-and-reusability-report.md`.
Проверен `git check-ignore -v` для отчёта; root allowlist сохранён.
5. Выполнена содержательная сверка отчёта с девятью общими разделами,
специальными результатами, всеми finding fields и финальной таблицей.
Read-only `rg --count-matches` подтвердил 9 общих заголовков, 10 DRY sections,
150 полей (15 на finding, включая Runtime consumer) и 3 Ownership model
для 3 SIMPLIFY. Агрегатный подсчёт дополнен чтением каждого блока.
Тесты не запускались. Node/npm-команды, install, codegen, migrations, build,
full-suite/verify, commit/PR и обращения к внешним API не выполнялись.
Исходники, конфигурации, snapshots, dated audits, архивы и references не
изменялись. Файлы БД, `.env`, секреты и реальные пользовательские данные не
читались. Два узких installed-library файла прочитаны только для проверки
семантики `MinLength`, не как кандидаты на ручной DRY cleanup.
`audit-results/technical-audit-2026-10-11.md` использован как набор гипотез:
F12 и F15 независимо подтверждены исходниками и callers. Другие прежние
рекомендации не принимались автоматически. Числа о LOC, fan-out и времени из
него не перенесены как собственные измерения.
При сводной перепроверке независимо повторён source trace discovery и всех
production callers `useScreenSync`. Исправлена прежняя ошибка reachability:
catalog HTTP paths не запускают discovery producer, а возвращаемый screen
retry не имеет найденных production consumers. DRY-009 сужен до живого
enrichment и используемых repository fences. Соседние отчёты не читались;
reachability/deletion discovery переданы audit 04, без требования KEEP этого
producer. Исходные SHA при перепроверке совпадают с таблицей выше.
## 2. Executive verdict
1. **Факт — DRY-001:** одна password policy имеет разные единицы в frontend
и backend. UI допускает не-ASCII пароль, который отклоняет API. Это
значимое локальное рассогласование, а не основание удалить серверную
валидацию или создать межрепозиторную библиотеку validators.
2. **Факт — DRY-002:** одинаковая whitelist-проекция instrument поддерживается
в двух сервисах одного backend module. У правила один владелец, одинаковые
типы, ошибки и причины изменения. Локальный mapper даст самый убедительный
reuse при сохранении самого whitelist.
3. **Риски сопровождения — DRY-003/004:** frontend повторяет список платёжных
систем внутри одной формы и один набор access-loss codes в трёх контекстах.
Сегодня копии согласованы; текущих пользовательских дефектов не доказано.
Это небольшие P3 cleanup-кандидаты, не обязательная большая миграция.
4. **Факт — DRY-005–009:** денежные trust boundaries, stock/bond семантика,
session-aware query wrappers, generated API copies и живой enrichment с
repository publication fence имеют конкретные гарантии. DRY-009 не требует
сохранять discovery `request/refresh/drain`: current HTTP consumers их
обходят, найденный caller `refresh` — только недоступный manual dispatch.
5. **Непроверенная гипотеза — DRY-010:** часть screen-level periodic-read
orchestration может перейти к query owners. Наличие нескольких triggers
подтверждено, но равноценность замены и runtime-экономия — `UNVERIFIED`.
Все восемь production callers игнорируют hook return; общей кнопки retry
из этого hook не найдено. Реальный counterargument — mount-based timer phase
с чтением текущего resource array. Разные `staleTime` не доказывают DRY-долг.
**Итог по реестру:** 4 actionable findings: P0 — 0, P1 — 0, P2 — 1,
P3 — 3; отдельно KEEP — 5, INVESTIGATE — 1. Actionable здесь означает
`FIX`, `SIMPLIFY` или `DELETE`; DELETE-кандидатов в scope 03 не подтверждено.
Подсчёт — по уникальным DRY-ID в разделе 4, без повторного подсчёта строк
Knowledge Register и Cross-audit signals. P2 у KEEP означает значимость
защищаемого поведения, а не urgency cleanup.
## 3. Inventory & coverage
### Фактически прослеженные области
| Область | Прослеженный flow / объекты | Граница вывода |
| --- | --- | --- |
| Auth validation | `AuthForm` → Zod schemas → auth provider/client → `CredentialsDto` через configured pipe → `AuthController` → `AuthService`; spec и docs | Установлены разные единицы длины; HTTP-воспроизведение не запускалось |
| Catalog projection | Stock/Bond controllers → catalog services → online discovery либо read-only observe; quote/company paths → enrichment section/load | Две одинаковые detail-проекции; catalog HTTP paths не запускают discovery producer |
| Banking forms | `form.schema.ts`, fields/form VM, generated bank-card types, backend DTO/input/RUB helpers, operation validation/transaction | Разделены локальная копия словаря и необходимое повторение trust-boundary checks |
| Frontend errors | client permanent classification, resource access-group invalidation, Account/Accounts VM и error views | Совпадает один набор codes; recovery actions и Accounts subset различаются |
| Stock/Bond UI | Page containers/VM/views, payout components, `instrument-details` model/UI, entity queries | Подтвержден existing reuse и реальная разница dividend/coupon и денежных единиц |
| Query identity/lifecycle | `apiQueryClient`, authenticated/resource queries, account/portfolio/stock/bond/bank hooks, connection boundary, screen timer | Не обнаружен competing handwritten key для изученных investment queries; банковский `all` — отдельный агрегат |
| Backend state transitions | Discovery entry-point trace отдельно; живой enrichment reservation/drain → repository publishSource/finish | KEEP касается enrichment/shared fence, не discovery producer; reachability/deletion — audit 04 |
| Contracts/config | manifests, `main.ts`, `openapi.ts`, frontend dependency-cruiser, API snapshot и точечные generated declarations | Generated files не исследовались как ручные дубли; freshness генерации не доказана |
| Calendar/money owners | existing investment calendar, instrument-market, shared display, common decimal, T-Bank protobuf decimal | Не предложена общая библиотека на основании похожего имени или арифметики |
Выборка risk-/cost-based: приоритет получили публичные проекции, валидируемые
политики, money/null semantics, recovery и повторяющиеся transitions.
Тесты читались как свидетельства задуманного поведения и кандидаты для
будущей проверки, не проводился отдельный аудит их качества или factories.
**Метод чисел:** количество копий получено symbol/content search с ручным
сопоставлением конкретных реализаций и consumers. Для `publicInstrument`
найдены две definitions и четыре call sites: discovery `:157,323`, enrichment
`:263,349`. Для access-loss набора — три полных места; transport содержит
ещё `UNAUTHORIZED`, а Accounts VM использует только два connection codes.
Для payment systems — две независимо редактируемые frontend runtime lists;
backend tuple и производные artifacts перечислены отдельно. Глобальные
проценты дублей, прочитанных файлов, LOC и coverage не вычислялись.
Восемь production call sites `useScreenSync` получены поиском символа в
`frontend/src` с исключением declarations/re-exports/tests и чтением каждого
полного VM. Во всех случаях вызов — standalone expression, без присваивания,
destructuring или передачи return дальше; таблица ниже фиксирует полный набор.
### Уточнённый consumer trace: discovery и живой enrichment
| Вход | Подтверждённая цепочка / evidence | Достигает discovery `request/drain`? |
| --- | --- | --- |
| Catalog HTTP endpoints | `backend/src/modules/investments/instruments/stocks/presentation/stock.controller.ts:58–82,112–119`; `backend/src/modules/investments/instruments/bonds/presentation/bond.controller.ts:55–89` передают `query.sync === 'observe'`; `backend/src/modules/investments/instruments/stocks/application/stock-catalog.service.ts:22–89` и `backend/src/modules/investments/instruments/bonds/application/bond-catalog.service.ts:25–88` вызывают только `search/detail/byTicker` | Нет при обоих значениях observe |
| Default `search` | `backend/src/modules/investments/instruments/application/catalog-discovery.service.ts:84–86,295–337`: ранний return `onlineSearch`, RPC напрямую через client | Нет |
| Default `detail` / `byTicker` | Тот же файл `:149–166,227–249`: online branches; `byTicker(false)` вызывает `detail` с default false | Нет |
| Observe branches | Тот же файл `:86–93,168–190,251–267`: `read(..., true)` и `enrichment.readMany(..., true)`; `read` запускает request только при `!observe` (`:66–70`) | Нет |
| Discovery producer entry | Тот же файл `:340–386,395–396`: `request` ставит drain, `refresh` вызывает request. Единственный найденный внешний production-text caller refresh — `backend/src/modules/investments/tbank-sync/tbank-sync.service.ts:192–201` внутри `dispatch` | Только dead-parent-reachable по найденному src trace, не текущий HTTP flow |
| Parent entry point | `backend/src/modules/investments/tbank-sync/tbank-sync.service.ts:54–78`: dispatch запускается только из request; `backend/src/modules/investments/tbank-sync/tbank-sync.controller.ts:31–41` вызывает только status; `backend/src/modules/investments/tbank-sync/tbank-sync.module.ts:23–25` регистрирует service, экспортирует scheduler, но не запускает request | Production consumer request не найден; DI registration не является вызовом |
| Реальный enrichment producer | `backend/src/modules/investments/instruments/stocks/presentation/stock.controller.ts:122–129` → `backend/src/modules/investments/instruments/stocks/application/stock-market.service.ts:34–45` → `backend/src/modules/investments/instruments/application/catalog-enrichment.service.ts:234–241,269–289,113–125,189–225,497–577` | Это другой, живой producer: `quote(false)` → `section` → `readMany(false)` → при missing/stale `requestMany` → drain → publishSource |
В таблице «тот же файл» означает указанный выше полный path discovery service.
Поиск ссылок `CatalogDiscoveryService`/`discovery.refresh` и
`TbankSyncService` в `backend/src` не обнаружил другого producer entry point.
Прямые вызовы `read(false)`/`request` существуют в
`backend/test/tbank-catalog-discovery.e2e-spec.ts:86,184–187,202–203`; они
подтверждают test reachability, но не production reachability. Удаление
неживой producer-ветки и точные границы compatibility — предмет audit 04.
### Production callers `useScreenSync`: return не используется
| Call site | Фактический consumer |
| --- | --- |
| `frontend/src/pages/account/ui/AccountPage/AccountPage.vm.ts:78–84` | Standalone вызов для пяти resources; return отброшен |
| `frontend/src/pages/accounts/ui/AccountsPage/AccountsPage.vm.ts:23` | Standalone вызов для accounts; return отброшен |
| `frontend/src/pages/account-events/ui/AccountEventsPage/AccountEventsPage.vm.ts:67–75` | Standalone вызов для events; return отброшен |
| `frontend/src/pages/account-income/ui/AccountIncomePage/AccountIncomePage.vm.ts:66–69` | Standalone вызов для analytics; return отброшен |
| `frontend/src/pages/account-operations/ui/AccountOperationsPage/AccountOperationsPage.vm.ts:96–99` | Standalone вызов для operations; return отброшен |
| `frontend/src/pages/stock/ui/StockPage/StockPage.vm.ts:45–55` | Standalone вызов для stock sections; return отброшен |
| `frontend/src/pages/bond/ui/BondPage/BondPage.vm.ts:45–55` | Standalone вызов для bond sections; return отброшен |
| `frontend/src/pages/bonds/ui/BondsPage/BondsPage.vm.ts:13` | Standalone вызов для search result; return отброшен |
Только test harness сохраняет return и вызывает retry/refresh:
`frontend/src/features/tbank-connection/model/screen-sync.test.tsx:37–50,68–78`.
Реальный production side effect hook — timer
`frontend/src/features/tbank-connection/model/screen-sync.ts:22–35`, не его
return `:53–61`. Timer effect имеет `[]`: его clock начинается при mount,
не перезапускается из-за resource array change; callback читает актуальный ref.
**Вне охвата:** полный clone detector по всему tree, generated/vendor/proto
duplication, зависимости как предмет отдельного аудита, dead code (04),
крупные module boundaries (01), эффективность API (07), качество tests (08),
test infrastructure/factories (09), docs/SDD duplication (15). Root release/CI
и похожие configs самостоятельных repos не объявлены DRY-долгом: они не
должны становиться общей обязательной tooling-зависимостью.
### Knowledge Duplication Register
| Rule/behavior | Copies | Divergence risk | Single owner? | Extraction trade-off | Decision |
| --- | --- | --- | --- | --- | --- |
| Password 12–72 UTF-8 bytes | FE `auth.schema.ts:10–11,36–43`; BE `auth.dto.ts:16–17,43–74`; snapshot `:5246–5250` | Нижняя граница уже разошлась по единицам | Да: auth policy; server владеет enforcement, spec фиксирует требование | Согласовать две независимые проверки; TS generator не реализует byte validation | FIX / DRY-001 |
| Detail instrument whitelist | `catalog-discovery.service.ts:499–531`; `catalog-enrichment.service.ts:646–678` | Новый field/filter требует двух правок | Да: instruments boundary | Один локальный mapper; не объединять online и durable execution | SIMPLIFY / DRY-002 |
| Runtime payment systems формы | FE `form.schema.ts:65–69`; `BankCardFields.vm.ts:19–22`; BE tuple `bank-card.dto.ts:45` | FE schema и selectable options могут расходиться | Да внутри FE bank-card; BE отдельно владеет wire vocabulary | Tuple у существующей schema; generated union проверяет допустимость, но не даёт runtime list | SIMPLIFY / DRY-003 |
| Четыре access-loss codes | FE `client.ts:162–169`; `resource-query.ts:42–51`; `AccountPage.vm.ts:59–64` | Добавление code может изменить retry, purge и page state по-разному | Да: API code taxonomy; actions имеют разных owners | Один predicate, сохранить отдельные действия и дополнительный auth code | SIMPLIFY / DRY-004 |
| Independent banking validation | FE `form.schema.ts:8–42`, `OperationForm.schema.ts:53–131`; BE `rub-money.ts:12–30`, `bank-operations.service.ts:94–210` | Значения policy требуют согласования; implementations специально различаются | Server — authority, browser — UX snapshot | Shared runtime validator не заменит ownership/CAS/transaction и ухудшит repo autonomy | KEEP / DRY-005 |
| Dividend/coupon forecast и UI | BE stock `stock-income.service.ts:114–178`, bond `bond-income.service.ts:145–201`; FE payout views | Опасность скрыть payment/record/date и per-share/per-bond differences | Нет для всего процесса; общие primitives уже имеют owners | Generic payout с mode/flags объединит разные причины изменения | KEEP / DRY-006 |
| Stock/Bond query declarations | FE `stock/api/queries.ts:21–95`, `bond/api/queries.ts:21–104` | Малое локальное повторение, разные endpoints/accessGroup/contracts | Два entity owners; transport общий | Оставить endpoint-specific generated options, не строить string-templated query factory | KEEP / DRY-006 |
| Query wrapper / Mantine primitives | `authenticated-query.ts:34–59`, `resource-query.ts:37–67,84–149`; UI packages | Удаление defaults потеряет session/access/resource/ARIA поведение | Да для каждой существующей focused boundary | Нативные controls плюс project defaults уже используются; passthrough переименование не предлагается | KEEP / DRY-007 |
| OpenAPI → snapshot → TS aliases | BE DTO/Swagger, FE snapshot/generated, `bank-card/api/types.ts`, `auth-context.ts` | Freshness artifact остаётся отдельным вопросом | Backend editable source; FE локальный consumer artifact | Сохранить offline generation, не создавать shared DTO package/runtime validator | KEEP / DRY-008 |
| Known UI errors vs open wire string | FE `problem-details.ts:1–28`; generated `schema.d.ts:731–747` | Unknown code должен сохранять fallback | Server code owners и FE known subset имеют разные задачи | Не превращать UI enum в закрытый весь wire contract | KEEP / DRY-008 |
| Живой enrichment vs похожий discovery producer | BE enrichment `:113–125,189–225,497–593`; discovery `:66–70,84–86,149–166,227–267,340–495` | Generic extraction из неживой ветки добавит framework; изменение enrichment может потерять batching/fences | Enrichment владеет текущим producer; общая repository fence уже есть | Сохранить только используемые enrichment guarantees; discovery reachability/deletion — audit 04, не KEEP | KEEP / DRY-009 (узкий scope) |
| Periodic reads и freshness triggers | FE `screen-sync.ts:22–35`; восемь standalone page calls; `App.tsx:13–20`; entity stale times | Триггеры распределены; return/retry не используются, runtime waste не измерен | Scheduling owner требует решения; freshness не равно interval | Query-owned interval требует проверки mount phase/latest-resource semantics, не гипотетической общей кнопки | INVESTIGATE / DRY-010 |
В таблице короткие имена относятся к точным полным paths из findings ниже.
Скопированные generated значения не посчитаны независимо editable знаниями.
### Intentional Duplication to Keep
| Место / evidence | Runtime consumer | Почему повторение оправдано | Что нельзя потерять |
| --- | --- | --- | --- |
| `frontend/src/entities/bank-card/model/money.ts:4–23` и `backend/src/modules/banking/bank-cards/domain/rub-money.ts:12–30` | Form draft → normalized request → server application input | UI допускает пробелы/запятую/leading zeros; server принимает канонический RUB decimal и проверяет storage range | bigint precision, positive checks, ошибки вместо округления |
| `frontend/src/pages/card/ui/OperationForm/OperationForm.schema.ts:53–131` и `backend/src/modules/banking/bank-cards/application/bank-operations.service.ts:94–210` | Draft validation и create/update operation | UI проверяет cached список; server повторно читает authoritative user-owned records в transaction | Cross-user isolation, refund CAS/limit, rollback, replay |
| `frontend/src/entities/stock/api/queries.ts:21–95`; `frontend/src/entities/bond/api/queries.ts:21–104`; `backend/src/modules/investments/instruments/stocks/application/stock-income.service.ts:114–178`; `backend/src/modules/investments/instruments/bonds/application/bond-income.service.ts:145–201` | Stock/Bond route и HTTP resource | Сходная композиция, разные payloads, null/date rules, quote units; общие chart/position/layout уже переиспользуются | Не смешать проценты номинала и Money, registry и payment date |
| `backend/src/modules/investments/instruments/application/catalog-enrichment.service.ts:113–125,189–225,497–577`; `backend/src/modules/investments/instruments/infrastructure/catalog-discovery.repository.ts:154–179` | Живой quote(false) → section → readMany(false) → enrichment producer → publishSource | Local enrichment использует batching/observers, repository уже разделяет authorization/publication checks; generic queue из сходства с неживым discovery не нужна | Authorized producer, private work handles, conditional publication только используемого enrichment; discovery producer не входит в KEEP |
| `backend/src/common/money.types.ts:1`; `backend/src/common/money.dto.ts:3–10`; `frontend/src/shared/api/generated/schema.d.ts:763–795` | Swagger/ValidationPipe и typed API client | Runtime metadata и независимые репозитории требуют разных форм представления одного контракта | Offline frontend input, open unknown errors, отсутствие Swagger в pure domain |
Эта выборка показывает несколько разных оснований KEEP, а не исключения,
добавленные после обязательного требования «всё объединить».
## 4. Findings
### DRY-001 — Password policy расходится по единицам, а не по месту проверки
- **Decision:** FIX.
- **Severity:** P2.
- **Confidence:** High.
- **Evidence:** `frontend/src/features/auth/model/auth.schema.ts:10–11,36–43`;
`backend/src/modules/auth/presentation/auth.dto.ts:16–17,43–74`;
`backend/src/main.ts:31–39`;
`specs/openspec/specs/user-authentication/spec.md:9–10,109–110`.
Installed semantics независимо проверены в
`backend/node_modules/class-validator/cjs/decorator/string/MinLength.js:14–29`
и `backend/node_modules/validator/lib/isLength.js:23–26`.
- **Claim:** две проверки одного принятого правила имеют несовместимый
минимум: frontend — bytes, backend — character length.
- **Current mechanism:** browser считает обе границы TextEncoder-ом;
`CredentialsDto` использует `MinLength(12)` плюс только upper-byte validator.
Например, `абвгде` — шесть кириллических символов / двенадцать UTF-8 байтов:
проходит FE правило 12–72, не проходит BE минимум 12 characters. Это вывод
по коду и encoding, не результат выполненного HTTP-теста.
- **Runtime consumer:** `frontend/src/features/auth/ui/AuthForm/AuthForm.tsx:24–40`
выбирает обе schemas; provider отправляет password без изменения
(`frontend/src/features/auth/model/auth.vm.ts:14–24,121–132`). Register/login
принимают тот же DTO (`backend/src/modules/auth/presentation/auth.controller.ts:55–79`),
далее password поступает в bcrypt (`backend/src/modules/auth/application/auth.service.ts:35–78`).
- **Impact / actual cost:** UI принимает draft, API отказывает; AuthForm
показывает общий requestError вместо успешного auth. Impact unmeasured:
частота таких паролей и пользовательских отказов неизвестна.
- **Why now:** уже доказан drift единицы policy; изменение локально у auth
owner и не требует нового validator framework.
- **Minimum change:** обе byte-границы проверять существующим
`PasswordBytesConstraint`, убрать несовместимый character minimum.
Согласовать Swagger описание bytes: snapshot сейчас содержит character
`minLength: 12` (`frontend/src/shared/api/spec/openapi.json:5246–5250`), что
не описывает byte-policy. При реализации явно исправить metadata и передать
обновлённый artifact потребителю, не править generated TS вручную.
- **Guarantees at risk:** server-side validation, bcrypt 72-byte ceiling,
отсутствие trim/normalization password, совместимость login и error pointers.
- **Counterarguments / trade-offs:** character minimum может быть отдельным
security/product решением, но такого изменения не подтверждают текущая spec
и docs (`docs/domains/authentication/overview.md:14–21`). Independent
validation нужна; общего исполняемого пакета между repos не нужно.
- **Verification or rollback:** будущая минимальная проверка — frontend schema
и configured HTTP DTO boundary на 11/12/72/73 bytes, ASCII/кириллице/emoji,
password whitespace без изменения. Existing DTO checks находятся в
`backend/src/common/dto-validation.spec.ts:13–30`; HTTP flow должен сохранить
400/VALIDATION_ERROR и success auth. Не менять stored hashes; rollback —
возврат DTO/metadata/artifact совместно. В этом аудите проверки не запускались.
- **Effort:** M.
- **Related finding IDs:** DRY-005, DRY-008.
### DRY-002 — Одна instrument detail projection имеет две идентичные копии
- **Decision:** SIMPLIFY.
- **Severity:** P3.
- **Confidence:** High.
- **Evidence:** `backend/src/modules/investments/instruments/application/catalog-discovery.service.ts:499–531`;
`backend/src/modules/investments/instruments/application/catalog-enrichment.service.ts:646–678`.
- **Claim:** `publicInstrument` копирует один и тот же allowlist для одного
`TbankBond | TbankShare` input, включая одинаковый brand.logoName whitelist.
- **Current mechanism:** две чистые функции без side effects или различающейся
error/null политики. Обе сохраняют optional/undefined fields; отсутствие
instrument обрабатывают callers, а не функция.
- **Runtime consumer:** online detail/search —
`backend/src/modules/investments/instruments/application/catalog-discovery.service.ts:143–165,295–325`;
company online и durable detail/bond-current load —
`backend/src/modules/investments/instruments/application/catalog-enrichment.service.ts:241–267,339–350`.
Ответы преобразуют
`backend/src/modules/investments/instruments/stocks/application/stock-catalog.service.ts:59–89`
и `backend/src/modules/investments/instruments/bonds/application/bond-catalog.service.ts:58–88`;
сохранение проходит через
`backend/src/modules/investments/instruments/infrastructure/catalog-discovery.repository.ts:154–223,252–276`.
- **Impact / actual cost:** изменение разрешённого field требует двух правок;
drift может сделать online и saved representation разными. Текущего drift
этих функций нет. Impact unmeasured; экономия maintenance/cognitive load.
- **Why now:** существует реальный reuse внутри одного module и нет
необходимости менять execution model ради него.
- **Minimum change:** оставить одну чистую функцию у instruments boundary,
например `instruments/infrastructure/public-instrument.mapper.ts`, и два
локальных imports. Не создавать Nest provider, generic mapper или общий
cross-repo package.
- **Ownership model:** этот module-owned mapper вызывают discovery и
enrichment application services; он зависит только от integration record
types. Application → infrastructure уже используется этими владельцами;
новых cross-module exports или forbidden imports не требуется.
- **Guarantees at risk:** explicit allowlist, отсутствие private quantities
и owner fields, nested brand whitelist, UID check, null/undefined semantics,
publication fences и оригинальное fetchedAt.
- **Counterarguments / trade-offs:** online и saved execution действительно
различаются. Общая projection не должна объединять их orchestration. Skinny
discovery payload
`backend/src/modules/investments/instruments/infrastructure/catalog-discovery.repository.ts:358–387`
имеет другие обязанности и
`update: {}`; это другой shape, не цель reuse полной detail projection.
Reachability/deletion этого discovery publication path относится к audit 04;
DRY-002 не требует сохранять его или discovery producer.
- **Verification or rollback:** future check одного share и bond с extra
private fields, unknown/null optional fields и лишним brand field через
обе paths; сохранить UID mismatch guard и CAS. Existing discovery assertions
`backend/test/tbank-catalog-discovery.e2e-spec.ts:119–177,197–229` защищают
близкую boundary через direct service calls, но не доказывают production
reachability discovery producer или полный online/enrichment parity.
Rollback — вернуть два локальных вызова/definitions без schema migration.
- **Effort:** S.
- **Related finding IDs:** DRY-006, DRY-009.
### DRY-003 — Select options и Zod enum дублируют словарь одной bank-card формы
- **Decision:** SIMPLIFY.
- **Severity:** P3.
- **Confidence:** High.
- **Evidence:** `frontend/src/entities/bank-card/model/form.schema.ts:65–69`;
`frontend/src/entities/bank-card/ui/BankCardForm/BankCardFields.vm.ts:19–25`;
`backend/src/modules/banking/bank-cards/presentation/bank-card.dto.ts:45,80–83,245–246`;
`frontend/src/shared/api/generated/schema.d.ts:767–795`.
- **Claim:** в frontend одну runtime vocabulary вручную поддерживают и schema,
и selectable options. Здесь одинаковы owner, причина изменения и semantics.
- **Current mechanism:** schema содержит `""` и три payment-system значения;
fields VM независимо содержит те же три значения. `""` — UI sentinel,
преобразуемый в `null`, а не четвёртая платёжная система.
- **Runtime consumer:** fields VM передаёт options форме; form VM использует
schema и отправляет `values.paymentSystem || null`
(`frontend/src/entities/bank-card/ui/BankCardForm/BankCardForm.vm.ts:30–39,50–65`).
- **Impact / actual cost:** при изменении словаря можно получить selectable,
но невалидное значение или валидное, но недоступное для выбора. Сегодня
списки совпадают. Impact unmeasured; малая maintenance cost.
- **Why now:** один локальный export может удалить вторую editable копию без
новых layers, dependencies или API change.
- **Minimum change:** существующая `form.schema.ts` владеет typed const tuple
реальных payment systems; Zod добавляет пустой sentinel, VM строит labels
из этого tuple. Проверять tuple через generated `BankCard`/request union
аналогично existing `model/operation-kinds.ts:1–8`.
- **Ownership model:** schema и fields VM остаются в `entities/bank-card`;
VM импортирует local schema export. Не нужен `shared/constants`, новый slice
или import backend checkout. `satisfies` проверяет допустимость values,
но не является доказательством exhaustive покрытия future enum.
- **Guarantees at risk:** clearable Select, `null` в API, `""` в form draft,
сохранение выбранного значения, перевод labels и independent server checks.
- **Counterarguments / trade-offs:** словарь маленький и меняется редко;
это optional cleanup при работе над формой. Generated TS union не является
runtime list; вводить второй generator ради этих трёх values невыгодно.
- **Verification or rollback:** existing visible selection/save case
`frontend/src/entities/bank-card/ui/BankCardForm/BankCardForm.test.tsx:86–105`,
clearing-to-null `:55–84`; при реализации проверить каждый option и empty
sentinel, затем local type/dependency checks. Rollback локальный, API прежний.
- **Effort:** S.
- **Related finding IDs:** DRY-005, DRY-008.
### DRY-004 — Access-loss taxonomy повторяется, recovery actions объединять не надо
- **Decision:** SIMPLIFY.
- **Severity:** P3.
- **Confidence:** High.
- **Evidence:** `frontend/src/shared/api/resource-query.ts:37–67`;
`frontend/src/shared/api/client.ts:162–179`;
`frontend/src/pages/account/ui/AccountPage/AccountPage.vm.ts:59–64,86–96`.
- **Claim:** один набор `FORBIDDEN`, `INVALID_TBANK_TOKEN`, `NOT_FOUND`,
`TBANK_NOT_CONNECTED` независимо перечислен для resource loss, permanent
transport failure и блокирующего account state.
- **Current mechanism:** resource отменяет/очищает access group; transport
запрещает retry и отдельно включает `UNAUTHORIZED`; Account VM выбирает
error вместо snapshot. Совпадает membership классификации, не actions.
- **Runtime consumer:** `useResourceQuery` применяет classifier в effect
(`frontend/src/shared/api/resource-query.ts:99–102`); `fetchAppRead` вызывает
transient classifier (`frontend/src/shared/api/client.ts:127–150,172–179`);
`frontend/src/pages/account/ui/AccountPage/AccountPage.tsx:9–12` использует VM state.
- **Impact / actual cost:** новая access-loss причина потребует синхронно
изменить три места, иначе retry/cache/page state могут разойтись. Сейчас
набор согласован. Impact unmeasured; maintenance risk, не доказанный дефект.
- **Why now:** existing owner `shared/api/problem-details.ts` уже владеет
известными codes и unknown fallback; классификацию можно расширить там.
- **Minimum change:** один predicate именно для этих четырёх codes у этого
owner; transport добавляет свой `UNAUTHORIZED`, resource и Account VM
сохраняют прежние actions. Не создавать error registry или общий UI handler.
- **Ownership model:** `client.ts`/`resource-query.ts` используют local import;
Account VM — public `@/shared/api`. Никаких dependencies из shared в
features/entities/pages. Это API error taxonomy, не банковский расчёт.
- **Guarantees at risk:** unknown fallback, auth refresh отдельно от broker
access, cached data при temporary failures и clearing после confirmed loss.
- **Counterarguments / trade-offs:**
`frontend/src/pages/accounts/ui/AccountsPage/AccountsPage.vm.ts:16–19` намеренно
имеет subset из двух connection codes; расширять его до четырёх нельзя.
Error views имеют разные settings/back actions
(`frontend/src/pages/account/ui/AccountPage/components/AccountPageError.view.tsx:15–43`,
`frontend/src/pages/accounts/ui/AccountsPage/components/AccountsPageError.view.tsx:22–38`);
эти branches остаются у pages. Если название helper скрывает это различие,
оставить текущие короткие списки дешевле плохого generic handler.
- **Verification or rollback:** future MSW checks для membership four codes,
special `UNAUTHORIZED`, `TBANK_UNAVAILABLE`, unknown/missing code; разные
actions остаются. Existing boundary tests:
`frontend/src/shared/api/resource-query.test.tsx:174–209`,
`frontend/src/shared/api/client.test.ts:261–281`. Rollback — вернуть predicates,
не менять error wire values или response shapes.
- **Effort:** S.
- **Related finding IDs:** DRY-007, DRY-008.
### DRY-005 — Banking validation и точные money representations оставить на обеих границах
- **Decision:** KEEP.
- **Severity:** P2.
- **Confidence:** High.
- **Evidence:** `frontend/src/entities/bank-card/model/money.ts:4–23`;
`frontend/src/entities/bank-card/model/form.schema.ts:8–42`;
`frontend/src/pages/card/ui/OperationForm/OperationForm.schema.ts:53–131`;
`backend/src/modules/banking/bank-cards/domain/rub-money.ts:12–38`;
`backend/src/modules/banking/bank-cards/application/bank-operations.service.ts:94–210`.
- **Claim:** повторение validation защищает разные trust boundaries; money
helpers также не являются одинаковым public interface.
- **Current mechanism:** UI draft понимает comma/trim/leading zeros, возвращает
null/field errors и проверяет cached данные. Server требует canonical RUB
decimal, storage range и positive operation amount; затем authoritative
records, ownership и refunds проверяются в write transaction.
- **Runtime consumer:** BankCardForm и OperationForm → typed mutation →
DTO/input → `createCard`/`createOperation`. Server write boundary:
`backend/src/modules/banking/bank-cards/infrastructure/bank-cards.repository.ts:35–59`;
replay fingerprint и ownership:
`backend/src/modules/banking/bank-cards/application/bank-cards.service.ts:74–95`,
`backend/src/modules/banking/bank-cards/application/bank-operations.service.ts:214–239`.
- **Impact / actual cost:** дополнительное сопровождение policy values есть,
но удаление server validation позволяет обойти browser checks, а reuse
UI normalizer изменит server semantics. Impact unmeasured.
- **Why now:** DRY-001 показывает риск расхождения значений, но не оправдывает
универсальный shared validator или замену bigint на floating point.
- **Minimum change:** сохранить независимые implementations; согласовывать
конкретные policy values и примеры при изменении границы. Existing
`backend/src/common/decimal.ts:19–83` уже обслуживает server arithmetic;
`backend/src/integrations/tbank/decimal.ts:7–13` занимается protobuf units/nano
отдельно.
- **Guarantees at risk:** signed-64-bit bounds input, precision сумм сверх
storage range, positive/zero distinction, user ownership, CAS, transaction
rollback и replay protection.
- **Counterarguments / trade-offs:** параллельные constants требуют parity
review; frontend/backend не должны делить исполняемые business modules.
`rubMoney` форматирует вычисленную сумму и не обязан ограничивать её input
storage range, в отличие от `parseRubMoney`.
- **Verification or rollback:** future changes проверять existing boundary
cases `frontend/src/entities/bank-card/model/form-validation.test.ts:13–48`
и `backend/src/modules/banking/bank-cards/domain/rub-money.spec.ts:4–56`.
Финансовые changes дополнительно требуют реальной изолированной transaction
proof. В аудите ничего не удалено, rollback не требуется.
- **Effort:** S.
- **Related finding IDs:** DRY-001, DRY-003, DRY-008.
### DRY-006 — Stock/Bond сходство не оправдывает generic page/query/payout pipeline
- **Decision:** KEEP.
- **Severity:** P2.
- **Confidence:** High.
- **Evidence:** `frontend/src/pages/stock/ui/StockPage/StockPage.vm.ts:23–98`;
`frontend/src/pages/bond/ui/BondPage/BondPage.vm.ts:23–98`;
`backend/src/modules/investments/instruments/stocks/application/stock-income.service.ts:114–178`;
`backend/src/modules/investments/instruments/bonds/application/bond-income.service.ts:145–201`.
- **Claim:** одинаковая композиция закрывает разные business semantics;
действительно общие primitives уже переиспользуются.
- **Current mechanism:** dividends фильтруют cancelled и выбирают timing по
paymentDate с recordDate fallback; coupons несут number/type/periodDays и
timing по paymentDate. Quotes stock — Money, bond — percent of nominal,
clean/dirty/ACI. Страницы используют общий instrument-details layout,
section, chart, position и operation rows, но разные company/metrics/payouts.
- **Runtime consumer:** route containers
`frontend/src/pages/stock/ui/StockPage/StockPage.tsx:10–21` и
`frontend/src/pages/bond/ui/BondPage/BondPage.tsx:10–21`;
reused composition —
`frontend/src/pages/stock/ui/StockPage/StockPage.view.tsx:38–72`,
`frontend/src/pages/bond/ui/BondPage/BondPage.view.tsx:134–205`.
HTTP consumers —
`backend/src/modules/investments/instruments/stocks/presentation/stock.controller.ts:170–181`,
`backend/src/modules/investments/instruments/bonds/presentation/bond.controller.ts:126–134`.
- **Impact / actual cost:** текущая локальная duplication проще universal
hook с динамическим endpoint, type casts и flags для income kind/company/
metrics/price unit. Impact unmeasured; savings от такой extraction не доказаны.
- **Why now:** защищает от ложного DRY на основании похожего JSX и одинакового
UUID regex. У entity hooks разные wire contracts и accessGroup usage.
- **Minimum change:** сохранить endpoint-specific entity query declarations
и page composition. Existing `features/instrument-details` остаётся общим
владельцем focused presentation, без объединения stock/bond entities.
- **Guarantees at risk:** exact decimal currency/percent semantics, unknown
amount vs zero, registry/payment dates, cancelled dividends, enabled inputs,
stable generated query keys и отсутствие запрещённых sibling imports.
- **Counterarguments / trade-offs:** horizon constants и часть grouping похожи;
отдельное маленькое правило может заслужить reuse при реальном совместном
изменении. Это не основание сейчас переносить весь orchestration или делать
boolean-driven payout component.
- **Verification or rollback:** future common changes должны сохранять
`backend/src/modules/investments/instruments/stocks/application/stock.spec.ts:10–37,45–69`,
`backend/src/modules/investments/instruments/bonds/application/bond.spec.ts:27–113,139–176`
и видимые stock/bond
payout fields. Local import boundaries проверяет
`frontend/dependency-cruiser.cjs:42–64`. В аудите изменения нет.
- **Effort:** S.
- **Related finding IDs:** DRY-002, DRY-007, DRY-010.
### DRY-007 — Query wrappers и UI primitives несут project responsibility
- **Decision:** KEEP.
- **Severity:** P2.
- **Confidence:** High.
- **Evidence:** `frontend/src/shared/api/authenticated-query.ts:34–59`;
`frontend/src/shared/api/resource-query.ts:37–67,84–149`;
`frontend/packages/ui/src/LoadingSkeleton/LoadingSkeleton.tsx:5–17`;
`frontend/packages/ui/src/StateView/StateView.tsx:5–27`.
- **Claim:** эти wrappers не просто воспроизводят API библиотеки под новым
названием; они задают конкретные session/resource/ARIA/presentation defaults.
- **Current mechanism:** authenticated query совмещает session и caller
enabled; resource wrapper очищает access group, проецирует envelope data/meta,
сохраняет transport status и refetch envelope. LoadingSkeleton задаёт busy
status/label, StateView — единый alert/layout; EntryCard — общую композицию,
tone и действия (`frontend/packages/ui/src/EntryCard/EntryCard.tsx:24–81`).
- **Runtime consumer:** portfolio query
`frontend/src/entities/portfolio/api/usePortfolio.ts:19–25`, banking query
`frontend/src/entities/bank-card/api/queries.ts:28–44`; instrument sections
используют LoadingSkeleton
(`frontend/src/features/instrument-details/ui/InstrumentSection.view.tsx:43–54`),
Account/Accounts error views используют StateView.
- **Impact / actual cost:** удаление wrappers размножит session gates,
resource access handling и accessibility defaults по callers. Impact
unmeasured; независимая стоимость текущих abstractions не измерялась.
- **Why now:** ponytail ladder о library/native API не означает удаления
useful project defaults. Mantine controls уже используются напрямую там,
где нет дополнительной ответственности
(`frontend/src/features/auth/ui/AuthForm/AuthForm.tsx:49–83`).
- **Minimum change:** сохранить focused wrappers и primitives. Не добавлять
собственные Button/Select passthrough wrappers. DRY-004 может упростить
классификацию внутри existing owner, не заменить whole wrapper.
- **Guarantees at risk:** no fetch before session, combining enabled, unknown
resource при успешном HTTP, access-loss cache clearing, сохранение cached
snapshot и доступные loading/error states.
- **Counterarguments / trade-offs:** wrapper вручную выбирает часть Query
return API; при новом consumer нужно расширять осмысленно. Observation
compatibility может быть отдельным dead-code вопросом audit 04, но её
возможная ненужность не доказывает ненужность wrapper целиком.
- **Verification or rollback:** existing resource boundary assertions
`frontend/src/shared/api/resource-query.test.tsx:72–138,174–209`
различают transport success,
projection и clearing. Future UI changes сохраняют keyboard/ARIA basics;
tests приложения не заменяют browser proof. В аудите изменений нет.
- **Effort:** S.
- **Related finding IDs:** DRY-004, DRY-006, DRY-010.
### DRY-008 — Generated contracts и known-error subset не являются competing DTO truths
- **Decision:** KEEP.
- **Severity:** P3.
- **Confidence:** High.
- **Evidence:** `backend/src/openapi.ts:4–10`;
`backend/.ai/conventions/http-api.md:69–81`;
`frontend/package.json:22–23`;
`frontend/src/entities/bank-card/api/types.ts:1–34`;
`frontend/src/features/auth/model/auth-context.ts:14–15`;
`frontend/src/shared/api/generated/schema.d.ts:731–795`.
- **Claim:** reviewed transport types выводятся из provided snapshot; не
найдена независимая handwritten DTO-копия для рассмотренных bank/auth flows.
- **Current mechanism:** editable contract — BE Nest DTO/Swagger; provided
snapshot — вход FE offline generation; aliases используют `paths`.
Form values выводятся из schemas. `MoneyDto implements Money`
(`backend/src/common/money.dto.ts:3–10`) сохраняет runtime Swagger metadata
отдельно от pure `backend/src/common/money.types.ts:1`.
- **Runtime consumer:** `frontend/src/shared/api/client.ts:1–6` импортирует
generated paths; form/auth/bank query callers используют derived aliases.
Known UI enum `frontend/src/shared/api/problem-details.ts:1–28` обслуживает
recovery, в то время как
generated ProblemDetails.code остаётся открытой строкой.
- **Impact / actual cost:** artifact freshness требует сопровождения, но
удаление snapshots или общий исполняемый DTO package нарушит independent
repo inputs. Impact unmeasured; текущего file-to-file drift OpenAPI нет.
- **Why now:** наличие нескольких файлов с одной shape не означает несколько
редактируемых источников значения. DRY-001 и DRY-003 касаются runtime policies,
которые TypeScript declarations не реализуют автоматически.
- **Minimum change:** оставить существующий contract flow и local aliases;
согласовывать metadata при DRY-001. Сохранить runtime known subset ошибок с
fallback, не закрывать wire string ради auto-generated exhaustive enum.
- **Guarantees at risk:** compatible wire contract, offline FE generation,
независимость CI/checkout, unknown-code fallback, domain без Swagger.
- **Counterarguments / trade-offs:** равенство existing OpenAPI файлов не
подтверждает runtime freshness, а typed `satisfies` не проверяет все future
enum values. Это ограничение существующего flow, не доказательство нужности
ещё одного hash/validator registry. Fresh generation parity — UNVERIFIED.
- **Verification or rollback:** при будущих API changes — предусмотренные
backend contract checks и frontend offline `openapi:check`, без чтения
соседнего source как обязательного входа. В аудите выполнено только сравнение
existing artifacts; generation/build не запускались.
- **Effort:** S.
- **Related finding IDs:** DRY-001, DRY-003, DRY-004, DRY-005.
### DRY-009 — Живой enrichment/shared fence: не вводить generic queue из сходства с discovery
- **Decision:** KEEP.
- **Severity:** P2.
- **Confidence:** High.
- **Evidence:** `backend/src/modules/investments/instruments/application/catalog-enrichment.service.ts:113–125,189–225,497–577`;
`backend/src/modules/investments/instruments/infrastructure/catalog-discovery.repository.ts:154–179,280–318`;
`backend/src/modules/investments/instruments/stocks/application/stock-market.service.ts:34–45`.
Reachability counterexample:
`backend/src/modules/investments/instruments/application/catalog-discovery.service.ts:66–70,84–93,149–190,227–267`;
`backend/src/modules/investments/tbank-sync/tbank-sync.service.ts:54–78,192–201`;
`backend/src/modules/investments/tbank-sync/tbank-sync.controller.ts:31–41`.
- **Claim:** KEEP касается используемых enrichment batching/observer/publication
guarantees и общей repository fence. Сходство с discovery не оправдывает
generic queue и не является основанием сохранять discovery producer.
- **Current mechanism:** enrichment `requestMany` при missing/stale sources
резервирует work и ставит drain; price batch ограничен одной user/revision,
поздние observers используют `publishSource` с initiating attempt. Repository
отдельно claim-ит initiator и observer. Discovery default HTTP branches online,
observe branches read-only; `read(true)` пропускает request.
- **Runtime consumer:** `backend/src/modules/investments/instruments/stocks/presentation/stock.controller.ts:122–129`
→ `backend/src/modules/investments/instruments/stocks/application/stock-market.service.ts:34–45`
→ enrichment `section('quote', false)`
(`backend/src/modules/investments/instruments/application/catalog-enrichment.service.ts:234–241,269–289`)
→ `readMany(false)` (`:113–125`)
→ при missing/stale `requestMany` (`:189–225`) → drain/publishSource (`:497–577`).
Bond quote имеет тот же живой entry point
(`backend/src/modules/investments/instruments/bonds/presentation/bond.controller.ts:92–99`;
`backend/src/modules/investments/instruments/bonds/application/bond-market.service.ts:38–49`).
Ни default catalog search/detail/byTicker, ни observe calls discovery producer
не запускают. Единственный найденный внешний production-text refresh caller
находится в manual dispatch без найденного production request consumer.
- **Impact / actual cost:** generic queue ради устранения сходства с
dead-parent-reachable кодом добавит callbacks/flags вместо устранения
неиспользуемого пути. Изменение живого enrichment может потерять authorization
и publication fences. Impact unmeasured; runtime savings не измерены.
- **Why now:** исправлен ложный runtime consumer прежней редакции. Наличие
definitions, DI registration и direct tests не доказывает producer reachability.
- **Minimum change:** не вводить generic queue; сохранить гарантии живого
enrichment и используемые им `reserve/publishSource/finish`. DRY-002 извлекает
stateless projection независимо. Discovery `request/refresh/drain` не входит
в KEEP; reachability/deletion и compatibility reads — предмет audit 04.
- **Guarantees at risk:** connection revision/attempt identity, isolation
initiating credentials, private observer handles, late-publication fences,
same-owner batching и conditional finish именно живого enrichment.
- **Counterarguments / trade-offs:** discovery producer в source действительно
похож и исполняется из direct tests, но current catalog HTTP paths его
обходят; это аргумент за отдельный dead-code разбор, не за сохранение двух
queues. KEEP не распространяется на весь CatalogDiscoveryService/repository,
unused methods или каждый completion helper enrichment. Равноценность общей
queue и снижение total cost — UNVERIFIED.
- **Verification or rollback:** при изменении живой boundary нужны controlled
quote/enrichment scenarios с late replace/delete, invalid initiating token,
batch/observer isolation и conditional publication. Direct discovery tests
(`backend/test/tbank-catalog-discovery.e2e-spec.ts:184–187,202–203`) не являются
proof текущего HTTP flow. Реализация в этом аудите не изменена; rollback
source не требуется. Audit 04 определяет deletion scope отдельно.
- **Effort:** S.
- **Related finding IDs:** DRY-002.
### DRY-010 — Scheduling ownership требует проверки, глобальная freshness policy преждевременна
- **Decision:** INVESTIGATE.
- **Severity:** P2.
- **Confidence:** Medium.
- **Evidence:** `frontend/src/features/tbank-connection/model/screen-sync.ts:20–35,53–61`;
`frontend/src/app/ui/App.tsx:13–20`;
`frontend/src/entities/portfolio/api/usePortfolio.ts:3–5,19–25`;
`frontend/src/entities/brokerage-account/api/useBrokerageAccounts.ts:3–18`;
`specs/openspec/specs/brokerage-accounts/spec.md:68,96–98`;
`specs/openspec/specs/portfolio-view/spec.md:122–124`.
- **Claim:** у одного HTTP-read flow есть page timer, query freshness и app
refetch triggers. Возможность уменьшить собственную coordination реальна,
но их полное объединение не является доказанным behavior-preserving cleanup.
- **Current mechanism:** screen hook каждые 1 800 000 ms вызывает refetch
текущего resource array; отдельные hooks задают 30 seconds для portfolio и
30 minutes для accounts; app включает focus/reconnect. Spec специально
различает freshness и periodic interval, а focus refetch запрещает.
- **Runtime consumer:** Account VM передаёт пять resources
(`frontend/src/pages/account/ui/AccountPage/AccountPage.vm.ts:78–84`);
Stock/Bond — свои section queries; BondsPage
передаёт search result (`frontend/src/pages/bonds/ui/BondsPage/BondsPage.vm.ts:10–13`).
Все восемь production call sites перечислены в разделе 3: ни один не
сохраняет/передаёт return hook. `refresh/retry` используются только test
harness (`frontend/src/features/tbank-connection/model/screen-sync.test.tsx:37–50,68–78`),
не кнопкой текущего production UI.
- **Impact / actual cost:** maintenance scheduling policy распределено по
owners; runtime число лишних GET, interval phase, background-tab behavior и
эффект от native query-owned interval не измерены. Impact unmeasured.
- **Why now:** важно не заменять все staleTime на один constant ради DRY.
Неиспользуемый return/retry не является основанием сохранять timer;
его reachability/deletion относится к audit 04.
- **Minimum change:** сначала проверить семантику interval относительно
последнего успешного read, focus/reconnect, disabled inputs и активных
observers. Затем решить, достаточно ли существующих query options для
periodic reads. До подтверждения не создавать global query factory или
universal policy object.
- **Guarantees at risk:** caller enabled/session gates, canonical cache key,
cleanup при unmount, HTTP-only periodic read, сохранение snapshot и отсутствие
hidden personal-import commands.
- **Counterarguments / trade-offs:** существующий timer реально имеет общую
mount-based фазу: effect с `[]` не перезапускается при смене resource array,
а callback читает актуальный ref
(`frontend/src/features/tbank-connection/model/screen-sync.ts:22–35`). Query-owned timers
могут изменить фазу, поведение при смене inputs или background/observer
semantics. Общая кнопка retry из return не является counterargument —
production consumer не найден. Разные 30 sec/30 min в specs не противоречат
друг другу. UNVERIFIED: equivalence замены, необходимость общего clock
как требования и фактические savings.
- **Verification or rollback:** дешевейшее подтверждение — controlled-time
MSW сценарий существующего query/page owner, assert на видимые данные и GET
при mount/focus/interval/unmount/disabled search, затем сверка native options
со штатной документацией используемой версии. Никакой реальной сети и
нагрузочного benchmark для первого решения не нужно. Future rollback —
вернуть только scheduling options/timer, сохранив generated keys.
- **Effort:** M.
- **Related finding IDs:** DRY-006, DRY-007.
## 5. DELETE / SIMPLIFY / KEEP / INVESTIGATE
| Decision | IDs / область | Итог |
| --- | --- | --- |
| DELETE | — | Подтверждённых удалений в scope 03 нет; недоступные flows относятся к audit 04 |
| SIMPLIFY | DRY-002 | Один module-local instrument projection mapper; whitelist сохраняется |
| SIMPLIFY | DRY-003 | Один typed runtime tuple у текущей bank-card schema |
| SIMPLIFY | DRY-004 | Один code-membership predicate у existing API error owner; actions остаются разными |
| FIX | DRY-001 | Согласовать единицу password minimum и API metadata, сохранить две trust-boundary checks |
| KEEP | DRY-005 | Money precision, server validation, transaction/CAS/refund/replay protections |
| KEEP | DRY-006 | Stock/Bond contracts и business/render semantics; existing focused reuse достаточен |
| KEEP | DRY-007 | Authenticated/resource wrappers и доступные UI primitives |
| KEEP | DRY-008 | Provider-owned OpenAPI, consumer artifacts, derived aliases, open-code fallback |
| KEEP | DRY-009 | Только гарантии живого enrichment и используемая repository fence; discovery producer — audit 04, не KEEP |
| INVESTIGATE | DRY-010 | Scheduling simplification только после проверки эквивалентности |
### Minimal ownership model для reuse
| Правило | Единственная implementation / value | Callers | Запрещённое направление / лишняя abstraction |
| --- | --- | --- | --- |
| Detail projection / DRY-002 | Локальный `instruments/infrastructure/public-instrument.mapper.ts` | Discovery/enrichment application внутри instruments | Не в global common/utils, не integration → business DTO, не DI provider |
| FE payment list / DRY-003 | Typed tuple в existing `entities/bank-card/model/form.schema.ts` | Zod enum и bank-card fields VM | Не frontend → backend checkout, не shared → entity, не новый generator |
| Access-loss membership / DRY-004 | Predicate в existing `shared/api/problem-details.ts` | Local client/resource; Account VM через `shared/api/index.ts` | Не shared → features/pages, не generic recovery component |
| Password policy / DRY-001 | BE CredentialsDto validator — enforcement; FE auth schema — UX implementation той же spec | Register/login boundaries | Не удалять server check, не shared cross-repo executable validator |
| Shared instrument presentation / KEEP | Existing `features/instrument-details` | Stock/Bond pages | Не entity sibling import, не generic boolean-driven page |
Сравнение вариантов для всех `SIMPLIFY`:
- **Оставить копии:** допустимо для P3, если область не меняется; сохраняется
несколько мест правки. Для DRY-002 benefit выше: одна visibility projection.
- **Перенести к owner:** предпочтительно для DRY-002–004; число business
owners не меняется, imports локальны или уже разрешены.
- **Генерировать:** полезно для wire types и уже выполняется; byte validators,
recovery actions и runtime UI lists нельзя получить из erased TS declarations
автоматически. Дополнительный generator здесь дороже tuple/predicate.
- **Сделать shared:** допустим только existing focused API code owner DRY-004.
Межрепозиторный shared package и огромный `shared/utils` создадут release,
build, trust-boundary и layer coupling без подтверждённой потребности.
## 6. Cost & benefit
| ID | Класс ожидаемой пользы | Фактическое измерение | Обязательная regression boundary |
| --- | --- | --- | --- |
| DRY-001 | Reliability/UX и maintenance согласованной policy | not measured; несовместимость доказана по source | UTF-8 границы, HTTP validation, password без преобразования, published metadata |
| DRY-002 | Cognitive load, maintenance; один visibility owner | not measured; две definitions подтверждены | Whitelist/nested brand, optional fields, online/saved parity, UID/CAS |
| DRY-003 | Небольшая maintenance/cognitive экономия | not measured; две local lists подтверждены | Все options, empty sentinel и null request |
| DRY-004 | Maintenance при изменении code taxonomy | not measured; три полных набора подтверждены | Unknown/temporary errors, auth special case, cache clearing и разные actions |
| DRY-005–009 | Избежать лишнего coupling и удаления используемой защиты | not measured | Money precision, server authority, live enrichment publication/session boundaries, accessibility; discovery producer исключён |
| DRY-010 | Возможная экономия собственных scheduling constructs | UNVERIFIED / not measured | Mount timer phase/latest-resource ref, enabled, active observers, cached snapshot, HTTP-only reads; unused return/retry не защищается |
Никаких claims об ускорении, CI minutes, bundle size или agent-token savings
не сделано. Для выбранных cleanup нет подтверждённого кандидата на удаление
dependency. Net LOC не вычислялся: нет implementation diff, а helper imports,
public exports и необходимые regression checks также стоят кода.
### Complexity-only pass в формате ponytail-audit
- `shrink:` убрать вторую `publicInstrument`; одна instruments boundary projection. [DRY-002; backend/src/modules/investments/instruments/application/catalog-discovery.service.ts:499–531; backend/src/modules/investments/instruments/application/catalog-enrichment.service.ts:646–678]
- `shrink:` убрать повторные полные access-loss списки; один predicate у existing API code owner, actions отдельно. [DRY-004; frontend/src/shared/api/client.ts:162–169; frontend/src/shared/api/resource-query.ts:42–51; frontend/src/pages/account/ui/AccountPage/AccountPage.vm.ts:59–64]
- `shrink:` убрать вторую runtime payment-system list формы; typed tuple existing schema. [DRY-003; frontend/src/entities/bank-card/model/form.schema.ts:65–69; frontend/src/entities/bank-card/ui/BankCardForm/BankCardFields.vm.ts:19–22]
`net: -N lines, -0 deps possible.` Здесь `N = not measured`: сокращение строк
без подготовленного и проверенного diff не оценено; 0 относится к предложенным
удалениям deps, не к исчерпывающему dependency audit. DRY-001 и защищаемые
инварианты не включены в complexity-only cuts.
## 7. Top actions
Все четыре шага ниже подтверждены source analysis; ранжирование учитывает
value/risk/effort, а не число одинаковых строк.
### Требует behavior/contract подтверждения
1. **DRY-001 / M:** восстановить согласованный byte minimum у auth validator,
согласовать metadata и artifact; проверить один набор UTF-8 границ на
frontend и configured backend boundary. Новый ADR нужен только при решении
действительно изменить принятую password policy, а не при её восстановлении.
### Quick wins при работе над соответствующим owner
2. **DRY-002 / S:** один local projection mapper с сохранением allowlist,
null/undefined и используемых enrichment publication fences. Это не требует
сохранения discovery producer; его deletion scope — audit 04. Максимальный
подтверждённый reuse benefit среди structural кандидатов этого аудита.
3. **DRY-003 / S:** использовать один typed runtime tuple для schema и options;
сохранить пустое form value и API null. Никакой tooling migration.
4. **DRY-004 / S:** один predicate для четырёх codes; сохранить отдельный auth
`UNAUTHORIZED`, Accounts subset и page recovery actions. Выполнять только
с boundary proof сохранения temporary-error snapshot.
DRY-010 остаётся verification question о timer phase, не подтверждённым шагом
migration и не защитой неиспользуемого return/retry. DRY-009 не требует
сохранения discovery `request/refresh/drain`; их reachability/deletion — audit 04.
KEEP findings не превращены в backlog на дополнительные layers/tests/rules.
## 8. Cross-audit signals
- **04 / Dead Code & Legacy:** observation compatibility в `frontend/src/shared/api/resource-query.ts:69–127`; проверить actual production consumers перед удалением, не удалять весь resource wrapper.
- **04 / Dead Code & Legacy:** `backend/src/modules/investments/instruments/application/catalog-discovery.service.ts:66–70,84–86,149–190,227–267,340–495` и `backend/src/modules/investments/tbank-sync/tbank-sync.service.ts:54–78,192–201`; default paths online, observe read-only, внешний refresh caller только в manual dispatch без найденного production request entry point; DRY-009 не защищает этот producer от удаления.
- **04 / Dead Code & Legacy:** return/refresh/retry `frontend/src/features/tbank-connection/model/screen-sync.ts:37–61` потребляется только tests `screen-sync.test.tsx:37–50,68–78`; все восемь production calls отбросили return; deletion scope отдельно от живого timer `:22–35`.
- **13 / Reliability & Security:** online catalog calls `backend/src/modules/investments/instruments/application/catalog-discovery.service.ts:149–165,295–317` и existing token translator `backend/src/integrations/tbank/tbank-token.service.ts:26–35`; проверить публичное invalid-token recovery, здесь отдельный error/security аудит не проводился.
- **05 / Frontend Architecture:** ownership периодического чтения и app focus/reconnect `frontend/src/features/tbank-connection/model/screen-sync.ts:20–35`, `frontend/src/app/ui/App.tsx:13–20`; связан с DRY-010, не повод унифицировать разные staleTime.
- **08 / Test Quality:** proof для DRY-001 должен различать byte/character limits; рассмотренный `backend/src/common/dto-validation.spec.ts:13–30` использует ASCII happy path, полноценный аудит auth tests не выполнялся.
- **15 / SDD, Specs & Documentation:** `docs/adr/019-hourly-reads-and-online-instrument-reference.md:27–29`, `specs/openspec/specs/portfolio-view/spec.md:122–124`, `specs/AI_INDEX.md:15–31`; различать общую формулировку read interval и endpoint-specific freshness, не объявлять docs копии автоматически лишними.
## 9. Open questions / verification plan
| Вопрос, реально меняющий решение | Дешевейшая проверка | Что изменит ответ |
| --- | --- | --- |
| Есть ли принятое изменение password minimum с bytes на characters? | Проверить accepted auth decision/requirement у owner; existing spec и docs уже говорят о byte-policy и непринятом drift | Изменяет целевую policy DRY-001; без нового решения восстановить bytes |
| Четыре codes по-прежнему имеют одинаковое membership во всех трёх contexts? | Existing MSW boundaries плюс unknown/temporary/auth code cases, без network | Если meanings расходятся, DRY-004 становится KEEP коротких local predicates |
| Periodic reads должны синхронно повторять весь экран или каждый active resource после собственного успешного read? | Controlled-time MSW scenario mount/focus/reconnect/interval/unmount; сопоставить Accounts/Portfolio specs | Определяет, нужна ли screen clock и эквивалентен ли query-owned interval в DRY-010 |
| Как native interval действует при disabled query и background tab в установленной версии? | Сверка документации версии и targeted existing owner test с disabled search, без install | Определяет безопасность и размер scheduling cleanup; пока UNVERIFIED |
### Финальная проверка полноты и ограничений
- Присутствуют все девять общих разделов, Knowledge Duplication Register,
Intentional Duplication to Keep, minimal ownership model и сравнение вариантов
local copies / owner / generated / shared.
- Для каждого из десяти DRY-ID заполнены Decision, Severity, Confidence,
Evidence, Claim, Current mechanism, Runtime consumer, Impact / actual cost,
Why now, Minimum change, Guarantees at risk, Counterarguments / trade-offs,
Verification or rollback, Effort и Related finding IDs. Для каждого
SIMPLIFY дополнительно указан owner и допустимое направление imports.
- Четыре actionable выводы отделены от пяти KEEP и одного INVESTIGATE.
Неизмеренные savings и непроверенная эквивалентность явно обозначены.
- Повторный consumer trace отделяет живой enrichment/shared fence DRY-009
от discovery producer без найденного production entry point; восемь
standalone `useScreenSync` calls не используют return. Связанные register,
intentional KEEP, summary/top/minimum change и final table согласованы.
- Boy Scout review отчёта: не добавлены competing policy values, generic
shared owner, duplicate findings для одного механизма или обязательная
tooling-зависимость от соседнего checkout. Исходники не правились.
- После записи проверены собственный новый текст, whitespace и повторные
HEAD/status всех пяти repos. Tracked/staged/unstaged source изменений нет.
Отчёт ignored root allowlist-ом и существует только в разрешённом месте.
- Статический audit не является runtime bug reproduction, успешным запуском
tests, подтверждением DB atomicity, свежей генерацией OpenAPI или измерением
производительности. Эти ограничения сохраняются после clean Git status.
## Machine-readable consolidation table
| ID | Decision | Severity | Confidence | Repo | Files | One-line impact | Effort | Related IDs |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| DRY-001 | FIX | P2 | High | backend,frontend | backend/src/modules/auth/presentation/auth.dto.ts:43–74; frontend/src/features/auth/model/auth.schema.ts:36–43; frontend/src/shared/api/spec/openapi.json:5246–5250 | Разные единицы password minimum дают UI/API отказ | M | DRY-005,DRY-008 |
| DRY-002 | SIMPLIFY | P3 | High | backend | backend/src/modules/investments/instruments/application/catalog-discovery.service.ts:499–531; backend/src/modules/investments/instruments/application/catalog-enrichment.service.ts:646–678 | Две одинаковые detail projections требуют двух правок whitelist | S | DRY-006,DRY-009 |
| DRY-003 | SIMPLIFY | P3 | High | frontend | frontend/src/entities/bank-card/model/form.schema.ts:65–69; frontend/src/entities/bank-card/ui/BankCardForm/BankCardFields.vm.ts:19–22 | Schema и selectable options вручную повторяют один словарь | S | DRY-005,DRY-008 |
| DRY-004 | SIMPLIFY | P3 | High | frontend | frontend/src/shared/api/client.ts:162–179; frontend/src/shared/api/resource-query.ts:37–67; frontend/src/pages/account/ui/AccountPage/AccountPage.vm.ts:59–64 | Три набора access-loss codes могут расходиться; actions оставить локальными | S | DRY-007,DRY-008 |
| DRY-005 | KEEP | P2 | High | backend,frontend | frontend/src/entities/bank-card/model/money.ts:4–23; frontend/src/pages/card/ui/OperationForm/OperationForm.schema.ts:53–131; backend/src/modules/banking/bank-cards/domain/rub-money.ts:12–38; backend/src/modules/banking/bank-cards/application/bank-operations.service.ts:94–210 | Independent validation сохраняет precision и server trust boundary | S | DRY-001,DRY-003,DRY-008 |
| DRY-006 | KEEP | P2 | High | backend,frontend | frontend/src/pages/stock/ui/StockPage/StockPage.vm.ts:23–98; frontend/src/pages/bond/ui/BondPage/BondPage.vm.ts:23–98; backend/src/modules/investments/instruments/stocks/application/stock-income.service.ts:114–178; backend/src/modules/investments/instruments/bonds/application/bond-income.service.ts:145–201 | Generic pipeline скроет разные dividend/coupon и quote semantics | S | DRY-002,DRY-007,DRY-010 |
| DRY-007 | KEEP | P2 | High | frontend | frontend/src/shared/api/authenticated-query.ts:34–59; frontend/src/shared/api/resource-query.ts:37–149; frontend/packages/ui/src/LoadingSkeleton/LoadingSkeleton.tsx:5–17; frontend/packages/ui/src/StateView/StateView.tsx:5–27 | Wrappers несут session/resource/accessibility defaults | S | DRY-004,DRY-006,DRY-010 |
| DRY-008 | KEEP | P3 | High | backend,frontend | backend/src/openapi.ts:4–10; frontend/src/entities/bank-card/api/types.ts:1–34; frontend/src/shared/api/generated/schema.d.ts:731–795; frontend/src/shared/api/problem-details.ts:1–28 | Generated copies сохраняют независимые repo inputs и open-code fallback | S | DRY-001,DRY-003,DRY-004,DRY-005 |
| DRY-009 | KEEP | P2 | High | backend | backend/src/modules/investments/instruments/stocks/application/stock-market.service.ts:34–45; backend/src/modules/investments/instruments/application/catalog-enrichment.service.ts:113–125,189–225,497–577; backend/src/modules/investments/instruments/infrastructure/catalog-discovery.repository.ts:154–179 | Сохранить live enrichment guarantees/shared fence; discovery producer не входит в KEEP, deletion — audit 04 | S | DRY-002 |
| DRY-010 | INVESTIGATE | P2 | Medium | frontend | frontend/src/features/tbank-connection/model/screen-sync.ts:22–35,53–61; frontend/src/app/ui/App.tsx:13–20; frontend/src/pages/account/ui/AccountPage/AccountPage.vm.ts:78–84; frontend/src/entities/portfolio/api/usePortfolio.ts:3–5 | Проверить equivalence timer phase; восемь production callers не используют return/retry, runtime savings UNVERIFIED | M | DRY-006,DRY-007 |