Files
Deal/docs/superpowers/reviews/2026-09-08-code-quality-review.md
T
Rustam Khalimov 27c7831910
ci / build-test (push) Canceled after 0s
Deal — единая кодовая база
SaaS-мониторинг Telegram: ядро (модули Cards/Kanban/Pipeline/Tenants/Settings/
Discovery, Api, Infrastructure), сервисы telegram/ai/ml/storage, фронт Vue,
контракты и grpc-hosting, деплой-конфиги (dev/prod/observability/CI-раннер),
Gitea Actions CI, документация (ТЗ, техдок, api-map, код-стайл, планы, бэклог).

Текущее состояние: все этапы роадмапа 0–12 закрыты, сборка 5 sln 0/0,
тесты 1340/130/52/38/9 зелёные.
2026-09-11 23:56:47 +03:00

24 KiB
Raw Blame History

Ревью качества кода «Дейл» (2026-09-08)

Исторический документ этапа 8 (ревью, 2026-09-08). Актуальное состояние — docs/superpowers/STATUS.md и docs/technical/Техническая-документация-Дейл.md.

Многоосевое ревью (корректность/читаемость/архитектура/безопасность/производительность) бэкенда и фронтенда. Проводилось 5 ревьюерами по непересекающимся зонам (чтение; правок не вносилось), ключевые находки перепроверены по коду. Проект НЕ git. Метки: [Critical]/[Required]/[Nit]/[Optional] (Required = исправить до прода; Nit = желательно; Optional = задел).

Сводка

Зона Объём Critical Required Nit Optional
Frontend (Vue3, JS) 25 файлов / 11.3k LOC 0 6 6 1
Core-каркас (Api/Infrastructure/Contracts) ~370 файлов 0 9 7 5
Модули Kanban/Pipeline/Projects ~140 файлов 0 10 5 2
Модули Settings/Telegram/Tenants/Discovery ~130 файлов 0 7 8 3
gRPC-сервисы (telegram/ai/ml) + proto ~135 файлов 0 8 8 4
Итого ~1100 файлов 0 40 34 15

Общий вердикт: код высокого качества — чистая port&adapter-архитектура, 1 тип=1 файл, тенант- изоляция через схему на тенанта спроектирована сильно, SQL параметризован, XSS/секреты на фронте и в сервисах чистые. Найдено 0 критических дыр класса «ключ наружу/доступ к чужому тенанту». Ниже — что требует исправления и что стоит улучшить. Подробности по зонам — в рабочем журнале сессии (5 отчётов субагентов с file:line); здесь — консолидированный список.


A. Безопасность (приоритет 1)

  1. [Required] SSRF через baseUrl ИИ-провайдера. Deal.Infrastructure/Integrations/AiConnectionChecker.cs (проверка ok:false/true) + PATCH настроек разрешает тенанту задать произвольный baseUrl (в т.ч. http://127.0.0.1:... — подтверждено acceptance-логом task-6). На не-local провайдере ключ API уходит на указанный адрес → аутентифицированный тенант мультитенантного SaaS получает blind-сканер внутренней сети/метаданных. Исправить: резолв DNS + запрет private/link-local/loopback при проверке и вызове (или egress-фильтр); не принимать переопределение хоста для каталоговых провайдеров.
  2. [Required] Rate-limit и анти-брутфорс выключены по умолчанию. Deal.Api/Program.cs (регистрация лимитера), RateLimitOptions дефолт Enabled=false → без env в проде нет ни лимитов, ни LoginAttemptGuard. compose.prod форсирует true, но дефолт кода опасен при запуске вне compose. Исправить: стартовая проверка «Production ⇒ RateLimit:Enabled задан явно» (fail-closed).
  3. [Required] CORS fail-open при пустом allowlist. Program.cs (AddCors): пустой Security:AllowedOrigins = любой origin + AllowCredentials (задумано для dev). Исправить: в Production пустой список = отказ на старте; «any origin» только в Development.
  4. [Required] Код инвайта пишется в audit_log сырым. JoinEndpoint.cs — capability-токен в вечном аудите операторов. Исправить: не логировать код (или его SHA-256).
  5. [Required] Пароль: минимум 4 символа. AuthEndpoints.cs, JoinEndpoint.cs. Для публичного SaaS — минимум 8–10 + проверка на границе; единая константа.
  6. [Required] Политика «ключ не перезаписывается маской» не реализована. SettingsService.cs (aiConfigs и tgKeys): PATCH со значением-маской (например sk-1…90ab, ≥8 симв., без enc:) зашифрует маску и безвозвратно потеряет ключ. Комментарий «пустой/маска → не меняется» не подкреплён кодом. Исправить: не шифровать значение, содержащее (U+2026) либо пустое; тест на roundtrip.
  7. [Required] DDL прикладной ролью на старте и из tenant-ручки. TenantProvisioningService.cs, FtsMaintenance.csCREATE SCHEMA/Migrate/INDEX на каждом старте и /api/admin/fts/rebuild. В проде это нарушение least privilege. Исправить: отдельные креды мигратора и runtime; fts-rebuild — операторской ручкой.
  8. [Required] TenantId без инварианта формата. Deal.SharedKernel/Tenants/TenantId.cs — значение идёт в Search Path строки подключения и в DDL; new TenantId(внешняя_строка) = connection-string-инъекция. Сейчас все потоки дают Guid, но тип не защищён. Исправить: конструктор от Guid / валидация 32 hex.
  9. [Required] gRPC-сервисы: нет серверных лимитов на входные данные. AiServiceImpl, MlServiceImpl, TelegramServiceImpl — контракты фиксируют лимиты («ядро обрежет»), но сервис их не enforcement: платные LLM-вызовы на мегабайтных промптах, гигантские SQLite-транзакции. Исправить: INVALID_ARGUMENT на границе + MaxReceiveMessageSize.
  10. [Required] mTLS по умолчанию выключен — тихая деградация до plaintext. MtlsOptions.cs — отсутствие/опечатка env молча даёт plaintext+только service-token. Исправить: fail-closed для Production (или warn-on-startup) как для session-ключа.
  11. [Required] Инвайт: не проверяется существование/статус тенанта. JoinService.cs — активация по «битому» инвайту даёт FK-500 или пользователя на несуществующем тенанте.
  12. [Required] AddUsageAsync не атомарно. ITenantLimitStore.cs — read-modify-write теряет списания при параллельных ИИ-вызовах. Исправить: UPDATE ... SET Used=Used+@n.
  13. [Required] Echo-маска: секрет ≤8 символов отдаётся как есть. SettingsService.Mask — маскировать всегда (кроме пустого).

B. Корректность / потеря данных (приоритет 2)

  1. [Required] Потеря данных при параллельных мутациях JSON-массивов проектной карточки. ProjectsService.cs (add_comment/add_link/remove_link), ProjectFilesService.cs: комментарии/ссылки/ файлы дописываются «read → PATCH полной заменой массива» без версии/транзакции; double-click теряет запись. Исправить: append одним SQL (jsonb ||/array_append) или optimistic concurrency по updated_at.
  2. [Required] Коллизия objectKey файла. ProjectFilesService.cs — «проект/карточка/мс_имя»: две загрузки в одну мс = перезапись объекта. Исправить: случайный суффикс / id записи в ключе.
  3. [Required] Дедуп-pump не атомарен. PipelineWorkerService.cs — Exists→Claim→create без проверки результата claim — два конкурентных прохода создадут две карточки. Исправить: повторный Exists/ проверка результата Claim перед созданием.
  4. [Required] Move из trash/archive на доску минует снятие спам-сигнала. CardsService.cs — валидируется только цель; «spam +1» не снимается (unlearn только в restore). Исправить: запрет исхода из archive/trash/taken в MoveLeadAsync (или симметричный unlearn).
  5. [Required] Параллельные пустые catch { } в модулях Telegram/Discovery — сбои зеркала/превью/ backfill невидимы (ILogger в модулях не используется). Исправить: логировать.
  6. [Required] ChangePassword (фронт) шлёт захардкоженный oldPassword='admin'. store.js, SettingsView.vue — после смены пароля повторная смена невозможна, и пароль живёт в реактивном state. Исправить: поле «текущий пароль», не хранить пароль в store.
  7. [Required] boot() роняет всё приложение одним сбоем (фронт). store.js: параллельные get без .catch — падение /api/rates (например) = toast «Сервер недоступен» + разлогин. Исправить: необязательные секции в индивидуальные .catch; разлогин только при 401.
  8. [Required] applySettings затирает несохранённые промпты (фронт). store.js — автосейв тумблера применяет полный ответ и перезаписывает textarea промптов. Исправить: применять только запатченные ключи.
  9. [Required] Гонки устаревших ответов поиска (фронт). store.js — старый ответ может перетереть свежий/очищенный. Исправить: seq-токен/AbortController.
  10. [Required] DeleteExpiredSessionsAsync на каждое разрешение сессии. AuthService.cs, OperatorAuthService.cs — глобальный DELETE по public-таблицам в hot-path каждого запроса. Исправить: фоновый цикл или «с вероятностью N%»/логин.
  11. [Required] ServiceTokenInterceptor проверяет токен только для unary RPC — первый же server-streaming RPC пройдёт без проверки; то же в access-логе. Исправить: все 4 handler'а.
  12. [Required] gRPC-логгер не логирует «прочие» исключения (только OCE/RpcException) — 500-эквивалент уходит мимо лога. Исправить: catch (Exception) → log + RpcException.
  13. [Required] Heartbeat/reconnect без таймаута — зависший ConnectAsync последовательно блокирует все тенанты и shutdown. Исправить: CancelAfter на попытку.
  14. [Required] QR: отмена RPC до первого URL не отменяет фоновую задачу — «скрытая» авторизация. Исправить: отменять саму задачу при отмене ожидания.
  15. [Required] TelegramBackfill fire-and-forget Task.Run из tenant-запроса без in-flight guard (параллельные полные перечитывания); фоновые задачи не отслеживаются хостом. Исправить: гейт операции
    • токен остановки хоста.
  16. [Required] int.Parse(apiId) из пользовательской KV-настройки TelegramEndpoints.cs — FormatException маскируется под 400 «не подключён». Исправить: TryParse + понятная ошибка.

C. Архитектура / дублирование (приоритет 3)

  1. [Required] 9 независимых реализаций чтения настроек (GetAsync+JsonDocument.Parse+дефолт) в Settings/IncomingRules/RatesService/Discovery*/DialogsService — расхождение семантики уже видно. + ~8 копий KV-хелперов (ReadBool/ReadInt/ReadString/ReadStringList) и 3 копии LoadRatesAsync в Kanban/Pipeline/Projects. Исправить: один публичный снапшот настроек в Settings или SharedKernel + общий RatesCacheReader.
  2. [Required] Обвязка gRPC-сервисов (ServiceTokenInterceptor/RpcCallLogging/MtlsOptions/MtlsCertificates/ Logging + Host) скопирована в 3 независимых sln. Исправить: общий проект Deal.Grpc.Hosting.
  3. [Required] Большие файлы: PipelineWorkerService (914), KanbanStore (726), DiscoveryStore (632), ProjectsService (576), ProjectsEndpoints (568), CardsService (475), Program.cs (695), LocalFieldsParser (438), GrpcTelegramClient (447), TelegramIngressService (409); фронт: SettingsView.vue (1779), DiscoveryView.vue (1243), store.js (2434). Исправить: декомпозиция (см. ниже).
  4. [Required] Фронт: MoveMenu вешает document-слушатель на каждую карточку (сотни карточек → сотни слушателей). Исправить: один глобальный обработчик + id открытого меню в store.
  5. [Required] Фронт: квадратичные пересчёты колонок. store.js — filter+sort на каждую колонку/ счётчик при каждом ре-рендере. Исправить: один computed Map<colId, sorted[]>.
  6. [Nit] Дублирование доменных констант между модулями (EmptyCommentDetail/JustNowLabel/MlSpamLabel/ DefaultChannelHue/PlannedStage-литералы) и расхождение предиката «активные правила» (Kanban vs AiClassifyContextBuilder) — вынести в единые реестры.
  7. [Nit] Middleware сессий (Session vs OperatorSession) и токен-генераторы (SessionTokens/ InviteCodeGenerator/TenantAdminService) дублируются — обобщить.
  8. [Nit] Легаси-ссылки на строки Python-прототипа в XML-doc (L177191 и т.п.) — устаревают; оставить «зачем/инвариант», убрать номера строк.
  9. [Nit] Форматтеры времени и «знание» о контактах/типах файлов в 3–4 местах (фронт) — единый модуль форматов и словари меток.
  10. [Nit] window.prompt в renameBoard на фоне единого ConfirmDialog; дубликаты 86400000; ширины колонок sm/md/lg в 3 местах — константы/единый RenameDialog.

D. Мёртвый код (кандидаты на удаление)

  • Фронт: utils.js fileTypeInfo/EXT_KINDS/KIND_LABELS (не импортируется); store.js — curName/fmtMoney вне store, moveLead-мёртвая ветка, trashLead-пустой if, openDialog (не используется), checkReminders (нигде не вызывается); опция «mock»-курсов — проверить, жив ли режим на бэкенде.
  • Бэкенд: Kanban DemoLeadFactory недостижимый fallback PrimaryContact; DiscoverySearchErrorCounter — singleton-счётчик без TTL/эвикции и с межтенантным ключом (переделать per-tenant или чистить).

E. Что соответствует хорошим практикам (подтверждено)

  • Тенант-изоляция сильная: схема на тенанта через Search Path, TenantDbContext запрещён вне tenant-запроса (fail-fast), AsyncLocal сбрасывается в finally, gRPC-ингресс берёт tenant-id только из metadata, SSE per-tenant.
  • SQL параметризован везде (FromSqlInterpolated/ExecuteSqlInterpolated); массовые операции — ExecuteUpdate/Delete; комментарии-батчи без N+1; AsNoTracking.
  • Секреты не покидают систему: ключи шифруются (enc:+nonce‖ct‖tag), наружу маски; токены сессий — SHA-256 хэши; пароли Argon2id; куки httpOnly+SameSite=Lax; fail-closed service-token (с явным гардом «пусто≠пусто»); path traversal защищён (SessionStore/ModelPool валидируют tenant-id как имя файла).
  • Фронт: XSS-аудит чистый (v-html только через экранирующий renderSourceMessage со схемами http/tg), токенов в localStorage нет (httpOnly-кука), все target=_blank с rel=noreferrer.
  • Чистая архитектура port&adapter в модулях (нет EF/HTTP в Application), DTO-рекорды, DI-Registrar'ы, направленные зависимости без циклов, константы-каталоги вместо магических строк.

F. Рекомендуемый порядок исправлений

  1. Безопасность (A1–A13) — до любого прода. Точечные правки + тесты.
  2. Потеря данных/корректность (B14–B29) — гонки, дедуп, маски, boot/applySettings фронта.
  3. Архитектура (C30–C34) — вынос общего grpc-hosting, снапшот настроек, декомпозиция больших файлов, фронт: leadsByCol-компьютед и глобальный слушатель меню.
  4. Чистка мёртвого кода (D) + реестры констант (C35–C39) — в рамках рефакторингов, не отдельно.
  5. Заделы (Optional) — пагинация колонок, виртуализация списков, LRU для кэшей сессий WTelegram, батчинг провижининга схем, MinIO tenant-префикс, per-request size-лимиты загрузок, single-flight DiscoveryWorker.

Статус исправлений (2026-09-08, после ревью)

Выполнено в ходе rework-захода (детали — .superpowers/sdd/deal-stage8-quality-rework/progress.md и docs/superpowers/STATUS.md). Тесты: core 1135/1135, telegram 118/118, ai 52/52, ml 38/38, фронт npm run build OK.

A. Безопасность — закрыто (A1–A13):

  • A1 SSRF: SettingsService — baseUrl каталоговых облачных провайдеров не переопределяется (только local/custom); AiConnectionChecker — запрет private/loopback/link-local адресов (в т.ч. 169.254.169.254).
  • A2/A3: fail-closed в Production (RateLimit:Enabled обязателен, CORS-allowlist непустой, conn-string без фолбэка) — стартовые проверки Program.cs.
  • A4: код инвайта в аудите → SHA-256 codeHash (3 события, тесты обновлены).
  • A5: пароль минимум 8 (единый AuthService.MinNewPasswordLength).
  • A6: PATCH с маской ключа («…») больше не шифрует маску (терялся бы ключ); A13: короткие секреты маскируются всегда (MaskSecret), apiId остаётся как есть (не секрет).
  • A7: DDL (провижининг схем/миграции) — опциональная мигратор-строка ConnectionStrings:DealMigrator (ConnectionStringProvider.ForSchemaDdl); dev/тесты — прежнее поведение.
  • A8: TenantId — инвариант 32 hex (Guid N), фабрика FromGuid.
  • A9: gRPC-сервисы — лимиты входных данных (INVALID_ARGUMENT) + MaxReceiveMessageSize=4MiB.
  • A10: mTLS fail-closed в Production (сервисы).
  • A11: JoinService — целевой тенант обязан существовать и быть активным до резервирования кода.
  • A12: атомарный инкремент токенов (UPDATE ... UsedTokens=UsedTokens+@n) для Npgsql; EF-путь для InMemory.
  • Доп.: int.TryParse apiId; ResolveSession учитывает статус пользователя; очистка протухших сессий — вне hot-path.

B. Корректность/потеря данных — закрыто (B14–B29): атомарные append (comment/link/file) в ProjectStore (1 SQL), objectKey файла с id записи, дедуп-pump атомарен (Claim→bool), move из trash/archive/taken запрещён, пустые catch логируются (DiscLog/ILogger), фронт: смена пароля (oldPass), boot с .catch, applySettings не затирает промпты, seq-токены поиска; интерцепторы gRPC на все 4 вида RPC, логгер catch(Exception), reconnect с таймаутом, QR-cancel; backfill с in-flight guard + lifetime-токеном.

C. Архитектура — закрыто: C30 (единый TenantSettingsSnapshot вместо ~9 копий чтения настроек и 3 копий LoadRatesAsync; удалён клон RateTable.cs), C31 (общий src/grpc-hosting/Deal.Grpc.Hosting; 15 файлов дублей удалены), C32-декомпозиция (KanbanStore→5, PipelineWorkerService→8, DiscoveryStore→5, ProjectsService→4, CardsService→3, SettingsService→6, DiscoveryWorkerService→6 partial; фронт: store.js→слайсы store/, вынесены Telegram/Stop/Scope-вкладки SettingsView, DiscoveryCandidateCard), C33/C34 (MoveMenu, leadsByCol), C36 (UrlSafeToken). Закрыто после ревью (2026-09-09): C35 — общие реестры MlLearningLabels/SourceDefaults в Deal.Contracts (метки обучения ML «spam»/«t:hire»/«t:order» и дефолтный цвет источника «#666») вместо дублей MlSpamLabel/DefaultChannelHue/DefaultDialogHue/SpamLabel в Pipeline/Discovery/Telegram/Infrastructure; единый предикат «активные правила» — AiClassifyContextBuilder переведён на ColumnRules.HasActiveRules (Kanban; было расхождение Count>0 vs терм после trim); реестр ProjectStages (9 id-констант вместо литералов) и общий CardsService.JustNowLabel (Projects/адаптер KanbanStore); DiscoverySearchErrorCounter — TTL-эвикция (см. D). Задел: полный вынос остальных вкладок SettingsView (риск без e2e).

D. Мёртвый код: удалён (фронт: fileTypeInfo/EXT_KINDS/curName/fmtMoney/openDialog/checkReminders и др.; бэкенд: недостижимый PrimaryContact DemoLeadFactory и др.). DiscoverySearchErrorCounter — добавлена TTL-эвикция записей (EntryTtlSeconds=1 ч, ленивая при Next/Reset, часы инъекцией; +4 теста) — задел D закрыт.