1
План stage6 services
stepan edited this page 2026-09-13 00:17:00 +03:00
This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

Перенесено из репозитория (docs/superpowers/plans/2026-09-05-deal-stage6-services.md). Актуальная версия — здесь, в вики.

Дейл (Deal) — Этап 6: Сервисы telegram/ai/ml (отдельные процессы) + Discovery + gRPC-ингресс Implementation Plan

Исторический документ этапа 6. Актуальное состояние — docs/superpowers/STATUS.md и docs/technical/Техническая-документация-Дейл.md.

Goal: Подключить к модульному монолиту src/core реальные автономные сервисы telegram/ai/ml как отдельные процессы (свои sln/контейнеры), общаясь по gRPC (.proto в src/contracts/), и оживить вкладки Vue-фронта «Каналы» (ChannelsView) и Discovery 1:1-контрактом /api: telegram-вкладка заменяет boot-заглушку GET /api/tg/status реальным статусом/QR-входом/списком диалогов/мониторингом/«Перечитать»; Discovery — полноценный модуль ядра (задачи поиска, кандидаты с оценкой по каскаду фильтров, чёрный список, авто-вступление с квотами, лог). Пайплайн и канбан начинают получать настоящие сообщения (входящий gRPC → EnqueueAsync), настоящие ИИ-классификацию/фильтр и ML-предсказания/обучение — за конфиг-флагами, с Local-заглушками как фолбэком, когда сервис недоступен/выключен.

Architecture: сервисы — самодостаточные процессы (namespace Deal.Telegram/Deal.Ml/Deal.Ai): telegram исполняет только команды ядра (сессии по тенантам 1:1, анти-бан, mark-as-read; ни БД-бизнеса, ни настроек), ml держит пул инкрементальных моделей per-tenant с сохраняемыми весами (онлайн-обучение без дата-сайентиста — 1:1 с проверенным python mlservice/model.py, не ONNX), ai — фасад LLM-провайдеров без БД: core передаёт заполненные промпты и конфиг провайдера в теле каждого запроса, сервис возвращает JSON-ответ модели + оценку токенов. В ядре: новый модуль Deal.Modules.Telegram (владелец tenant-таблиц Dialogs/TgMessages, каталог каналов и статус) с портом-гейтом ITelegramGateway, gRPC-сервер ингресса в Deal.Api (PushMessage → IngestService, SyncDialogs, StatusReport → SSE); новые модульные части Discovery (таблицы/сервисы/воркер 5 с/оценка/анти-бан); gRPC-адаптеры в Deal.Infrastructure заменяют Local-заглушки за флагом Services:{Ml,Ai,Telegram}:UseLocal.

Tech Stack: .NET 10 (Grpc.Tools/Google.Protobuf/Grpc.AspNetCore), WTelegramClient (NuGet), Net.Codecrete.QrCodeGenerator (SVG QR), Microsoft.Data.Sqlite (веса моделей), HttpClient (OpenAI-совместимые + Anthropic), существующие порты Contracts. Docker: сервисы добавляются в deploy/compose.dev.yml.

Spec: docs/architecture/2026-09-05-deal-architecture-design.md §6.27 (L145187: gRPC-контракты, сервисы, пул моделей, учёт токенов, mTLS+service-token); docs/api/api-map.md §3.3/3.7/3.8 (L123142, L187217), §2 SSE (L2743), §4.6/4.8/4.9/4.10 (настройки, каналы/discovery, статус), «кривые места» п.4/п.8/п.9 (L390400); roadmap этапа 6 (L8291); ТЗ §4.2/4.3/4.9, §5, §8; референс-семантика прототипа: backend/app/services/telegram.py (целиком), services/{discovery,discovery_worker,discovery_eval,ai,suggest,ml_client,ban_guard}.py, routers/{tg_routes, discovery_routes,ml_routes}.py, mlservice/model.py, backend/app/{db.py,constants.py,config.py,main.py}; фронт ChannelsView.vue/DiscoveryView.vue/store.js/api.js; образцы планов этапов 1–5.

Global Constraints

  • Проект НЕ git; фиксация — отчёты задач task-N-report.md и progress.md в .superpowers/sdd/deal-stage6-services/.
  • .NET 10 SDK; каждая sln собирается 0 warnings/0 errors (TreatWarningsAsErrors); dev-Postgres deal-postgres (:5433); curl-приёмка core :5080 (scripts/build.sh/scripts/test.sh — собирают/тестируют только src/core).
  • Код-стайл этапов 1–5: 1 тип = 1 файл; XML-doc на public; комментарии на русском; без регионов; без магических чисел (именованные константы); времена DateTimeOffset (UTC), наружу epoch-ms; JSON camelCase; {detail}-ошибки.
  • Сервисы — отдельные sln (src/{telegram-service,ml-service,ai-service}), ничего общего с core, кроме .proto и NuGet; ни один сервис не ходит в БД тенантов и не знает домен. Core — единственное место с БД и бизнес-логикой.
  • Vue-фронт, backend/, mlservice/ (python), корневой docker-compose.yml НЕ трогаем.
  • Сервисы подключаются флагами: по умолчанию dev = Local-заглушки (этапы 2–5), реальные сервисы — UseLocal=false.
  • Строки ошибок/тостов/причин 1:1 с прототипом (см. задачи): «Telegram не подключён», «Сначала сохраните Telegram api_id и api_hash в настройках», «QR не активен — начните вход по QR», «Неверный код», «Код истёк — запросите новый», «Неверный облачный пароль», «Telegram подключён, сессия сохранена», «Telegram отключён», «Уже вступили в этот источник», «Уже вступили — удалите источник из каналов», «Задача не найдена», «Кандидат не найден» и т.д.
  • НЕ выполнять автоматических сетевых подключений к Telegram/LLM в тестах: приёмка сервисов — unit + in-proc gRPC с фейками; живые проверки Telegram помечены «ручная проверка» (нужны api_id/api_hash/QR).
  • Новые NuGet в сервисах: Grpc.AspNetCore, Grpc.Tools, Google.Protobuf, WTelegramClient, Net.Codecrete.QrCodeGenerator, Microsoft.Data.Sqlite; в core: Grpc.AspNetCore, Grpc.Tools, Google.Protobuf, Microsoft.Extensions.Http (есть).

Зафиксированные решения (Rulings этапа)

Сокращения путей: TG= src/telegram-service/, ML= src/ml-service/, AI= src/ai-service/, PR= src/contracts/, C= src/core/Deal.Contracts/, I= src/core/Deal.Infrastructure/, A= src/core/Deal.Api/, PL= src/core/Deal.Modules.Pipeline/, KB= src/core/Deal.Modules.Kanban/, ST= src/core/Deal.Modules.Settings/, TM= src/core/Deal.Modules.Telegram/, DC= src/core/Deal.Modules.Discovery/, T= src/core/tests/Deal.Tests.Unit/, DEP= deploy/compose.dev.yml.

  • Ruling 1 (а) — контракты .proto, кодогенерация, metadata. Три файла: PR/telegram.proto, PR/ai.proto, PR/ml.proto (пакеты deal.telegram.v1/deal.ai.v1/deal.ml.v1, option csharp_namespace Deal.Grpc.Telegram/Deal.Grpc.Ai/Deal.Grpc.Ml). Каждый RPC несёт обязательные gRPC-metadata: tenant-id (строка) и service-token; серверный interceptor (общий шаблон в каждом процессе) проверяет service-token против env DEAL_SERVICE_TOKEN (общий в compose; отказ — UNAUTHENTICATED). Каждый сервис проверяет принадлежность по своей модели (сессия/модель тенанта есть — иначе NOT_FOUND/FAILED_PRECONDITION), полю не доверяет. Ошибки домена — INVALID_ARGUMENT/NOT_FOUND/UNAVAILABLE с detail = текст причины 1:1; FloodWait → RESOURCE_EXHAUSTED с кодом flood. Кодогенерация — Grpc.Tools: каждый процесс компилирует только свои .proto через <Protobuf Include="..\..\contracts\X.proto" GrpcServices="Both" Link="Protos/X.proto"/> (генерация client+server в одном проходе; неиспользуемая сторона игнорируется): telegram-service — telegram.proto, ml/ai-сервисы — свои; core (Deal.Api и Deal.Infrastructure по месту использования) — все три (telegram: сервер Ingress + клиент-гейт; ai/ml: клиенты). Контракты — единственный «язык» между процессами (дизайн-док L145–152).
  • Ruling 2 (безопасность dev/prod). Dev (этап 6): gRPC без mTLS — plaintext в локальной сети/хосте (localhost/compose-сеть) + обязательный service-token вторым фактором. mTLS-сертификаты, их генерация и prod-compose — этап 7 (roadmap L9597: «compose-prod … безопасность (mTLS…)»); код интерцепторов один и тот же, включение TLS в этап 7 не меняет контракты. Обоснование: 4 процесса + генерация/ротация сертификатов в dev — высокая трудоёмкость без защиты реальных данных; service-token закрывает сценарий «случайный процесс в сети».
  • Ruling 3 (б) — telegram-service: библиотека и сессии. Библиотека — WTelegramClient (де-факто стандарт .NET, активная поддержка, API-уровень MTProto; TeleSharp/TLSharp заброшены). Один клиент на тенанта (tenantId → WTelegram.Client, 1:1; команды исполняются только на сессии своего тенанта; нет сессии → отказ). Хранение сессий — файлы data/sessions/<tenantId>.session (session_pathname WTelegramClient; volume в compose). Шифрование at-rest: файл сессии оборачивается AES-GCM (существующий AesGcmSecretCipher-паттерн этапа 2; ключ — env DEAL_TELEGRAM_SESSION_KEY, 32 байта base64): сервис держит расшифрованный файл только в памяти процесса (temp-файл под личным каталогом процесса) и перешифровывает при сохранении/остановке. api_id/api_hash — НЕ env, а настройка tgKeys тенанта (Settings, шифруется AES-GCM с этапа 2; api-map §4.6 L337); core расшифровывает и передаёт в теле запросов подключения. Внутренний анти-бан сервиса (паузы между сетевыми операциями одной сессии): backfill 1.5–3 с/сообщение и 3–6 с/диалог, поиск 2–4 с (константы telegram.py L3536, ban_guard.search_pause L7880); mark-as-read сразу после приёма/чтения. Внешний анти-бан (суточная квота авто-вступлений, паузы 50–70 с, flood-день, стоп-кран) — владение core (воркер Discovery), счётчики в tenant-БД.
  • Ruling 4 (в) — ml-service: алгоритм и сохраняемость. НЕ ONNX и НЕ ML.NET: переносим инкрементальную наивно-байесовскую модель по терминам 1:1 с mlservice/model.py (tokenize L7887, upsert L105131, predict L184293, adaptive margin L4255, самооценка eval L296322, status/reset L325354). Обоснование: (1) python-прототип уже даёт работающее онлайн-обучение на русском тексте без дата-сайентиста, порт-контракт Deal (MlPredictResultDto/status) спроектирован 1:1 под его ответы; (2) ONNX Runtime не умеет онлайн-обучение (нужен экспорт/переобучение вне процесса), ML.NET — не для инкрементального обучения; (3) сохраняемость весов = три таблицы. Хранилище — SQLite-файл на тенанта data/ml/<tenantId>.sqlite (Microsoft.Data.Sqlite), таблицы classes(label,n,updated_at)/terms(label,term,count)/eval_log(created_at,expected,predicted,correct) 1:1 db-схемы model.py L64–75; запись — транзакциями, batch-вставка терминов (executemany-эквивалент). Пул: ConcurrentDictionary<tenantId, TenantModel>, модель лениво грузится по первому обращению, у каждой — свой lock (predict/learn сериализованы на тенанта). Перенос «мозгов» между инстансами (экспорт/импорт, дизайн-док L176) — по решению владельца НЕ делаем; сохранение между рестартами обязательно (файлы). Пороги: MIN_TOTAL 20, MIN_WINNER 6, MIN_WINNER_SPAM 4, MIN_HITS 2, MARGIN 0.9; адаптивный отрыв 0.35/0.5/0.7 после 400/150/60 примеров; классы типа t:hire/t:order (MIN_TYPE_WINNER 4); веса сигналов 1.0 (пользователь), 0.4 (ИИ), 0.6 (правила) — константы ml_client.py L2628.
  • Ruling 5 (г) — ai-service: устройство и контракт с core. ai-service без БД: core передаёт в теле каждого запроса (1) заполненные промпты (fill_prompt L63–77: подстановка {domain}/{keywords} из настроек тенанта делает core), (2) конфиг активного провайдера (id/base/model/apiKey/api_style — расшифрованный core из aiConfigs), (3) текст. Методы: Filter (промпт aiFilterPrompt, текст) → {pass,reason}; Classify (system_prompt = aiPrompt+cardPrompt, user-контекст «Доски + примеры разметки + Сообщение» — собирает core) → {ok,json}json-строка извлечённого ответа модели (типовая схема ответа задаётся промптом, python держит его сырым dict; строгий маппинг json→AiParsedLeadDto делает core, 1:1 normalize_stack/clean_budget/ build_contacts/python _store_lead); GenerateKeywords (фикс. промпт L36–47 routes + описание) → {keywords} (очистка _clean_keywords в core); EvaluateFit (текст + description + keywords задачи, промпт discovery_eval L5054) → {fit,reason}. Вызовы LLM: OpenAI-совместимые POST {base}/chat/completions (Bearer), Anthropic POST {base}/v1/messages (x-api-key+anthropic-version); temperature 0.2; таймауты 90 с (openai) / 60 с (anthropic); retry max_retries=2 с паузами 0.8/2 с; извлечение JSON из markdown-обёрток (extract_json L175183); ошибки провайдера наружу как UNAVAILABLE с текстом «ИИ (имя) не ответил корректно — повторите попытку через несколько секунд». Учёт токенов: ответ несёт usage{prompt/completion/total} — берётся из usage API-ответа провайдера, при отсутствии оценивается по символам (≈chars/4); core копит в tenant-KV aiTokenUsage (этап 7 — лимиты/бюджеты). Выключатели aiEnabled/aiFilterEnabled читает core (как в воркере этапа 4) — сервис их не знает.
  • Ruling 6 (д) — core-интеграция ML/AI: флаги, адаптеры, судьба MlOutbox. Секция конфигурации Services:Ml|Ai{UseLocal: bool (default true), Endpoint: string} (env SERVICES__ML__USELOCAL=false, SERVICES__ML__ENDPOINT=http://localhost:5103). В AddDealIntegrations регистрируются gRPC-адаптеры (GrpcMlClient: IMlClient, GrpcAiClassifier: IAiClassifier, GrpcAiTools: IAiTools — новый порт, Ruling 9), когда UseLocal=false, иначе текущие Local-* (фолбэк). Никакой логики переключения в рантайме — выбор на старте. Судьба MlOutbox: PushAsync ВСЕГДА пишет в MlOutbox (этап 3), новый фоновый MlOutboxFlushScheduler (10 с, per-tenant цикл, эталон PipelineWorkerScheduler) выгружает по 10 строк (ORDER BY created_at), батч ≤100/цикл, в ml.proto TrainBatch; удаляет строки только после успеха; при недоступности сервиса строки остаются (python L5682). ResetAsync: сервис Reset + ClearOutboxAsync (1:1 reset_model L110124). Кэш статуса сервиса 15 с (python L3031, refresh_status) → reachable в /api/ml/status; недоступен — Predict → «не уверен», Status → кэш. Счётчики/выключатели/SSE воркера не меняются (Ruling 5 этапа 4; исключения порта воркер уже ловит). Входящий gRPC telegram: сервер в Deal.Api (отдельный порт) — см. Ruling 7.
  • Ruling 7 (д/ж) — Telegram-ингресс и каталог каналов. Новый чистый модуль TM Deal.Modules.Telegram — владелец tenant-таблиц (миграция TenantTelegram контекста TenantDbContext): Dialogs (Id string PK, Name/Handle/Kind/Hue, Monitor bool, LastText/LastAt, Backfilled bool, UpdatedAt; 1:1 db.py L7686) и TgMessages (Id m_<dialog>_<msg> PK, DialogId, Text, MsgAt, LeadId nullable; L6774). Порт ITelegramStore + DTO (диалог §4.8 L349, сообщение превью L351) + DialogsService: List, SetMonitor (первое включение → фон Backfill), SetMonitorAll (1:1 L548567), SyncFromTelegram(entries) — авто-мониторинг новых по autoMonitorNew, обновление имени/типа, удаление отсутствующих (1:1 _persist_dialogs L468503), MarkBackfilled, SavePreview. Порт-гейт C/Integrations/ITelegramGateway.cs (команды наружу): StatusAsync, StartQrAsync, StartPhoneAsync, SendCodeAsync, SendPasswordAsync, LogoutAsync, RefreshDialogsAsync (→entries), SetMonitorAsync (id, enabled), SetMonitorAllAsync, BackfillAsync(id, force), ReadRecentAsync(id, limit) (превью), SearchAsync, InfoAsync, ReadForEvalAsync(id, limit), JoinAsync(username), LeaveAsync(id); недоступность сервиса → исключение → ветки эндпоинтов как «не подключён». Входящий gRPC в core (сервер A/Telegram/TelegramIngressService.cs, RPC PushMessage/SyncDialogs/ReportStatus): kestrel-порт :5082 (env GRPC_INGRESS_PORT), Http2; интерцептор service-token; tenantId из metadata → собственный scope с ITenantContext.SetTenant (доверенный источник, не сессия); PushMessage (dialogId/msgId/text/канальные поля/hue/msgAt — hue считает сервис по DIALOG_HUES-палитре) → PipelineIngestService.EnqueueAsync (тот же контракт, что demo-ingest, L759) + пишет превью в TgMessages; SyncDialogsDialogsService.SyncFromTelegram, ответ = актуальный список monitored id (сервис держит зеркало мониторинга в памяти); ReportStatus{phase,connected,listener,account,error,qrUrl} → KV tgAccount/tgStatus (внутренние ключи SettingsKeys) + из Api-слоя SSE system_status и тосты «Telegram подключён, сессия сохранена»/«Telegram отключён» при переходах фаз (python L178–207). Сервис сам фильтрует события по своему зеркалу monitored (обновляется ответом SyncDialogs и командой SetMonitor) — как python _monitored.
  • Ruling 8 (ж) — /api/tg и статус. Снимается boot-заглушка BootStubEndpoints (остаётся в коде до Task 14). Эндпоинты 1:1 api-map §3.3 (13 шт., фронт): статус/start-phone/start-qr/send-code/send-password/logout/qr-image/ dialogs/refresh/monitor-all/backfill-all/{id}/monitor/{id}/backfill(сервер-only)/preview. GET /api/tg/status (§4.9): live-поля (phase/connected/listener/error/qrUrl) из gateway (сервис недоступен → idle-форма), account из KV tgAccount, monitored = count(Dialogs WHERE Monitor), keysSet из настроек. GET /qr-image — SVG через Net.Codecrete.QrCodeGenerator (SVG-first, без внешних зависимостей; 404 «QR не активен — начните вход по QR»). Публикации SSE system_status/toast из Api-слоя (Ruling 5 этапа 3); фронт-флоу 1:1 (store.js L14191508).
  • Ruling 9 (д/е) — Discovery: модуль, таблицы, порт ИИ-инструментов. Новый модуль DC (чистый) — владелец таблиц (миграция TenantDiscovery): DiscTasks/DiscCandidates/DiscBlacklist/DiscLog 1:1 db.py L136196 (+idx L177/196; json-колонки marks/topics/keywords text). DTO §4.8 L353355; IDiscoveryStore; сервисы DiscoveryTasksService/DiscoveryCandidatesService/DiscoveryBlacklistService/DiscoveryLogService + DiscoveryPlanGuard — 1:1 discovery.py: create (имя; plan 1..discJoinLimit; бюджет активных задач, L234–282), patch (рост plan с бюджетом), delete (с кандидатами и логом), start (пустые ключи → 400 «Нет ключевых слов для поиска — добавьте их в задачу»; reset прогресса для done/failed), pause, advance_search, кандидаты (add с исключениями «уже мониторится»/«чёрный список»/«кандидат есть», L409453; set_candidate; mark_joined/rejected L497567; blacklist), лог. Новый порт C/Integrations/IAiTools.cs: GenerateKeywordsAsync(description){ok, keywords, error}, EvaluateFitAsync(text, description, keywords){fit, reason} — локальные реализации на этапе 6 не нужны (Disco-воркер сам падает в эвристику при сбое/aiEnabled=false, python L187194); порт реализуется gRPC-адаптером GrpcAiTools за тем же флагом Services:Ai:UseLocal=false.
  • Ruling 10 (е) — Discovery: воркер, оценка, анти-бан. DiscoveryWorkerScheduler (5 с, per-tenant, эталон PipelineWorkerScheduler) + DiscoveryWorkerService.TickAsync — одно действие за тик, порядок шагов 1:1 discovery_worker.tick L444484: (1) план достигнут → done+лог; (2) поиск — следующий ключ задачи через gateway.Search (личные чаты/боты пропускаются, kind→channel/group/forum); (3) оценка первого new кандидата: info (kind/forum/участники; minSubscribers → skip), чтение выборки (ReadForEval; история недоступна → метки «канал: история недоступна»/«закрытая группа (история скрыта) — вступите сами»; язык ru → skip при «не русский», иначе метка; <3 сообщений → метка «мало сообщений»), фит: ML-спам (Predict через IMlClient, только mlEnabled) → не подходит; ИИ (IAiTools.EvaluateFit, если aiEnabled) → иначе эвристика по ключам; форумы — по темам (group_by_topic; passed — есть проходная тема); вердикт — total>=3 && ratio*100>=threshold (passed L229237). (4) авто-вступление первого review при autoJoin: повторная проверка «не состоим» → пауза discJoinDelayMin..Max (core) → join; FloodWait/ошибка → лог flood/error, join_failures (3 → delete); успех → mark_joined(auto), +в Dialogs (монитор on), +фоновый Backfill, −чёрный список. Лимит: авто-вступления за сутки по DiscLog event='join_auto' (UTC) < discJoinLimit; discFloodDay (внутренний KV, ключ SettingsKeys DiscFloodDay — новый) и discPaused стопят сетевые шаги. Метки/поля кандидата и fitRatio 1:1 (L154173, marks L8589).
  • Ruling 11 (е) — эндпоинты Discovery. 1:1 api-map §3.8 (13 шт.): tasks CRUD+start/pause+generate-keywords, candidates(статус-фильтр), join/reject (ручные, вне квот; ошибки 400 «Уже вступили…»), blacklist, log. generate-keywords: aiEnabled/ключ-недоступность → {keywords:[], error} HTTP 200 (мягкие ошибки, api-map L209, «кривое место» п.7), успех — _clean_keywords-фильтр в core (≤30, ≤60 симв., дедуп). Счётчики/статусы задач и кандидатов — как discovery.py.
  • Ruling 12 (з) — compose и окружение dev. В DEP добавляются сервисы telegram-service/ai-service/ ml-service: build из src/<svc>/Deal.*.sln (Dockerfile в корне сервиса), порты 5101/5102/5103 на host, volumes deal_tg_sessions (/data/sessions), deal_ml_data (/data/ml), общий env DEAL_SERVICE_TOKEN; healthcheck — gRPC health (встроенный Grpc.HealthCheck, порт health на том же endpoint). core dev запускается из хоста и ходит на localhost:5101..5103 (SERVICES__*__ENDPOINT), сервисы ходят в core-ингресс через SERVICES__CORE__INGRESS=http://host.docker.internal:5082 (env). Порядок подъёма не критичен: Local-фолбэки переживают отсутствие сервисов; сквозная приёмка — при поднятых процессах.
  • Ruling 13 (и/к) — события/безопасность. Новых типов SSE нет: используются system_status/toast (telegram), существующие new_lead (после карточки — уже в PipelineWorkerScheduler). Аудит команд сервиса (tenantId, действие, диалог, результат) — структурированные логи Serilog на каждом RPC (этап 7 — аудит-поток); rate-лимиты gRPC-ингресса — этап 7. Ключи/секреты не логируются; DEAL_ENCRYPTION_KEY/DEAL_SERVICE_TOKEN/ DEAL_TELEGRAM_SESSION_KEY — только env.

Задачи

Отчёты — task-N-report.md в .superpowers/sdd/deal-stage6-services/. Пути сокращены по Rulings.

Task 1: .proto-контракты telegram/ai/ml + спецификация

Files: Create: PR/telegram.proto, PR/ai.proto, PR/ml.proto, PR/README.md (сервисы/RPC/messages/поля, metadata tenant-id+service-token, коды ошибок, deadline-рекомендации). telegram.proto: TelegramService GetStatus/StartQr/StartPhone/SendCode/SendPassword/Logout/RefreshDialogs(→entries[])/SetMonitor/SetMonitorAll/ Backfill/ReadRecent/Search/GetInfo/ReadForEval/Join/Leave + IngressService PushMessage/SyncDialogs/ReportStatus (контракты Rulings 7). ai.proto: AiService Filter/Classify/GenerateKeywords/EvaluateFit (Ruling 5; usage в каждом reply). ml.proto: MlService Predict/Status/Reset/TrainBatch (поля 1:1 с MlPredictResultDto/status: classes map, eval{count,correct,accuracy}, take/label/scores/hits/ready/margin/terms/type).

Источники: Rulings 1/3/5/7; IMlClient/IAiClassifier + Models/*.cs (core Contracts, формы DTO); mlservice/model.py predict/status; ai.py filter_incoming/classify; telegram.py методы (имена L134–873).

Acceptance: файлы + README со схемой каждого RPC (поля/messages/коды) согласованы; контракты валидируются компиляцией в Task 2–4 (кодогенерация — первый прогон здесь невозможен без csproj). Отчёт: task-1-report.md.

Task 2: Каркас telegram-service (sln, host gRPC, health, service-token)

Files: Create: TG/Deal.Telegram.sln, TG/Deal.Telegram/Deal.Telegram.csproj (link telegram.proto, Server), TG/Deal.Telegram/Program.cs (Kestrel :5101 Http2; AddGrpc+HealthChecks; env PORT/GRPC_PORT), TG/Deal.Telegram/ServiceTokenInterceptor.cs, TG/Deal.Telegram/TelegramServiceImpl.cs (заглушки: методы → UNIMPLEMENTED), TG/Deal.Telegram/Dockerfile, TG/Deal.Telegram.Tests/ (хост поднимается, health OK, запрос без токена → UNAUTHENTICATED), DEP — запись telegram-service.

Источники: Rulings 1/2/12; эталон gRPC-сервера — настройка AddGrpc/HealthChecks (документация Grpc.AspNetCore).

Acceptance: dotnet build Deal.Telegram.sln 0/0 (доказывает кодогенерацию telegram.proto); юнит-тесты: health ready; интерцептор отклоняет пустой/неверный токен. Отчёт: task-2-report.md.

Task 3: Каркас ml-service (sln, host gRPC, health)

Files: Create: ML/Deal.Ml.sln, ML/Deal.Ml/Deal.Ml.csproj (link ml.proto Server), ML/Deal.Ml/Program.cs (Kestrel :5103, env GRPC_PORT), ML/Deal.Ml/ServiceTokenInterceptor.cs, ML/Deal.Ml/MlServiceImpl.cs (заглушки), ML/Deal.Ml/Dockerfile, ML/Deal.Ml.Tests/ (health; token), запись ml-service в DEP.

Источники: Rulings 1/2/12; Task 2 (эталон).

Acceptance: build 0/0 (кодогенерация ml.proto); тесты health/token PASS. Отчёт: task-3-report.md.

Task 4: Каркас ai-service (sln, host gRPC, health)

Files: Create: AI/Deal.Ai.sln, AI/Deal.Ai/Deal.Ai.csproj (link ai.proto Server), AI/Deal.Ai/Program.cs (Kestrel :5102, env GRPC_PORT), AI/Deal.Ai/ServiceTokenInterceptor.cs, AI/Deal.Ai/AiServiceImpl.cs (заглушки), AI/Deal.Ai/Dockerfile, AI/Deal.Ai.Tests/ (health; token), запись ai-service в DEP.

Источники: Rulings 1/2/12; Task 2.

Acceptance: build 0/0 (кодогенерация ai.proto); тесты PASS. Отчёт: task-4-report.md.

Task 5: ml-service — движок инкрементальной модели (per-tenant, SQLite)

Files: Create: ML/Deal.Ml/Model/ModelConstants.cs (пороги Ruling 4), ML/Deal.Ml/Model/MlTokenizer.cs (снятие ссылок regex + токены [a-zа-яё0-9@+.#]+, len≥3 и «~prefix» len≥6 — 1:1 L7887), ML/Deal.Ml/Model/OnlineNaiveBayes.cs (upsert/learn/batch/predict/status/reset/_maybe_eval, математика L184–323: score термина w<1→1.0 иначе 1+(w1)/(w+1); prior n/total; best=score+3·prior; adaptive margin; type-решение), ML/Deal.Ml/Storage/MlDb.cs (Microsoft.Data.Sqlite; EnsureSchema/Tables), ML/Deal.Ml/Model/TenantModel.cs + ModelPool.cs (lazy-load по тенанту, lock на модель), ML/Deal.Ml/Model/ModelState.cs (состояние: классы/термины/ eval-окно, JSON). Тесты ML/Deal.Ml.Tests/: tokenize; learn→predict спам/колонка; ready-пороги (20/6/4/2); адаптивный margin; delta<0 «разучивание»; eval-окно (50/200); перезапуск пула сохраняет веса (2-й инстанс на тот же файл).

Источники: mlservice/model.py целиком; Ruling 4; референс predict-математики L184293.

Acceptance: build 0/0; тесты PASS (обучение/предсказание на русских примерах: «нужен middle python…» → колонка/тип; «резюме…» → spam после обучения). Отчёт: task-5-report.md.

Task 6: ml-service — gRPC-сервис поверх пула

Files: Modify: ML/Deal.Ml/MlServiceImpl.cs — Predict/Status/Reset/TrainBatch; tenantId metadata → ModelPool (модели нет — она создаётся лениво: для Predict отсутствие опыта даёт «не готов» — не ошибка; Ruling 4); TrainBatch = learn_batch (1 транзакция) → число примеров; Reset — reset модели + пересоздание файла (очистка); Status — ready/classes/learned/eval 1:1. Тесты: in-proc gRPC (GrpcChannel к тестовому хосту): train → predict; train-батч из 3; reset обнуляет; неверный service-token → UNAUTHENTICATED.

Источники: mlservice/server.py (эталон форм ответов), model.py status/reset; Rulings 1/4/6.

Acceptance: build 0/0; in-proc gRPC-тесты PASS. Отчёт: task-6-report.md.

Task 7: ai-service — LLM-фасад (OpenAI-совместимые + Anthropic)

Files: Create: AI/Deal.Ai/Llm/LlmConfig.cs (provider: id/name/base/model/key/apiStyle/local), AI/Deal.Ai/Llm/ LlmHttpClient.cs (HttpClientFactory; OpenAI POST {base}/chat/completions Bearer temperature 0.2 max_tokens 8000; Anthropic POST {base}/v1/messages x-api-key+version; таймауты 90/60 с), AI/Deal.Ai/Llm/LlmRetryPolicy.cs (2 ретрая: 0.8 с/2 с — ai.py L96117), AI/Deal.Ai/Llm/JsonExtractor.cs (extract_json L175183), AI/Deal.Ai/Llm/TokenEstimator.cs (usage провайдера или chars/4), AI/Deal.Ai/Llm/ProviderCaller.cs (ошибки → AiException с кодом). Тесты: фейковый HttpMessageHandler: OpenAI-ответ; Anthropic-ответ; markdown-обёртка; usage из ответа и оценка; 3 неудачи → исключение с текстом L115–117; таймаут.

Источники: ai.py _call_openai/_call_anthropic/chat_json/extract_json (L80183); Ruling 5.

Acceptance: build 0/0; unit-тесты PASS (без сети). Отчёт: task-7-report.md.

Task 8: ai-service — gRPC AiService

Files: Modify: AI/Deal.Ai/AiServiceImpl.cs — Filter (chat_json по фильтр-промпту → pass/reason; при ok=false из модели — pass:true,skipped? нет: воркер шлёт только при aiFilterEnabled; ошибка → UNAVAILABLE), Classify (json → reply{ok,json}), GenerateKeywords (промпт Ruling 5 → keywords), EvaluateFit (промпт discovery_eval → fit/reason); каждый reply + usage. Тесты in-proc: все 4 метода с фейковым провайдером; недоступный провайдер → UNAVAILABLE.

Источники: ai.py L188258; discovery_routes L3647/189211; discovery_eval L5054/153194; Rulings 1/5.

Acceptance: build 0/0; in-proc тесты PASS. Отчёт: task-8-report.md.

Task 9: telegram-service — сессии, подключение, QR, статус

Files: Create: TG/Deal.Telegram/Sessions/TgOptions.cs (session dir, DEAL_TELEGRAM_SESSION_KEY), TG/Deal.Telegram/ Sessions/SessionFileCipher.cs (AES-GCM обёртка файла), TG/Deal.Telegram/Sessions/TenantSession.cs (id тенанта, клиент WTelegramClient, состояние), TG/Deal.Telegram/Sessions/SessionFarm.cs (пул 1 акк/тенант, auto_resume на старте — авторизованная сессия → ready, L209222), TG/Deal.Telegram/Telegram/ClientFactory.cs (конфиг: api_id/api_hash из запроса; session_pathname; внутренние паузы). Реализация методов: StartQr/StartPhone/SendCode/ SendPassword/Logout/GetStatus (фазы idle|phone|code|password|qr|ready, account, qrUrl; heartbeat/авто-возобновление фоновым циклом 30 с). Тесты: cipher roundtrip; farm: tenant-изоляция (нет сессии → отказ); фазовые переходы на fake-клиенте (абстракция ISessionClient); ручная проверка QR — отдельно.

Источники: telegram.py L82222, L286329; config.py L31 (SESSIONS_DIR); Rulings 1/3.

Acceptance: build 0/0; unit PASS. ⚠ Ручная проверка: реальный QR-вход/код/2FA с кредов (api_id/api_hash), auto_resume после рестарта контейнера. Отчёт: task-9-report.md.

Task 10: telegram-service — диалоги, мониторинг, backfill, поток в core

Files: Create: TG/Deal.Telegram/Dialogs/DialogCatalog.cs (зеркало monitored-набора тенанта: SetMonitor/ SetMonitorAll/актуализация ответом SyncDialogs), TG/Deal.Telegram/Dialogs/RealtimeListener.cs (NewMessage → фильтр по зеркалу → PushMessage в core; mark-as-read sendReadAcknowledge; сохранение last_text? нет — только пуш), TG/Deal.Telegram/Dialogs/BackfillService.cs (последние 10 с паузами 1.5–3 с/сообщение и 3–6 с/диалог; read-ack; реверс-порядок от старых к новым; force; L331390), TG/Deal.Telegram/Dialogs/RealtimeSweep.cs (30 с: догон непрочитанных по unread_count, паузы, read-ack, L392456), TG/Deal.Telegram/Core/CoreIngressClient.cs (gRPC-клиент к SERVICES__CORE__INGRESS; PushMessage/SyncDialogs; сбой — лог, упущенное догоняет sweep). Реализация RPC RefreshDialogs/SetMonitor/SetMonitorAll/Backfill/ReadRecent (превью, свежие из TG). Тесты: фильтр мониторинга; backfill-паузы (fake clock); PushMessage-клиент к in-proc fake-серверу ингресса.

Источники: telegram.py L244283, L331456, L505620; Rulings 3/7.

Acceptance: build 0/0; unit/in-proc PASS (без реальной сети). ⚠ Ручная проверка: refresh/подписка/backfill живого аккаунта. Отчёт: task-10-report.md.

Task 11: telegram-service — discovery-операции (search/info/read/join)

Files: Create: TG/Deal.Telegram/Discovery/DiscoveryOps.cs — Search (contacts.SearchRequest, пауза 24 с, кэш entities, выходные id подписанные, kind «канал»/«группа»/«чат», L624664), GetInfo (participants via GetFullChannel/GetFullChat, is_forum, L666716), ReadForEval (обычная лента; форумы — темы GetForumTopics + на тему get_messages(reply_to), per-topic 3..10, cap 5 тем; ошибки → ok:false no_history; L718816), Join (по username, FloodWait → RpcException RESOURCE_EXHAUSTED + код flood, L818839), Leave (L841848). Тесты: нормализация kind/username; формат ответов (fake-слой TL не трогаем — тесты на чистых мапперах ответов).

Источники: telegram.py L622873; ban_guard.search_pause; Rulings 3/7.

Acceptance: build 0/0; unit PASS (мапперы/валидация). ⚠ Ручная проверка: поиск/чтение/join живого аккаунта. Отчёт: task-11-report.md.

Task 12: core — gRPC-ингресс telegram (PushMessage/SyncDialogs/ReportStatus)

Files: Modify: A/Program.cs (второй Kestrel-listen :5082, Http2, GRPC_INGRESS_PORT; AddGrpc; AddAuthentication не нужен — интерцептор), A/Infrastructure/ не трогаем. Create: A/Telegram/IngressServiceTokenInterceptor.cs, A/Telegram/TelegramIngressService.cs (Grpc Deal.Grpc.Telegram.IngressServiceBase): PushMessage → scope с SetTenant(metadata tenant-id)PipelineIngestService.EnqueueAsync (+ ITelegramStore.SavePreview) → reply {accepted/duplicate}; SyncDialogs → DialogsService.SyncFromTelegram → reply{monitoredIds}; ReportStatus → KV tgStatus/tgAccount + публикация (через DI Api-слоя) SSE system_status/тостов на переходах фаз. Create: A/Telegram/TelegramIngressAuth.md? нет. Тесты (in-proc WebApplicationFactory+gRPC-канал): PushMessage кладёт строку очереди тенанта (эмуляция входящего сообщения — сквозная проверка без Telegram); неверный токен → отказ; PushMessage для несуществующего тенанта не падает (нет схемы → ошибка ловится, reply not-accepted).

Источники: Rulings 7/13; PipelineIngestService L759; паттерн scope/SetTenant — PipelineWorkerScheduler L169213.

Acceptance: build 0/0; тесты PASS (см. выше). Отчёт: task-12-report.md.

Task 13: core — модуль Telegram (таблицы, DTO, порт, DialogsService)

Files: Create: TM/.../TelegramModuleMarker.cs, I/Persistence/Entities/{DialogEntity,TgMessageEntity}.cs + конфигурации (JSON не нужен; индексы DialogId/MsgAt), TM/Application/Models/{TelegramDialogDto,TelegramMessageDto,TgStatusDto}.cs, TM/Application/ITelegramStore.cs, TM/Application/DialogsService.cs (List/SetMonitor/SetMonitorAll/SyncFromTelegram/ MarkBackfilled/SavePreview — Ruling 7), TM/Application/TelegramModuleRegistrar.cs, I/Persistence/Repositories/ TelegramStore.cs, C/Integrations/ITelegramGateway.cs (Ruling 7). Modify: I/Persistence/TenantDbContext.cs — DbSet Dialogs/TgMessages + ApplyConfiguration. EF: миграция TenantTelegram для TenantDbContext (dotnet ef migrations add TenantTelegram --context TenantDbContext --output-dir Migrations/TenantDb --project src/core/Deal.Infrastructure --startup-project src/core/Deal.Api; старт Api применяет к дефолтной схеме). csproj-ссылки: TM → ST (настройки) + Contracts; реестр AddTelegramModule() в Api. Тесты: SyncFromTelegram (новый+autoMonitorNew/обновление/удаление отсутствующих), SetMonitor-семантика; порт-контракт гейта.

Источники: db.py L6786; telegram.py _persist_dialogs/list_dialogs/set_monitor* L468581; api-map §4.8 L349351; Rulings 7/8.

Acceptance: build 0/0; миграция применяется к дефолтной схеме; тесты PASS. Отчёт: task-13-report.md.

Task 14: core — эндпоинты /api/tg (каналы, статус, QR) + SSE; замена boot-заглушки

Files: Modify: A/Program.cs (+MapTelegramEndpoints), A/Endpoints/BootStubEndpoints.cs (удаляется вызов MapBootStubEndpoints, файл — delete). Create: A/Endpoints/TelegramEndpoints.cs (13 шт. api-map §3.3; тела запросов — record'ы; ошибки гейта → {detail} 400; refresh → upsert через DialogsService и ответ {ok,count} или {ok:false, reason:"not-connected", count:0}; monitor/backfill по Ruling 8 с фоновым backfill-спуском при первом включении), A/Endpoints/QrImageEndpoint.cs (SVG Net.Codecrete; 404 «QR не активен — начните вход по QR»), A/Telegram/TgStatusService.cs (сборка §4.9: гейт+KV+monitored count+keysSet), события SSE/toast при ReportStatus (из Task 12). Tests: юнит-тесты TgStatusService (сервис недоступен → idle-форма); curl-приёмка эндпоинтов со стаб-гейтом (фейк-реализация ITelegramGateway в тестах, не Local).

Источники: api-map §3.3/§4.9; tg_routes.py целиком (тексты и статусы); store.js L13521508 (фронт-флоу); Rulings 7/8.

Acceptance: build 0/0; юнит+curl: GET /api/tg/status (idle без гейта), dialogs, monitor, backfill-all, preview, start-qr (фейк) → phase/qrUrl; 401 без куки. Отчёт: task-14-report.md.

Task 15: core — ai-интеграция: контекст запроса, GrpcAiClassifier/GrpcAiTools, маппер, usage

Files: Modify: C/Integrations/IAiClassifier.cs — контракт остаётся, НО Classify/Filter переходят на запросные record'ы: ClassifyAsync(AiClassifyRequest, ct), FilterAsync(AiFilterRequest, ct) (в C/Integrations/ Models/: AiClassifyRequest{Text, SystemPrompt, UserContext}, AiFilterRequest{Text, SystemPrompt}); сигнатуры LocalAiClassifier адаптируются (строит запрос сам: Filter — skipped; Classify — локальный разбор, Ruling 5 этапа 4). Modify: PL/Application/PipelineWorkerService.cs — call-site'ы фильтра/классификации переходят на новые сигнатуры через AiClassifyContextBuilder (Логика веток/выключателей/обучения ML не меняется — Ruling 5 этапа 4/6). Create: PL/Application/AiClassifyContextBuilder.cs (fill_prompt 1:1 ai.py L6377; доски non-suggested с правилами/ ключами — python L226–243; примеры разметки по CardMoves/learning-истории, ≤8, L201215), PL/Application/ AiRawLeadMapper.cs (json-ответ модели → AiParsedLeadDto 1:1 python: title ≤140, стек normalize, бюджет BudgetNormalizer=clean_budget L316339, контакты ContactsQualifier=build_contacts L389421, типы/spam/board; доску решает CardComposer BoardAccepts — как сейчас), I/Integrations/GrpcAiClassifier.cs, I/Integrations/GrpcAiTools.cs (IAiTools: GenerateKeywords/EvaluateFit; usage→KV aiTokenUsage — SettingsKeys новый внутренний ключ), I/Integrations/LocalAiTools.cs (для UseLocal: методы не поддерживаются → исключение/ пустой результат — воркер Discovery сам выбирает эвристику), регистрация в AddDealIntegrations по флагу Services:Ai (Ruling 6). Tests: контекст-билдер (промпты/доски/примеры); маппер json→DTO (бюджет «2к»/валюты/ контакты); адаптеры (in-proc gRPC ai-service); Local-фолбэк.

Источники: ai.py L61258; pipeline.py _store_lead L433514; CardComposer; Rulings 5/6.

Acceptance: build 0/0; тесты PASS; воркер с GrpcAiClassifier (UseLocal=false) проходит фильтр/классификацию против in-proc ai-service. Отчёт: task-15-report.md.

Task 16: core — ml-интеграция: GrpcMlClient + MlOutboxFlushScheduler

Files: Create: I/Integrations/GrpcMlClient.cs (IMlClient: Predict/Status/Reset/PushAsync — Push остаётся записью в MlOutbox через IMlLearningStore как LocalMlClient; Predict → gRPC, сбой → NotReadyPrediction; Status → service-статус + кэш 15 с (reachable), статистика из KV/таблиц; Reset → gRPC Reset + ClearOutbox), A/Hosting/MlOutboxFlushScheduler.cs (10 с per-tenant; по 10 строк, ≤100 за цикл, TrainBatch; delete после успеха; эталон PipelineWorkerScheduler). Регистрация по флагу Services:Ml (Ruling 6). Modify: I/Integrations/ LocalMlClient.cs — не трогаем (фолбэк); ST/Application/SettingsKeys.cs — внутренние ключи AiTokenUsage/ DiscFloodDay/TgStatus/TgAccount. Tests: flush (фейк-gRPC): 25 строк → 3 батча, строки удалены, сбой → строки остались; reset; reachable false при недоступности; predict-fallback.

Источники: ml_client.py L3031/56135; Rulings 4/6; LocalMlClient (эталон Push/Status).

Acceptance: build 0/0; тесты PASS. Отчёт: task-16-report.md.

Task 17: core — Discovery: таблицы, порт, сервисы задач/кандидатов/чёрного списка/лога

Files: Create: DC/Application/Models/*.cs (§4.8 DTO: задача L353, кандидат L355, чёрный список, лог), DC/Application/DiscoveryIdPrefixes.cs (dt_/dl_), DC/Application/IDiscoveryStore.cs, DC/Application/ DiscoveryTasksService.cs (create/patch/delete/start/pause/advance/bump, план-бюджет 1:1 L234381), DC/Application/DiscoveryCandidatesService.cs (add с исключениями, set_candidate, mark_joined/mark_rejected, delete; метки/топики JSON), DC/Application/DiscoveryBlacklistService.cs, DC/Application/DiscoveryLogService.cs, DC/Application/DiscoveryModuleRegistrar.cs; миграция TenantDiscovery (TenantDbContext — DbSet DiscTasks/DiscCandidates/DiscBlacklist/DiscLog + ApplyConfiguration; команда как в Task 13); I/Persistence/Repositories/DiscoveryStore.cs; csproj DC → ST + Contracts. Tests: валидации (имя/бюджет/план), start без ключей, исключения add_candidate, mark_joined→joined/autoJoined/счётчики, blacklist-перезапись rejected.

Источники: discovery.py (создание/кандидаты/чёрный список/лог L234608), db.py L136196, api-map §3.8/§4.8; Rulings 9/10.

Acceptance: build 0/0; миграция применяется; тесты PASS. Отчёт: task-17-report.md.

Task 18: core — Discovery-воркер (5 с): поиск/оценка/авто-join, бан-гард

Files: Create: DC/Application/DiscoveryBanGuard.cs (лимит дня по DiscLog join_auto за UTC-сутки; discFloodDay; discPaused; wait-пауза из настроек 1:1 ban_guard.py), DC/Application/DiscoveryLangDetector.cs (detect_lang_ru L6283), DC/Application/DiscoveryEvaluator.cs (фит: короткие → нет; ML-спам при mlEnabled (IMlClient.Predict); ИИ EvaluateFit при aiEnabled (IAiTools), сбой → эвристика; форумы по темам — group_by_topic L96117 + passed L229237), DC/Application/DiscoveryWorkerService.cs (шаги 14 tick L444484 через ITelegramGateway; маркеры и логи 1:1; join_failures=3→delete), A/Hosting/DiscoveryWorkerScheduler.cs (5 с, per-tenant, эталон PipelineWorkerScheduler). Тесты: бан-гард (лимит/флуд/пауза), оценка (язык/фит/порог/форумы/метки), воркер-шаги с фейковым гейтом (search→candidate; eval→review; join с паузами; план выполнен → done).

Источники: discovery_worker.py целиком; discovery_eval.py целиком; ban_guard.py целиком; Rulings 9/10.

Acceptance: build 0/0; тесты PASS. Отчёт: task-18-report.md.

Task 19: core — эндпоинты /api/discovery + generate-keywords; curl-приёмка

Files: Create: A/Endpoints/DiscoveryEndpoints.cs — 13 эндпоинтов api-map §3.8: tasks (list/create/patch/delete/ start/pause), generate-keywords (мягкая ошибка HTTP 200 {keywords:[], error}; _clean_keywords в core), candidates(фильтр), join (ручной: валидация статуса, Join→add в Dialogs monitor→фон backfill→mark_joined(auto:false) →remove_blacklist, ошибки 400 с текстом), reject (→blacklist reason «отклонено вручную»), blacklist list/delete, log. Curl-приёмка discovery: создание задачи → start (после добавления ключей) → симуляция работы воркера (фейк-гейт в тестовом host) → кандидаты new/review → reject → blacklist → лог; 404/400 ветки.

Источники: discovery_routes.py целиком; store.js L21712370; Rulings 9/11.

Acceptance: build 0/0; curl PASS (или тестовая приёмка) по сценарию выше. Отчёт: task-19-report.md.

Task 20: compose-dev, сквозная интеграция и финал этапа

  • DEP: сервисы из Task 2–4 доводятся (healthcheck gRPC, volumes, env DEAL_SERVICE_TOKEN, ingress env); docker compose -f deploy/compose.dev.yml config валиден; локальный подъём всех процессов (ручной шаг — docker).
  • Сквозная эмуляция (без реального Telegram/LLM): подняты core+3 сервиса (SERVICES__*__USELOCAL=false); gRPC-вызов PushMessage в core (клиент-эмулятор, скрипт scripts/grpc-emit.ps1/.sh на grpcurl или тест-проект) → очередь → admin/tick → карточка (new_lead); ml: TrainBatch → /api/ml/status показывает ready; ai: фильтр/классификация через фейковый OpenAI-сервер? НЕТ — ai-сервис без ключа отдаёт UNAVAILABLE, воркер падает в локальный разбор (проверяем); затем SERVICES__AI__USELOCAL=true — фолбэк жив.
  • Обновить docs/technical/Техническая-документация-Дейл.md (сервисы/порты/gRPC-контракты, каналы-вкладка, Discovery, флаги, ml-модель и веса) и roadmap (этап 6 → «Выполнено», ограничения этапа 7).
  • Полный прогон: scripts/build.sh + scripts/test.sh (620 + новые PASS), build каждой sln 0/0.
  • Отчёт task-20-report.md + финальная строка progress.md.

Источники: Rulings 2/12/13; compose.dev.yml (эталон minio-записи); паттерны отчётов этапов 1–5.

Acceptance: см. пункты выше; любые живые проверки Telegram/LLM — ⚠ ручные, по возможности, с кредами.

Self-Review

  1. Spec coverage: прото-контракты (а) — Task 1 + Rulings 1/3/5/7; каркасы сервисов — Task 2–4; ml-алгоритм/ сохраняемость — Task 5/6 (Ruling 4); ai-фасад/промпты/таймауты/токены — Task 7/8/15 (Ruling 5); core gRPC-клиенты за флагом и судьба MlOutbox — Task 15/16 (Ruling 6); входящий telegram-gRPC→EnqueueAsync — Task 12 (Ruling 7); замена Local-заглушек с фолбэком — Task 15/16/20; Discovery (таблицы/воркер/оценка/чёрный список/квоты/история/ генерация ключей/эндпоинты) — Task 17/18/19 (Rulings 911); каналы-эндпоинты и QR/статус/марк-as-рид/мониторинг/ «Перечитать»/ключи/авто-мониторинг — Task 10/13/14 (Rulings 3/7/8); SSE system_status/toast/new_lead — Task 12/14 (Ruling 13); compose-dev — Task 24/20; безопасность dev (service-token, mTLS-решение) — Rulings 1/2, Task 24/12. Roadmap-скоуп (L8291) покрыт; ТЗ §4/§5/§8 — через api-map/референсы выше.
  2. Placeholder scan: TODO/«добавьте обработку» нет; «ручная проверка» — явно помеченные живые проверки с кредами (задачи 9/10/11/20), авто-приёмка — эмуляция ингресса и фейки. Onnx/TeleSharp альтернативы не оставлены «на потом» — зафиксированы решения (Rulings 3/4). IColumnSuggester (LocalColumnSuggester) сознательно НЕ заменяется gRPC (эвристика читает карточки тенанта в ядре; ai-service участвует только через IAiTools GenerateKeywords — Kanban-suggest остаётся локальным, api-map L120–121 без изменений) — это решение, не TODO.
  3. Type consistency: имена контрактов и методы: IAiClassifier переходит на запросные record'ы (Task 15) — воркер Pipeline (Ruling 5 этапа 4) вызывает ClassifyAsync/FilterAsync; адаптеры Local/Grpc реализуют один порт; IMlClient не меняет сигнатур (Predict/Status/Reset/Push) — GrpcMlClient/LocalMlClient взаимозаменяемы; новые внутренние SettingsKeys (AiTokenUsage/DiscFloodDay/TgStatus/TgAccount) добавляются в ST-каталог как внутренние; ITelegramGateway (Task 13) реализуется клиентом Task 12–14 и потребляется эндпоинтами/воркером Discovery (Task
  1. — единый список методов Ruling 7; QueuedMessage (контракт ингресса) тот же, что у demo-ingest; MlPredictResultDto/status-поля 1:1 с ml.proto (Task 1/6). Циклов ссылок нет: TM→ST+Contracts; DC→ST+Contracts; TM/DC не знают друг о друге; Api оркестрирует.
  1. Вне scope этапа 6: mTLS-сертификаты и prod-compose (этап 7); лимиты/бюджеты токенов (учёт уже есть); оператор/админка/аудит-поток; экспорт/импорт ML-моделей (решение владельца); мультиаккаунтность на тенанта; события pipeline_stats/boards_changed/leads_reclassified (фронт не слушает); reclassify ИИ-переклассификации на реальном ИИ (контракт-заглушка остаётся; реальный вызов — вместе с операторским контуром этапа 7); шифрование сессий и их бэкап-интеграция (сессии шифруются файлово, но ротация ключей/бэкап-политика — этап 7).