Table of Contents
- Ревью качества кода «Дейл» (2026-09-08)
- Сводка
- A. Безопасность (приоритет 1)
- B. Корректность / потеря данных (приоритет 2)
- C. Архитектура / дублирование (приоритет 3)
- D. Мёртвый код (кандидаты на удаление)
- E. Что соответствует хорошим практикам (подтверждено)
- F. Рекомендуемый порядок исправлений
- Статус исправлений (2026-09-08, после ревью)
Перенесено из репозитория (
docs/superpowers/reviews/2026-09-08-code-quality-review.md). Актуальная версия — здесь, в вики.
Ревью качества кода «Дейл» (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)
- [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-фильтр); не принимать переопределение хоста для каталоговых провайдеров. - [Required] Rate-limit и анти-брутфорс выключены по умолчанию.
Deal.Api/Program.cs(регистрация лимитера),RateLimitOptionsдефолтEnabled=false→ без env в проде нет ни лимитов, ниLoginAttemptGuard. compose.prod форсируетtrue, но дефолт кода опасен при запуске вне compose. Исправить: стартовая проверка «Production ⇒ RateLimit:Enabled задан явно» (fail-closed). - [Required] CORS fail-open при пустом allowlist.
Program.cs(AddCors): пустойSecurity:AllowedOrigins= любой origin +AllowCredentials(задумано для dev). Исправить: в Production пустой список = отказ на старте; «any origin» только в Development. - [Required] Код инвайта пишется в audit_log сырым.
JoinEndpoint.cs— capability-токен в вечном аудите операторов. Исправить: не логировать код (или его SHA-256). - [Required] Пароль: минимум 4 символа.
AuthEndpoints.cs,JoinEndpoint.cs. Для публичного SaaS — минимум 8–10 + проверка на границе; единая константа. - [Required] Политика «ключ не перезаписывается маской» не реализована.
SettingsService.cs(aiConfigs и tgKeys): PATCH со значением-маской (напримерsk-1…90ab, ≥8 симв., безenc:) зашифрует маску и безвозвратно потеряет ключ. Комментарий «пустой/маска → не меняется» не подкреплён кодом. Исправить: не шифровать значение, содержащее…(U+2026) либо пустое; тест на roundtrip. - [Required] DDL прикладной ролью на старте и из tenant-ручки.
TenantProvisioningService.cs,FtsMaintenance.cs—CREATE SCHEMA/Migrate/INDEXна каждом старте и/api/admin/fts/rebuild. В проде это нарушение least privilege. Исправить: отдельные креды мигратора и runtime; fts-rebuild — операторской ручкой. - [Required] TenantId без инварианта формата.
Deal.SharedKernel/Tenants/TenantId.cs— значение идёт в Search Path строки подключения и в DDL;new TenantId(внешняя_строка)= connection-string-инъекция. Сейчас все потоки дают Guid, но тип не защищён. Исправить: конструктор от Guid / валидация 32 hex. - [Required] gRPC-сервисы: нет серверных лимитов на входные данные. AiServiceImpl, MlServiceImpl, TelegramServiceImpl — контракты фиксируют лимиты («ядро обрежет»), но сервис их не enforcement: платные LLM-вызовы на мегабайтных промптах, гигантские SQLite-транзакции. Исправить: INVALID_ARGUMENT на границе + MaxReceiveMessageSize.
- [Required] mTLS по умолчанию выключен — тихая деградация до plaintext.
MtlsOptions.cs— отсутствие/опечатка env молча даёт plaintext+только service-token. Исправить: fail-closed для Production (или warn-on-startup) как для session-ключа. - [Required] Инвайт: не проверяется существование/статус тенанта.
JoinService.cs— активация по «битому» инвайту даёт FK-500 или пользователя на несуществующем тенанте. - [Required] AddUsageAsync не атомарно.
ITenantLimitStore.cs— read-modify-write теряет списания при параллельных ИИ-вызовах. Исправить:UPDATE ... SET Used=Used+@n. - [Required] Echo-маска: секрет ≤8 символов отдаётся как есть.
SettingsService.Mask— маскировать всегда (кроме пустого).
B. Корректность / потеря данных (приоритет 2)
- [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. - [Required] Коллизия objectKey файла.
ProjectFilesService.cs— «проект/карточка/мс_имя»: две загрузки в одну мс = перезапись объекта. Исправить: случайный суффикс / id записи в ключе. - [Required] Дедуп-pump не атомарен.
PipelineWorkerService.cs— Exists→Claim→create без проверки результата claim — два конкурентных прохода создадут две карточки. Исправить: повторный Exists/ проверка результата Claim перед созданием. - [Required] Move из trash/archive на доску минует снятие спам-сигнала.
CardsService.cs— валидируется только цель; «spam +1» не снимается (unlearn только в restore). Исправить: запрет исхода из archive/trash/taken в MoveLeadAsync (или симметричный unlearn). - [Required] Параллельные пустые
catch { }в модулях Telegram/Discovery — сбои зеркала/превью/ backfill невидимы (ILogger в модулях не используется). Исправить: логировать. - [Required] ChangePassword (фронт) шлёт захардкоженный oldPassword='admin'.
store.js,SettingsView.vue— после смены пароля повторная смена невозможна, и пароль живёт в реактивном state. Исправить: поле «текущий пароль», не хранить пароль в store. - [Required] boot() роняет всё приложение одним сбоем (фронт).
store.js: параллельные get без .catch — падение /api/rates (например) = toast «Сервер недоступен» + разлогин. Исправить: необязательные секции в индивидуальные .catch; разлогин только при 401. - [Required] applySettings затирает несохранённые промпты (фронт).
store.js— автосейв тумблера применяет полный ответ и перезаписывает textarea промптов. Исправить: применять только запатченные ключи. - [Required] Гонки устаревших ответов поиска (фронт).
store.js— старый ответ может перетереть свежий/очищенный. Исправить: seq-токен/AbortController. - [Required] DeleteExpiredSessionsAsync на каждое разрешение сессии.
AuthService.cs,OperatorAuthService.cs— глобальный DELETE по public-таблицам в hot-path каждого запроса. Исправить: фоновый цикл или «с вероятностью N%»/логин. - [Required] ServiceTokenInterceptor проверяет токен только для unary RPC — первый же server-streaming RPC пройдёт без проверки; то же в access-логе. Исправить: все 4 handler'а.
- [Required] gRPC-логгер не логирует «прочие» исключения (только OCE/RpcException) — 500-эквивалент уходит мимо лога. Исправить: catch (Exception) → log + RpcException.
- [Required] Heartbeat/reconnect без таймаута — зависший ConnectAsync последовательно блокирует все тенанты и shutdown. Исправить: CancelAfter на попытку.
- [Required] QR: отмена RPC до первого URL не отменяет фоновую задачу — «скрытая» авторизация. Исправить: отменять саму задачу при отмене ожидания.
- [Required] TelegramBackfill fire-and-forget Task.Run из tenant-запроса без in-flight guard
(параллельные полные перечитывания); фоновые задачи не отслеживаются хостом. Исправить: гейт операции
- токен остановки хоста.
- [Required] int.Parse(apiId) из пользовательской KV-настройки
TelegramEndpoints.cs— FormatException маскируется под 400 «не подключён». Исправить: TryParse + понятная ошибка.
C. Архитектура / дублирование (приоритет 3)
- [Required] 9 независимых реализаций чтения настроек (GetAsync+JsonDocument.Parse+дефолт) в Settings/IncomingRules/RatesService/Discovery*/DialogsService — расхождение семантики уже видно. + ~8 копий KV-хелперов (ReadBool/ReadInt/ReadString/ReadStringList) и 3 копии LoadRatesAsync в Kanban/Pipeline/Projects. Исправить: один публичный снапшот настроек в Settings или SharedKernel + общий RatesCacheReader.
- [Required] Обвязка gRPC-сервисов (ServiceTokenInterceptor/RpcCallLogging/MtlsOptions/MtlsCertificates/
Logging + Host) скопирована в 3 независимых sln. Исправить: общий проект
Deal.Grpc.Hosting. - [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). Исправить: декомпозиция (см. ниже).
- [Required] Фронт: MoveMenu вешает document-слушатель на каждую карточку (сотни карточек → сотни слушателей). Исправить: один глобальный обработчик + id открытого меню в store.
- [Required] Фронт: квадратичные пересчёты колонок.
store.js— filter+sort на каждую колонку/ счётчик при каждом ре-рендере. Исправить: один computed Map<colId, sorted[]>. - [Nit] Дублирование доменных констант между модулями (EmptyCommentDetail/JustNowLabel/MlSpamLabel/ DefaultChannelHue/PlannedStage-литералы) и расхождение предиката «активные правила» (Kanban vs AiClassifyContextBuilder) — вынести в единые реестры.
- [Nit] Middleware сессий (Session vs OperatorSession) и токен-генераторы (SessionTokens/ InviteCodeGenerator/TenantAdminService) дублируются — обобщить.
- [Nit] Легаси-ссылки на строки Python-прототипа в XML-doc (L177–191 и т.п.) — устаревают; оставить «зачем/инвариант», убрать номера строк.
- [Nit] Форматтеры времени и «знание» о контактах/типах файлов в 3–4 местах (фронт) — единый модуль форматов и словари меток.
- [Nit]
window.promptв renameBoard на фоне единого ConfirmDialog; дубликаты 86400000; ширины колонок sm/md/lg в 3 местах — константы/единый RenameDialog.
D. Мёртвый код (кандидаты на удаление)
- Фронт:
utils.jsfileTypeInfo/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. Рекомендуемый порядок исправлений
- Безопасность (A1–A13) — до любого прода. Точечные правки + тесты.
- Потеря данных/корректность (B14–B29) — гонки, дедуп, маски, boot/applySettings фронта.
- Архитектура (C30–C34) — вынос общего grpc-hosting, снапшот настроек, декомпозиция больших файлов, фронт: leadsByCol-компьютед и глобальный слушатель меню.
- Чистка мёртвого кода (D) + реестры констант (C35–C39) — в рамках рефакторингов, не отдельно.
- Заделы (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 закрыт.