Files
main/audits/2026-10-11/reports/03-duplication-and-reusability-report.md
codex-bot d07fbfabb1 docs: archive technical audit 2026-10-11 in develop (#36)
Переносит архив `audits/2026-10-11/` (19 файлов) из codex/audit-remediation-2026-10-11 в develop и добавляет точечный allowlist в .gitignore.
2026-10-17 19:38:03 +03:00

88 KiB
Raw Permalink Blame History

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

  1. DRY-002 / S: один local projection mapper с сохранением allowlist, null/undefined и используемых enrichment publication fences. Это не требует сохранения discovery producer; его deletion scope — audit 04. Максимальный подтверждённый reuse benefit среди structural кандидатов этого аудита.
  2. DRY-003 / S: использовать один typed runtime tuple для schema и options; сохранить пустое form value и API null. Никакой tooling migration.
  3. 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