# Аудит 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 |