diff --git a/scripts/__pycache__/_slim_comments.cpython-312.pyc b/scripts/__pycache__/_slim_comments.cpython-312.pyc
index f0ce159..843531d 100644
Binary files a/scripts/__pycache__/_slim_comments.cpython-312.pyc and b/scripts/__pycache__/_slim_comments.cpython-312.pyc differ
diff --git a/src/ai-service/Deal.Ai.Tests/Ai/AiTestHost.cs b/src/ai-service/Deal.Ai.Tests/Ai/AiTestHost.cs
index 0ee5f0b..4210a3e 100644
--- a/src/ai-service/Deal.Ai.Tests/Ai/AiTestHost.cs
+++ b/src/ai-service/Deal.Ai.Tests/Ai/AiTestHost.cs
@@ -12,10 +12,6 @@ using Microsoft.Extensions.DependencyInjection;
namespace Deal.Ai.Tests.Ai;
-// в процессе теста на эфемерном порту и через configureServices-хук подменяет LLM-фасад фейком
-// (FakeProviderClient, без сети) и функцию паузы ретраев (мгновенная) — сценарии не
-// ждут 0.8/2 с между попытками. Регистрация, добавленная харнессом после дефолтных, побеждает
-// (DI резолвит последнюю). Исходное значение env DEAL_SERVICE_TOKEN восстанавливается.
internal static class AiTestHost
{
///
@@ -101,7 +97,6 @@ internal static class AiTestHost
///
/// Подменяет HTTP-фасад вызовов модели фейком сценария
///
- /// Коллекция сервисов хоста.
/// Фейк-провайдер сценария.
public static void UseFakeProvider(IServiceCollection services, FakeProviderClient fake)
=> services.AddSingleton(fake);
@@ -109,7 +104,6 @@ internal static class AiTestHost
///
/// Делает паузы ретраев мгновенными
///
- /// Коллекция сервисов хоста.
public static void DisableRetryDelays(IServiceCollection services)
=> services.AddSingleton>(static (_, _) => Task.CompletedTask);
diff --git a/src/ai-service/Deal.Ai.Tests/Ai/FakeProviderClient.cs b/src/ai-service/Deal.Ai.Tests/Ai/FakeProviderClient.cs
index 5e2397f..4434d17 100644
--- a/src/ai-service/Deal.Ai.Tests/Ai/FakeProviderClient.cs
+++ b/src/ai-service/Deal.Ai.Tests/Ai/FakeProviderClient.cs
@@ -8,8 +8,6 @@ namespace Deal.Ai.Tests.Ai;
// Config: Конфиг провайдера вызова.
internal sealed record FakeProviderCall(string SystemPrompt, string UserText, LlmConfig Config);
-// usage либо сбой), каждый вызов записывается в Calls — тесты проверяют и собранные
-// RPC-слоем промпты, и число попыток ретраев.
internal sealed class FakeProviderClient : IProviderClient
{
private readonly Func> _handler;
diff --git a/src/ai-service/Deal.Ai.Tests/Grpc/AiServiceHostTests.cs b/src/ai-service/Deal.Ai.Tests/Grpc/AiServiceHostTests.cs
index b7ace63..3b87edd 100644
--- a/src/ai-service/Deal.Ai.Tests/Grpc/AiServiceHostTests.cs
+++ b/src/ai-service/Deal.Ai.Tests/Grpc/AiServiceHostTests.cs
@@ -119,11 +119,6 @@ public sealed class AiServiceHostTests
channel => AssertRejectedAsync(channel, tokenHeader, expected));
}
- // Вызывает Classify и проверяет, что сервер ответил ожидаемым кодом статуса.
- // Запрос несёт полный metadata (service-token + tenant-id), чтобы верный токен доходил
- // channel: Канал к хосту ai-service.
- // tokenHeader: Значение metadata «service-token» либо null (нет заголовка).
- // expected: Ожидаемый StatusCode.
private static async Task AssertRejectedAsync(
GrpcChannel channel,
string? tokenHeader,
diff --git a/src/ai-service/Deal.Ai.Tests/Support/StubHttpMessageHandler.cs b/src/ai-service/Deal.Ai.Tests/Support/StubHttpMessageHandler.cs
index 75cc353..db55682 100644
--- a/src/ai-service/Deal.Ai.Tests/Support/StubHttpMessageHandler.cs
+++ b/src/ai-service/Deal.Ai.Tests/Support/StubHttpMessageHandler.cs
@@ -13,7 +13,6 @@ internal sealed record CapturedHttpRequest(
IReadOnlyDictionary Headers,
string? Body);
-// запросы (URL/заголовки/тело) и отвечает по сценарию; опциональная задержка — для теста таймаута.
internal sealed class StubHttpMessageHandler : HttpMessageHandler
{
private readonly Func _responder;
diff --git a/src/ai-service/Deal.Ai/AiServiceHost.cs b/src/ai-service/Deal.Ai/AiServiceHost.cs
index a4b30bb..2ae225d 100644
--- a/src/ai-service/Deal.Ai/AiServiceHost.cs
+++ b/src/ai-service/Deal.Ai/AiServiceHost.cs
@@ -26,15 +26,11 @@ public static class AiServiceHost
{
WebApplicationBuilder builder = WebApplication.CreateBuilder(args ?? []);
- // Общая серверная обвязка (Deal.Grpc.Hosting, C31): mTLS env DEAL_MTLS_* — загрузка
- // сертификатов сразу с fail-fast (compose-prod монтирует deploy/certs, scripts/mtls-certs.sh);
MtlsCertificates? mtlsCertificates = GrpcServer.LoadMtlsCertificates(builder);
GrpcServer.ConfigureKestrelHttp2Endpoint(builder, grpcPort, mtlsCertificates);
builder.Services.AddDealGrpcServer();
builder.Services.AddReadyHealthCheck("хост ai-service готов");
- // попытки управляется внутри — 90 с OpenAI / 60 с Anthropic; клиент без общего таймаута) и
- // оркестратор ретраев/JSON/usage поверх него. Ключи API — в конфиге запроса, не в DI/логах.
builder.Services.AddHttpClient(static httpClient =>
httpClient.Timeout = Timeout.InfiniteTimeSpan);
builder.Services.AddSingleton(
diff --git a/src/ai-service/Deal.Ai/AiServiceImpl.cs b/src/ai-service/Deal.Ai/AiServiceImpl.cs
index e6a130c..b9d94f0 100644
--- a/src/ai-service/Deal.Ai/AiServiceImpl.cs
+++ b/src/ai-service/Deal.Ai/AiServiceImpl.cs
@@ -275,7 +275,6 @@ public sealed class AiServiceImpl : AiService.AiServiceBase
}
}
- // context: Контекст вызова.
private static string RequireTenantId(ServerCallContext context)
{
string? tenantId = context.RequestHeaders.GetValue(TenantIdMetadataKey);
@@ -349,10 +348,6 @@ public sealed class AiServiceImpl : AiService.AiServiceBase
providerConfig.ApiStyle);
}
- // method: Имя RPC для аудита.
- // tenantId: Id тенанта.
- // config: Конфиг провайдера вызова.
- // callError: Итоговая ошибка фасада.
private void LogAiUnavailable(
string method,
string tenantId,
@@ -365,14 +360,9 @@ public sealed class AiServiceImpl : AiService.AiServiceBase
config.DisplayName,
callError.Kind);
- // callError: Итоговая ошибка фасада.
private static RpcException ToUnavailable(LlmCallException callError)
=> new(new Status(StatusCode.Unavailable, callError.Message));
- // Читает булево поле JSON-ответа модели: bool как есть; строка — ложь только для значений из
- // json: Корневой объект ответа модели.
- // fieldName: Имя поля.
- // defaultValue: Значение при отсутствии/неразбираемости поля.
private static bool ReadBoolField(
JsonObject json,
string fieldName,
@@ -416,9 +406,6 @@ public sealed class AiServiceImpl : AiService.AiServiceBase
return trimmed.Length == 0 ? null : trimmed;
}
- // Причина решения fit: поле reason модели, при отсутствии — «подходит»/«не подходит»
- // json: Корневой объект ответа модели.
- // fit: Решение модели.
private static string ReadFitReason(JsonObject json, bool fit)
{
string reason = ReadStringField(json, ReasonFieldName) ?? (fit ? FitReasonDefault : NotFitReasonDefault);
@@ -447,7 +434,6 @@ public sealed class AiServiceImpl : AiService.AiServiceBase
}
}
- // keywords: Ключи задачи.
private static string JoinKeywords(IEnumerable keywords)
=> string.Join(
", ",
diff --git a/src/ai-service/Deal.Ai/Llm/JsonExtractor.cs b/src/ai-service/Deal.Ai/Llm/JsonExtractor.cs
index c3eeda7..16ad6ff 100644
--- a/src/ai-service/Deal.Ai/Llm/JsonExtractor.cs
+++ b/src/ai-service/Deal.Ai/Llm/JsonExtractor.cs
@@ -46,8 +46,6 @@ public static class JsonExtractor
}
}
- // между открывающей и закрывающей обёртками; обёртки нет/незакрыта — null.
- // raw: Текст ответа (уже обрезанный).
private static string? UnwrapFence(string raw)
{
int open = raw.IndexOf(CodeFence, StringComparison.Ordinal);
diff --git a/src/ai-service/Deal.Ai/Llm/LlmHttpClient.cs b/src/ai-service/Deal.Ai/Llm/LlmHttpClient.cs
index 636a3f6..ea88e16 100644
--- a/src/ai-service/Deal.Ai/Llm/LlmHttpClient.cs
+++ b/src/ai-service/Deal.Ai/Llm/LlmHttpClient.cs
@@ -97,10 +97,6 @@ public sealed class LlmHttpClient : IProviderClient
}
}
- // Собирает запрос OpenAI-совместимого чата: {base}/chat/completions, Bearer при заданном ключе,
- // config: Конфиг провайдера.
- // systemPrompt: Системный промпт.
- // userText: Пользовательское сообщение.
private static HttpRequestMessage BuildOpenAiRequest(
LlmConfig config,
string systemPrompt,
@@ -126,10 +122,6 @@ public sealed class LlmHttpClient : IProviderClient
return request;
}
- // Собирает запрос Anthropic Messages API: {base}/v1/messages, x-api-key + anthropic-version,
- // config: Конфиг провайдера.
- // systemPrompt: Системный промпт.
- // userText: Пользовательское сообщение.
private static HttpRequestMessage BuildAnthropicRequest(
LlmConfig config,
string systemPrompt,
@@ -171,7 +163,6 @@ public sealed class LlmHttpClient : IProviderClient
return isAnthropic ? ReadAnthropicBody(body) : ReadOpenAiBody(body);
}
- // body: Тело ответа.
private static ProviderChatResult ReadOpenAiBody(string body)
{
JsonObject? payload = ParseObjectOrThrow(body);
@@ -193,7 +184,6 @@ public sealed class LlmHttpClient : IProviderClient
return new ProviderChatResult(content ?? string.Empty, ReadOpenAiUsage(payload["usage"]));
}
- // body: Тело ответа.
private static ProviderChatResult ReadAnthropicBody(string body)
{
JsonObject? payload = ParseObjectOrThrow(body);
@@ -265,7 +255,6 @@ public sealed class LlmHttpClient : IProviderClient
throw UnexpectedApiResponse("тело не является JSON-объектом");
}
- // failureKind: Короткая причина (без тела ответа и секретов).
private static LlmHttpException UnexpectedApiResponse(string failureKind)
=> new($"Неожиданный ответ ИИ-провайдера: {failureKind}");
@@ -310,6 +299,5 @@ public sealed class LlmHttpClient : IProviderClient
return result;
}
- // baseUrl: Базовый URL из конфига.
private static string NormalizeBaseUrl(string baseUrl) => baseUrl.TrimEnd('/');
}
diff --git a/src/ai-service/Deal.Ai/Program.cs b/src/ai-service/Deal.Ai/Program.cs
index a72e627..7c2ffda 100644
--- a/src/ai-service/Deal.Ai/Program.cs
+++ b/src/ai-service/Deal.Ai/Program.cs
@@ -1,11 +1,4 @@
//
-// Kestrel HTTP/2 на порту 5102 (env GRPC_PORT, затем PORT) + AddGrpc с интерцепторами service-token
-// и access-лога + стандартный gRPC-health (grpc.health.v1.Health). Транспорт: dev — plaintext
-// Production без mTLS не стартует (GrpcHostEnvironment.RequireMtlsInProduction).
-// Серверная обвязка (Kestrel/AddGrpc/health) — общий Deal.Grpc.Hosting (C31): хост-фабрика
-// AiServiceHost.Create используется и интеграционными тестами (Deal.Ai.Tests), которые поднимают
-// его в своём процессе на эфемерном порту. Методы AiService (Filter/Classify/GenerateKeywords/
-// задачи 7–8): фасад живёт в Deal.Ai/Llm.
using Deal.Ai;
using Deal.Grpc.Hosting.Interceptors;
@@ -13,16 +6,12 @@ using Deal.Grpc.Hosting.Models;
using Deal.Grpc.Hosting.Options;
using Deal.Grpc.Hosting.Services;
-// или PORT (общий конвенциональный env хостинг-платформ) — см. GrpcHostEnvironment.ResolveGrpcPort.
const int defaultGrpcPort = 5102;
const string aiProcessName = "ai";
int grpcPort = GrpcHostEnvironment.ResolveGrpcPort(defaultGrpcPort);
int metricsPort = DealMetricsHosting.ResolveMetricsPort(DealMetricsHosting.DefaultMetricsPort);
-// конфигурируется production-точкой входа через configureBuilder-хук хоста (тесты хост поднимают
-// без Serilog, DealLogging.Configure в AiServiceHost/Create вызывается только здесь). Метрики
-// (OTel → Prometheus, /metrics) — тем же хуком до builder.Build().
WebApplication app = AiServiceHost.Create(
grpcPort,
configureBuilder: builder =>
diff --git a/src/contracts/telegram.proto b/src/contracts/telegram.proto
index 83b3e95..4d8cea7 100644
--- a/src/contracts/telegram.proto
+++ b/src/contracts/telegram.proto
@@ -41,7 +41,6 @@ service TelegramService {
// само. Нет сессии — FAILED_PRECONDITION «Telegram не подключён».
rpc GetStatus(GetStatusRequest) returns (GetStatusReply);
- // api_id/api_hash — глобальные ключи приложения Telegram, задаёт оператор (ТЗ §4.1/§8.1),
// не заданы оператором» до вызова. Ответ: новая фаза ("code").
rpc StartPhone(StartPhoneRequest) returns (StartPhoneReply);
@@ -73,7 +72,6 @@ service TelegramService {
rpc Backfill(BackfillRequest) returns (BackfillReply);
// свежие из Telegram; признак lead и фолбэк на БД добавляет ядро
- // (у сервиса нет БД тенанта). limit 1..50 (api-map /dialogs/preview).
rpc ReadRecent(ReadRecentRequest) returns (ReadRecentReply);
// Пауза анти-бана после поиска — внутри сервиса. Личные чаты/боты ядро
@@ -208,7 +206,6 @@ message SetMonitorAllRequest {
message SetMonitorAllReply {
bool ok = 1;
- // Сколько диалогов в каталоге тенанта (api-map /monitor-all → count).
int32 count = 2;
bool enabled = 3;
}
@@ -228,7 +225,6 @@ message BackfillReply {
message ReadRecentRequest {
// Id диалога.
string dialog_id = 1;
- // Сколько последних сообщений (1..50; api-map /dialogs/preview limit 1..50).
int32 limit = 2;
}
diff --git a/src/core/Deal.Api/Endpoints/AiCheckEndpoint.cs b/src/core/Deal.Api/Endpoints/AiCheckEndpoint.cs
index 16bf566..cd1f27e 100644
--- a/src/core/Deal.Api/Endpoints/AiCheckEndpoint.cs
+++ b/src/core/Deal.Api/Endpoints/AiCheckEndpoint.cs
@@ -48,11 +48,6 @@ public static class AiCheckEndpoint
return Results.Ok(result);
}
- // store: KV-хранилище настроек тенанта.
- // secretCipher: Шифр секретов (расшифровка apiKey).
- // ct: Токен отмены.
- // Возвращает: Запрос проверки: id провайдера + эффективные base/model + расшифрованный ключ.
- // Эффективные значения = дефолты SettingsDefaults, перекрытые сохранёнными
private static async Task BuildActiveCheckRequestAsync(
ISettingsStore store,
ISecretCipher secretCipher,
diff --git a/src/core/Deal.Api/Endpoints/AiSuggestEndpoints.cs b/src/core/Deal.Api/Endpoints/AiSuggestEndpoints.cs
index 1577fae..197edc7 100644
--- a/src/core/Deal.Api/Endpoints/AiSuggestEndpoints.cs
+++ b/src/core/Deal.Api/Endpoints/AiSuggestEndpoints.cs
@@ -38,8 +38,6 @@ public static class AiSuggestEndpoints
return app;
}
- // POST /api/ai/suggest-columns: анализ «Неразобранного» и создание колонок-предложений.
- // SSE-toast), {ok:false, reason} (+ cooldown) — мягкая причина (HTTP 200). Кулдаун/«мало карточек»/
private static async Task SuggestColumnsAsync(HttpContext context, CancellationToken ct)
{
if (!context.HasUser())
@@ -61,7 +59,6 @@ public static class AiSuggestEndpoints
return Results.Ok(result);
}
- // POST /api/ai/suggest-keywords: слова-маркеры сферы по карточкам (настройки «Сфера и ключи»).
private static async Task SuggestKeywordsAsync(HttpContext context, CancellationToken ct)
{
if (!context.HasUser())
diff --git a/src/core/Deal.Api/Endpoints/AuthEndpoints.cs b/src/core/Deal.Api/Endpoints/AuthEndpoints.cs
index 8cf15f8..26e47b3 100644
--- a/src/core/Deal.Api/Endpoints/AuthEndpoints.cs
+++ b/src/core/Deal.Api/Endpoints/AuthEndpoints.cs
@@ -31,7 +31,6 @@ public static class AuthEndpoints
{
var group = app.MapGroup(AuthGroupPrefix).WithTags(AuthOpenApiTag);
- // ручки входа; остальные ручки группы — под глобальной API-политикой (по тенанту/IP).
group.MapPost("/login", LoginAsync).RequireRateLimiting(RateLimitPolicies.AuthPolicy);
group.MapPost("/logout", LogoutAsync);
group.MapGet("/me", MeAsync);
@@ -51,7 +50,6 @@ public static class AuthEndpoints
{
string? attemptedLogin = NormalizeLogin(body.Login);
- // 429 «Слишком много попыток входа…» (в dev при RateLimit:Enabled=false гвард выключен).
if (await loginAttemptGuard.IsBlockedAsync(ClientIp(context), attemptedLogin, ct))
{
return EndpointResults.TooManyRequests(LoginAttemptGuard.BlockedDetail);
@@ -74,7 +72,6 @@ public static class AuthEndpoints
if (result.Login is null || result.Token is null)
{
- // счётчик неудач гварда растёт там же — пустые логины ключа не имеют (блокирует только auth-политика).
if (attemptedLogin is not null)
{
await loginAttemptGuard.RecordFailureAsync(ClientIp(context), attemptedLogin, ct);
@@ -104,7 +101,6 @@ public static class AuthEndpoints
return Results.Ok(new { ok = true, login = result.Login });
}
- // POST /api/auth/logout: удаление сессии по токену из куки и очистка куки (всегда ok).
private static async Task LogoutAsync(
AuthService authService,
AuditService auditService,
diff --git a/src/core/Deal.Api/Endpoints/CardDetailsEndpoints.cs b/src/core/Deal.Api/Endpoints/CardDetailsEndpoints.cs
index 6420992..2adcd02 100644
--- a/src/core/Deal.Api/Endpoints/CardDetailsEndpoints.cs
+++ b/src/core/Deal.Api/Endpoints/CardDetailsEndpoints.cs
@@ -425,8 +425,6 @@ public static class CardDetailsEndpoints
// Возвращает: Имя, безопасное для заголовка.
private static string ToDownloadFileName(string name) => name.Replace(FileNameQuoteCharacter, string.Empty);
- // body: Тело запроса.
- // Возвращает: DTO модуля для CardsService.CreateLocalCardAsync.
private static CardLocalCreateDto ToCreateLocalDto(CreateCardRequest body)
{
return new CardLocalCreateDto(
diff --git a/src/core/Deal.Api/Endpoints/CardsEndpoints.cs b/src/core/Deal.Api/Endpoints/CardsEndpoints.cs
index 1f20ad2..f7f18c9 100644
--- a/src/core/Deal.Api/Endpoints/CardsEndpoints.cs
+++ b/src/core/Deal.Api/Endpoints/CardsEndpoints.cs
@@ -158,7 +158,6 @@ public static class CardsEndpoints
if (body.Col is null)
{
- // Пустая/отсутствующая col попала бы в mark_seen как «не задана» и сняла бы «новое» со ВСЕХ
return EndpointResults.BadRequest(UnknownColumnDetail);
}
@@ -167,10 +166,6 @@ public static class CardsEndpoints
return Results.Ok(new { ok = true });
}
- // Маршрутизацию цели (стадия «Выбранных» vs дашборд-контейнер) и побочные эффекты выполняет единый
- // доменный механизм перехода ICardMover: стадия — запись истории и сброс напоминания
- // (move_stage), дашборд-контейнер — журнал/обучение ML. Ответ — обновлённая карточка; 400 при
- // несуществующем контейнере, 404 — карточки нет.
private static async Task MoveAsync(
string cardId,
MoveBody body,
@@ -380,11 +375,6 @@ public static class CardsEndpoints
reason = result.Reason,
};
- // Публикуется только когда проход реально выполнен и что-то изменил (started и
- // reclassified > 0): пустой inbox/всё пропущено не меняют доску — событие не шлём. Нагрузка
- // минимальная: сколько обработано и перемещено (фронт перечитывает доску). Без подписчиков — no-op.
- // context: Контекст запроса (тенант-канал сессии).
- // result: Итог прохода.
private static void PublishReclassified(HttpContext context, ReclassifyResultDto result)
{
if (!result.Started || result.Reclassified == 0)
diff --git a/src/core/Deal.Api/Endpoints/DiscoveryEndpoints.cs b/src/core/Deal.Api/Endpoints/DiscoveryEndpoints.cs
index 51e4414..b6c9e02 100644
--- a/src/core/Deal.Api/Endpoints/DiscoveryEndpoints.cs
+++ b/src/core/Deal.Api/Endpoints/DiscoveryEndpoints.cs
@@ -210,9 +210,6 @@ public static class DiscoveryEndpoints
return task is null ? EndpointResults.NotFound(TaskNotFoundDetail) : Results.Ok(task);
}
- // POST /api/discovery/tasks/{task_id}/generate-keywords: ИИ-ключи по описанию задачи
- // символов, дедуп casefold); описание режет до 4000 сам адаптер (GrpcAiTools.MaxDescriptionCodePoints).
- // Локальный режим (LocalAiTools, UseLocal=true) — NotSupportedException → та же мягкая ветка с текстом причины.
private static async Task GenerateKeywordsAsync(
string task_id,
HttpContext context,
@@ -259,7 +256,6 @@ public static class DiscoveryEndpoints
}
}
- // GET /api/discovery/tasks/{task_id}/candidates?status=: кандидаты задачи с фильтром
private static async Task ListCandidatesAsync(
string task_id,
string? status,
@@ -283,8 +279,6 @@ public static class DiscoveryEndpoints
return Results.Ok(new { items });
}
- // RPC Join → строка каталога Dialogs (монитор on) + зеркало → фоновый первый разбор → снятие чёрного списка →
- // mark_joined(auto:false). Ошибка Telegram → 400 с текстом причины.
private static async Task JoinCandidateAsync(
string dialog_id,
HttpContext context,
@@ -319,11 +313,9 @@ public static class DiscoveryEndpoints
return EndpointResults.BadRequest(string.Format(JoinFailedFormat, username, reason));
}
- // Источник в каталоге (монитор on, backfilled=false) + монитор-зеркало telegram-service
DialogsService dialogs = context.RequestServices.GetRequiredService();
await dialogs.AddDiscoveredMonitoredAsync(dialog_id, row.Name, username, row.Kind, row.Hue, ct);
- // Догон последних сообщений — в фоне: join из UI не должен висеть на паузах backfill
context.RequestServices.GetRequiredService().ScheduleFirstBackfill(dialog_id);
DiscoveryBlacklistService blacklist = context.RequestServices.GetRequiredService();
@@ -340,7 +332,6 @@ public static class DiscoveryEndpoints
}
}
- // POST /api/discovery/candidates/{dialog_id}/reject: отклонить кандидата — в чёрный список
private static async Task RejectCandidateAsync(
string dialog_id,
HttpContext context,
@@ -462,9 +453,6 @@ public static class DiscoveryEndpoints
private const string ServiceUnavailableText = "ИИ недоступен — повторите попытку через несколько секунд";
- // Читает настройку aiEnabled (KV; отсутствие строки — дефолт SettingsDefaults).
- // settings: KV-хранилище настроек тенанта.
- // ct: Токен отмены.
private static async Task ReadAiEnabledAsync(ISettingsStore settings, CancellationToken ct)
{
SettingValue? row = await settings.GetAsync(SettingsKeys.AiEnabled, ct);
@@ -505,8 +493,6 @@ public static class DiscoveryEndpoints
};
}
- // body: Тело запроса (wire-поля camelCase).
- // Возвращает: Патч задачи (DiscoveryTaskPatch).
private static DiscoveryTaskPatch ToPatch(DiscoveryTaskPatchBody body)
{
return new DiscoveryTaskPatch
diff --git a/src/core/Deal.Api/Endpoints/MlCandidatesRequest.cs b/src/core/Deal.Api/Endpoints/MlCandidatesRequest.cs
index 2fd69e8..0478842 100644
--- a/src/core/Deal.Api/Endpoints/MlCandidatesRequest.cs
+++ b/src/core/Deal.Api/Endpoints/MlCandidatesRequest.cs
@@ -3,6 +3,6 @@ namespace Deal.Api.Endpoints;
///
/// Тело POST /api/ml/candidates.
///
-/// Id диалога/канала Telegram; пусто — выборка по всем источникам тенанта (§8).
+/// Id диалога/канала Telegram; пусто — выборка по всем источникам тенанта.
/// Сколько последних сообщений вернуть (кламп 1..60, дефолт 10).
public sealed record MlCandidatesRequest(string DialogId, int Limit);
diff --git a/src/core/Deal.Api/Endpoints/OperatorAnalyticsEndpoints.cs b/src/core/Deal.Api/Endpoints/OperatorAnalyticsEndpoints.cs
index e919b8b..85d7f84 100644
--- a/src/core/Deal.Api/Endpoints/OperatorAnalyticsEndpoints.cs
+++ b/src/core/Deal.Api/Endpoints/OperatorAnalyticsEndpoints.cs
@@ -36,13 +36,6 @@ public static class OperatorAnalyticsEndpoints
return app;
}
- // GET /api/operator/analytics/suspicious?from=&to=: находки детектора подозрительной активности (§10.5).
- // from: Начало окна анализа (включительно; ISO-8601); null — последние 24 часа.
- // to: Конец окна анализа (включительно; ISO-8601); null — «сейчас».
- // context: Контекст запроса.
- // suspiciousService: Детектор подозрительной активности (scoped).
- // ct: Токен отмены.
- // Возвращает: 200 сводка находок или 401 без операторской сессии.
private static async Task SuspiciousAsync(
DateTimeOffset? from,
DateTimeOffset? to,
diff --git a/src/core/Deal.Api/Endpoints/OperatorAuditEndpoints.cs b/src/core/Deal.Api/Endpoints/OperatorAuditEndpoints.cs
index 4d567b1..9d55690 100644
--- a/src/core/Deal.Api/Endpoints/OperatorAuditEndpoints.cs
+++ b/src/core/Deal.Api/Endpoints/OperatorAuditEndpoints.cs
@@ -28,18 +28,6 @@ public static class OperatorAuditEndpoints
return app;
}
- // GET /api/operator/audit?eventType=&actorType=&actorId=&tenantId=&from=&to=&limit=&offset=: лента аудита.
- // eventType: Фильтр по типу события (равенство; пусто — без фильтра).
- // actorType: Фильтр по типу актора operator|tenant|system (равенство).
- // actorId: Фильтр по идентификатору актора (равенство).
- // tenantId: Фильтр по тенанту (равенство).
- // from: Нижняя граница At (включительно; ISO-8601).
- // to: Верхняя граница At (включительно; ISO-8601).
- // limit: Размер выборки (дефолт 100, клампится 1..500).
- // context: Контекст запроса.
- // auditService: Сервис аудита (scoped).
- // ct: Токен отмены.
- // Возвращает: 200 {items:[...], total} или 401 без операторской сессии.
private static async Task ListAsync(
string? eventType,
string? actorType,
diff --git a/src/core/Deal.Api/Endpoints/OperatorAuthEndpoints.cs b/src/core/Deal.Api/Endpoints/OperatorAuthEndpoints.cs
index bf297e5..ba3095f 100644
--- a/src/core/Deal.Api/Endpoints/OperatorAuthEndpoints.cs
+++ b/src/core/Deal.Api/Endpoints/OperatorAuthEndpoints.cs
@@ -29,7 +29,6 @@ public static class OperatorAuthEndpoints
{
var group = app.MapGroup(OperatorAuthGroupPrefix).WithTags(OperatorAuthOpenApiTag);
- // входа оператора; остальные ручки группы — под глобальной API-политикой (по тенанту/IP).
group.MapPost("/login", LoginAsync).RequireRateLimiting(RateLimitPolicies.AuthPolicy);
group.MapPost("/logout", LogoutAsync);
group.MapGet("/me", MeAsync);
@@ -48,7 +47,6 @@ public static class OperatorAuthEndpoints
{
string? attemptedLogin = NormalizeLogin(body.Login);
- // ip|login до проверки учётных данных (в dev при RateLimit:Enabled=false гвард выключен).
if (await loginAttemptGuard.IsBlockedAsync(ClientIp(context), attemptedLogin, ct))
{
return EndpointResults.TooManyRequests(LoginAttemptGuard.BlockedDetail);
@@ -57,8 +55,6 @@ public static class OperatorAuthEndpoints
var result = await operatorAuthService.LoginAsync(body.Login, body.Password, ct);
if (result.Login is null || result.Token is null)
{
- // Неверные учётные данные оператора — одно сообщение (зеркало AuthEndpoints).
- // счётчик неудач гварда растёт там же (пустые логины ключа не имеют).
if (attemptedLogin is not null)
{
await loginAttemptGuard.RecordFailureAsync(ClientIp(context), attemptedLogin, ct);
diff --git a/src/core/Deal.Api/Endpoints/OperatorTenantsEndpoints.cs b/src/core/Deal.Api/Endpoints/OperatorTenantsEndpoints.cs
index 8b77967..215cea4 100644
--- a/src/core/Deal.Api/Endpoints/OperatorTenantsEndpoints.cs
+++ b/src/core/Deal.Api/Endpoints/OperatorTenantsEndpoints.cs
@@ -81,7 +81,6 @@ public static class OperatorTenantsEndpoints
return Results.Ok(new { items });
}
- // POST /api/operator/tenants: создание тенанта (Status active + провижининг схемы); аудит tenant_created.
private static async Task CreateAsync(
OperatorTenantCreateRequest body,
HttpContext context,
diff --git a/src/core/Deal.Api/Endpoints/PipelineEndpoints.cs b/src/core/Deal.Api/Endpoints/PipelineEndpoints.cs
index 0db71cc..916eeec 100644
--- a/src/core/Deal.Api/Endpoints/PipelineEndpoints.cs
+++ b/src/core/Deal.Api/Endpoints/PipelineEndpoints.cs
@@ -84,7 +84,6 @@ public static class PipelineEndpoints
return Results.Ok(new { items, counts, rejected });
}
- // первыми; offset ≥ 0, limit 1..500 (clamp в сервисе), значения эхом в ответе {items,total,offset,limit}.
private static async Task RejectedAsync(
string? q,
int? offset,
diff --git a/src/core/Deal.Api/Endpoints/RatesEndpoints.cs b/src/core/Deal.Api/Endpoints/RatesEndpoints.cs
index b1f6c1b..afd9874 100644
--- a/src/core/Deal.Api/Endpoints/RatesEndpoints.cs
+++ b/src/core/Deal.Api/Endpoints/RatesEndpoints.cs
@@ -46,7 +46,6 @@ public static class RatesEndpoints
RatesDto current = await ratesService.GetAsync(ct);
- // фоновый RefreshAsync в отдельном scope; ответ — текущий кэш.
if (await ratesService.ShouldFetchAsync(ct))
{
context.RequestServices.GetRequiredService().Schedule();
diff --git a/src/core/Deal.Api/Endpoints/SettingsEndpoints.cs b/src/core/Deal.Api/Endpoints/SettingsEndpoints.cs
index a5fd33e..1545384 100644
--- a/src/core/Deal.Api/Endpoints/SettingsEndpoints.cs
+++ b/src/core/Deal.Api/Endpoints/SettingsEndpoints.cs
@@ -53,7 +53,6 @@ public static class SettingsEndpoints
return EndpointResults.Unauthorized(AuthHelpers.UnauthorizedDetail);
}
- // Произвольный JSON-объект: ключи тела — публичные ключи §4.6 (как их шлёт фронт).
Dictionary? body;
try
{
@@ -64,7 +63,6 @@ public static class SettingsEndpoints
}
catch (JsonException)
{
- // Не-JSON или не-объект целиком — ошибка запроса: 400 + detail
return EndpointResults.BadRequest(InvalidBodyDetail);
}
@@ -86,8 +84,6 @@ public static class SettingsEndpoints
return Results.Ok(result);
}
- // body: Тело PATCH — публичные ключи §4.6.
- // Возвращает: True — поле rateSource передано «правдивым» значением (не null/пустая строка).
private static bool ShouldScheduleRatesRefresh(Dictionary body)
{
if (!body.TryGetValue(SettingsKeys.RateSource, out JsonElement element))
diff --git a/src/core/Deal.Api/Endpoints/StorageEndpoints.cs b/src/core/Deal.Api/Endpoints/StorageEndpoints.cs
index 1790c00..6379c14 100644
--- a/src/core/Deal.Api/Endpoints/StorageEndpoints.cs
+++ b/src/core/Deal.Api/Endpoints/StorageEndpoints.cs
@@ -30,9 +30,6 @@ public static class StorageEndpoints
return app;
}
- // Весь состав тика — AdminTickOrchestrator (вынесен из эндпоинта для unit-тестов логики и
- // переиспользования): тик StorageTickService (Kanban) → PurgeExpiredAsync (отсев 3 суток, merge в
- // storage.purgedRejected) → тосты статистики (включая «Отсев очищен: N записей (3 дн.)») → CheckDueAsync
private static async Task AdminTickAsync(HttpContext context, CancellationToken ct)
{
if (!context.HasUser())
@@ -45,8 +42,6 @@ public static class StorageEndpoints
return Results.Ok(await orchestrator.TickAsync(tenantId, ct));
}
- // SearchTsv — генерируемые STORED-колонки Cards/RejectedItems: авто-актуальны, «пересборка» = создание
- // отсутствующих GIN-индексов (CREATE INDEX IF NOT EXISTS) + ANALYZE таблиц (FtsMaintenance.RebuildAsync).
private static async Task FtsRebuildAsync(HttpContext context, CancellationToken ct)
{
if (!context.HasUser())
diff --git a/src/core/Deal.Api/Endpoints/TelegramEndpoints.cs b/src/core/Deal.Api/Endpoints/TelegramEndpoints.cs
index 2a88bb1..6b72878 100644
--- a/src/core/Deal.Api/Endpoints/TelegramEndpoints.cs
+++ b/src/core/Deal.Api/Endpoints/TelegramEndpoints.cs
@@ -55,13 +55,11 @@ public static class TelegramEndpoints
// Путь включения/выключения мониторинга одного диалога (POST, параметр dialog_id).
private const string DialogMonitorPath = "/dialogs/{dialog_id}/monitor";
- // Путь разбора одного диалога (POST, параметр dialog_id; сервер-only, api-map п.9).
private const string DialogBackfillPath = "/dialogs/{dialog_id}/backfill";
// Деталь 400: сохранённый api_id не является корректным числом (Security review: int.TryParse).
private const string InvalidApiIdDetail = "Некорректный api_id — проверьте глобальные ключи Telegram";
- // Деталь 400 без ключей приложения: оператор не задал глобальные ключи Telegram (ТЗ §4.1/§8.1).
private const string NoKeysDetail = "Ключи Telegram не заданы оператором";
// Деталь недоступного telegram-service/неподключённого аккаунта (глобальная строка контракта).
@@ -296,7 +294,6 @@ public static class TelegramEndpoints
DialogsService dialogs = context.RequestServices.GetRequiredService();
ITelegramGateway gateway = context.RequestServices.GetRequiredService();
- // {ok:false, reason:"not-connected", count:0} HTTP 200 — refresh не ошибка запроса.
bool connected;
try
{
@@ -456,8 +453,6 @@ public static class TelegramEndpoints
return keys.KeysSet ? keys : null;
}
- // exception: Исключение вызова гейта (RpcException домена/транспорта, прочее).
- // Возвращает: 400-ответ с текстом причины.
private static IResult GatewayError(Exception exception)
{
return EndpointResults.BadRequest(GatewayErrorText(exception));
diff --git a/src/core/Deal.Api/Extensions/AuthHelpers.cs b/src/core/Deal.Api/Extensions/AuthHelpers.cs
index a958a9d..6958fb0 100644
--- a/src/core/Deal.Api/Extensions/AuthHelpers.cs
+++ b/src/core/Deal.Api/Extensions/AuthHelpers.cs
@@ -30,7 +30,6 @@ public static class AuthHelpers
///
/// Кладёт пользователя в HttpContext.Items.
///
- /// Контекст запроса.
/// Пользователь, разрешённый по сессии.
public static void SetCurrentUser(this HttpContext context, CurrentUser user) =>
context.Items[CurrentUserItemKey] = user;
@@ -38,7 +37,6 @@ public static class AuthHelpers
///
/// Возвращает текущего пользователя запроса или null, если сессия не разрешена.
///
- /// Контекст запроса.
/// Пользователь или null.
public static CurrentUser? GetCurrentUser(this HttpContext context) =>
context.Items[CurrentUserItemKey] as CurrentUser;
@@ -46,14 +44,12 @@ public static class AuthHelpers
///
/// Проверяет, разрешена ли для запроса пользовательская сессия.
///
- /// Контекст запроса.
/// True — текущий пользователь установлен.
public static bool HasUser(this HttpContext context) => context.GetCurrentUser() is not null;
///
/// Кладёт оператора в HttpContext.Items.
///
- /// Контекст запроса.
/// Оператор, разрешённый по операторской сессии.
public static void SetCurrentOperator(this HttpContext context, CurrentOperator operatorIdentity) =>
context.Items[CurrentOperatorItemKey] = operatorIdentity;
@@ -61,7 +57,6 @@ public static class AuthHelpers
///
/// Возвращает текущего оператора запроса или null, если операторская сессия не разрешена.
///
- /// Контекст запроса.
/// Оператор или null (ручка /api/operator/* отвечает 401).
public static CurrentOperator? GetCurrentOperator(this HttpContext context) =>
context.Items[CurrentOperatorItemKey] as CurrentOperator;
diff --git a/src/core/Deal.Api/Hosting/BudgetAlertScheduler.cs b/src/core/Deal.Api/Hosting/BudgetAlertScheduler.cs
index 9cc9976..266b549 100644
--- a/src/core/Deal.Api/Hosting/BudgetAlertScheduler.cs
+++ b/src/core/Deal.Api/Hosting/BudgetAlertScheduler.cs
@@ -158,7 +158,6 @@ public sealed class BudgetAlertScheduler : IHostedService
{
ITenantLimitStore limitStore = tenantScope.ServiceProvider.GetRequiredService();
- // TryMark* — атомарная установка флага: true только в момент первого наблюдения порога за период
if (await limitStore.TryMarkWarnedAsync(tenant.Id, ct))
{
_logger.LogDebug("Бюджет тенанта {TenantId}: порог 80% пересечён — SSE-тост", tenant.Id);
diff --git a/src/core/Deal.Api/Hosting/MlOutboxFlushScheduler.cs b/src/core/Deal.Api/Hosting/MlOutboxFlushScheduler.cs
index 1210ee0..2d723eb 100644
--- a/src/core/Deal.Api/Hosting/MlOutboxFlushScheduler.cs
+++ b/src/core/Deal.Api/Hosting/MlOutboxFlushScheduler.cs
@@ -140,11 +140,6 @@ public sealed class MlOutboxFlushScheduler : IHostedService
}
}
- // Выгрузка очереди одного тенанта в собственном scope: SetTenant → порции по 10 до ≤100/цикл.
- // цикл тенанта завершается (следующая попытка — следующий тик). Сбой хранилища тенанта не валит проход:
- // ошибка логируется, остальные тенанты обрабатываются; отмена (OCE) пробрасывается наверх.
- // tenant: Тенант реестра (Id в формате Guid; схема — tenant_<N>).
- // ct: Токен отмены прохода.
private async Task FlushTenantAsync(TenantRecordDto tenant, CancellationToken ct)
{
await using AsyncServiceScope tenantScope = _scopeFactory.CreateAsyncScope();
@@ -176,7 +171,6 @@ public sealed class MlOutboxFlushScheduler : IHostedService
}
catch (Exception exception)
{
- // попытка — на следующем тике; флашер не роняет проход цикла.
_logger.LogWarning(
exception,
"Флашер ML-outbox: отправка {RowCount} строк тенанта {TenantId} не удалась — строки остались",
diff --git a/src/core/Deal.Api/Hosting/StorageTickScheduler.cs b/src/core/Deal.Api/Hosting/StorageTickScheduler.cs
index a127a8e..bb7dd97 100644
--- a/src/core/Deal.Api/Hosting/StorageTickScheduler.cs
+++ b/src/core/Deal.Api/Hosting/StorageTickScheduler.cs
@@ -143,13 +143,6 @@ public sealed class StorageTickScheduler : IHostedService
}
}
- // Тик одного тенанта в собственном scope: SetTenant → Kanban-тик → purge отсева → SSE-тосты →
- // тосты → напоминания).
- // Контекст AsyncLocal сбрасывается в finally, чтобы не переживать scope тенанта (как
- // SessionMiddleware). Тик одного тенанта не валит проход: ошибка ветки/тенанта логируется, остальные
- // тенанты обрабатываются; отмена (OCE) пробрасывается наверх — проход завершается.
- // tenant: Тенант реестра (Id в формате Guid; схема — tenant_<N>).
- // ct: Токен отмены прохода.
private async Task TickTenantAsync(TenantRecordDto tenant, CancellationToken ct)
{
await using AsyncServiceScope tenantScope = _scopeFactory.CreateAsyncScope();
@@ -162,15 +155,12 @@ public sealed class StorageTickScheduler : IHostedService
StorageTickService tickService = tenantScope.ServiceProvider.GetRequiredService();
StorageTickStatsDto stats = await tickService.TickAsync(ct);
- // (AdminTickOrchestrator), тост «Отсев очищен: N записей (3 дн.)» публикуется этой же веткой.
PipelineProcessingService processing = tenantScope.ServiceProvider.GetRequiredService();
int purgedRejected = await processing.PurgeExpiredAsync(ct);
StorageTickStatsDto mergedStats = stats with { PurgedRejected = purgedRejected };
_toastPublisher.PublishTickToasts(tenant.Id, mergedStats);
- // возвращает их {id,title,containerId}. Сбой проверки НЕ роняет тик тенанта/проход: лог-предупреждение,
- // остальные тенанты обрабатываются (паттерн ветки AdminTickOrchestrator). SSE reminder_due по каждой
IReadOnlyList dueReminders;
try
{
diff --git a/src/core/Deal.Api/Hosting/TenantBootstrapService.cs b/src/core/Deal.Api/Hosting/TenantBootstrapService.cs
index eb97c75..b171fe1 100644
--- a/src/core/Deal.Api/Hosting/TenantBootstrapService.cs
+++ b/src/core/Deal.Api/Hosting/TenantBootstrapService.cs
@@ -38,7 +38,6 @@ public sealed class TenantBootstrapService(IServiceScopeFactory scopeFactory) :
var password = configuration[BootstrapPasswordEnvKey] ?? DefaultAdminPassword;
var environment = scope.ServiceProvider.GetRequiredService();
- // в Development или при DEAL_BOOTSTRAP_DEFAULT_TENANT=1. В Production тенантов заводит оператор.
var seedDefaultTenant = environment.IsDevelopment()
|| configuration[DefaultTenantBootstrapEnvKey] == DefaultTenantBootstrapEnabledValue;
if (seedDefaultTenant)
@@ -65,8 +64,6 @@ public sealed class TenantBootstrapService(IServiceScopeFactory scopeFactory) :
}
}
- // что созданного в dev: пакетная идемпотентная миграция (CREATE SCHEMA IF NOT EXISTS + Migrate,
- // применяющий только неприменённые миграции) с ограниченным параллелизмом и логированием прогресса —
var tenantSchemaMigrationService = scope.ServiceProvider.GetRequiredService();
await tenantSchemaMigrationService.MigrateAllAsync(ct);
}
diff --git a/src/core/Deal.Api/Logging/DealLogging.cs b/src/core/Deal.Api/Logging/DealLogging.cs
index 2e09e59..b6d7853 100644
--- a/src/core/Deal.Api/Logging/DealLogging.cs
+++ b/src/core/Deal.Api/Logging/DealLogging.cs
@@ -4,17 +4,6 @@ using Serilog.Formatting.Compact;
namespace Deal.Api.Logging;
-// Все четыре процесса (core + telegram/ai/ml-сервисы) логируют через Serilog: консоль — JSON в
-// prod-стиле (CompactJsonFormatter: одна JSON-строка на событие, поля @t/@mt/@l — парсинг
-// Loki/Promtail) либо текст в Development; плюс rolling-файл data/logs/deal-<процесс>.json
-// (под ContentRoot; у core каталог data смонтирован volume-ом, compose.prod). Уровень/каталог
-// переопределяются env: DEAL_LOG_LEVEL, DEAL_LOGS_DIR.
-// Конфигурация кодом, а не секцией appsettings: у трёх сервисов appsettings.json нет (весь конфиг —
-// стек: Serilog-логи → docker-логи → Promtail → Loki → Grafana.
-// Вызов — из Program.cs процесса (entry point): DealLogging.Configure(builder, "…") ДО
-// builder.Build(). Интеграционные тесты сервисов поднимают хост через *ServiceHost.Create
-// БЕЗ этого вызова (конфигурация логирования — забота production-точки входа), поэтому тесты не пишут
-// файлы-логи и не меняют своё логирование.
internal static class DealLogging
{
// Env-ключ минимального уровня Serilog (Debug/Information/Warning/Error; дефолт Information).
diff --git a/src/core/Deal.Api/Middleware/HttpAccessLogMiddleware.cs b/src/core/Deal.Api/Middleware/HttpAccessLogMiddleware.cs
index 499bdb7..4bfede3 100644
--- a/src/core/Deal.Api/Middleware/HttpAccessLogMiddleware.cs
+++ b/src/core/Deal.Api/Middleware/HttpAccessLogMiddleware.cs
@@ -29,7 +29,6 @@ public sealed class HttpAccessLogMiddleware
///
/// Обрабатывает запрос
///
- /// Контекст запроса.
public async Task InvokeAsync(HttpContext context)
{
if (context.Request.ContentType?.StartsWith(GrpcContentType, StringComparison.OrdinalIgnoreCase) == true)
diff --git a/src/core/Deal.Api/Middleware/OperatorSessionMiddleware.cs b/src/core/Deal.Api/Middleware/OperatorSessionMiddleware.cs
index e617ec4..9f8e6c7 100644
--- a/src/core/Deal.Api/Middleware/OperatorSessionMiddleware.cs
+++ b/src/core/Deal.Api/Middleware/OperatorSessionMiddleware.cs
@@ -24,7 +24,6 @@ public sealed class OperatorSessionMiddleware
///
/// Обрабатывает запрос
///
- /// Контекст запроса.
public async Task InvokeAsync(HttpContext context)
{
var cookieName = _cookieOptions.CurrentValue.Name;
diff --git a/src/core/Deal.Api/Middleware/OriginGuardMiddleware.cs b/src/core/Deal.Api/Middleware/OriginGuardMiddleware.cs
index a952f66..0dfc24c 100644
--- a/src/core/Deal.Api/Middleware/OriginGuardMiddleware.cs
+++ b/src/core/Deal.Api/Middleware/OriginGuardMiddleware.cs
@@ -30,7 +30,6 @@ public sealed class OriginGuardMiddleware
///
/// Обрабатывает запрос
///
- /// Контекст запроса.
public async Task InvokeAsync(HttpContext context)
{
HttpRequest request = context.Request;
diff --git a/src/core/Deal.Api/Middleware/RateLimitPolicies.cs b/src/core/Deal.Api/Middleware/RateLimitPolicies.cs
index 7627d8a..6c9a434 100644
--- a/src/core/Deal.Api/Middleware/RateLimitPolicies.cs
+++ b/src/core/Deal.Api/Middleware/RateLimitPolicies.cs
@@ -49,9 +49,7 @@ public static class RateLimitPolicies
///
/// Регистрирует AddRateLimiter с политиками "auth"/"api" и глобальным лимитером API-партиции.
///
- /// Коллекция сервисов хоста.
/// Настройки rate limiting (секция RateLimit).
- /// Коллекция сервисов для цепочки вызовов.
public static IServiceCollection AddDealRateLimiter(this IServiceCollection services, RateLimitOptions options)
{
ArgumentNullException.ThrowIfNull(services);
@@ -126,8 +124,6 @@ public static class RateLimitPolicies
return string.IsNullOrEmpty(ip) ? UnknownClientKey : IpKeyPrefix + ip;
}
- // context: Контекст отклонённого запроса.
- // cancellationToken: Токен отмены ответа.
private static async ValueTask OnRejectedAsync(OnRejectedContext context, CancellationToken cancellationToken)
{
context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
diff --git a/src/core/Deal.Api/Middleware/SessionMiddleware.cs b/src/core/Deal.Api/Middleware/SessionMiddleware.cs
index fc85f89..12a16f0 100644
--- a/src/core/Deal.Api/Middleware/SessionMiddleware.cs
+++ b/src/core/Deal.Api/Middleware/SessionMiddleware.cs
@@ -31,7 +31,6 @@ public sealed class SessionMiddleware
///
/// Обрабатывает запрос
///
- /// Контекст запроса.
public async Task InvokeAsync(HttpContext context)
{
try
diff --git a/src/core/Deal.Api/Program.cs b/src/core/Deal.Api/Program.cs
index 33d7a81..7182d58 100644
--- a/src/core/Deal.Api/Program.cs
+++ b/src/core/Deal.Api/Program.cs
@@ -52,25 +52,17 @@ const string serverUrlsKey = "urls";
// Фолбэк основного HTTP-адреса при отсутствии явных URL (дефолт ASP.NET Core http://localhost:5000).
const string defaultHttpUrl = "http://localhost:5000";
const string defaultAiBudgetEnvKey = "DEAL_DEFAULT_AI_BUDGET";
-// умолчанию (curl-приёмки не режутся); PROD включает env-переопределением RateLimit__Enabled=true
const string rateLimitSectionName = "RateLimit";
-// фоновый цикл DataRetentionScheduler чистит audit_log по retention, лимиты/счётчики прошедших окон.
const string dataRetentionSectionName = "DataRetention";
const string securitySectionName = "Security";
-// KnownNetworks — доверенные прокси (Caddy в PROD); dev-дефолт в appsettings — loopback.
const string forwardedHeadersSectionName = "ForwardedHeaders";
const string coreProcessName = "core";
var builder = WebApplication.CreateBuilder(args);
-// rolling-файл data/logs/deal-core-*.json (data — volume контейнера). Уровень/каталог — env
-// DEAL_LOG_LEVEL/DEAL_LOGS_DIR (см. DealLogging). Регистрируется до остальных сервисов: логирование
-// заменяет провайдеры Microsoft при builder.Build().
DealLogging.Configure(builder, coreProcessName);
-// отдельном HTTP/1.1 Kestrel-эндпоинте (порт 9464/env METRICS_PORT) + инструментация входящих HTTP-
-// запросов и исходящих HTTP-клиентов; прикладные метрики — SharedKernel/Observability/DealMetrics.
int metricsPort = DealMetricsHosting.ResolveMetricsPort(DealMetricsHosting.DefaultMetricsPort);
DealMetricsHosting.AddDealMetrics(builder, metricsPort);
@@ -82,9 +74,6 @@ builder.Services.AddDbContext(options => options.UseNpgsql(connec
builder.Services.AddSingleton();
builder.Services.AddSingleton();
-// (генерация — scripts/mtls-certs.sh). При DEAL_MTLS_ENABLED=1 сертификаты грузятся сразу (fail-fast на
-// битые пути/пароли) — один экземпляр используют и Kestrel-ингресс ниже, и gRPC-клиенты
-// (Ml/Ai/Telegram-каналы + ServiceHealthProbe).
MtlsOptions mtlsOptions = MtlsOptions.FromConfiguration(builder.Configuration);
MtlsCertificates? mtlsCertificates = MtlsCertificates.Load(mtlsOptions);
if (mtlsCertificates is not null)
@@ -92,9 +81,6 @@ if (mtlsCertificates is not null)
builder.Services.AddSingleton(mtlsCertificates);
}
-// Kestrel: основной HTTP/1.1-эндпоинт из URL-конфигурации (как раньше — --urls/ASPNETCORE_URLS/
-// launchSettings) + второй endpoint gRPC-ингресса telegram-service (:5082, HTTP/2, env GRPC_INGRESS_PORT;
-// пере-биндим адресами конфигурации "urls" явно (см. BindMainHttpEndpoints ниже). Ingress слушает все
builder.WebHost.ConfigureKestrel(kestrel =>
{
BindMainHttpEndpoints(kestrel, builder.Configuration[serverUrlsKey]);
@@ -104,7 +90,6 @@ builder.WebHost.ConfigureKestrel(kestrel =>
listen.Protocols = HttpProtocols.Http2;
if (mtlsCertificates is not null)
{
- // сертификат (цепочка до CA из DEAL_MTLS_CA_PEM). Основной HTTP :5080 остаётся http — TLS наружу
listen.UseHttps(https =>
{
https.ServerCertificate = mtlsCertificates.ServerCertificate;
@@ -141,83 +126,49 @@ builder.Services.AddDbContext(
builder.Services.AddTenantsModule();
-// env DEAL_DEFAULT_AI_BUDGET (токенов в месяц) с фолбэком на константу модуля TokenBudgetDefaults (10 000 000);
-// период нового тенанта — month (константа). Значение читается один раз на старте и передаётся адаптеру
-// TenantLimitStore (GetOrCreateAsync при первом чтении/списании, задачи 7/10 list-путь тоже закрыт).
TokenLimitDefaults tenantLimitDefaults = new(
ResolveDefaultAiBudget(builder.Configuration), TokenBudgetDefaults.DefaultPeriod);
builder.Services.AddDealPersistence(tenantLimitDefaults);
-// либо файла data/encryption.key под ContentRoot (dev). Ключ разрешается на старте —
-// невалидный env-ключ останавливает запуск.
builder.Services.AddDealSecurity(builder.Environment.ContentRootPath);
-// ml-service :5103). Local-адаптеры читают KV-настройки тенанта через ISettingsStore — scoped (вне
MlServiceOptions mlOptions = builder.Configuration.GetSection(servicesSectionName).Get() ?? new MlServiceOptions();
builder.Services.AddSingleton(mlOptions);
-// default; локальный разбор ядра / инструменты не поддерживаются) либо декораторы бюджетного гейта поверх
-// gRPC-клиентов ai-service (UseLocal=false, ai-service :5102; AddDealIntegrations регистрирует GrpcAiClassifier/
-// GrpcAiTools + транспорт AiGrpcConnection — fail-fast, как MlGrpcConnection — и оборачивает их в
AiServiceOptions aiOptions = builder.Configuration.GetSection(aiServicesSectionName).Get() ?? new AiServiceOptions();
builder.Services.AddSingleton(aiOptions);
-// нейтральный no-op/idle) либо gRPC-клиент GrpcTelegramClient (UseLocal=false, telegram-service :5101).
TelegramServiceOptions telegramOptions = builder.Configuration.GetSection(telegramServicesSectionName).Get() ?? new TelegramServiceOptions();
builder.Services.AddSingleton(telegramOptions);
builder.Services.AddDealIntegrations(mlOptions, aiOptions, telegramOptions, mtlsCertificates);
-// к Services:*:Endpoint с дедлайном 3 с (ServiceHealthProbe). Stateless, singleton — пробы строят
builder.Services.AddSingleton(new ServiceHealthProbe(mtlsCertificates));
-// под ContentRoot) — dev/curl/unit по умолчанию; MinioFileStorage регистрируется, только когда сконфигурирован
-// MinIO (секция Storage:Minio либо env-алиасы DEAL_MINIO_*; compose-сервис deal-minio, порты 9000/9001).
-// Singleton: хранилище не привязано к схеме тенанта (объекты — в едином бакете/каталоге, мульти-аренда
builder.Services.AddDealFileStorage(builder.Configuration, builder.Environment.ContentRootPath);
-// Модуль Settings (сервис настроек тенанта); адаптеры ISettingsStore/ISecretCipher уже
builder.Services.AddSettingsModule();
-// порт-адаптер ICardStore → KanbanStore уже зарегистрирован AddDealPersistence.
builder.Services.AddKanbanModule();
-// IPipelineStore → PipelineStore и внешние порты (IMlClient/IAiClassifier) уже зарегистрированы
-// AddDealPersistence/AddDealIntegrations выше; сервисы модуля вызывают из эндпоинтов /api/pipeline/*
builder.Services.AddPipelineModule();
-// Dialogs/TgMessages схемы тенанта (миграция TenantTelegram). Порт-адаптеры ITelegramStore → TelegramStore и
-// ITelegramGateway → LocalTelegramGateway/GrpcTelegramClient зарегистрированы AddDealPersistence/AddDealIntegrations
builder.Services.AddTelegramModule();
-// и воркер (оценка/бан-гард/паузы). Порт-адаптер IDiscoveryStore → DiscoveryStore зарегистрирован
-// AddDealPersistence; внешние порты (ITelegramGateway/IAiTools/IMlClient) — AddDealIntegrations выше. Эндпоинты
builder.Services.AddDiscoveryModule();
-// счётчик мониторящихся + keysSet) и чтение глобальных ключей приложения (telegramKeys в public.global_settings,
-// расшифровка apiHash — задаёт оператор, ТЗ §4.1/§8.1). Scoped: зависимости — ISettingsStore/ITelegramStore
-// на TenantDbContext схемы тенанта запроса, IGlobalSettingsStore — на системном DealDbContext.
builder.Services.AddScoped();
builder.Services.AddScoped();
-// мониторинга/«Перечитать» отвечают сразу, тяжёлый разбор идёт в отдельном scope с захваченным tenant-контекстом.
builder.Services.AddSingleton();
-// Pipeline + один проход pump + SSE-публикации (тосты/new_card) для POST /api/admin/tick (StorageEndpoints).
-// Scoped: зависимости живут в рамках tenant-запроса (scoped-сервисы модулей на TenantDbContext схемы).
builder.Services.AddScoped();
-// CREATE INDEX IF NOT EXISTS + ANALYZE (FtsMaintenance) на TenantDbContext запроса (scoped, как адаптеры).
builder.Services.AddScoped();
builder.Services.AddSingleton();
builder.Services.AddSingleton();
-// POST /api/admin/tick (AdminTickOrchestrator) и фоновый цикл разбора очереди (PipelineWorkerScheduler)
-// не разбирают очередь одного тенанта одновременно (singleton per-tenant флагов, Interlocked).
builder.Services.AddSingleton();
-// "RateLimit" (appsettings.json + env RateLimit__*). Enabled=false в dev/тестах — политики/middleware/
-// LoginAttemptGuard (окно ip|login 5 неудач/15 мин в public.rate_limit_counters) регистрируется всегда,
-// но активен только при Enabled.
RateLimitOptions rateLimitOptions = builder.Configuration
.GetSection(rateLimitSectionName)
.Get() ?? new RateLimitOptions();
@@ -230,18 +181,12 @@ if (rateLimitOptions.Enabled)
builder.Services.AddDealRateLimiter(rateLimitOptions);
}
-// на отдельном Kestrel-endpoint (:5082, HTTP/2, см. ConfigureKestrel выше) в том же процессе. Token
-// из metadata «service-token» проверяет интерцептор (fail-closed, DEAL_SERVICE_TOKEN); AddAuthentication
-// не нужен — пользовательская сессия HTTP ингрессом не используется (tenant-id из metadata → SetTenant).
builder.Services.AddGrpc(grpc =>
{
- // вызовы (401/429); gRPC-health не логируется (см. RpcCallLoggingInterceptor).
grpc.Interceptors.Add();
grpc.Interceptors.Add();
if (rateLimitOptions.Enabled)
{
- // singleton-лимитер (CreateLimiter) — экземпляры интерцептора общий PartitionedRateLimiter
- // разделяют; health-методы освобождены (см. IngressRateLimitInterceptor).
grpc.Interceptors.Add();
}
});
@@ -255,62 +200,39 @@ if (rateLimitOptions.Enabled)
builder.Services.AddScoped();
-// grpc.health.v1.Health интерцептор токеном не проверяет (инфраструктурный liveness, как в сервисах
-// Живучесть интеграций (ml/ai/telegram) health не проверяет — недоступность сервиса это UNAVAILABLE
builder.Services
.AddGrpcHealthChecks()
.AddCheck("ready", () => HealthCheckResult.Healthy("хост Deal.Api готов"));
-// HTTP-адаптер Infrastructure с собственным HttpClient (фабрика AddHttpClient, таймаут 12 с).
-// HTTP наружу ходит только по действию Settings-экрана (POST /api/ai/check) — GET {base}/models.
builder.Services.AddHttpClient(
client => client.Timeout = TimeSpan.FromSeconds(AiConnectionChecker.RequestTimeoutSeconds));
-// адаптера (SSRF-allowlist), источник тенантом не настраивается. Типизированный клиент
builder.Services.AddHttpClient(
client => client.Timeout = TimeSpan.FromSeconds(CbrRateSource.RequestTimeoutSeconds));
-// собственный scope + in-flight guard (см. RatesRefreshScheduler).
builder.Services.AddSingleton();
builder.Services.AddHostedService();
-// dev-дефолт operator/operator в Development; в Production без env — warning и пропуск. Идёт после
-// TenantBootstrapService: операторские public-таблицы не зависят от провижининга схем тенантов.
builder.Services.AddHostedService();
-// 30 с тикает ВСЕ тенанты (StorageTickService + автоочистка отсева пайплайна 3 суток) и публикует
-// SSE-тосты. Регистрируется после Bootstrap — первый проход стартует уже после провижининга схем.
builder.Services.AddHostedService();
-// проверяет ВСЕ тенанты и публикует в канал тенанта тост при пересечении порогов 80/100% (TryMark*-CAS —
-// один тост на порог за период). Идёт после Bootstrap: реестр тенантов провижинен до первого прохода.
builder.Services.AddHostedService();
-// каждые 2 с pump'ит ВСЕ тенанты (PipelineWorkerService.PumpOnceAsync под общим PipelinePumpGate) и
-// После StorageTickScheduler: очередь цикла — 2 с, первый проход сразу после старта.
builder.Services.AddHostedService();
-// выгружает MlOutbox тенантов в ml-service (TrainBatch, порции по 10, ≤100/цикл; удаление после успеха).
if (!mlOptions.UseLocal)
{
builder.Services.AddHostedService();
}
-// делает ОДИН шаг (поиск/оценка/авто-вступление/done) для каждой running-задачи всех тенантов. Работает
-// всегда: в Local-режиме гейт нейтрален (поиск пуст/история недоступна), реальные действия — при
-// подключённом telegram-service (UseLocal=false). После Bootstrap: первый проход стартует после провижининга.
builder.Services.AddHostedService();
builder.Services.AddSingleton();
-// MlOutbox) по всем тенантам и число активных сессий в meter Deal (callback /metrics отдаёт их Prometheus).
-// Регистрируется последним из фоновых: после Bootstrap (реестр тенантов провижинен до первого прохода).
builder.Services.AddHostedService();
-// записи audit_log старше DataRetention:AuditRetentionDays (дефолт 180 дней), сбрасывает накопительные
-// поля лимитов прошедших периодов и убирает завершившиеся окна распределённых счётчиков. Регистрируется
-// последним из фоновых: после Bootstrap (реестр тенантов провижинен до первого прохода).
DataRetentionOptions dataRetentionOptions = builder.Configuration
.GetSection(dataRetentionSectionName)
.Get() ?? new DataRetentionOptions();
@@ -320,28 +242,21 @@ builder.Services.AddHostedService();
// Кука сессии: имя/срок/Secure из секции "Cookies" (appsettings.json + env Cookies__*).
builder.Services.Configure(builder.Configuration.GetSection(cookiesSectionName));
-// "OperatorCookies" (appsettings.json + env OperatorCookies__*) — отдельная от тенантной deal_session.
builder.Services.Configure(builder.Configuration.GetSection(operatorCookiesSectionName));
builder.Services.ConfigureHttpJsonOptions(options =>
options.SerializerOptions.Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping);
-// проверки мутаций и CORS (пусто — dev-режим «свой origin», см. AddCors ниже; PROD — домен фронта в
-// compose-prod). Инстанс регистрируется в DI: значение читается один раз на старте (политики формируются
-// при старте хоста), OriginGuardMiddleware получает его конструктором.
SecurityOptions securityOptions = builder.Configuration
.GetSection(securitySectionName)
.Get() ?? new SecurityOptions();
builder.Services.AddSingleton(securityOptions);
-// dev-дефолт — false (прокси в dev-стеке нет, compose.dev публикует core напрямую).
ForwardedHeadersConfig forwardedHeadersConfig = builder.Configuration
.GetSection(forwardedHeadersSectionName)
.Get() ?? new ForwardedHeadersConfig();
builder.Services.AddSingleton(forwardedHeadersConfig);
-// (любой origin/method/header, credentials=true; AllowAnyOrigin + AllowCredentials несовместимы — любой
-// Security-заголовки ответов (nosniff/X-Frame-Options/Referrer-Policy; CSP/HSTS) — на edge (Caddyfile,
builder.Services.AddCors(options =>
options.AddPolicy(corsPolicyName, cors =>
{
@@ -380,7 +295,6 @@ if (app.Environment.IsProduction())
}
}
-// с путём data/attachments; при сконфигурированном MinIO — MinioFileStorage с endpoint/бакетом).
app.Logger.LogInformation("Файловое хранилище: {FileStorage}", app.Services.GetRequiredService());
app.Logger.LogInformation(
@@ -404,14 +318,9 @@ app.Logger.LogInformation(
if (forwardedHeadersConfig.Enabled)
{
- // только клиентам из ForwardedHeaders:KnownProxies/KnownNetworks (конфиг; appsettings — loopback для dev).
- // Middleware — ПЕРВЫЙ в конвейере: RemoteIpAddress/Scheme читают слои ниже (CORS, Session/OperatorSession —
- // audit-IP эндпоинтов, RateLimiter — ключи по IP, LoginAttemptGuard). Без него за Caddy (compose-prod,
app.UseForwardedHeaders(BuildForwardedHeadersOptions(forwardedHeadersConfig));
}
-// и статус всего пути обработки. gRPC-ингресс (Content-Type application/grpc) middleware пропускает —
-// его логирует интерцептор RpcCallLoggingInterceptor (см. HttpAccessLogMiddleware).
app.UseMiddleware();
app.UseCors(corsPolicyName);
@@ -419,13 +328,9 @@ app.UseMiddleware();
app.UseMiddleware();
if (rateLimitOptions.Enabled)
{
- // CurrentUser.TenantId либо IP анонима; сессии уже разрешены). При Enabled=false лимитер не
app.UseRateLimiter();
}
-// RateLimiter → OriginGuard (сессии разрешены, 429 важнее 403). Проверяются не-GET/HEAD/OPTIONS запросы
-// с заголовком Origin: Origin == «свой» origin (схема + Host; за Caddy — https из X-Forwarded-Proto)
-// либо входит в Security:AllowedOrigins; иначе 403 {detail}. Без Origin (curl/сервер-сервер) пропускаются;
app.UseMiddleware();
app.MapGet("/api/health", () => Results.Ok(new { ok = true, service = "deal" }));
@@ -435,12 +340,9 @@ app.MapOperatorAuditEndpoints();
app.MapOperatorAnalyticsEndpoints();
app.MapOperatorInvitesEndpoints();
app.MapOperatorTenantsEndpoints();
-// .../limit, аудит tenant_limit_changed) + health ядра/БД и сервисов ml/ai/telegram.
app.MapOperatorLimitsEndpoints();
app.MapOperatorHealthEndpoints();
-// Глобальные настройки оператора (ТЗ §4.1/§8.1): ключи приложения Telegram — чтение (маска) и смена.
app.MapOperatorSettingsEndpoints();
-// (ограниченный параллелизм + логирование прогресса) — для SaaS с сотнями/тысячами схем.
app.MapOperatorMaintenanceEndpoints();
app.MapJoinEndpoint();
app.MapSettingsEndpoints();
@@ -455,12 +357,9 @@ app.MapStorageEndpoints();
app.MapEventsEndpoint();
app.MapAiSuggestEndpoints();
app.MapPipelineEndpoints();
-// (BootStubEndpoints удалён); qr-image — отдельным файлом.
app.MapTelegramEndpoints();
app.MapTelegramQrImageEndpoint();
app.MapDiscoveryEndpoints();
-// входящего потока считает IngressRateLimitInterceptor по tenant-id из metadata (иначе общее окно на IP
-// telegram-service резало бы весь ингресс раньше интерцептора); health — инфраструктурный liveness.
app.MapGrpcService().DisableRateLimiting();
app.MapGrpcHealthChecksService().DisableRateLimiting();
@@ -531,7 +430,6 @@ static List ParseHttpAddresses(string? urlsConfig)
static int? ParsePort(string? rawValue)
=> int.TryParse(rawValue, out int parsedPort) ? parsedPort : null;
-// значение (пустая переменная, опечатка) — константа модуля TokenBudgetDefaults.DefaultBudgetTokens. Период
long ResolveDefaultAiBudget(IConfiguration configuration)
{
string? rawValue = configuration[defaultAiBudgetEnvKey];
diff --git a/src/core/Deal.Api/Services/AdminTickOrchestrator.cs b/src/core/Deal.Api/Services/AdminTickOrchestrator.cs
index 3c2e62e..794babc 100644
--- a/src/core/Deal.Api/Services/AdminTickOrchestrator.cs
+++ b/src/core/Deal.Api/Services/AdminTickOrchestrator.cs
@@ -51,7 +51,6 @@ public sealed class AdminTickOrchestrator(
toastPublisher.PublishTickToasts(tenantId, mergedStorage);
- // проверки НЕ роняет тик: лог + reminders ответа пуст (очередь/хранение продолжают работать).
IReadOnlyList dueReminders;
try
{
@@ -68,13 +67,11 @@ public sealed class AdminTickOrchestrator(
dueReminders = Array.Empty();
}
- // шлём — у фронта модалка ReminderNotice; без подписчиков канала публикация — no-op). После MarkFired
foreach (CardReminderDueDto due in dueReminders)
{
broker.Publish(tenantId, ReminderDueEventType, due);
}
- // (5) Один проход pump под гейтом тенанта; сбой не роняет тик: pipeline={}, очередь дождётся
PipelinePumpResult? pump = await PumpOnceSafelyAsync(tenantId, ct);
if (pump is not null)
@@ -94,11 +91,8 @@ public sealed class AdminTickOrchestrator(
queueCounts.Total);
}
- // ct: Токен отмены запроса.
- // Возвращает: Результат прохода либо null — гейт занят другим воркером/pump упал (pipeline ответа пуст).
private async Task PumpOnceSafelyAsync(Guid tenantId, CancellationToken ct)
{
- // разбирают очередь тенанта одновременно. Гейт занят (фоновый цикл уже pump'ит) — проход пропускаем,
if (!pumpGate.TryEnter(tenantId))
{
return null;
@@ -124,8 +118,6 @@ public sealed class AdminTickOrchestrator(
}
}
- // wire не выходят — они ушли отдельными SSE new_card).
- // pump: Результат успешного прохода pump.
private static IReadOnlyDictionary ToPipelineWireDict(PipelinePumpResult pump)
{
int[] counters =
diff --git a/src/core/Deal.Api/Services/AuditAppender.cs b/src/core/Deal.Api/Services/AuditAppender.cs
index 46076fc..2700a50 100644
--- a/src/core/Deal.Api/Services/AuditAppender.cs
+++ b/src/core/Deal.Api/Services/AuditAppender.cs
@@ -13,7 +13,6 @@ public static class AuditAppender
///
/// Пишет событие действия пользователя тенанта
///
- /// Контекст запроса (источник актора и IP).
/// Тип события — константа .
/// Минимальные детали события (обычно анонимный объект) или null.
public static async Task AppendTenantAsync(
@@ -43,7 +42,6 @@ public static class AuditAppender
///
/// Пишет событие действия оператора
///
- /// Контекст запроса (источник актора и IP).
/// Тип события — константа .
/// Минимальные детали события (обычно анонимный объект) или null.
public static async Task AppendOperatorAsync(
diff --git a/src/core/Deal.Api/Services/SessionCookieWriter.cs b/src/core/Deal.Api/Services/SessionCookieWriter.cs
index 7babf74..8b9533c 100644
--- a/src/core/Deal.Api/Services/SessionCookieWriter.cs
+++ b/src/core/Deal.Api/Services/SessionCookieWriter.cs
@@ -11,7 +11,6 @@ public static class SessionCookieWriter
///
/// Выставляет httpOnly-куку сессии на текущем ответе.
///
- /// Контекст запроса.
/// Настройки куки из конфигурации (секция Cookies).
/// Raw-токен сессии.
public static void Append(
diff --git a/src/core/Deal.Api/Telegram/TelegramIngressService.cs b/src/core/Deal.Api/Telegram/TelegramIngressService.cs
index 4a42cd7..97721ee 100644
--- a/src/core/Deal.Api/Telegram/TelegramIngressService.cs
+++ b/src/core/Deal.Api/Telegram/TelegramIngressService.cs
@@ -107,7 +107,6 @@ public sealed class TelegramIngressService(
}
catch (Exception exception)
{
- // Сбой схемы/БД тенанта (напр. схема ещё не провижинена): RPC не падает — reply not-accepted
logger.LogWarning(exception, "Аудит: PushMessage {TenantId} → не принято (сбой схемы/БД)", tenant.Id);
return new PushMessageReply();
}
@@ -226,11 +225,6 @@ public sealed class TelegramIngressService(
}
}
- // Разрешает тенанта запроса: metadata tenant-id → реестр public.tenants.
- // Отсутствующий/пустой tenant-id — RPC-отказ UNAUTHENTICATED (README: tenant-id обязателен).
- // Id не Guid либо записи нет в реестре — неизвестный тенант: лог аудита и null (RPC отвечает не-принято,
- // context: Контекст вызова.
- // Возвращает: Запись тенанта реестра либо null (тенант неизвестен).
private async Task ResolveTenantAsync(ServerCallContext context)
{
string tenantId = RequireTenantIdMetadata(context);
@@ -265,12 +259,6 @@ public sealed class TelegramIngressService(
return tenantId;
}
- // Пишет превью принятого сообщения (TgMessages + «последнее сообщение» каталога) без влияния на приём.
- // не роняет RPC и не меняет accepted — очередь уже записана, упущенное догонит realtime-sweep (как
- // tenantScope: Scope тенанта (TenantDbContext построен на схеме тенанта).
- // tenant: Тенант канала (для лога аудита).
- // request: Сообщение PushMessage.
- // ct: Токен отмены.
private async Task SavePreviewSafelyAsync(
AsyncServiceScope tenantScope,
TenantRecordDto tenant,
@@ -325,9 +313,6 @@ public sealed class TelegramIngressService(
}
}
- // tenantId: Тенант-получатель.
- // text: Текст тоста.
- // icon: Иконка тоста (набор Icon.vue фронта).
private void PublishToast(
Guid tenantId,
string text,
diff --git a/src/core/Deal.Api/Telegram/TgStatusService.cs b/src/core/Deal.Api/Telegram/TgStatusService.cs
index 43c6086..28ab356 100644
--- a/src/core/Deal.Api/Telegram/TgStatusService.cs
+++ b/src/core/Deal.Api/Telegram/TgStatusService.cs
@@ -31,7 +31,7 @@ public sealed class TgStatusService(
};
///
- /// Форма GET /api/tg/status текущего тенанта
+ /// Форма GET /api/tg/status текущего тенанта.
///
/// Полный статус вкладки Telegram.
public async Task GetAsync(CancellationToken ct)
@@ -52,8 +52,6 @@ public sealed class TgStatusService(
QrUrl: live.QrUrl);
}
- // ct: Токен отмены.
- // Возвращает: Статус гейта либо idle-поля.
private async Task ReadLiveAsync(CancellationToken ct)
{
try
@@ -66,8 +64,6 @@ public sealed class TgStatusService(
}
}
- // ct: Токен отмены.
- // Возвращает: Аккаунт или пустая строка.
private async Task ReadAccountAsync(CancellationToken ct)
{
SettingValue? row = await settings.GetAsync(SettingsKeys.TgAccount, ct).ConfigureAwait(false);
diff --git a/src/core/Deal.Contracts/Integrations/Abstractions/IAiClassifier.cs b/src/core/Deal.Contracts/Integrations/Abstractions/IAiClassifier.cs
index 19ea94d..a26b349 100644
--- a/src/core/Deal.Contracts/Integrations/Abstractions/IAiClassifier.cs
+++ b/src/core/Deal.Contracts/Integrations/Abstractions/IAiClassifier.cs
@@ -15,7 +15,7 @@ public interface IAiClassifier
public Task FilterAsync(string text, CancellationToken ct);
///
- /// Полный разбор карточки — структура классификации ТЗ §5 и classify.
+ /// Полный разбор карточки — структура классификации и classify.
///
/// Текст сообщения (как в очереди, уже обрезанный до 6000 при приёме).
/// Структурированный разбор карточки (поля карточки + решение о типе/колонке).
diff --git a/src/core/Deal.Contracts/Integrations/Abstractions/ITelegramGateway.cs b/src/core/Deal.Contracts/Integrations/Abstractions/ITelegramGateway.cs
index 0f6acb4..23d661b 100644
--- a/src/core/Deal.Contracts/Integrations/Abstractions/ITelegramGateway.cs
+++ b/src/core/Deal.Contracts/Integrations/Abstractions/ITelegramGateway.cs
@@ -96,7 +96,7 @@ public interface ITelegramGateway
/// Последние сообщения диалога для превью, свежие из Telegram.
///
/// Id диалога.
- /// Сколько последних сообщений (1..50; api-map /dialogs/preview).
+ /// Сколько последних сообщений.
/// Сообщения от новых к старым; признак lead и фолбэк на БД добавляет ядро.
public Task> ReadRecentAsync(
string dialogId,
diff --git a/src/core/Deal.Infrastructure/Data/ConnectionStringProvider.cs b/src/core/Deal.Infrastructure/Data/ConnectionStringProvider.cs
index e640c34..2b961c1 100644
--- a/src/core/Deal.Infrastructure/Data/ConnectionStringProvider.cs
+++ b/src/core/Deal.Infrastructure/Data/ConnectionStringProvider.cs
@@ -15,7 +15,6 @@ public sealed class ConnectionStringProvider
{
_baseConnectionString = configuration.GetConnectionString("DealPostgres")
?? throw new InvalidOperationException("ConnectionStrings:DealPostgres не задан");
- // Опциональная роль мигратора: задаётся только в проде (см. техдок §10/§13).
_migratorConnectionString = configuration.GetConnectionString("DealMigrator");
}
diff --git a/src/core/Deal.Infrastructure/Integrations/Services/AiConnectionChecker.cs b/src/core/Deal.Infrastructure/Integrations/Services/AiConnectionChecker.cs
index 33b65c1..e48e29c 100644
--- a/src/core/Deal.Infrastructure/Integrations/Services/AiConnectionChecker.cs
+++ b/src/core/Deal.Infrastructure/Integrations/Services/AiConnectionChecker.cs
@@ -151,11 +151,6 @@ public sealed class AiConnectionChecker : IAiConnectionChecker
}
}
- // request: Запрос проверки (поля статуса провайдера).
- // name: Имя провайдера из каталога AiProviders.
- // ok: Результат подключения.
- // message: Сообщение ветки.
- // Возвращает: DTO ответа (наружу — camelCase).
private static AiCheckResultDto BuildResult(
AiCheckRequest request,
string name,
@@ -174,8 +169,6 @@ public sealed class AiConnectionChecker : IAiConnectionChecker
KeyMasked: MaskKey(request.ApiKey));
}
- // key: Ключ открытым текстом.
- // Возвращает: Маскированная строка.
private static string MaskKey(string key)
{
if (string.IsNullOrEmpty(key))
@@ -191,10 +184,6 @@ public sealed class AiConnectionChecker : IAiConnectionChecker
return string.Concat(key.AsSpan(0, 4), "…", key.AsSpan(key.Length - 4));
}
- // baseUrl: Эффективный базовый URL из конфигурации провайдера.
- // apiStyle: Стиль API провайдера (null — OpenAI-совместимый).
- // modelsUri: URL списка моделей (валиден только при возврате true).
- // Возвращает: True — URL построен; False — base URL не абсолютный http(s) (SSRF-гейт).
private static bool TryBuildModelsUri(
string baseUrl,
string? apiStyle,
diff --git a/src/core/Deal.Infrastructure/Integrations/Services/AiProviderConfigBuilder.cs b/src/core/Deal.Infrastructure/Integrations/Services/AiProviderConfigBuilder.cs
index bfa422a..a85a7de 100644
--- a/src/core/Deal.Infrastructure/Integrations/Services/AiProviderConfigBuilder.cs
+++ b/src/core/Deal.Infrastructure/Integrations/Services/AiProviderConfigBuilder.cs
@@ -87,8 +87,6 @@ public sealed class AiProviderConfigBuilder
return config;
}
- // ct: Токен отмены.
- // Возвращает: Id провайдера (каталога AiProviders).
private async Task ReadProviderIdAsync(CancellationToken ct)
{
SettingValue? row = await _store.GetAsync(SettingsKeys.AiProvider, ct);
diff --git a/src/core/Deal.Infrastructure/Integrations/Services/BudgetedAiClassifier.cs b/src/core/Deal.Infrastructure/Integrations/Services/BudgetedAiClassifier.cs
index ad0de46..9a3c746 100644
--- a/src/core/Deal.Infrastructure/Integrations/Services/BudgetedAiClassifier.cs
+++ b/src/core/Deal.Infrastructure/Integrations/Services/BudgetedAiClassifier.cs
@@ -62,7 +62,6 @@ public sealed class BudgetedAiClassifier : IAiClassifier
return await _paidClassifier.FilterAsync(text, ct);
}
- // Запрет гейта — фильтр через Local-реализацию {pass:true, skipped:true} (семантика «фильтр недоступен»,
_logger.LogDebug(
"ИИ-фильтр: {Reason} — Local-пропуск (тенант {TenantId})", GateDeniedLogText, TenantIdForLog());
return await _localClassifier.FilterAsync(text, ct);
@@ -76,14 +75,11 @@ public sealed class BudgetedAiClassifier : IAiClassifier
return await _paidClassifier.ClassifyAsync(text, ct);
}
- // без платного ИИ, приём не блокируется.
_logger.LogDebug(
"ИИ-классификация: {Reason} — Local-разбор (тенант {TenantId})", GateDeniedLogText, TenantIdForLog());
return await _localClassifier.ClassifyAsync(text, ct);
}
- // ct: Токен отмены.
- // Возвращает: True — можно звать платного исполнителя.
private async Task IsPaidAllowedAsync(CancellationToken ct)
{
BudgetStateDto state = await _tenantLimits.GetStateAsync(RequireTenantId(), ct);
diff --git a/src/core/Deal.Infrastructure/Integrations/Services/BudgetedAiTools.cs b/src/core/Deal.Infrastructure/Integrations/Services/BudgetedAiTools.cs
index 6369794..df30c05 100644
--- a/src/core/Deal.Infrastructure/Integrations/Services/BudgetedAiTools.cs
+++ b/src/core/Deal.Infrastructure/Integrations/Services/BudgetedAiTools.cs
@@ -90,8 +90,6 @@ public sealed class BudgetedAiTools : IAiTools
state.Status == TenantStatuses.Suspended ? SuspendedFitError : ExhaustedFitError);
}
- // ct: Токен отмены.
- // Возвращает: Состояние бюджета тенанта на сейчас.
private async Task GateStateAsync(CancellationToken ct)
=> await _tenantLimits.GetStateAsync(RequireTenantId(), ct);
diff --git a/src/core/Deal.Infrastructure/Integrations/Services/CbrRateSource.cs b/src/core/Deal.Infrastructure/Integrations/Services/CbrRateSource.cs
index aba1332..4b3bccd 100644
--- a/src/core/Deal.Infrastructure/Integrations/Services/CbrRateSource.cs
+++ b/src/core/Deal.Infrastructure/Integrations/Services/CbrRateSource.cs
@@ -100,9 +100,6 @@ public sealed class CbrRateSource : IRatesSource
return rates;
}
- // currency: Пара «код валюты → объект {Value, Nominal}».
- // rate: Курс единицы валюты к рублю (валиден при возврате true).
- // Возвращает: True — запись распознана; False — повреждённая запись (весь fetch — сбой).
private static bool TryParseCurrency(JsonProperty currency, out double rate)
{
rate = 0;
diff --git a/src/core/Deal.Infrastructure/Integrations/Services/GrpcAiClassifier.cs b/src/core/Deal.Infrastructure/Integrations/Services/GrpcAiClassifier.cs
index e75b33b..e71e6fd 100644
--- a/src/core/Deal.Infrastructure/Integrations/Services/GrpcAiClassifier.cs
+++ b/src/core/Deal.Infrastructure/Integrations/Services/GrpcAiClassifier.cs
@@ -89,7 +89,6 @@ public sealed class GrpcAiClassifier : IAiClassifier
CallOptions(tenantId.Value, ct));
await _usageRecorder.AddAsync(reply.Usage, providerConfig.ProviderId, providerConfig.Model, ct);
- // Фильтр применён (воркер звал его только при aiFilterEnabled и не force) — skipped=false
return new AiFilterResultDto(
Pass: reply.Pass,
Reason: reply.HasReason ? reply.Reason : null,
@@ -142,7 +141,6 @@ public sealed class GrpcAiClassifier : IAiClassifier
await _usageRecorder.AddAsync(reply.Usage, providerConfig.ProviderId, providerConfig.Model, ct);
if (!reply.Ok)
{
- // Модель не вернула разбираемый JSON после ретраев — контрактная ok=false (README ai.proto):
_logger.LogWarning("ИИ-классификация: ok=false (тенант {TenantId})", tenantId.Value);
throw new AiUnavailableException(NoJsonAnswerText);
}
@@ -167,27 +165,18 @@ public sealed class GrpcAiClassifier : IAiClassifier
?? throw new InvalidOperationException(
"GrpcAiClassifier запрошен вне tenant-контекста (ITenantContext.TenantId == null).");
- // tenantId: Id тенанта (формат N).
- // ct: Токен отмены вызова.
- // Возвращает: Опции вызова с заголовками, deadline и отменой.
private CallOptions CallOptions(string tenantId, CancellationToken ct)
=> new(
headers: _connection.CreateMetadata(tenantId),
deadline: DateTime.UtcNow.Add(TimeSpan.FromSeconds(RpcDeadlineSeconds)),
cancellationToken: ct);
- // секреты/тела ответов не логируются и в текст не попадают).
- // exception: Исключение RPC-вызова.
- // Возвращает: Текст ошибки.
private static string ErrorText(RpcException exception)
{
string detail = exception.Status.Detail?.Trim() ?? string.Empty;
return detail.Length > 0 ? detail : ServiceUnavailableText;
}
- // text: Строка.
- // max: Лимит.
- // Возвращает: Усечённая строка.
private static string SliceCodePoints(string text, int max)
{
return text.Length <= max ? text : SliceByCodePoints(text, max);
diff --git a/src/core/Deal.Infrastructure/Integrations/Services/GrpcAiTools.cs b/src/core/Deal.Infrastructure/Integrations/Services/GrpcAiTools.cs
index e639492..6202dbe 100644
--- a/src/core/Deal.Infrastructure/Integrations/Services/GrpcAiTools.cs
+++ b/src/core/Deal.Infrastructure/Integrations/Services/GrpcAiTools.cs
@@ -158,27 +158,18 @@ public sealed class GrpcAiTools : IAiTools
?? throw new InvalidOperationException(
"GrpcAiTools запрошен вне tenant-контекста (ITenantContext.TenantId == null).");
- // tenantId: Id тенанта (формат N).
- // ct: Токен отмены вызова.
- // Возвращает: Опции вызова с заголовками, deadline и отменой.
private CallOptions CallOptions(string tenantId, CancellationToken ct)
=> new(
headers: _connection.CreateMetadata(tenantId),
deadline: DateTime.UtcNow.Add(TimeSpan.FromSeconds(RpcDeadlineSeconds)),
cancellationToken: ct);
- // секреты/тела ответов не логируются и в текст не попадают).
- // exception: Исключение RPC-вызова.
- // Возвращает: Текст ошибки.
private static string ErrorText(RpcException exception)
{
string detail = exception.Status.Detail?.Trim() ?? string.Empty;
return detail.Length > 0 ? detail : ServiceUnavailableText;
}
- // text: Строка.
- // max: Лимит.
- // Возвращает: Усечённая строка.
private static string SliceCodePoints(string text, int max)
{
if (text.Length <= max)
diff --git a/src/core/Deal.Infrastructure/Integrations/Services/GrpcMlClient.cs b/src/core/Deal.Infrastructure/Integrations/Services/GrpcMlClient.cs
index e85bc9a..020725d 100644
--- a/src/core/Deal.Infrastructure/Integrations/Services/GrpcMlClient.cs
+++ b/src/core/Deal.Infrastructure/Integrations/Services/GrpcMlClient.cs
@@ -133,7 +133,6 @@ public sealed class GrpcMlClient : IMlClient, IMlTrainClient
new PredictRequest { Text = text ?? string.Empty },
CallOptions(tenantId.Value, TimeSpan.FromSeconds(PredictDeadlineSeconds), ct));
- // бюджет/lifetime AI-счётчик не затрагиваются (локальная модель бесплатна).
await _usageRecorder.AddEstimatedAsync(text, TokenUsageSources.Local, TokenUsageSources.Ml, ct);
return MapPredict(reply);
}
@@ -246,8 +245,6 @@ public sealed class GrpcMlClient : IMlClient, IMlTrainClient
}
}
- // reply: Ответ ml-service.
- // Возвращает: DTO статуса модели.
private static MlServiceStatusDto MapStatus(StatusReply reply)
{
return new MlServiceStatusDto(
@@ -260,8 +257,6 @@ public sealed class GrpcMlClient : IMlClient, IMlTrainClient
Accuracy: reply.Eval?.Accuracy ?? 0.0));
}
- // reply: Ответ ml-service.
- // Возвращает: DTO предсказания.
private static MlPredictResultDto MapPredict(PredictReply reply)
{
return new MlPredictResultDto(
@@ -289,10 +284,6 @@ public sealed class GrpcMlClient : IMlClient, IMlTrainClient
Margin: decision.Margin);
}
- // tenantId: Id тенанта (формат N).
- // deadline: Лимит времени вызова.
- // ct: Токен отмены вызова.
- // Возвращает: Опции вызова с заголовками, deadline и отменой.
private CallOptions CallOptions(
string tenantId,
TimeSpan deadline,
@@ -327,8 +318,6 @@ public sealed class GrpcMlClient : IMlClient, IMlTrainClient
Learned: 0,
Eval: new MlEvalDto(Count: 0, Correct: 0, Accuracy: 0.0));
- // ct: Токен отмены.
- // Возвращает: True, если ключ отсутствует, повреждён или хранит JSON-true.
private async Task ReadMlEnabledAsync(CancellationToken ct)
{
SettingValue? row = await _store.GetAsync(SettingsKeys.MlEnabled, ct);
diff --git a/src/core/Deal.Infrastructure/Integrations/Services/GrpcTelegramClient.cs b/src/core/Deal.Infrastructure/Integrations/Services/GrpcTelegramClient.cs
index a76c4eb..a5a7de3 100644
--- a/src/core/Deal.Infrastructure/Integrations/Services/GrpcTelegramClient.cs
+++ b/src/core/Deal.Infrastructure/Integrations/Services/GrpcTelegramClient.cs
@@ -398,11 +398,6 @@ public sealed class GrpcTelegramClient : ITelegramGateway
?? throw new InvalidOperationException(
"GrpcTelegramClient запрошен вне tenant-контекста (ITenantContext.TenantId == null).");
- // TReply: Тип ответа RPC.
- // tenantId: Id тенанта (формат N).
- // ct: Токен отмены вызова.
- // call: Вызов клиента (принимает клиент и CallOptions).
- // Возвращает: Ответ RPC.
private async Task CallAsync(
TenantId tenantId,
TimeSpan deadline,
@@ -418,13 +413,6 @@ public sealed class GrpcTelegramClient : ITelegramGateway
return await call(client, options);
}
- // Нормализует транспортные сбои в RpcException «Telegram не подключён»; RpcException домена — как есть.
- // Отмена по токену вызывающего пробрасывается без нормализации (не сбой сервиса). Доменные
- // RPC-ошибки (INVALID_ARGUMENT/FAILED_PRECONDITION/…) несут канонический detail — их трогать нельзя:
- // exception: Исключение вызова.
- // tenantId: Id тенанта (лог).
- // operation: Имя RPC (лог-аудит).
- // Возвращает: Исключение для проброса: транспортный сбой — нормализованный RpcException.
private Exception TranslateTransportFailure(
Exception exception,
TenantId tenantId,
@@ -436,8 +424,6 @@ public sealed class GrpcTelegramClient : ITelegramGateway
return exception;
}
- // Доменная RPC-ошибка сервиса (INVALID_ARGUMENT/FAILED_PRECONDITION/NOT_FOUND…) несёт канонический
- // Unavailable с detail (сервис сам ответил причиной) — тоже как есть.
if (exception is RpcException rpc &&
(rpc.StatusCode != StatusCode.Unavailable || !string.IsNullOrEmpty(rpc.Status.Detail)))
{
@@ -448,8 +434,6 @@ public sealed class GrpcTelegramClient : ITelegramGateway
return new RpcException(new Status(StatusCode.Unavailable, NotConnectedDetail));
}
- // entries: Записи каталога telegram-service.
- // Возвращает: Записи в форме контракта (username → handle).
private static IReadOnlyList MapEntries(Google.Protobuf.Collections.RepeatedField entries)
{
return entries
diff --git a/src/core/Deal.Infrastructure/Integrations/Services/LocalColumnSuggester.cs b/src/core/Deal.Infrastructure/Integrations/Services/LocalColumnSuggester.cs
index fa9befb..737a7d5 100644
--- a/src/core/Deal.Infrastructure/Integrations/Services/LocalColumnSuggester.cs
+++ b/src/core/Deal.Infrastructure/Integrations/Services/LocalColumnSuggester.cs
@@ -84,7 +84,6 @@ ContainersService containersService) : IColumnSuggester
///
public async Task SuggestKeywordsAsync(CancellationToken ct)
{
- // свежие 40 (ListCardsAsync(null) = «все, кроме taken», ORDER BY received_at DESC).
IReadOnlyList cards = await store.ListCardsAsync(new CardsQuery(null), ct);
List texts = cards
.Where(card => card.Col != CardIds.Trash
@@ -107,12 +106,6 @@ ContainersService containersService) : IColumnSuggester
return new SuggestKeywordsResultDto(Ok: true, Keywords: keywords, Reason: null);
}
- // inbox: Снимок «Неразобранного» (карточки планов берутся из него).
- // plans: Планы колонок (SuggestHeuristics.PlanColumns, ≤4).
- // ct: Токен отмены.
- // Возвращает: Сколько досок реально создано (0 — все откатаны из-за разобранных карточек).
- // Каждая доска — suggested=true c правилами {mode:"any", keywords:[тема]} и note-обоснованием.
- // Перед раскладкой перечитывается «Неразобранное»: карточки, ушедшие из inbox между снимком и
private async Task StoreSuggestedColumnsAsync(
IReadOnlyList inbox,
IReadOnlyList plans,
@@ -176,9 +169,6 @@ ContainersService containersService) : IColumnSuggester
return created;
}
- // Сработал ли кулдаун: с последнего успешного предложения прошло меньше 20 минут.
- // ct: Токен отмены.
- // Возвращает: True — повторный вызов слишком рано (ответ {ok:false, reason, cooldown:true}).
private async Task WithinCooldownAsync(CancellationToken ct)
{
SettingValue? row = await settings.GetAsync(SettingsKeys.LastSuggestAt, ct);
@@ -204,7 +194,6 @@ ContainersService containersService) : IColumnSuggester
}
}
- // ct: Токен отмены.
private Task WriteLastSuggestAtAsync(CancellationToken ct) =>
settings.SetAsync(
SettingsKeys.LastSuggestAt,
diff --git a/src/core/Deal.Infrastructure/Integrations/Services/LocalMlClient.cs b/src/core/Deal.Infrastructure/Integrations/Services/LocalMlClient.cs
index 6ef2d57..cd0ba7d 100644
--- a/src/core/Deal.Infrastructure/Integrations/Services/LocalMlClient.cs
+++ b/src/core/Deal.Infrastructure/Integrations/Services/LocalMlClient.cs
@@ -75,12 +75,9 @@ public sealed class LocalMlClient(ISettingsStore store, IMlLearningStore learnin
double delta,
CancellationToken ct)
{
- // пустые — тихий no-op, text[:6000], id mle_+hex) — в MlOutboxQueue, общем для Local/Grpc-адаптеров.
await MlOutboxQueue.PushAsync(learningStore, text, label, delta, ct);
}
- // ct: Токен отмены.
- // Возвращает: True, если ключ отсутствует, повреждён или хранит JSON-true.
private async Task ReadMlEnabledAsync(CancellationToken ct)
{
SettingValue? row = await store.GetAsync(SettingsKeys.MlEnabled, ct);
diff --git a/src/core/Deal.Infrastructure/Integrations/Services/MlOutboxQueue.cs b/src/core/Deal.Infrastructure/Integrations/Services/MlOutboxQueue.cs
index 4ffa670..0369c00 100644
--- a/src/core/Deal.Infrastructure/Integrations/Services/MlOutboxQueue.cs
+++ b/src/core/Deal.Infrastructure/Integrations/Services/MlOutboxQueue.cs
@@ -4,8 +4,6 @@ using Deal.Modules.Kanban.Application.Models;
namespace Deal.Infrastructure.Integrations.Services;
-// тихий no-op, text обрезается до 6000 символов (без разрыва суррогатной пары), id — mle_ +
-// и в Local-, и в gRPC-режиме сигнал сначала пишется в outbox, отправку в ml-service делает фоновый
internal static class MlOutboxQueue
{
internal const int MaxLearningTextLength = 6000;
@@ -42,14 +40,9 @@ internal static class MlOutboxQueue
ct);
}
- // Возвращает: Короткий id записи очереди.
private static string NewOutboxId()
=> KanbanIdPrefixes.MlOutbox + Convert.ToHexString(RandomNumberGenerator.GetBytes(OutboxIdRandomBytes)).ToLowerInvariant();
- // Обрезает текст до MaxLearningTextLength символов, не разбивая суррогатную пару на конце.
- // text: Текст (уже trim-нут).
- // Возвращает: Первые 6000 символов (или весь текст, если короче).
- // (text[:6000]) режет по code points — хвостовой high-surrogate убираем, чтобы в БД не ушла «битая» пара.
private static string TruncateText(string text)
{
if (text.Length <= MaxLearningTextLength)
diff --git a/src/core/Deal.Infrastructure/Integrations/Services/TokenUsageRecorder.cs b/src/core/Deal.Infrastructure/Integrations/Services/TokenUsageRecorder.cs
index 5033fa0..9defcba 100644
--- a/src/core/Deal.Infrastructure/Integrations/Services/TokenUsageRecorder.cs
+++ b/src/core/Deal.Infrastructure/Integrations/Services/TokenUsageRecorder.cs
@@ -76,7 +76,6 @@ public sealed class TokenUsageRecorder
}
await AddToLifetimeAsync(usage, ct);
- // token_usage_events (без tenantId в метках).
DealMetrics.RecordAiUsage(usage.Prompt, usage.Completion);
await RecordEventAsync(
provider,
@@ -102,7 +101,6 @@ public sealed class TokenUsageRecorder
CancellationToken ct)
{
long promptTokens = EstimateTokens(text);
- // что и событие token_usage_events, kind=ml).
DealMetrics.RecordMlUsage(promptTokens);
await RecordEventAsync(
provider,
@@ -170,8 +168,6 @@ public sealed class TokenUsageRecorder
return id;
}
- // usage: Оценка токенов ответа.
- // ct: Токен отмены.
private async Task AddToLifetimeAsync(Usage usage, CancellationToken ct)
{
JsonObject? current = await ReadAsync(ct);
diff --git a/src/core/Deal.Infrastructure/Integrations/Storage/Services/FileStorageRegistrar.cs b/src/core/Deal.Infrastructure/Integrations/Storage/Services/FileStorageRegistrar.cs
index 9f57e1c..8c318bc 100644
--- a/src/core/Deal.Infrastructure/Integrations/Storage/Services/FileStorageRegistrar.cs
+++ b/src/core/Deal.Infrastructure/Integrations/Storage/Services/FileStorageRegistrar.cs
@@ -38,10 +38,8 @@ public static class FileStorageRegistrar
///
/// Регистрирует IFileStorage — LocalFileStorage или MinioFileStorage по конфигурации.
///
- /// Коллекция сервисов.
/// Конфигурация приложения (секция Storage + env-алиасы DEAL_MINIO_*).
/// ContentRoot приложения — база для дефолтного корня data/attachments.
- /// Коллекция сервисов для цепочки вызовов.
public static IServiceCollection AddDealFileStorage(
this IServiceCollection services,
IConfiguration configuration,
@@ -51,7 +49,6 @@ public static class FileStorageRegistrar
StorageOptions options = ReadOptions(configuration);
- // недоступен» — dev/curl/unit по умолчанию работают на LocalFileStorage без MinIO).
if (options.Minio.IsConfigured())
{
services.AddSingleton(serviceProvider =>
diff --git a/src/core/Deal.Infrastructure/Integrations/Storage/Services/LocalFileStorage.cs b/src/core/Deal.Infrastructure/Integrations/Storage/Services/LocalFileStorage.cs
index b5e9afc..3a75e94 100644
--- a/src/core/Deal.Infrastructure/Integrations/Storage/Services/LocalFileStorage.cs
+++ b/src/core/Deal.Infrastructure/Integrations/Storage/Services/LocalFileStorage.cs
@@ -40,8 +40,6 @@ public sealed class LocalFileStorage : IFileStorage
{
ArgumentNullException.ThrowIfNull(content);
- // может быть прочитан эндпоинтом раньше; перемотаемые потоки сбрасываем (неперемотаемые читаются
- // с текущей позиции, как есть). Выравнивание с Minio-адаптером PutAsync.
if (content.CanSeek && content.Position != 0)
{
content.Position = 0;
@@ -95,9 +93,6 @@ public sealed class LocalFileStorage : IFileStorage
return Task.CompletedTask;
}
- // objectKey: Ключ объекта (сегменты по '/', без «.»/«..»).
- // Возвращает: Полный путь файла под root.
- // Исключение ArgumentException: objectKey пуст либо содержит обходные сегменты «.»/«..».
private string ResolvePath(string objectKey)
{
if (string.IsNullOrWhiteSpace(objectKey))
diff --git a/src/core/Deal.Infrastructure/Integrations/Storage/Services/MinioFileStorage.cs b/src/core/Deal.Infrastructure/Integrations/Storage/Services/MinioFileStorage.cs
index 6730772..54a3254 100644
--- a/src/core/Deal.Infrastructure/Integrations/Storage/Services/MinioFileStorage.cs
+++ b/src/core/Deal.Infrastructure/Integrations/Storage/Services/MinioFileStorage.cs
@@ -77,7 +77,6 @@ public sealed class MinioFileStorage : IFileStorage
await EnsureBucketAsync(ct);
- // (Content-Length), поэтому поток буферизуется — Local-адаптер буферизации не требует.
if (content.CanSeek && content.Position != 0)
{
content.Position = 0;
@@ -134,7 +133,6 @@ public sealed class MinioFileStorage : IFileStorage
{
try
{
- // → null-семантика порта). Иные ошибки (MinIO недоступен) уходят вызывающему — он мапит их в 404.
ObjectStat stat = await _client.StatObjectAsync(
new StatObjectArgs().WithBucket(_bucket).WithObject(objectKey),
ct);
@@ -167,7 +165,6 @@ public sealed class MinioFileStorage : IFileStorage
}
}
- // ct: Токен отмены.
private async Task EnsureBucketAsync(CancellationToken ct)
{
if (_bucketChecked)
@@ -193,7 +190,6 @@ public sealed class MinioFileStorage : IFileStorage
}
catch (MinioException exception)
{
- // Бакет не проверить/создать (MinIO недоступен и т.п.): put продолжит и упадёт с понятной
_logger.LogWarning(
exception,
"Не удалось проверить/создать бакет MinIO «{Bucket}»: {Message}",
diff --git a/src/core/Deal.Infrastructure/Persistence/Configurations/CardConfiguration.cs b/src/core/Deal.Infrastructure/Persistence/Configurations/CardConfiguration.cs
index fa572d5..d03e178 100644
--- a/src/core/Deal.Infrastructure/Persistence/Configurations/CardConfiguration.cs
+++ b/src/core/Deal.Infrastructure/Persistence/Configurations/CardConfiguration.cs
@@ -36,7 +36,6 @@ public sealed class CardConfiguration : IEntityTypeConfiguration
// Сортировка пространства «Выбранные» — updated_at DESC.
builder.HasIndex(x => x.UpdatedAt).IsDescending();
- // Полнотекстовый вектор карточки (russian): Title+Summary+SourceMsg+Contact — вычисляемая STORED-
builder.Property(x => x.SearchTsv)
.HasComputedColumnSql(
"to_tsvector('russian', coalesce(\"Title\",'')||' '||coalesce(\"Summary\",'')||' '||coalesce(\"SourceMsg\",'')||' '||coalesce(\"Contact\",''))",
diff --git a/src/core/Deal.Infrastructure/Persistence/Repositories/AuditLogStore.cs b/src/core/Deal.Infrastructure/Persistence/Repositories/AuditLogStore.cs
index 5c4d912..00df77d 100644
--- a/src/core/Deal.Infrastructure/Persistence/Repositories/AuditLogStore.cs
+++ b/src/core/Deal.Infrastructure/Persistence/Repositories/AuditLogStore.cs
@@ -107,8 +107,6 @@ public sealed class AuditLogStore(DealDbContext dbContext) : IAuditLogStore
return query;
}
- // limit: Запрошенный размер.
- // Возвращает: Клампированное значение.
private static int ClampLimit(int limit) => Math.Max(1, Math.Min(AuditService.MaxQueryLimit, limit));
private static AuditRecordDto ToAuditRecordDto(AuditLogEntity entity) =>
diff --git a/src/core/Deal.Infrastructure/Persistence/Repositories/InviteStore.cs b/src/core/Deal.Infrastructure/Persistence/Repositories/InviteStore.cs
index 1ab9003..d3857a3 100644
--- a/src/core/Deal.Infrastructure/Persistence/Repositories/InviteStore.cs
+++ b/src/core/Deal.Infrastructure/Persistence/Repositories/InviteStore.cs
@@ -72,7 +72,6 @@ public sealed class InviteStore(DealDbContext dbContext) : IInviteStore
DateTimeOffset activatedAt,
CancellationToken ct)
{
- // иначе параллельный отзыв/активация не перезаписываются (ExecuteUpdate выполняется одним оператором SQL).
int affected = await dbContext.Invites
.Where(i => i.Code == code && i.Status == InviteStatuses.Pending)
.ExecuteUpdateAsync(s => s
diff --git a/src/core/Deal.Infrastructure/Persistence/Repositories/KanbanStore.Cards.cs b/src/core/Deal.Infrastructure/Persistence/Repositories/KanbanStore.Cards.cs
index 9dde964..e8e66d0 100644
--- a/src/core/Deal.Infrastructure/Persistence/Repositories/KanbanStore.Cards.cs
+++ b/src/core/Deal.Infrastructure/Persistence/Repositories/KanbanStore.Cards.cs
@@ -15,7 +15,6 @@ public sealed partial class KanbanStore
IQueryable queryable = _dbContext.Cards.AsNoTracking();
if (query.Col is null)
{
- // в работу, живёт в том же пространстве «Выбранных», а не на дашборде.
queryable = queryable.Where(card => !SelectedStageIds.Contains(card.Col));
}
else
@@ -41,9 +40,6 @@ public sealed partial class KanbanStore
return Array.Empty();
}
- // исключены, порядок — ts_rank DESC, ReceivedAt DESC, limit 12. plainto_tsquery со стоп-словами даёт
- // пустой tsquery — совпадений нет (не ошибка), LIKE ниже всё равно отработает. Всё параметризовано
- // (никакой конкатенации пользовательского ввода); LIKE-паттерн — как в отсев-поиске PipelineStore.
string pattern = $"%{query}%";
List entities = await _dbContext.Cards
.FromSqlInterpolated(
@@ -133,7 +129,6 @@ public sealed partial class KanbanStore
///
public async Task UpdateColumnAsync(CardColumnUpdateDto update, CancellationToken ct)
{
- // через ExecuteUpdate потребовало бы двух запросов, чтение со слежением проще и точнее.
CardEntity? entity = await _dbContext.Cards.SingleOrDefaultAsync(card => card.Id == update.CardId, ct);
if (entity is null)
{
@@ -155,7 +150,6 @@ public sealed partial class KanbanStore
///
public async Task ApplyReclassificationAsync(CardReclassificationDto update, CancellationToken ct)
{
- // валюты и отсутствие конверсии очищают поля (BudgetCur/ConvCur = ""), PrevCol/ArchivedAt не трогаются.
CardEntity? entity = await _dbContext.Cards.SingleOrDefaultAsync(card => card.Id == update.CardId, ct);
if (entity is null)
{
@@ -205,7 +199,6 @@ public sealed partial class KanbanStore
///
public async Task DeleteForeverAsync(string cardId, CancellationToken ct)
{
- // «сирота» заблокирует повторное создание карточки при перечитывании канала.
await using var transaction = await _dbContext.Database.BeginTransactionAsync(ct);
await _dbContext.DedupEntries
.Where(entry => entry.LeadId == cardId)
@@ -219,7 +212,6 @@ public sealed partial class KanbanStore
///
public async Task ClearColAsync(string col, CancellationToken ct)
{
- // корзины/архива тоже должна снимать «сирот», иначе текст из удалённой карточки не пройдёт дедуп.
await using var transaction = await _dbContext.Database.BeginTransactionAsync(ct);
await _dbContext.DedupEntries
.Where(entry => _dbContext.Cards.Any(card => card.Col == col && card.Id == entry.LeadId))
diff --git a/src/core/Deal.Infrastructure/Persistence/Repositories/KanbanStore.Comments.cs b/src/core/Deal.Infrastructure/Persistence/Repositories/KanbanStore.Comments.cs
index 0bca7ea..34179d6 100644
--- a/src/core/Deal.Infrastructure/Persistence/Repositories/KanbanStore.Comments.cs
+++ b/src/core/Deal.Infrastructure/Persistence/Repositories/KanbanStore.Comments.cs
@@ -30,8 +30,6 @@ public sealed partial class KanbanStore
string text,
CancellationToken ct)
{
- // комментарий без карточки не запишется (404-семантику несуществующей карточки отдаёт сервис,
- // прочитав карточку перед добавлением, — адаптеру возвращать нечего: порт void).
_dbContext.LeadComments.Add(new LeadCommentEntity
{
Id = commentId,
@@ -64,7 +62,6 @@ public sealed partial class KanbanStore
///
public async Task> GetAiMarkupExamplesAsync(int limit, CancellationToken ct)
{
- // действия move/restore, цель не служебная (trash/archive), source_msg непустой, свежие первыми.
return await _dbContext.CardMoves
.AsNoTracking()
.Where(move => move.ToCol != null && move.ToCol != CardIds.Trash && move.ToCol != CardIds.Archive)
diff --git a/src/core/Deal.Infrastructure/Persistence/Repositories/KanbanStore.Selected.cs b/src/core/Deal.Infrastructure/Persistence/Repositories/KanbanStore.Selected.cs
index 3a1d8eb..cc74042 100644
--- a/src/core/Deal.Infrastructure/Persistence/Repositories/KanbanStore.Selected.cs
+++ b/src/core/Deal.Infrastructure/Persistence/Repositories/KanbanStore.Selected.cs
@@ -34,7 +34,6 @@ public sealed partial class KanbanStore
CardPatch patch,
CancellationToken ct)
{
- // Точечная правка по присутствующим полям патча: null-поле не меняется, JSON-поля заменяются
CardEntity? entity = await _dbContext.Cards
.SingleOrDefaultAsync(card => card.Id == cardId, ct);
if (entity is null)
@@ -152,7 +151,6 @@ public sealed partial class KanbanStore
long atMs,
CancellationToken ct)
{
- // Смена контейнера-стадии: Col=containerId, сброс напоминания, updated_at=atMs, история + запись
CardEntity? entity = await _dbContext.Cards
.AsNoTracking()
.SingleOrDefaultAsync(card => card.Id == cardId, ct);
@@ -206,7 +204,6 @@ public sealed partial class KanbanStore
///
public async Task ClearStageAsync(string containerId, CancellationToken ct)
{
- // снимает каскад FK LeadComments → Cards.
return await _dbContext.Cards
.Where(card => card.Col == containerId)
.ExecuteDeleteAsync(ct);
diff --git a/src/core/Deal.Infrastructure/Persistence/Repositories/KanbanStore.StorageRules.cs b/src/core/Deal.Infrastructure/Persistence/Repositories/KanbanStore.StorageRules.cs
index a2b182d..71851ba 100644
--- a/src/core/Deal.Infrastructure/Persistence/Repositories/KanbanStore.StorageRules.cs
+++ b/src/core/Deal.Infrastructure/Persistence/Repositories/KanbanStore.StorageRules.cs
@@ -13,7 +13,6 @@ public sealed partial class KanbanStore
///
public async Task> ListArchiveCandidatesAsync(DateTimeOffset receivedBeforeUtc, CancellationToken ct)
{
- // Кандидаты автоархива: карточки пользовательских колонок (kind=board) и «Неразобранного»
List boardIds = await _dbContext.Containers
.AsNoTracking()
.Where(container => container.Kind == ContainerKinds.Board)
@@ -34,7 +33,6 @@ public sealed partial class KanbanStore
DateTimeOffset archivedAt,
CancellationToken ct)
{
- // UpdateColumnAsync автоархива: col=archive, is_new=false, archived_at=now, matchHits пусто;
if (cardIds.Count == 0)
{
return 0;
@@ -94,7 +92,6 @@ public sealed partial class KanbanStore
///
public async Task> ListCardsForConversionAsync(CancellationToken ct)
{
- // пересчёт читает только бюджетные/conv-поля, комментарии не нужны.
List entities = await _dbContext.Cards
.AsNoTracking()
.Where(card => card.BudgetCur != string.Empty
diff --git a/src/core/Deal.Infrastructure/Persistence/Repositories/KanbanStore.cs b/src/core/Deal.Infrastructure/Persistence/Repositories/KanbanStore.cs
index de6fcfa..dc99096 100644
--- a/src/core/Deal.Infrastructure/Persistence/Repositories/KanbanStore.cs
+++ b/src/core/Deal.Infrastructure/Persistence/Repositories/KanbanStore.cs
@@ -204,7 +204,6 @@ public sealed partial class KanbanStore : ICardStore
private static CardCommentDto ToCommentDto(LeadCommentEntity entity) =>
new(entity.Id, entity.By, entity.Text, HumanAge(entity.CreatedAt));
- // timestamp: Время события (получения сообщения/добавления комментария).
private static string HumanAge(DateTimeOffset timestamp)
{
long deltaMs = Math.Max(0, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - timestamp.ToUnixTimeMilliseconds());
diff --git a/src/core/Deal.Infrastructure/Persistence/Repositories/PipelineStore.cs b/src/core/Deal.Infrastructure/Persistence/Repositories/PipelineStore.cs
index c6e95ee..b6f11f3 100644
--- a/src/core/Deal.Infrastructure/Persistence/Repositories/PipelineStore.cs
+++ b/src/core/Deal.Infrastructure/Persistence/Repositories/PipelineStore.cs
@@ -90,8 +90,6 @@ public sealed class PipelineStore(TenantDbContext dbContext) : IPipelineStore
string id = record.DeterministicId ?? PrefixId.New(PipelineIdPrefixes.Rejected);
DateTimeOffset rejectedAt = DateTimeOffset.UtcNow;
- // Upsert по детерминированному id: повторное отбрасывание того же сообщения (dialog+msgId)
- // аудит возврата (returned/returnedAt/returnReason) при повторном отсеве переживает.
await dbContext.Database.ExecuteSqlInterpolatedAsync(
$"""
INSERT INTO "RejectedItems" ("Id", "DialogId", "MsgId", "ChannelName", "ChannelHandle", "ChannelHue", "Text", "Stage", "Reason", "Kw", "Source", "MsgAt", "RejectedAt", "Returned", "ReturnReason")
@@ -139,7 +137,6 @@ public sealed class PipelineStore(TenantDbContext dbContext) : IPipelineStore
return Array.Empty();
}
- // plainto_tsquery со стоп-словами даёт пустой tsquery — совпадений нет (не ошибка), LIKE ниже всё
List fts = await dbContext.RejectedItems
.FromSqlInterpolated(
$"""
@@ -154,7 +151,6 @@ public sealed class PipelineStore(TenantDbContext dbContext) : IPipelineStore
var rows = fts.Select(ToRejectedItemDto).ToList();
var seen = new HashSet(fts.Select(item => item.Id), StringComparer.Ordinal);
- // последнего перестроения вектора и слова, которых нет в словаре морфологии. limitLike — как
string pattern = $"%{query}%";
List like = await dbContext.RejectedItems
.AsNoTracking()
@@ -236,8 +232,6 @@ public sealed class PipelineStore(TenantDbContext dbContext) : IPipelineStore
///
public async Task ClaimAsync(string hash, CancellationToken ct)
{
- // NOTHING — параллельные дубли одного текста не проходят (сырой SQL: EF-вставка упала бы на PK).
- // Число вставленных строк — результат: 1 = заявка за этим вызовом, 0 = хэш уже заявлен другим
int inserted = await dbContext.Database.ExecuteSqlInterpolatedAsync(
$"""
INSERT INTO "DedupEntries" ("Hash", "LeadId", "CreatedAt")
@@ -251,7 +245,6 @@ public sealed class PipelineStore(TenantDbContext dbContext) : IPipelineStore
///
public async Task DeleteClaimAsync(string hash, CancellationToken ct)
{
- // (LeadId IS NOT NULL), не трогаем — иначе повторный текст прошёл бы дедуп после отсева.
await dbContext.DedupEntries
.Where(entry => entry.Hash == hash && entry.LeadId == null)
.ExecuteDeleteAsync(ct);
@@ -271,8 +264,6 @@ public sealed class PipelineStore(TenantDbContext dbContext) : IPipelineStore
///
public async Task DeleteByCardAsync(string cardId, CancellationToken ct)
{
- // дублируется тем же DELETE (см. KanbanStore.DeleteForeverAsync/PurgeAsync/ClearColAsync) —
- // чтобы Kanban-адаптер не зависел от порта Pipeline; этот метод — для потребителей модуля.
await dbContext.DedupEntries
.Where(entry => entry.LeadId == cardId)
.ExecuteDeleteAsync(ct);
diff --git a/src/core/Deal.Infrastructure/Persistence/Repositories/TelegramStore.cs b/src/core/Deal.Infrastructure/Persistence/Repositories/TelegramStore.cs
index b5ec200..617088f 100644
--- a/src/core/Deal.Infrastructure/Persistence/Repositories/TelegramStore.cs
+++ b/src/core/Deal.Infrastructure/Persistence/Repositories/TelegramStore.cs
@@ -146,8 +146,6 @@ public sealed class TelegramStore(TenantDbContext dbContext) : ITelegramStore
string hue,
CancellationToken ct)
{
- // discovery-вступления — метаданные источника, monitor=TRUE, backfilled=FALSE (разбор подхватит
- // первый фоновый Backfill). Значения нормализованы вызывающим (DialogsService).
DialogEntity? existing = await dbContext.Dialogs.FirstOrDefaultAsync(dialog => dialog.Id == dialogId, ct);
DateTimeOffset now = DateTimeOffset.UtcNow;
if (existing is null)
@@ -234,8 +232,6 @@ public sealed class TelegramStore(TenantDbContext dbContext) : ITelegramStore
row.Id, row.Text, row.MsgAt.ToUnixTimeMilliseconds(), row.LeadId is not null)).ToList();
}
- // row: Строка Dialogs.
- // Возвращает: Форма §4.8: type = kind (EN-канон), on = monitor, last {text, time}.
private static TelegramDialogDto ToDialogDto(DialogEntity row)
{
long? lastAtMs = row.LastAt?.ToUnixTimeMilliseconds();
diff --git a/src/core/Deal.Infrastructure/Persistence/Repositories/TenantLimitStore.cs b/src/core/Deal.Infrastructure/Persistence/Repositories/TenantLimitStore.cs
index 5768446..7da6eeb 100644
--- a/src/core/Deal.Infrastructure/Persistence/Repositories/TenantLimitStore.cs
+++ b/src/core/Deal.Infrastructure/Persistence/Repositories/TenantLimitStore.cs
@@ -164,10 +164,6 @@ public sealed class TenantLimitStore : ITenantLimitStore
return true;
}
- // tenantId: Тенант.
- // defaults: Дефолт-параметры создаваемой строки.
- // ct: Токен отмены.
- // Возвращает: Отслеживаемая строка лимита.
private async Task LoadOrCreateAsync(
Guid tenantId,
TokenLimitDefaults defaults,
@@ -196,8 +192,6 @@ public sealed class TenantLimitStore : ITenantLimitStore
return created;
}
- // entity: Отслеживаемая строка лимита.
- // ct: Токен отмены.
private async Task ResetIfPeriodExpiredAsync(TenantLimitEntity entity, CancellationToken ct)
{
if (ResetIfExpired(entity, _utcNow()))
@@ -249,12 +243,8 @@ public sealed class TenantLimitStore : ITenantLimitStore
return reset;
}
- // entity: Строка лимита (после ленивого reset).
- // ct: Токен отмены.
- // Возвращает: Состояние бюджета тенанта.
private async Task ToStateDtoAsync(TenantLimitEntity entity, CancellationToken ct)
{
- // Статус тенанта — из public.tenants тем же контекстом; отсутствие строки трактуем suspended
string? tenantStatus = await _dbContext.Tenants
.AsNoTracking()
.Where(t => t.Id == entity.TenantId)
diff --git a/src/core/Deal.Infrastructure/ServiceCollectionExtensions.cs b/src/core/Deal.Infrastructure/ServiceCollectionExtensions.cs
index 475c2d4..831daeb 100644
--- a/src/core/Deal.Infrastructure/ServiceCollectionExtensions.cs
+++ b/src/core/Deal.Infrastructure/ServiceCollectionExtensions.cs
@@ -30,62 +30,44 @@ public static class ServiceCollectionExtensions
///
/// Регистрирует EF-адаптеры портов модулей
///
- /// Коллекция сервисов.
/// Дефолт-бюджет лениво создаваемых строк tenant_limits. Передаётся из конфигурации/env DEAL_DEFAULT_AI_BUDGET в Program.cs.
- /// Коллекция сервисов для цепочки вызовов.
public static IServiceCollection AddDealPersistence(this IServiceCollection services, TokenLimitDefaults? tenantLimitDefaults = null)
{
services.AddScoped();
- // схемы public — отдельный порт от IAuthStore (оператор ≠ пользователь тенанта).
services.AddScoped();
services.AddScoped();
services.AddScoped();
- // + агрегаты для аналитики; порт без Update/Delete.
services.AddScoped();
services.AddScoped();
- // дефолт-бюджет из конфигурации (Program.cs) — ленивый GetOrCreate новой строки использует его;
- // DealDbContext уже зарегистрирован в Api (AddDbContext до AddDealPersistence).
services.AddScoped(provider => new TenantLimitStore(
provider.GetRequiredService(),
tenantLimitDefaults ?? TokenBudgetDefaults.Default));
- // общее хранилище распределённого rate limiting (auth/api/gRPC) и учёта попыток входа. Scoped
- // (DealDbContext); лимитер резолвит его в собственном scope на каждое приобретение.
services.AddScoped();
- // контекст — scoped TenantDbContext запроса (см. AddDbContext в Deal.Api).
services.AddScoped();
- // KV-хранилище глобальных (системных) настроек оператора (ТЗ §4.1/§8.1): таблица
- // public.global_settings, контекст — системный DealDbContext (public-схема). Ключи Telegram
- // задаёт оператор, ядро читает их для команд входа (TelegramKeysService).
services.AddScoped();
services.AddScoped();
- // KanbanStore при жёстком удалении карточки чистит DedupEntries напрямую тем же TenantDbContext
services.AddScoped();
services.AddScoped();
services.AddScoped();
- // тенанта. Проверки «уже мониторится» читают таблицу Dialogs (владелец — Telegram) тем же TenantDbContext.
services.AddScoped();
- // ConnectionStringProvider — он регистрируется в Deal.Api (Program.cs) как singleton.
services.AddScoped();
- // параллелизм + логирование прогресса поверх ITenantRepository и ITenantProvisioner.
services.AddScoped();
- // Выбранных vs дашборд-контейнер» живёт в адаптере, а не в эндпоинте. Scoped —
- // композирует scoped-сервис карточек в рамках tenant-запроса.
services.AddScoped();
return services;
}
@@ -93,11 +75,9 @@ public static class ServiceCollectionExtensions
///
/// Регистрирует адаптеры внешних интеграций
///
- /// Коллекция сервисов.
/// Конфигурация секции Services:Ml — выбор реализации IMlClient.
/// Конфигурация секции Services:Ai — выбор реализации IAiClassifier/IAiTools.
/// Конфигурация секции Services:Telegram — выбор реализации ITelegramGateway.
- /// Коллекция сервисов для цепочки вызовов.
public static IServiceCollection AddDealIntegrations(
this IServiceCollection services,
MlServiceOptions mlOptions,
@@ -105,9 +85,6 @@ public static class ServiceCollectionExtensions
TelegramServiceOptions telegramOptions,
MtlsCertificates? mtlsCertificates = null)
{
- // остаются plaintext + service-token (dev); при включённом флаге каждый транспорт подписывает запрос
- // клиентским сертификатом и проверяет CA сервера (fail-fast на загрузку — в MtlsCertificates.Load).
- // событий. Регистрируется независимо от AI-режима — им пользуется и gRPC ML-клиент (событие kind=ml).
services.AddScoped();
if (mlOptions.UseLocal)
@@ -116,8 +93,6 @@ public static class ServiceCollectionExtensions
}
else
{
- // gRPC-клиент ml-service: транспорт создаётся сразу (fail-fast: пустой endpoint/токен останавливают
- // KV/таблицы тенанта (как LocalMlClient) и ходит в сервис по metadata tenant-id/service-token.
services.AddSingleton(new MlGrpcConnection(mlOptions, mtlsCertificates));
services.AddSingleton();
services.AddScoped();
@@ -125,18 +100,8 @@ public static class ServiceCollectionExtensions
services.AddScoped(provider => provider.GetRequiredService());
}
- // LocalColumnSuggester читает карточки через ICardStore, считает группы ядром SuggestHeuristics
- // (модуль Kanban) и создаёт доски suggested=true через ContainersService; scoped — его зависимости
- // gRPC-клиентом ai-service с тем же контрактом.
services.AddScoped();
- // (UseLocal=true) либо gRPC-адаптер ai-service (UseLocal=false). LocalAiClassifier разбирает сообщение
- // ядром LocalFieldsParser модуля Pipeline (маркерная гипотеза типа: is_vacancy_known=false, board=null —
- // «смысловые колонки до ИИ не назначаем») и всегда пропускает ИИ-фильтр {pass:true, skipped:true};
- // scoped — LocalFieldsParser читает маркеры hireMarkers/levelTerms из KV-настроек тенанта (ISettingsStore).
- // GrpcAiClassifier строит промпты/контекст классификации (AiClassifyContextBuilder модуля Pipeline,
- // регистрируется AddPipelineModule), ходит в ai-service с ProviderConfig из настроек (расшифровка apiKey)
- // и копит usage в KV aiTokenUsage; недоступность/ok=false → AiUnavailableException — воркер падает в
if (aiOptions.UseLocal)
{
services.AddScoped();
@@ -144,16 +109,10 @@ public static class ServiceCollectionExtensions
}
else
{
- // gRPC-клиент ai-service: транспорт создаётся сразу (fail-fast: пустой endpoint/токен останавливают
- // в сервис по metadata tenant-id/service-token; usage ответов списывает TokenUsageRecorder
services.AddSingleton(new AiGrpcConnection(aiOptions, mtlsCertificates));
services.AddScoped();
services.AddScoped();
- // исчерпанном бюджете/приостановке уводит вызов на детерминированный бесплатный локальный разбор
- // (LocalAiClassifier; LocalFieldsParser регистрирует AddPipelineModule в Program.cs). Инструменты локального
services.AddScoped();
- // Декораторы бюджетного гейта (порядок Grpc → Budgeted → наружу): перед каждым платным вызовом
- // GetStateAsync (ITenantLimitStore, scoped) — исчерпано/приостановлено → Local-классификатор либо
services.AddScoped(provider => new BudgetedAiClassifier(
provider.GetRequiredService(),
provider.GetRequiredService(),
@@ -168,10 +127,6 @@ public static class ServiceCollectionExtensions
provider.GetRequiredService>()));
}
- // локальная заглушка dev LocalTelegramGateway (нейтральный no-op/idle — сервис не поднят); при
- // UseLocal=false — gRPC-клиент GrpcTelegramClient (telegram.proto) + синглтон-транспорт
- // TelegramGrpcConnection (fail-fast, как MlGrpcConnection). Scoped-адаптер: tenant-id для metadata
- // берёт из ITenantContext (AsyncLocal) scope вызова (HTTP-запрос/tenant-циклы ингресса).
if (telegramOptions.UseLocal)
{
services.AddSingleton();
@@ -189,9 +144,7 @@ public static class ServiceCollectionExtensions
///
/// Регистрирует сервисы шифрования секретов
///
- /// Коллекция сервисов.
/// ContentRoot приложения — каталог по умолчанию для файла-ключа data/encryption.key.
- /// Коллекция сервисов для цепочки вызовов.
public static IServiceCollection AddDealSecurity(this IServiceCollection services, string contentRootPath)
{
EncryptionKeyProvider keyProvider = new(contentRootPath);
diff --git a/src/core/Deal.Infrastructure/Services/CardMover.cs b/src/core/Deal.Infrastructure/Services/CardMover.cs
index c5c1a7b..5739095 100644
--- a/src/core/Deal.Infrastructure/Services/CardMover.cs
+++ b/src/core/Deal.Infrastructure/Services/CardMover.cs
@@ -19,8 +19,6 @@ public sealed class CardMover(CardsService cardsService) : ICardMover
TransitionContext ctx,
CancellationToken ct)
{
- // Контекст перехода (инициатор/обучение) учтён внутри маршрутов: дашборд-перенос обучает ML по
- // цели пользователя, переход по стадии пишет историю и сбрасывает напоминание. Отдельного
CardResultDto result = CardsDefaultContainers.Contains(toContainerId)
? await cardsService.MoveStageCardAsync(cardId, toContainerId, ct)
: await cardsService.MoveDashboardCardAsync(cardId, toContainerId, ct);
diff --git a/src/core/Deal.Modules.Cards/Application/Abstractions/IContainerRules.cs b/src/core/Deal.Modules.Cards/Application/Abstractions/IContainerRules.cs
index ac5abf0..b4c7ebb 100644
--- a/src/core/Deal.Modules.Cards/Application/Abstractions/IContainerRules.cs
+++ b/src/core/Deal.Modules.Cards/Application/Abstractions/IContainerRules.cs
@@ -33,7 +33,7 @@ public interface IContainerRules
public IReadOnlyList Grades { get; }
///
- /// Уровень (отдельная группа §6.3; пусто — не участвует).
+ /// Уровень.
///
public IReadOnlyList Levels { get; }
@@ -53,7 +53,7 @@ public interface IContainerRules
public CardBudget? Budget { get; }
///
- /// Диапазон цены
+ /// Диапазон цены.
///
public CardBudget? Prices { get; }
diff --git a/src/core/Deal.Modules.Cards/Application/Abstractions/ITzCard.cs b/src/core/Deal.Modules.Cards/Application/Abstractions/ITzCard.cs
index 32948fd..ecd8dc6 100644
--- a/src/core/Deal.Modules.Cards/Application/Abstractions/ITzCard.cs
+++ b/src/core/Deal.Modules.Cards/Application/Abstractions/ITzCard.cs
@@ -1,12 +1,12 @@
namespace Deal.Modules.Cards.Application.Abstractions;
///
-/// Модуль «ТЗ»: текст технического задания/заметки-задания по карточке.
+/// Модуль « »: текст технического задания/заметки-задания по карточке.
///
public interface ITzCard
{
///
- /// Текст ТЗ (пусто — ТЗ нет).
+ /// Текст.
///
public string TzText { get; }
}
diff --git a/src/core/Deal.Modules.Cards/Application/Models/Card.cs b/src/core/Deal.Modules.Cards/Application/Models/Card.cs
index 33881ea..6ed171e 100644
--- a/src/core/Deal.Modules.Cards/Application/Models/Card.cs
+++ b/src/core/Deal.Modules.Cards/Application/Models/Card.cs
@@ -66,7 +66,6 @@ public sealed class Card :
///
public IReadOnlyList Files { get; init; } = Array.Empty();
- // ── Модуль «ТЗ» ──
///
public string TzText { get; init; } = string.Empty;
diff --git a/src/core/Deal.Modules.Discovery/Application/Registrars/DiscoveryModuleRegistrar.cs b/src/core/Deal.Modules.Discovery/Application/Registrars/DiscoveryModuleRegistrar.cs
index 09468ce..1da58be 100644
--- a/src/core/Deal.Modules.Discovery/Application/Registrars/DiscoveryModuleRegistrar.cs
+++ b/src/core/Deal.Modules.Discovery/Application/Registrars/DiscoveryModuleRegistrar.cs
@@ -13,8 +13,6 @@ public static class DiscoveryModuleRegistrar
///
/// Регистрирует сервисы модуля Discovery в контейнере.
///
- /// Коллекция сервисов.
- /// Коллекция сервисов для цепочки вызовов.
public static IServiceCollection AddDiscoveryModule(this IServiceCollection services)
{
services.AddScoped();
@@ -23,8 +21,6 @@ public static class DiscoveryModuleRegistrar
services.AddScoped();
services.AddScoped();
- // (DiscoveryWorkerScheduler); IDiscoverySearchErrorCounter — singleton (счётчик ошибок ключей живёт
- // чтобы не резолвить опциональный параметр utcNow (дефолт — DateTimeOffset.UtcNow).
services.AddSingleton();
services.AddSingleton(sp => sp.GetRequiredService());
services.AddScoped();
diff --git a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryBanGuard.cs b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryBanGuard.cs
index b778575..f4e4ae5 100644
--- a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryBanGuard.cs
+++ b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryBanGuard.cs
@@ -90,9 +90,6 @@ public sealed class DiscoveryBanGuard
private long StartOfDayMs() => StartOfDayUtc().ToUnixTimeMilliseconds();
- // floodDay: Epoch-ms начала суток flood-блокировки (0 — не было).
- // startOfDayMs: Начало текущих UTC-суток в epoch-ms.
- // Возвращает: True — flood фиксировался сегодня.
private static bool IsFloodToday(long floodDay, long startOfDayMs)
=> floodDay != 0 && floodDay == startOfDayMs;
}
diff --git a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryCandidatesService.cs b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryCandidatesService.cs
index ddec8df..2629914 100644
--- a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryCandidatesService.cs
+++ b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryCandidatesService.cs
@@ -81,7 +81,6 @@ public sealed class DiscoveryCandidatesService(
DiscoveryTaskDto? task = await store.GetTaskAsync(taskId, ct).ConfigureAwait(false);
if (task is null)
{
- // для существующих running-задач; строки нет → источник просто не добавляется).
return null;
}
@@ -274,8 +273,6 @@ public sealed class DiscoveryCandidatesService(
return store.DeleteCandidateAsync(dialogId, ct);
}
- // patch: Патч из запроса.
- // Возвращает: Патч с нормализованными значениями (null-поля — «не менять»).
private static DiscoveryCandidatePatch NormalizePatch(DiscoveryCandidatePatch patch, DiscoveryCandidateDto current)
{
return new DiscoveryCandidatePatch
@@ -293,9 +290,6 @@ public sealed class DiscoveryCandidatesService(
};
}
- // value: Значение патча (null — не менялось).
- // current: Текущее значение строки.
- // Возвращает: Обрезанное значение или null (поле не меняется).
private static string? NormalizeKeptOr(string? value, string current)
{
if (value is null)
diff --git a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryTasksService.cs b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryTasksService.cs
index 75ad431..ca634a6 100644
--- a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryTasksService.cs
+++ b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryTasksService.cs
@@ -225,8 +225,6 @@ public sealed class DiscoveryTasksService(IDiscoveryStore store, DiscoveryPlanGu
return await store.AdvanceSearchAsync(taskId, nextIndex, searchDone, ct).ConfigureAwait(false);
}
- // patch: Патч из запроса.
- // Возвращает: Патч с нормализованными значениями (null-поля сохраняются как «не менять»).
private static DiscoveryTaskPatch NormalizeTaskPatch(DiscoveryTaskPatch patch)
{
int? threshold = patch.Threshold is int value ? Clamp(value, min: 1, max: 100) : null;
@@ -249,8 +247,6 @@ public sealed class DiscoveryTasksService(IDiscoveryStore store, DiscoveryPlanGu
};
}
- // keywords: Сырые ключи (null — пусто).
- // Возвращает: Список непустых ключей.
private static IReadOnlyList CleanKeywords(IReadOnlyList? keywords)
{
return keywords?.Where(keyword => !string.IsNullOrWhiteSpace(keyword))
diff --git a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Constants.cs b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Constants.cs
index f5784e0..8ad1bdd 100644
--- a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Constants.cs
+++ b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Constants.cs
@@ -1,6 +1,5 @@
namespace Deal.Modules.Discovery.Application.Services;
-// Часть DiscoveryWorkerService: константы — действия тика, пороги/размеры воркера и метки кандидатов
public sealed partial class DiscoveryWorkerService
{
///
diff --git a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Evaluate.cs b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Evaluate.cs
index 260df3c..4a80fe9 100644
--- a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Evaluate.cs
+++ b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Evaluate.cs
@@ -3,13 +3,8 @@ using Deal.Modules.Discovery.Application.Models;
namespace Deal.Modules.Discovery.Application.Services;
-// Часть DiscoveryWorkerService: шаг оценки кандидата status='new' — инфо источника, выборка, язык, объём
public sealed partial class DiscoveryWorkerService
{
- // task: Running-задача кандидата.
- // candidate: Самый старый кандидат статуса new.
- // ct: Токен отмены.
- // Возвращает: review — кандидат оценён; skip — пропущен (delete); error — сбой шага.
private async Task EvalStepAsync(
DiscoveryTaskDto task,
DiscoveryCandidateDto candidate,
@@ -46,7 +41,6 @@ public sealed partial class DiscoveryWorkerService
};
if (resolved)
{
- // Имя/username обновляем только при успешном резолве: при fallback discovery_info возвращает
patch = patch with
{
Name = string.IsNullOrWhiteSpace(info.Name) ? null : info.Name.Trim(),
@@ -195,13 +189,6 @@ public sealed partial class DiscoveryWorkerService
return new DiscoveryWorkerOutcome(ActionSkip, taskId);
}
- // taskId: Id задачи.
- // dialogId: Id источника.
- // marks: Метки (пустые отфильтровываются).
- // langRu: Подтверждённый язык (null — не менять).
- // fitRatio: Доля подходящих (null — контент не оценён).
- // topics: Темы форума (null — не менять; для не-форумов не заполняется).
- // ct: Токен отмены.
private async Task FinishReviewAsync(
string taskId,
string dialogId,
diff --git a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Helpers.cs b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Helpers.cs
index 9ec51fd..0fbbc0e 100644
--- a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Helpers.cs
+++ b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Helpers.cs
@@ -7,12 +7,6 @@ namespace Deal.Modules.Discovery.Application.Services;
// источника (KindCode) и тексты сообщений для оценки (GroupTexts).
public sealed partial class DiscoveryWorkerService
{
- // Flood-ли это исключение гейта: RpcException RESOURCE_EXHAUSTED c detail-префиксом «flood:»
- // текст исключения: RpcException.Message кодирует Status как
- // Status(StatusCode="ResourceExhausted", Detail="flood: FLOOD_WAIT_…") — ищем маркер «flood:»
- // (префикс detail, которым сервис помечает FloodWait; обычные ошибки его не несут).
- // exception: Исключение вызова гейта.
- // Возвращает: True — FloodWait Telegram (стоп до конца суток, note_flood).
internal static bool IsFlood(Exception exception)
{
string message = exception.Message ?? string.Empty;
@@ -21,9 +15,6 @@ public sealed partial class DiscoveryWorkerService
|| message.Contains("flood:", StringComparison.OrdinalIgnoreCase);
}
- // kind: Kind из гейта (EN-канон) либо русская форма каталога.
- // isForum: True — мегагруппа-форум (темы) → forum.
- // Возвращает: Код кандидата: channel/group/forum (неизвестное → group).
private static string KindCode(string kind, bool isForum = false)
{
if (isForum)
diff --git a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Join.cs b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Join.cs
index 16b1606..b52f8b7 100644
--- a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Join.cs
+++ b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Join.cs
@@ -3,13 +3,8 @@ using Deal.Modules.Discovery.Application.Models;
namespace Deal.Modules.Discovery.Application.Services;
-// Часть DiscoveryWorkerService: шаг авто-вступления кандидата status='review' — повторные проверки,
public sealed partial class DiscoveryWorkerService
{
- // task: Running-задача с autoJoin.
- // candidate: Самый старый кандидат статуса review.
- // ct: Токен отмены.
- // Возвращает: join/reject/flood/error/skip — действие; none — выход без вступления (изменения за паузу).
private async Task JoinStepAsync(
DiscoveryTaskDto task,
DiscoveryCandidateDto candidate,
@@ -37,8 +32,6 @@ public sealed partial class DiscoveryWorkerService
await _pacer.WaitJoinDelayAsync(ct).ConfigureAwait(false);
- // За время паузы задача/кандидат/состояние BanGuard могли измениться: вступаем только если кандидат
- // всё ещё есть и в review, задача ещё running с autoJoin, мы не состоим и авто-вступления разрешены
DiscoveryTaskDto? taskNow = await _tasks.GetAsync(taskId, ct).ConfigureAwait(false);
DiscoveryCandidateDto? fresh = await _store.GetCandidateAsync(dialogId, ct).ConfigureAwait(false);
if (fresh is null
@@ -71,7 +64,6 @@ public sealed partial class DiscoveryWorkerService
return new DiscoveryWorkerOutcome(ActionFlood, taskId);
}
- // Между паузой и неудачным join кандидата могли отклонить/удалить: счётчик и удаление трогаем
DiscoveryCandidateDto? rowNow = await _store.GetCandidateAsync(dialogId, ct).ConfigureAwait(false);
if (rowNow is null || rowNow.Status != DiscoveryCandidateStatuses.Review)
{
@@ -100,7 +92,6 @@ public sealed partial class DiscoveryWorkerService
return new DiscoveryWorkerOutcome(ActionError, taskId);
}
- // Вступление состоялось: joined(auto) + каталог/зеркало (монитор on) + фоновый разбор последних
await _candidates.MarkJoinedAsync(dialogId, auto: true, ct).ConfigureAwait(false);
try
@@ -129,7 +120,6 @@ public sealed partial class DiscoveryWorkerService
}
catch (Exception exception)
{
- // но фиксируется в DiscLog, иначе «немое» глотание скрывает регулярные сбои.
await _log.AddAsync(taskId, DiscoveryLogEvents.Error, $"фоновый разбор {dialogId}: {exception.Message}", ct)
.ConfigureAwait(false);
}
diff --git a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Search.cs b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Search.cs
index bc3a26d..39d3572 100644
--- a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Search.cs
+++ b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.Search.cs
@@ -3,12 +3,8 @@ using Deal.Modules.Discovery.Application.Models;
namespace Deal.Modules.Discovery.Application.Services;
-// Часть DiscoveryWorkerService: шаг поиска — один ключ keywords[SearchIdx] → кандидаты + advance_search
public sealed partial class DiscoveryWorkerService
{
- // task: Running-задача с незавершённым проходом.
- // ct: Токен отмены.
- // Возвращает: search — ключ обработан; flood/error — сбой (индекс ключа/счётчик ошибок по сценарию).
private async Task SearchStepAsync(DiscoveryTaskDto task, CancellationToken ct)
{
string taskId = task.Id;
@@ -40,7 +36,6 @@ public sealed partial class DiscoveryWorkerService
int errors = _searchErrors.Next(taskId);
if (errors >= SearchErrorsToSkip)
{
- // 3 ошибки подряд одного ключа: пропускаем (битый ключ не должен зацикливать поиск и
_searchErrors.Reset(taskId);
await _log.AddAsync(
taskId,
diff --git a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.cs b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.cs
index 50ca516..1648bec 100644
--- a/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.cs
+++ b/src/core/Deal.Modules.Discovery/Application/Services/DiscoveryWorkerService.cs
@@ -81,7 +81,6 @@ public sealed partial class DiscoveryWorkerService
return DiscoveryWorkerOutcome.None;
}
- // 1. План достигнут — закрываем задачу (важно до поиска/оценки/join: задачу с выполненным планом
foreach (DiscoveryTaskDto task in running)
{
if (task.Joined >= task.PlanJoins)
diff --git a/src/core/Deal.Modules.Kanban/Application/ColumnRules/AmountParser.cs b/src/core/Deal.Modules.Kanban/Application/ColumnRules/AmountParser.cs
index 48bffe0..59e5263 100644
--- a/src/core/Deal.Modules.Kanban/Application/ColumnRules/AmountParser.cs
+++ b/src/core/Deal.Modules.Kanban/Application/ColumnRules/AmountParser.cs
@@ -138,9 +138,6 @@ public static class AmountParser
return result;
}
- // Число из строки суммы: пробелы и неразрывные пробелы убираем, «,» — десятичный разделитель,
- // raw: Сырая цифровая часть из regex (может содержать «к»/«К» на конце).
- // Возвращает: Значение суммы или null при ошибке парсинга.
private static double? NormalizeAmount(string raw)
{
string s = raw.Replace("\u00a0", " ").Replace(" ", string.Empty).Replace(",", ".");
@@ -160,10 +157,6 @@ public static class AmountParser
: null;
}
- // Код валюты рядом с суммой: символ/слово сразу после позиции либо символ за ≤3 символа до неё
- // text: Весь текст сообщения.
- // position: Позиция сразу после конца суммы (m.end).
- // Возвращает: Код валюты или null, если валюты рядом нет.
private static string? CurFromTail(string text, int position)
{
int tailLength = Math.Min(12, text.Length - position);
diff --git a/src/core/Deal.Modules.Kanban/Application/ColumnRules/ColumnMatcher.cs b/src/core/Deal.Modules.Kanban/Application/ColumnRules/ColumnMatcher.cs
index 94187be..8ecc1ef 100644
--- a/src/core/Deal.Modules.Kanban/Application/ColumnRules/ColumnMatcher.cs
+++ b/src/core/Deal.Modules.Kanban/Application/ColumnRules/ColumnMatcher.cs
@@ -192,8 +192,6 @@ public static class ColumnMatcher
return count;
}
- // terms: Термы как сохранены.
- // Возвращает: Непустые термы в нижнем регистре.
private static IReadOnlyList NormalizeTerms(IReadOnlyList? terms)
{
var result = new List();
diff --git a/src/core/Deal.Modules.Kanban/Application/ColumnRules/MatchHitBuilder.cs b/src/core/Deal.Modules.Kanban/Application/ColumnRules/MatchHitBuilder.cs
index 7d694e5..84c6216 100644
--- a/src/core/Deal.Modules.Kanban/Application/ColumnRules/MatchHitBuilder.cs
+++ b/src/core/Deal.Modules.Kanban/Application/ColumnRules/MatchHitBuilder.cs
@@ -18,19 +18,15 @@ public static class MatchHitBuilder
// Метка группы «грейд/уровень».
private const string GradeLabel = "Грейд/уровень";
- // Метка отдельной группы «уровень» (§6.3).
private const string LevelsLabel = "Уровень";
- // Метка группы «локация» (§6.3).
private const string LocationsLabel = "Локация";
- // Метка группы «тип» (§6.3).
private const string TypesLabel = "Тип";
// Метка группы «бюджет».
private const string BudgetLabel = "Бюджет";
- // Метка отдельной группы «цена» (§6.3).
private const string PricesLabel = "Цена";
///
@@ -130,10 +126,6 @@ public static class MatchHitBuilder
}
}
- // rawTerms: Термы как сохранены.
- // label: Метка группы.
- // lower: Текст в нижнем регистре.
- // Возвращает: Hits по каждому совпавшему терму (term сохраняет регистр пользователя).
private static IEnumerable MatchedTermHits(
IReadOnlyList? rawTerms,
string label,
@@ -154,8 +146,6 @@ public static class MatchHitBuilder
}
}
- // budget: Бюджет правил.
- // Возвращает: «от X до Y CUR» / «до X CUR» / «от X CUR» / «бюджет».
private static string DescribeRange(BudgetRangeDto budget)
{
string cur = (budget.Cur ?? string.Empty).ToUpperInvariant();
@@ -177,8 +167,6 @@ public static class MatchHitBuilder
return "бюджет";
}
- // value: Число.
- // Возвращает: Строка по инвариантной культуре.
private static string FormatAmount(double value)
{
return value.ToString("G6", System.Globalization.CultureInfo.InvariantCulture).Replace('E', 'e');
diff --git a/src/core/Deal.Modules.Kanban/Application/ColumnRules/RulesDescriber.cs b/src/core/Deal.Modules.Kanban/Application/ColumnRules/RulesDescriber.cs
index 93dd4be..aa963c0 100644
--- a/src/core/Deal.Modules.Kanban/Application/ColumnRules/RulesDescriber.cs
+++ b/src/core/Deal.Modules.Kanban/Application/ColumnRules/RulesDescriber.cs
@@ -106,8 +106,6 @@ public static class RulesDescriber
parts.Add($"{title}: {lo}–{hi} {cur}".Replace("– ", "–").Replace(" –", "–"));
}
- // terms: Термы группы.
- // limit: Максимум термов.
private static string JoinLimited(IReadOnlyList terms, int limit)
{
int count = Math.Min(terms.Count, limit);
diff --git a/src/core/Deal.Modules.Kanban/Application/ColumnRules/TypeAliases.cs b/src/core/Deal.Modules.Kanban/Application/ColumnRules/TypeAliases.cs
index 3d082ad..72cf90f 100644
--- a/src/core/Deal.Modules.Kanban/Application/ColumnRules/TypeAliases.cs
+++ b/src/core/Deal.Modules.Kanban/Application/ColumnRules/TypeAliases.cs
@@ -1,7 +1,7 @@
namespace Deal.Modules.Kanban.Application.ColumnRules;
///
-/// Синонимы типов заявки для группы types правил колонки и глобальных исключений
+/// Синонимы типов заявки для группы types правил колонки и глобальных исключений.
///
public static class TypeAliases
{
diff --git a/src/core/Deal.Modules.Kanban/Application/Registrars/KanbanModuleRegistrar.cs b/src/core/Deal.Modules.Kanban/Application/Registrars/KanbanModuleRegistrar.cs
index 35f65ab..087e2a6 100644
--- a/src/core/Deal.Modules.Kanban/Application/Registrars/KanbanModuleRegistrar.cs
+++ b/src/core/Deal.Modules.Kanban/Application/Registrars/KanbanModuleRegistrar.cs
@@ -12,15 +12,12 @@ public static class KanbanModuleRegistrar
///
/// Регистрирует сервисы модуля Kanban в контейнере.
///
- /// Коллекция сервисов.
- /// Коллекция сервисов для цепочки вызовов.
public static IServiceCollection AddKanbanModule(this IServiceCollection services)
{
services.AddScoped();
services.AddScoped();
services.AddScoped();
- // оповещают его после записи кэша курсов / смены targetCurrency|conversionOn (список может быть пуст).
services.AddScoped();
return services;
}
diff --git a/src/core/Deal.Modules.Kanban/Application/Services/BudgetNormalizer.cs b/src/core/Deal.Modules.Kanban/Application/Services/BudgetNormalizer.cs
index 1acba93..8eb7ef8 100644
--- a/src/core/Deal.Modules.Kanban/Application/Services/BudgetNormalizer.cs
+++ b/src/core/Deal.Modules.Kanban/Application/Services/BudgetNormalizer.cs
@@ -100,8 +100,6 @@ public static class BudgetNormalizer
return new CardBudgetDto(convFrom, convTo, target);
}
- // value: Значение границы из бюджета.
- // Возвращает: Значение или null при 0.
private static double? NormalizeBound(double? value)
{
if (value is null || value.Value == 0)
@@ -112,8 +110,6 @@ public static class BudgetNormalizer
return value.Value;
}
- // raw: Валюта как пришла («рублей», «$», «usd», …).
- // Возвращает: Код валюты (USD/RUB/…) или null, если не распознана.
private static string? NormalizeCurrency(string? raw)
{
string s = (raw ?? string.Empty).Trim().ToUpperInvariant();
diff --git a/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Files.cs b/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Files.cs
index 5726d13..3bb8261 100644
--- a/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Files.cs
+++ b/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Files.cs
@@ -120,10 +120,6 @@ public sealed partial class CardsService
?? throw new InvalidOperationException("Карточка не прочиталась после удаления файла: " + cardId);
}
- // cardId: Id карточки (c_...).
- // fileId: Id записи файла (pf_...; уникальный суффикс ключа).
- // name: Имя файла как прислано (в ключ идёт санитизированная часть).
- // Возвращает: Ключ объекта (opaque для хранилища).
private static string BuildObjectKey(
string cardId,
string fileId,
diff --git a/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Helpers.cs b/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Helpers.cs
index 14de2f4..f8c543d 100644
--- a/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Helpers.cs
+++ b/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Helpers.cs
@@ -15,17 +15,12 @@ public sealed partial class CardsService
{
// ── Внутреннее ─────────────────────────────────────────────────────────
- // card: Карточка.
private static string LearningText(CardDto card)
{
string source = card.SourceMsg.Trim();
return source.Length > 0 ? source : card.Title;
}
- // card: Карточка ДО переноса (для prev_col/from_col журнала).
- // toCol: Новая колонка.
- // action: Действие журнала: move/trash.
- // ct: Токен отмены.
private async Task MoveToColumnAsync(
CardDto card,
string toCol,
@@ -43,11 +38,6 @@ public sealed partial class CardsService
await LogMoveAsync(card.Id, action, card.Col, toCol, ct);
}
- // cardId: Id карточки.
- // action: Действие: move/trash/restore/comment.
- // fromCol: Прежняя колонка (для comment — null).
- // toCol: Новая колонка (для comment — null).
- // ct: Токен отмены.
private async Task LogMoveAsync(
string cardId,
string action,
@@ -63,10 +53,6 @@ public sealed partial class CardsService
toCol), ct);
}
- // boardId: Id доски (b_...).
- // text: Текст карточки для правил (source_msg или title).
- // ct: Токен отмены.
- // Возвращает: Список совпавших критериев; доски нет/правил нет → пусто.
private async Task> ComputeHitsForBoardAsync(
string boardId,
string text,
@@ -78,11 +64,6 @@ public sealed partial class CardsService
: await ComputeHitsAsync(board.Rules, text, ct);
}
- // Совпавшие критерии правил колонки-доски (ColumnRules.ComputeHits с курсами для бюджета).
- // Резолв имени — через алиас KanbanColumnRules: одноимённые класс и namespace ColumnRules в одном модуле.
- // text: Текст карточки для правил.
- // ct: Токен отмены.
- // Возвращает: Совпавшие критерии (label/term[/word]); нет активных правил → пусто.
private async Task> ComputeHitsAsync(
ContainerRulesDto? rules,
string text,
@@ -94,9 +75,6 @@ public sealed partial class CardsService
return KanbanColumnRules.ComputeHits(rules, text, rates);
}
- // prevCol: Сохранённая prev_col карточки.
- // ct: Токен отмены.
- // Возвращает: Колонка возврата (inbox/доска).
private async Task ResolveReturnColAsync(string prevCol, CancellationToken ct)
{
if (prevCol == CardIds.Inbox)
diff --git a/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Reminders.cs b/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Reminders.cs
index c871a8f..e0fb89a 100644
--- a/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Reminders.cs
+++ b/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Reminders.cs
@@ -102,8 +102,6 @@ public sealed partial class CardsService
return due;
}
- // ct: Токен отмены.
- // Возвращает: Значение настройки; отсутствие/повреждённый JSON → дефолт SettingsDefaults.RemindersEnabled.
private async Task ReadRemindersEnabledAsync(CancellationToken ct)
{
return (await TenantSettingsSnapshot.LoadAsync(_settings, ct))
diff --git a/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Selected.cs b/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Selected.cs
index 6030090..7eba535 100644
--- a/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Selected.cs
+++ b/src/core/Deal.Modules.Kanban/Application/Services/CardsService.Selected.cs
@@ -247,9 +247,6 @@ public sealed partial class CardsService
return _store.ClearStageAsync(RejectedStage, ct);
}
- // Переводит тело PATCH в точечный патч хранилища: ключ присутствует → поле меняется;
- // body: Тело PATCH (ключ → значение).
- // Возвращает: Патч со значениями присутствующих ключей; остальные поля null («не менять»).
private static CardPatch ResolvePatch(IReadOnlyDictionary body)
{
string? title = null;
diff --git a/src/core/Deal.Modules.Kanban/Application/Services/FileKindDetector.cs b/src/core/Deal.Modules.Kanban/Application/Services/FileKindDetector.cs
index 576f173..2331a8b 100644
--- a/src/core/Deal.Modules.Kanban/Application/Services/FileKindDetector.cs
+++ b/src/core/Deal.Modules.Kanban/Application/Services/FileKindDetector.cs
@@ -103,8 +103,6 @@ public static class FileKindDetector
return new CardFileKind(kind, LabelsByKind[kind]);
}
- // name: Имя файла.
- // Возвращает: Расширение без точки; пустая строка, если точки в имени нет (или имя заканчивается точкой).
private static string GetExtension(string name)
{
int lastDot = name.LastIndexOf('.');
diff --git a/src/core/Deal.Modules.Kanban/Application/Services/StorageTickService.cs b/src/core/Deal.Modules.Kanban/Application/Services/StorageTickService.cs
index 4b40589..a8c1e42 100644
--- a/src/core/Deal.Modules.Kanban/Application/Services/StorageTickService.cs
+++ b/src/core/Deal.Modules.Kanban/Application/Services/StorageTickService.cs
@@ -30,7 +30,6 @@ public sealed class StorageTickService(ICardStore store, ISettingsStore settings
int archived = 0;
if (autoArchive)
{
- // UPDATE (архивация как системный перенос: col=archive, is_new=false, archived_at=now, matchHits
IReadOnlyList candidates = await store.ListArchiveCandidatesAsync(
now.AddDays(-archiveAfterDays), ct);
archived = candidates.Count == 0 ? 0 : await store.ArchiveAsync(candidates, now, ct);
@@ -45,9 +44,6 @@ public sealed class StorageTickService(ICardStore store, ISettingsStore settings
return new StorageTickStatsDto(archived, purgedArchive, purgedTrash, PurgedRejected: 0);
}
- // cardIds: Id карточек-кандидатов из выборки хранилища.
- // ct: Токен отмены.
- // Возвращает: Сколько карточек реально удалено (0 — кандидатов не было).
private async Task PurgeAsync(IReadOnlyList cardIds, CancellationToken ct)
{
return cardIds.Count == 0 ? 0 : await store.PurgeAsync(cardIds, ct);
diff --git a/src/core/Deal.Modules.Kanban/Application/Services/SuggestHeuristics.cs b/src/core/Deal.Modules.Kanban/Application/Services/SuggestHeuristics.cs
index ad92020..a052605 100644
--- a/src/core/Deal.Modules.Kanban/Application/Services/SuggestHeuristics.cs
+++ b/src/core/Deal.Modules.Kanban/Application/Services/SuggestHeuristics.cs
@@ -95,7 +95,6 @@ public static class SuggestHeuristics
/// Планы колонок (≤4, каждая ≥2 карточки); пусто — мало карточек/нечего сгруппировать.
public static IReadOnlyList PlanColumns(IReadOnlyList inbox, IReadOnlyList existingBoardNames)
{
- // детерминизма: адаптер уже отдаёт received_at DESC, но вход не должен влиять на выход).
List window = inbox
.Where(card => card.SourceMsg.Trim().Length > 0)
.OrderByDescending(card => card.ReceivedAtMs)
@@ -233,10 +232,6 @@ public static class SuggestHeuristics
terms.Add(term);
}
- // Похоже ли слово-тема на существующую доску: равенство или вхождение имени в слово/наоборот
- // term: Слово-кандидат (нижний регистр).
- // existingLowered: Имена существующих досок в нижнем регистре.
- // Возвращает: True — тема уже покрыта существующей колонкой (кандидат пропускается).
private static bool SimilarToExisting(string term, List existingLowered)
{
foreach (string name in existingLowered)
@@ -250,7 +245,5 @@ public static class SuggestHeuristics
return false;
}
- // term: Слово-тема в нижнем регистре (непустое).
- // Возвращает: Слово с заглавной первой буквой.
private static string Capitalize(string term) => char.ToUpperInvariant(term[0]) + term[1..];
}
diff --git a/src/core/Deal.Modules.Pipeline/Application/Abstractions/IPipelineStore.cs b/src/core/Deal.Modules.Pipeline/Application/Abstractions/IPipelineStore.cs
index a290249..4d3c49b 100644
--- a/src/core/Deal.Modules.Pipeline/Application/Abstractions/IPipelineStore.cs
+++ b/src/core/Deal.Modules.Pipeline/Application/Abstractions/IPipelineStore.cs
@@ -55,7 +55,7 @@ public interface IPipelineStore
CancellationToken ct);
///
- /// Удаляет строку очереди безвозвратно
+ /// Удаляет строку очереди безвозвратно.
///
/// Id строки (p_...).
public Task RemoveAsync(string id, CancellationToken ct);
diff --git a/src/core/Deal.Modules.Pipeline/Application/Models/GlobalExcludeSettings.cs b/src/core/Deal.Modules.Pipeline/Application/Models/GlobalExcludeSettings.cs
index bd3e0be..e9865dc 100644
--- a/src/core/Deal.Modules.Pipeline/Application/Models/GlobalExcludeSettings.cs
+++ b/src/core/Deal.Modules.Pipeline/Application/Models/GlobalExcludeSettings.cs
@@ -1,7 +1,7 @@
namespace Deal.Modules.Pipeline.Application.Models;
///
-/// Снимок глобальных исключений тенанта
+/// Снимок глобальных исключений тенанта.
///
/// Ключевые слова/фразы/технологии (пусто — группа выключена).
/// Локации/языки (пусто — группа выключена).
diff --git a/src/core/Deal.Modules.Pipeline/Application/Models/MlApplyResult.cs b/src/core/Deal.Modules.Pipeline/Application/Models/MlApplyResult.cs
index 78e99f0..a7821f9 100644
--- a/src/core/Deal.Modules.Pipeline/Application/Models/MlApplyResult.cs
+++ b/src/core/Deal.Modules.Pipeline/Application/Models/MlApplyResult.cs
@@ -1,7 +1,7 @@
namespace Deal.Modules.Pipeline.Application.Models;
///
-/// Результат ручного решения по сообщению
+/// Результат ручного решения по сообщению.
///
/// Текст ошибки 400 (неизвестная доска/действие); null — решение применено.
/// True — решение принято.
diff --git a/src/core/Deal.Modules.Pipeline/Application/Models/MlCandidateDto.cs b/src/core/Deal.Modules.Pipeline/Application/Models/MlCandidateDto.cs
index 729dd3d..15ff178 100644
--- a/src/core/Deal.Modules.Pipeline/Application/Models/MlCandidateDto.cs
+++ b/src/core/Deal.Modules.Pipeline/Application/Models/MlCandidateDto.cs
@@ -1,7 +1,7 @@
namespace Deal.Modules.Pipeline.Application.Models;
///
-/// Сообщение-кандидат для ручной проверки ML
+/// Сообщение-кандидат для ручной проверки ML.
///
public sealed record MlCandidateDto
{
diff --git a/src/core/Deal.Modules.Pipeline/Application/Models/MlCandidatePredictionDto.cs b/src/core/Deal.Modules.Pipeline/Application/Models/MlCandidatePredictionDto.cs
index 7671c24..9f533fe 100644
--- a/src/core/Deal.Modules.Pipeline/Application/Models/MlCandidatePredictionDto.cs
+++ b/src/core/Deal.Modules.Pipeline/Application/Models/MlCandidatePredictionDto.cs
@@ -1,7 +1,7 @@
namespace Deal.Modules.Pipeline.Application.Models;
///
-/// Мнение ML по сообщению-кандидату
+/// Мнение ML по сообщению-кандидату.
///
/// Модель «взяла бы» сообщение (готова и уверена).
/// Метка решения (id доски либо spam); null — модель не уверена/не готова.
diff --git a/src/core/Deal.Modules.Pipeline/Application/Models/PipelineRejectConstants.cs b/src/core/Deal.Modules.Pipeline/Application/Models/PipelineRejectConstants.cs
index ab3444d..f4fbddf 100644
--- a/src/core/Deal.Modules.Pipeline/Application/Models/PipelineRejectConstants.cs
+++ b/src/core/Deal.Modules.Pipeline/Application/Models/PipelineRejectConstants.cs
@@ -46,7 +46,7 @@ public static class PipelineRejectConstants
};
///
- /// Подпись отсева
+ /// Подпись отсева: словарь, иначе сам stage; пустой stage — «отсев».
///
/// Этап отсева.
/// Подпись для UI.
diff --git a/src/core/Deal.Modules.Pipeline/Application/Models/RejectRecord.cs b/src/core/Deal.Modules.Pipeline/Application/Models/RejectRecord.cs
index b10ae95..b22e319 100644
--- a/src/core/Deal.Modules.Pipeline/Application/Models/RejectRecord.cs
+++ b/src/core/Deal.Modules.Pipeline/Application/Models/RejectRecord.cs
@@ -46,7 +46,7 @@ public sealed record RejectRecord
public string Source { get; init; } = string.Empty;
///
- /// Этап отсева: length|stop|resume|type|budget|stale|spam_ml|spam_ai|filter_ai|dup
+ /// Этап отсева: length|stop|resume|type|budget|stale|spam_ml|spam_ai|filter_ai|dup.
///
public string Stage { get; init; } = string.Empty;
diff --git a/src/core/Deal.Modules.Pipeline/Application/Models/RejectedPageDto.cs b/src/core/Deal.Modules.Pipeline/Application/Models/RejectedPageDto.cs
index c32b152..dbf6b2e 100644
--- a/src/core/Deal.Modules.Pipeline/Application/Models/RejectedPageDto.cs
+++ b/src/core/Deal.Modules.Pipeline/Application/Models/RejectedPageDto.cs
@@ -3,7 +3,7 @@ namespace Deal.Modules.Pipeline.Application.Models;
///
/// Страница отсева — тело GET /api/pipeline/rejected.
///
-/// Записи страницы (полные item'ы отсева §4.5, порядок RejectedAt DESC).
+/// Записи страницы.
/// Всего записей по условию поиска.
/// Сдвиг от начала страницы (эхо запроса).
/// Размер страницы (эхо запроса; ≤500).
diff --git a/src/core/Deal.Modules.Pipeline/Application/Parse/ContactsQualifier.cs b/src/core/Deal.Modules.Pipeline/Application/Parse/ContactsQualifier.cs
index 41e6d64..5bf96ad 100644
--- a/src/core/Deal.Modules.Pipeline/Application/Parse/ContactsQualifier.cs
+++ b/src/core/Deal.Modules.Pipeline/Application/Parse/ContactsQualifier.cs
@@ -238,8 +238,6 @@ public static class ContactsQualifier
return result.Count > MaxTextCandidates ? result.Take(MaxTextCandidates).ToList() : result;
}
- // phone: Телефон как встретился в тексте.
- // Возвращает: Нормализованный телефон (≤MaxNormalizedPhoneLength символов).
internal static string NormalizePhone(string phone)
{
string normalized = phone.Replace(" ", string.Empty)
@@ -252,16 +250,11 @@ public static class ContactsQualifier
: normalized;
}
- // type: Тип контакта (tg/phone/whatsapp/email/linkedin/site).
- // Возвращает: Приоритет (меньше — важнее); неизвестный тип — UnknownTypePriority.
private static int OrderOf(string type)
{
return PrimaryOrder.TryGetValue(type, out int order) ? order : UnknownTypePriority;
}
- // candidates: Сырые кандидаты.
- // text: Текст сообщения для извлечения кандидатов, если список пуст.
- // Возвращает: Квалифицированные контакты без дублей (≤MaxContacts).
private static IReadOnlyList BuildFromCandidates(List candidates, string? text)
{
if (candidates.Count == 0)
@@ -295,8 +288,6 @@ public static class ContactsQualifier
return result;
}
- // result: Список-накопитель.
- // values: Найденные совпадения.
private static void AddUnique(List result, IEnumerable values)
{
foreach (string value in values)
diff --git a/src/core/Deal.Modules.Pipeline/Application/Parse/DedupHasher.cs b/src/core/Deal.Modules.Pipeline/Application/Parse/DedupHasher.cs
index 82f2709..fa83f3d 100644
--- a/src/core/Deal.Modules.Pipeline/Application/Parse/DedupHasher.cs
+++ b/src/core/Deal.Modules.Pipeline/Application/Parse/DedupHasher.cs
@@ -20,8 +20,6 @@ public static class DedupHasher
return Convert.ToHexString(hash).ToLowerInvariant();
}
- // text: Исходный текст.
- // Возвращает: Нормализованная строка без регистра, пробелов и пунктуации.
private static string Normalize(string text)
{
var builder = new StringBuilder(text.Length);
diff --git a/src/core/Deal.Modules.Pipeline/Application/Parse/LocalFieldsParser.cs b/src/core/Deal.Modules.Pipeline/Application/Parse/LocalFieldsParser.cs
index 2d6d8d8..d4c8708 100644
--- a/src/core/Deal.Modules.Pipeline/Application/Parse/LocalFieldsParser.cs
+++ b/src/core/Deal.Modules.Pipeline/Application/Parse/LocalFieldsParser.cs
@@ -213,8 +213,6 @@ public sealed class LocalFieldsParser(ISettingsStore store)
return new LocalParsedFields(title, summary, resultStack, resultGrades, budget, contactsText, isVacancy);
}
- // line: Строка текста (очищенная).
- // Возвращает: Категория поля и значение метки; null — это не метка-поле.
internal static (string Category, string Value)? FieldOf(string line)
{
Match match = LabelRe.Match(line);
@@ -241,8 +239,6 @@ public sealed class LocalFieldsParser(ISettingsStore store)
return null;
}
- // value: Значение метки («Стек: Java, Kotlin» → «Java, Kotlin»).
- // Возвращает: Токены-технологии (до MaxPickTokens).
private static IReadOnlyList PickStackTokens(string value)
{
var tokens = new List();
@@ -264,8 +260,6 @@ public sealed class LocalFieldsParser(ISettingsStore store)
return tokens;
}
- // value: Строка значения.
- // Возвращает: Все совпадения токенов.
private static IEnumerable TokensOf(string value)
{
foreach (Match match in TokenRe.Matches(value))
@@ -274,8 +268,6 @@ public sealed class LocalFieldsParser(ISettingsStore store)
}
}
- // source: Текст (значение метки «Бюджет:» либо всё тело).
- // Возвращает: Бюджет формы хранения (валюта-код) или null — суммы с валютой нет.
private static BudgetRangeDto? FirstAmount(string source)
{
IReadOnlyList amounts = AmountParser.Parse(source);
@@ -288,8 +280,6 @@ public sealed class LocalFieldsParser(ISettingsStore store)
return new BudgetRangeDto(first.From, first.To, first.Cur);
}
- // markers: Список маркеров как сохранён (или дефолт); null — пусто.
- // Возвращает: Нормализованный набор (порядок исходного, без дублей).
private static IReadOnlySet NormalizeMarkers(IReadOnlyCollection? markers)
{
var result = new HashSet(StringComparer.Ordinal);
@@ -310,9 +300,6 @@ public sealed class LocalFieldsParser(ISettingsStore store)
return result;
}
- // lower: Текст в нижнем регистре.
- // markers: Нормализованные маркеры.
- // Возвращает: True — найден хотя бы один маркер.
private static bool ContainsAny(string lower, IReadOnlySet markers)
{
foreach (string marker in markers)
@@ -337,7 +324,6 @@ public sealed class LocalFieldsParser(ISettingsStore store)
// Категория поля «Бюджет:». Константа-строка (свитч по категориям меток).
private const string FieldCategoryBudget = "budget";
- // Возвращает: Список: категория → синонимы меток (нижний регистр).
private static IReadOnlyList<(string Category, IReadOnlySet Synonyms)> BuildFieldLabels()
{
return new List<(string, IReadOnlySet)>
diff --git a/src/core/Deal.Modules.Pipeline/Application/Parse/MessageListNormalizer.cs b/src/core/Deal.Modules.Pipeline/Application/Parse/MessageListNormalizer.cs
index bd8a98c..1e3a596 100644
--- a/src/core/Deal.Modules.Pipeline/Application/Parse/MessageListNormalizer.cs
+++ b/src/core/Deal.Modules.Pipeline/Application/Parse/MessageListNormalizer.cs
@@ -68,9 +68,6 @@ public static class MessageListNormalizer
///
public static IReadOnlySet StackStopWords => StopWordsSet;
- // Очищает части списка: обрезка, снятие обрамляющих *`#, висящих запятых/точек,
- // parts: Сырые части (после разбиения или элементы списка).
- // Возвращает: Очищенный список.
private static IReadOnlyList CleanParts(IEnumerable parts)
{
var result = new List();
@@ -88,8 +85,6 @@ public static class MessageListNormalizer
return result;
}
- // items: Нормализованные элементы списка.
- // Возвращает: Стек ≤12 элементов длиной ≥2.
private static IReadOnlyList TakeStack(IReadOnlyList items)
{
var stack = new List(Math.Min(items.Count, MaxStackItems));
@@ -110,7 +105,6 @@ public static class MessageListNormalizer
return stack;
}
- // Возвращает: Набор слов в нижнем регистре.
private static IReadOnlySet BuildStopWords()
{
return new HashSet(StringComparer.Ordinal)
diff --git a/src/core/Deal.Modules.Pipeline/Application/Parse/MessageTextCleaner.cs b/src/core/Deal.Modules.Pipeline/Application/Parse/MessageTextCleaner.cs
index e57fb4c..90de883 100644
--- a/src/core/Deal.Modules.Pipeline/Application/Parse/MessageTextCleaner.cs
+++ b/src/core/Deal.Modules.Pipeline/Application/Parse/MessageTextCleaner.cs
@@ -103,8 +103,6 @@ public static class MessageTextCleaner
return LineEdgesRe.Replace(line ?? string.Empty, string.Empty).Trim();
}
- // value: Строка (не null).
- // Возвращает: Число кодовых точек.
internal static int CountCodePoints(string value)
{
int count = 0;
@@ -120,10 +118,6 @@ public static class MessageTextCleaner
return count;
}
- // суррогатных пар).
- // value: Строка.
- // max: Максимум кодовых точек; ≤0 или ≥ длины — строка как есть.
- // Возвращает: Усечённая строка.
internal static string SliceCodePoints(string value, int max)
{
if (max <= 0)
@@ -156,10 +150,6 @@ public static class MessageTextCleaner
return builder.ToString();
}
- // 1F000–1FAFF (включая региональные флаги 1F1E6–1F1FF), разные символы 2600–27BF, стрелки 2B00–2BFF
- // и variation selector FE0F.
- // value: Текст после снятия markdown-разметки.
- // Возвращает: Текст без эмодзи-диапазонов (суррогатные пары удаляются целиком).
private static string RemoveEmojiCodePoints(string value)
{
var builder = new StringBuilder(value.Length);
@@ -212,10 +202,6 @@ public static class MessageTextCleaner
return current;
}
- // Обрезка по границе последнего переноса/пробела в первых limit кодовых точках
- // value: Текст длиннее лимита.
- // limit: Лимит кодовых точек.
- // Возвращает: Усечённый текст с многоточием в конце.
private static string CutByBoundary(string value, int limit)
{
string cut = SliceCodePoints(value, limit);
diff --git a/src/core/Deal.Modules.Pipeline/Application/Parse/SummaryComposer.cs b/src/core/Deal.Modules.Pipeline/Application/Parse/SummaryComposer.cs
index 34ba04a..7bdc8a1 100644
--- a/src/core/Deal.Modules.Pipeline/Application/Parse/SummaryComposer.cs
+++ b/src/core/Deal.Modules.Pipeline/Application/Parse/SummaryComposer.cs
@@ -19,7 +19,6 @@ public static class SummaryComposer
private const int MaxSummaryLength = 600;
- // Сколько первых строк списка пропускает локальная суть по умолчанию — первая строка это заголовок
private const int DefaultSkipFirst = 1;
private static readonly IReadOnlySet TrivialWords = new HashSet(StringComparer.Ordinal)
@@ -79,7 +78,6 @@ public static class SummaryComposer
return string.Join("\n", blocks);
}
- // Структурированных полей нет. «summary» разбора часто является копией исходника/шумом — если в нём есть
string legacy = MessageTextCleaner.CleanShort(source.Summary);
if (legacy.Length > 0 && !legacy.ContainsFooterHint())
{
@@ -163,9 +161,6 @@ public static class SummaryComposer
return MessageTextCleaner.SliceCodePoints(summary, MaxSummaryLength);
}
- // Нормализует пункты блока: элементы списка чистятся как в normalize_list(list), затем каждый —
- // values: Сырые пункты разбора (может быть null).
- // Возвращает: Очищенные непустые пункты.
private static IReadOnlyList ContentItems(IReadOnlyList? values)
{
var result = new List();
diff --git a/src/core/Deal.Modules.Pipeline/Application/Registrars/PipelineModuleRegistrar.cs b/src/core/Deal.Modules.Pipeline/Application/Registrars/PipelineModuleRegistrar.cs
index a344c24..f5c6abe 100644
--- a/src/core/Deal.Modules.Pipeline/Application/Registrars/PipelineModuleRegistrar.cs
+++ b/src/core/Deal.Modules.Pipeline/Application/Registrars/PipelineModuleRegistrar.cs
@@ -12,8 +12,6 @@ public static class PipelineModuleRegistrar
///
/// Регистрирует сервисы модуля Pipeline в контейнере.
///
- /// Коллекция сервисов.
- /// Коллекция сервисов для цепочки вызовов.
public static IServiceCollection AddPipelineModule(this IServiceCollection services)
{
// Ядра разбора задачи 4: статические (MessageTextCleaner/…/SummaryComposer) не регистрируются;
@@ -23,14 +21,11 @@ public static class PipelineModuleRegistrar
services.AddScoped();
services.AddScoped();
- // оба читают настройки тенанта/доски (ISettingsStore/ICardStore scoped, как IncomingRules).
services.AddScoped();
services.AddScoped();
services.AddScoped();
- // и доски/журнал тенанта — scoped, как CardComposer. Потребитель — gRPC-адаптер GrpcAiClassifier
- // (Infrastructure, реализует порт IAiClassifier); локальный путь контекст не строит.
services.AddScoped();
// Этап 12, пакет D: ручная переклассификация карточек (POST /api/cards/reclassify|{id}/reclassify).
@@ -39,9 +34,6 @@ public static class PipelineModuleRegistrar
services.AddSingleton();
services.AddScoped();
- // Этап 12, добивка по ТЗ §8: ручная проверка/разметка ML на сообщениях канала — кандидаты и применение
- // решения (обучение ML + перенос/корзина/отсев через существующие сервисы). Scoped — как остальные
- // сервисы модуля (EF-контекст tenant-запроса).
services.AddScoped();
return services;
}
diff --git a/src/core/Deal.Modules.Pipeline/Application/Services/AiClassifyContextBuilder.cs b/src/core/Deal.Modules.Pipeline/Application/Services/AiClassifyContextBuilder.cs
index 79e1c60..a5b7489 100644
--- a/src/core/Deal.Modules.Pipeline/Application/Services/AiClassifyContextBuilder.cs
+++ b/src/core/Deal.Modules.Pipeline/Application/Services/AiClassifyContextBuilder.cs
@@ -108,8 +108,6 @@ public sealed class AiClassifyContextBuilder(ISettingsStore settings, ICardStore
return context.ToString();
}
- // ct: Токен отмены.
- // Возвращает: Список строк «- id: имя (критерии) — описание»; пусто — фраза «колонок нет».
private async Task BuildBoardMapAsync(CancellationToken ct)
{
IReadOnlyList containers = await kanjStore.ListContainersAsync(ContainerSpaces.Dashboard, ct);
@@ -127,8 +125,6 @@ public sealed class AiClassifyContextBuilder(ISettingsStore settings, ICardStore
return lines.Count == 0 ? NoBoardsLine : string.Join("\n", lines);
}
- // container: Колонка (non-suggested, kind=board).
- // Возвращает: Однострочное описание колонки для промпта.
private static string BuildBoardLine(ContainerDto container)
{
string line = "- " + container.Id + ": " + container.Name;
@@ -146,9 +142,6 @@ public sealed class AiClassifyContextBuilder(ISettingsStore settings, ICardStore
return line;
}
- // defaultValue: Дефолт из SettingsDefaults (когда переопределения нет).
- // settingsSnapshot: Типизированный снимок настроек тенанта (промпты/«Сфера и ключи»).
- // Возвращает: Текст промпта с подставленными {domain}/{keywords}.
private static string FillPrompt(
string key,
string defaultValue,
diff --git a/src/core/Deal.Modules.Pipeline/Application/Services/AiRawCardMapper.cs b/src/core/Deal.Modules.Pipeline/Application/Services/AiRawCardMapper.cs
index 20cf0da..e4760e6 100644
--- a/src/core/Deal.Modules.Pipeline/Application/Services/AiRawCardMapper.cs
+++ b/src/core/Deal.Modules.Pipeline/Application/Services/AiRawCardMapper.cs
@@ -73,9 +73,6 @@ public static class AiRawCardMapper
Board: board);
}
- // root: Корневой объект ответа модели.
- // field: Имя поля.
- // Возвращает: Значение строкой (отсутствие/null → пустая строка).
private static string ReadRawString(JsonObject root, string field)
{
if (!root.TryGetPropertyValue(field, out JsonNode? node) || node is null)
@@ -86,18 +83,12 @@ public static class AiRawCardMapper
return node is JsonValue value ? ValueToString(value) : string.Empty;
}
- // root: Корневой объект ответа модели.
- // field: Имя поля.
- // Возвращает: Значение или null.
private static string? ReadNullableString(JsonObject root, string field)
{
string value = ReadRawString(root, field);
return value.Length == 0 ? null : value;
}
- // root: Корневой объект ответа модели.
- // field: Имя поля.
- // Возвращает: True/False; отсутствие/неразбираемое → false.
private static bool ReadBool(JsonObject root, string field)
{
if (!root.TryGetPropertyValue(field, out JsonNode? node) || node is null)
@@ -129,9 +120,6 @@ public static class AiRawCardMapper
return false;
}
- // root: Корневой объект ответа модели.
- // field: Имя поля.
- // Возвращает: Очищенные пункты; отсутствие/null → null (блока нет), пустой список → пустой список.
private static IReadOnlyList? ReadItems(JsonObject root, string field)
{
if (!root.TryGetPropertyValue(field, out JsonNode? node) || node is null)
@@ -157,8 +145,6 @@ public static class AiRawCardMapper
return result;
}
- // root: Корневой объект ответа модели.
- // Возвращает: Нормализованный стек.
private static IReadOnlyList ReadStack(JsonObject root)
{
if (!root.TryGetPropertyValue("stack", out JsonNode? node) || node is null)
@@ -173,8 +159,6 @@ public static class AiRawCardMapper
: Array.Empty();
}
- // root: Корневой объект ответа модели.
- // Возвращает: Нормализованный бюджет контракта или null (нет объекта/валюты/границ).
private static AiBudgetDto? ReadBudget(JsonObject root)
{
if (!root.TryGetPropertyValue("budget", out JsonNode? node) || node is not JsonObject budget)
@@ -196,9 +180,6 @@ public static class AiRawCardMapper
: new AiBudgetDto(normalized.From, normalized.To, normalized.Cur);
}
- // budget: Объект бюджета ответа модели.
- // field: Имя границы (from/to).
- // Возвращает: Число или null — поле отсутствует/пусто/не разбирается.
private static double? ParseBudgetBound(JsonObject budget, string field)
{
if (!budget.TryGetPropertyValue(field, out JsonNode? node) || node is null)
@@ -242,9 +223,6 @@ public static class AiRawCardMapper
return result == 0 ? null : result;
}
- // root: Корневой объект ответа модели.
- // text: Исходный текст сообщения (fallback-кандидаты, если контактов в ответе нет).
- // Возвращает: Квалифицированные контакты (≤6, дедуп по casefold-значению).
private static IReadOnlyList ReadContacts(JsonObject root, string text)
{
List candidates = new();
@@ -295,13 +273,9 @@ public static class AiRawCardMapper
return result;
}
- // Значение JsonNode строкой: JsonValue — как ValueToString, прочее/отсутствие — пустая строка.
- // node: Узел JSON (элемент массива).
private static string NodeToString(JsonNode? node)
=> node is JsonValue value ? ValueToString(value) : string.Empty;
- // value: JSON-значение.
- // Возвращает: Строковое представление.
private static string ValueToString(JsonValue value)
{
if (value.TryGetValue(out string? text))
diff --git a/src/core/Deal.Modules.Pipeline/Application/Services/CardComposer.cs b/src/core/Deal.Modules.Pipeline/Application/Services/CardComposer.cs
index 41a71d0..1ae1861 100644
--- a/src/core/Deal.Modules.Pipeline/Application/Services/CardComposer.cs
+++ b/src/core/Deal.Modules.Pipeline/Application/Services/CardComposer.cs
@@ -128,11 +128,6 @@ public sealed class CardComposer(ICardStore kanjStore, ISettingsStore settings)
};
}
- // Бюджет карточки: нормализация из разбора, иначе fallback первой суммы по исходнику/«О заявке»
- // parsedBudget: Бюджет разбора (форма контракта; null — разбор не выделил сумму).
- // text: Текст исходного сообщения (первый источник fallback).
- // summary: Блок «О заявке» (второй источник fallback — сумма часто уходит в «Условия»).
- // Возвращает: Нормализованный бюджет либо null — суммы с валютой нет ни в разборе, ни в тексте.
private static CardBudgetDto? ComposeBudget(
AiBudgetDto? parsedBudget,
string? text,
@@ -147,8 +142,6 @@ public sealed class CardComposer(ICardStore kanjStore, ISettingsStore settings)
return fallback is null ? null : BudgetNormalizer.Normalize(fallback);
}
- // parsed: Разбор (пустые/отсутствующие поля блок не дают — SummaryComposer).
- // Возвращает: Структура для SummaryComposer.Compose.
private static ParsedCardContent ToParsedContent(AiParsedCardDto parsed) => new(
Company: parsed.Company,
Format: parsed.Format,
diff --git a/src/core/Deal.Modules.Pipeline/Application/Services/CardReclassifier.cs b/src/core/Deal.Modules.Pipeline/Application/Services/CardReclassifier.cs
index d1fb123..68a86a3 100644
--- a/src/core/Deal.Modules.Pipeline/Application/Services/CardReclassifier.cs
+++ b/src/core/Deal.Modules.Pipeline/Application/Services/CardReclassifier.cs
@@ -195,10 +195,6 @@ public sealed class CardReclassifier(
await AiCardLearning.PushSignalsAsync(store, mlClient, snapshot.Col, parsed, text, MlLearningLabels.AiPushWeight, ct);
}
- // card: Карточка.
- // text: Исходный текст (обучающий пример).
- // pass: Накопители прохода.
- // ct: Токен отмены.
private async Task TrashAsync(
CardDto card,
string text,
@@ -253,9 +249,6 @@ public sealed class CardReclassifier(
string cur) =>
cur.Length == 0 ? null : new CardBudgetDto(from, to, cur);
- // card: Карточка до переклассификации (старый контакт).
- // computed: Контакт, собранный из нового разбора.
- // Возвращает: Значение основного контакта либо пустая строка.
private static string ResolveContact(CardDto card, string computed)
{
if (computed.Length > 0)
@@ -267,9 +260,6 @@ public sealed class CardReclassifier(
return oldContact.Length > 0 && ContactsQualifier.Qualify(oldContact) is not null ? oldContact : string.Empty;
}
- // inbox: Карточки «Неразобранного» (порядок хранилища).
- // ids: Опциональный фильтр id.
- // Возвращает: Целевые карточки в порядке хранилища.
private static List SelectTarget(IReadOnlyList inbox, IReadOnlyList? ids)
{
if (ids is null || ids.Count == 0)
diff --git a/src/core/Deal.Modules.Pipeline/Application/Services/GlobalExclusionRules.cs b/src/core/Deal.Modules.Pipeline/Application/Services/GlobalExclusionRules.cs
index 3c48c6f..ec32b0a 100644
--- a/src/core/Deal.Modules.Pipeline/Application/Services/GlobalExclusionRules.cs
+++ b/src/core/Deal.Modules.Pipeline/Application/Services/GlobalExclusionRules.cs
@@ -4,7 +4,7 @@ using Deal.Modules.Pipeline.Application.Models;
namespace Deal.Modules.Pipeline.Application.Services;
///
-/// Глобальные исключения тенанта — стоп-фильтр ДО ML/ИИ
+/// Глобальные исключения тенанта — стоп-фильтр ДО ML/ИИ.
///
public static class GlobalExclusionRules
{
diff --git a/src/core/Deal.Modules.Pipeline/Application/Services/MlReviewService.cs b/src/core/Deal.Modules.Pipeline/Application/Services/MlReviewService.cs
index 7fb2fa0..564ed0b 100644
--- a/src/core/Deal.Modules.Pipeline/Application/Services/MlReviewService.cs
+++ b/src/core/Deal.Modules.Pipeline/Application/Services/MlReviewService.cs
@@ -10,7 +10,7 @@ using Deal.Modules.Pipeline.Application.Models;
namespace Deal.Modules.Pipeline.Application.Services;
///
-/// Ручная проверка/разметка ML на сообщениях канала
+/// Ручная проверка/разметка ML на сообщениях канала.
///
public sealed class MlReviewService(
IPipelineStore pipelineStore,
diff --git a/src/core/Deal.Modules.Pipeline/Application/Services/PipelineProcessingService.cs b/src/core/Deal.Modules.Pipeline/Application/Services/PipelineProcessingService.cs
index 5a9a88f..0e4e8d0 100644
--- a/src/core/Deal.Modules.Pipeline/Application/Services/PipelineProcessingService.cs
+++ b/src/core/Deal.Modules.Pipeline/Application/Services/PipelineProcessingService.cs
@@ -91,7 +91,7 @@ public sealed class PipelineProcessingService(
/// Сырые сообщения очереди в порядке постановки
///
/// Максимум строк; clamp 1..500 — запрашивается больше — вернётся не больше 500.
- /// Строки очереди (item'ы §4.5; внутренний Force в JSON не выходит).
+ /// Строки очереди.
public async Task> ListQueueAsync(int limit, CancellationToken ct)
{
int clamped = Math.Clamp(limit, 1, MaxPageSize);
@@ -213,7 +213,6 @@ public sealed class PipelineProcessingService(
}
else
{
- // Старые записи (до сохранения dialog_id/msg_id): текст сохранился — возвращаем без ссылки
var item = new QueueItemDto
{
Id = PrefixId.New(PipelineIdPrefixes.Queue),
diff --git a/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Checks.cs b/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Checks.cs
index 4ef47dd..06e239e 100644
--- a/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Checks.cs
+++ b/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Checks.cs
@@ -12,11 +12,6 @@ public sealed partial class PipelineWorkerService
{
// ── Проверки воркера ───────────────────────────────────────────────────
- // включён и msg_at старше archiveAfterDays суток → устарело. msg_at=0/пусто — не устарело. Флаги — из
- // снимка прохода (читаются один раз на pump, не на каждое сообщение).
- // row: Строка очереди (время исходного сообщения).
- // run: Снимок настроек воркера (autoArchive/archiveAfterDays).
- // Возвращает: True — сообщение старше срока до автоархива (не заводим в систему).
private bool IsStaleAsync(QueueItemDto row, WorkerRunSettings run)
{
if (row.MsgAtMs == 0)
@@ -39,14 +34,6 @@ public sealed partial class PipelineWorkerService
return nowMs - row.MsgAtMs > maxAgeMs;
}
- // соответствующий флаг (budgetRequiredHire/Order) и суммы нет ни в разборе, ни в тексте → карточку не создаём.
- // Суммой считаем нормализованный бюджет разбора либо сумму с валютой в тексте (AmountParser.Parse,
- // rules_svc.extract_amounts). Claim дедупа при отсеве снимается — после выключения фильтра сообщение можно
- // parsed: Разбор сообщения (тип заявки и бюджет).
- // text: Исходный текст сообщения (fallback-источник суммы).
- // run: Снимок настроек воркера (флаги budgetRequiredHire/Order).
- // ct: Токен отмены.
- // Возвращает: True — фильтр требует сумму, а её нет (сообщение пропускаем).
private Task SkipNoBudgetAsync(
AiParsedCardDto parsed,
string text,
@@ -72,10 +59,6 @@ public sealed partial class PipelineWorkerService
return Task.FromResult(AmountParser.Parse(text).Count == 0);
}
- // ведущей «~», не стоп-слова стека, не дубликаты — до MaxMlTermsAdded терминов.
- // terms: Термины класса из ответа ML (dec.terms).
- // stack: Стек локального разбора (к нему добавляем; лимит 12 доберёт CardComposer).
- // Возвращает: Стек с добавленными терминами ML.
private static IReadOnlyList MergeMlTerms(IReadOnlyList terms, IReadOnlyList stack)
{
var merged = new List(stack);
@@ -110,9 +93,6 @@ public sealed partial class PipelineWorkerService
return merged;
}
- // text: Текст сообщения.
- // ct: Токен отмены.
- // Возвращает: Решение модели либо фиксированный «не готов/не уверен».
private async Task PredictSafelyAsync(string text, CancellationToken ct)
{
try
@@ -125,9 +105,6 @@ public sealed partial class PipelineWorkerService
}
}
- // text: Текст сообщения.
- // ct: Токен отмены.
- // Возвращает: Решение фильтра либо пропуск при сбое.
private async Task FilterSafelyAsync(string text, CancellationToken ct)
{
try
diff --git a/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Decisions.cs b/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Decisions.cs
index d83c6af..af52578 100644
--- a/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Decisions.cs
+++ b/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Decisions.cs
@@ -9,10 +9,6 @@ namespace Deal.Modules.Pipeline.Application.Services;
public sealed partial class PipelineWorkerService
{
- // Инкрементирует KV-счётчики решений после pump: mlDecisions = mlStored+mlDrop, aiDecisions = aiStored+aiDrop.
- // Read-modify-write через ISettingsStore (те же ключи читает LocalMlClient.StatusAsync —
- // state: Накопители результата pump.
- // ct: Токен отмены.
private async Task TrackDecisionsAsync(PumpState state, CancellationToken ct)
{
int ml = state.MlStored + state.MlDrop;
diff --git a/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Learning.cs b/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Learning.cs
index c88cd50..e0a6d14 100644
--- a/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Learning.cs
+++ b/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Learning.cs
@@ -11,15 +11,6 @@ public sealed partial class PipelineWorkerService
{
// ── Карточка и обучение ML ────────────────────────────────────────────
- // Создаёт карточку (CardComposer + PipelineCardWriter: AddCard → LinkDedup → чтение) и снимает строку очереди.
- // Dedup-claim НЕ удаляется: он уже связан с карточкой (IPipelineStore.LinkAsync в писателе;
- // state: Накопители результата pump.
- // parsed: Разбор сообщения (поля карточки + назначенная колонка).
- // row: Строка очереди (метаданные канала/текст/время).
- // digest: SHA1-hex текста (claim уже заявлен и будет связан с карточкой).
- // isAiPath: true — ИИ/локальный путь (aiStored); false — ML-ветка (mlStored).
- // ct: Токен отмены.
- // Возвращает: Созданная карточка (полный CardDto, для CreatedCards/обучения).
private async Task CreateCardAndDropAsync(
PumpState state,
AiParsedCardDto parsed,
@@ -43,12 +34,6 @@ public sealed partial class PipelineWorkerService
return card;
}
- // без активных правил и не-suggested) и тип (t:hire/t:order при is_vacancy_known).
- // «Не знаю» (inbox) и служебные колонки не учим; доска с активными правилами/ИИ-предложение — тоже
- // card: Созданная карточка (реальная колонка после ContainerAccepts-страховки).
- // parsed: Разбор, на котором собрана карточка (тип/спам из классификатора).
- // text: Текст сообщения (обучающий пример — как source_msg карточки).
- // ct: Токен отмены.
private Task LearnFromAiCardAsync(
CardDto card,
AiParsedCardDto parsed,
diff --git a/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Pump.cs b/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Pump.cs
index c75c634..86175d0 100644
--- a/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Pump.cs
+++ b/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Pump.cs
@@ -21,8 +21,6 @@ public sealed partial class PipelineWorkerService
/// Сводка прохода: счётчики решений + созданные карточки (SSE new_card).
public async Task PumpOnceAsync(CancellationToken ct)
{
- // значения/дефолты резолвятся в снимок, который передаётся во все проверки сообщений батча и
- // LocalFieldsParser (раньше настройки читались на каждое сообщение — до 16 чтений таблицы за pump).
WorkerRunSettings runSettings = await LoadRunSettingsAsync(_settings, ct);
var state = new PumpState();
await PumpNewPassAsync(state, runSettings, ct);
@@ -32,10 +30,6 @@ public sealed partial class PipelineWorkerService
}
- // Батч сообщений статуса new: устарело/правила/повтор отсекаются, остальные — ML или в filtered.
- // Настройки (сроки/флаги ML/ИИ/бюджета) — из снимка прохода: читаются один раз, не на каждое сообщение.
- // state: Накопители результата pump (счётчики/карточки).
- // ct: Токен отмены.
private async Task PumpNewPassAsync(
PumpState state,
WorkerRunSettings run,
@@ -62,8 +56,6 @@ public sealed partial class PipelineWorkerService
continue;
}
- // Глобальные исключения тенанта (§5.14): стоп ДО дедупа/ML/ИИ, чтобы не тратить токены;
- // причина называет конкретное сработавшее исключение (source=stop).
GlobalExclusionResult? exclusion = GlobalExclusionRules.Match(row.Text, run.GlobalExclusions);
if (exclusion is not null)
{
@@ -81,8 +73,6 @@ public sealed partial class PipelineWorkerService
continue;
}
- // Claim атомарен (INSERT … ON CONFLICT DO NOTHING): false — хэш уже заявлен ПАРАЛЛЕЛЬНЫМ проходом
- // pump после нашей проверки ExistsAsync. Карточку не создаём (иначе два прохода завели бы две) —
if (!await _store.ClaimAsync(digest, ct))
{
await RejectRowAsync(row, SourceDup, StageDup, DupReason, string.Empty, ct);
@@ -190,10 +180,6 @@ public sealed partial class PipelineWorkerService
}
- // Батч сообщений статуса filtered: локальный путь (aiEnabled=false) или фильтр+классификация ИИ.
- // Настройки — из снимка прохода (читаются один раз, не на каждое сообщение).
- // state: Накопители результата pump (счётчики/карточки).
- // ct: Токен отмены.
private async Task PumpFilteredPassAsync(
PumpState state,
WorkerRunSettings run,
@@ -253,15 +239,12 @@ public sealed partial class PipelineWorkerService
parsed = null;
}
- // эвристикой). Стемп ставится ДО создания карточки (карточка получает is_vacancy_known=true) и
- // переживает force-отмену вердикта «спам»; локальный разбор aiFail/aiEnabled=false стемпа не имеет.
if (parsed is not null)
{
parsed = parsed with { IsVacancyKnown = true };
}
}
- // отменяет вердикт — пользователь уже подтвердил релевантность, карточку создаём без обучения «спаму».
bool isSpam = !filter.Pass || (parsed?.IsSpam ?? false);
if (force && filter.Pass && parsed?.IsSpam == true)
{
diff --git a/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Rejections.cs b/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Rejections.cs
index 60c2111..7d0cf5c 100644
--- a/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Rejections.cs
+++ b/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Rejections.cs
@@ -10,11 +10,6 @@ public sealed partial class PipelineWorkerService
{
// ── Отсев / удаление строк ─────────────────────────────────────────────
- // row: Строка очереди — источник полей записи (текст/канал/время).
- // source: Источник решения: stop|ml|ai|stale|dup.
- // stage: Этап отсева: length|stop|resume|type|budget|stale|spam_ml|spam_ai|filter_ai|dup.
- // kw: Сработавшая фраза/маркер правила (пусто — не правило).
- // ct: Токен отмены.
private Task RejectRowAsync(
QueueItemDto row,
string source,
@@ -39,10 +34,6 @@ public sealed partial class PipelineWorkerService
}, ct);
}
- // row: Строка очереди.
- // digest: Хэш текста (для снятия незанятого dedup-claim).
- // withDedup: Снять claim дедупа (true — сообщение можно обработать заново после отсева).
- // ct: Токен отмены.
private async Task DropRowAsync(
QueueItemDto row,
string digest,
@@ -57,9 +48,6 @@ public sealed partial class PipelineWorkerService
await _store.RemoveAsync(row.Id, ct);
}
- // row: Строка очереди со старым msg_at.
- // run: Снимок настроек воркера (срок до автоархива уже прочитан).
- // ct: Токен отмены.
private async Task DropStaleAsync(
QueueItemDto row,
WorkerRunSettings run,
diff --git a/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Settings.cs b/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Settings.cs
index d871067..2bc9fd6 100644
--- a/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Settings.cs
+++ b/src/core/Deal.Modules.Pipeline/Application/Services/PipelineWorkerService.Settings.cs
@@ -10,19 +10,6 @@ namespace Deal.Modules.Pipeline.Application.Services;
public sealed partial class PipelineWorkerService
{
- // Снимок настроек воркера на один проход pump: значения прочитаны ОДИН раз (LoadRunSettingsAsync)
- // и передаются во все проверки сообщений батча и в LocalFieldsParser — раньше настройки читались на каждое
- // сообщение (ReadBool/ReadInt + GetAllAsync парсера — до 16 чтений таблицы за pump). Значения — из
- // типизированного снимка TenantSettingsSnapshot (C30).
- // Settings: Типизированный снимок переопределений тенанта (передаётся LocalFieldsParser).
- // AutoArchive: Флаг автоархива (срок актуальности сообщения).
- // ArchiveAfterDays: Срок до автоархива, сутки (stale/причина отсева).
- // MlEnabled: ML-слот включён (не false — прогноз/решения).
- // AiEnabled: ИИ-слот включён (классификация/фильтр).
- // BudgetRequiredHire: Фильтр «без суммы» для найма.
- // BudgetRequiredOrder: Фильтр «без суммы» для разовых заказов.
- // WantedType: Тип заявок «что собираем» (both|vacancy|freelance).
- // GlobalExclusions: Снимок глобальных исключений (стоп до ML/ИИ, §5.14).
private sealed record WorkerRunSettings(
TenantSettingsSnapshot Settings,
bool AutoArchive,
@@ -56,11 +43,6 @@ public sealed partial class PipelineWorkerService
LoadGlobalExclusions(settings));
}
- // Собирает снимок глобальных исключений тенанта (§5.14): списки слов/локаций/типов и границы бюджета.
- // Граница бюджета 0 в настройках трактуется как «не задана» (null); сравнение сумм — числовое,
- // без конвертации валют (у настройки нет валюты).
- // settings: Типизированный снимок настроек тенанта.
- // Возвращает: Снимок исключений для чистой проверки сообщений батча.
private static GlobalExcludeSettings LoadGlobalExclusions(TenantSettingsSnapshot settings)
{
int from = settings.GetInt(SettingsKeys.ExcludeBudgetFrom, SettingsDefaults.ExcludeBudgetFrom);
@@ -73,9 +55,6 @@ public sealed partial class PipelineWorkerService
BudgetTo: to > 0 ? to : null);
}
- // ствующая/повреждённая строка → дефолт «both» (даёт GetString с дефолтом + нормализация).
- // value: Значение настройки wantedType (JSON-строка либо дефолт из SettingsDefaults).
- // Возвращает: Значение в нижнем регистре (пустое хранимое значение остаётся пустым — как wantedType="").
private static string NormalizeWantedType(string value)
{
return value.Trim().ToLowerInvariant();
diff --git a/src/core/Deal.Modules.Settings/Application/Models/IncomingRules.cs b/src/core/Deal.Modules.Settings/Application/Models/IncomingRules.cs
index b64fdee..d2da8e4 100644
--- a/src/core/Deal.Modules.Settings/Application/Models/IncomingRules.cs
+++ b/src/core/Deal.Modules.Settings/Application/Models/IncomingRules.cs
@@ -65,12 +65,10 @@ public sealed class IncomingRules(ISettingsStore store)
/// Вердикт: {pass, reason, stage:1, kind, kw}.
public async Task CheckAsync(string? text, CancellationToken ct)
{
- // Читаем переопределения одним запросом (GetAllAsync) и считаем чистой функцией над снимком
RulesSettings settings = ReadSettings(await TenantSettingsSnapshot.LoadAsync(store, ct));
return Evaluate(text, settings);
}
- // Возвращает: Вердикт: причина/правило/фраза либо проход.
private static IncomingRulesResult Evaluate(string? text, RulesSettings settings)
{
string trimmed = (text ?? string.Empty).Trim();
@@ -124,12 +122,6 @@ public sealed class IncomingRules(ISettingsStore store)
return new IncomingRulesResult(Pass: true, Reason: null, Stage, KindPass, Kw: string.Empty);
}
- // lower: Текст, приведённый к нижнему регистру (casefold).
- // resumeMarkers: Нормализованные маркеры резюме (в порядке списка настроек).
- // hireMarkers: Нормализованные маркеры найма (для guard).
- // Возвращает: Найденный маркер (нормализованный) или null — текст не похож на резюме.
- // работодателя («…вакансия…, присылайте резюме»), а не резюме соискателя — пропускаем маркер
- // порядок произвольный).
private static string? FindResumeMarker(
string lower,
IReadOnlyList resumeMarkers,
@@ -172,9 +164,6 @@ public sealed class IncomingRules(ISettingsStore store)
return false;
}
- // Собирает вердикт-отсев: stage всегда 1, причина фиксированная.
- // kind: Правило, сработавшее на тексте (length|stop|resume|type).
- // reason: Причина для UI.
private static IncomingRulesResult Fail(
string kind,
string reason,
@@ -183,8 +172,6 @@ public sealed class IncomingRules(ISettingsStore store)
return new IncomingRulesResult(Pass: false, Reason: reason, Stage, kind, Kw);
}
- // settings: Снимок настроек тенанта (дефолты, перекрытые сохранёнными).
- // Возвращает: Снимок значений для чистой проверки.
private static RulesSettings ReadSettings(TenantSettingsSnapshot settings)
{
return new RulesSettings(
@@ -208,8 +195,6 @@ public sealed class IncomingRules(ISettingsStore store)
return wanted.Length > 0 ? wanted : WantedTypeBoth;
}
- // markers: Список маркеров как сохранён (или дефолт).
- // Возвращает: Нормализованный список в порядке исходного (детерминированно).
private static IReadOnlyList NormalizeMarkers(IReadOnlyList markers)
{
var result = new List(markers.Count);
@@ -225,12 +210,6 @@ public sealed class IncomingRules(ISettingsStore store)
return result;
}
- // MinLen: Минимальная длина текста (minLen).
- // StopPhrases: Стоп-фразы как сохранены (kw отдаётся исходным списком).
- // BlockResumes: True — резюме соискателей отсекаем.
- // ResumeMarkers: Нормализованные маркеры резюме (для FindResumeMarker).
- // HireMarkers: Нормализованные маркеры найма (guard «резюме» + тип заявки).
- // WantedType: Тип собираемых заявок (both|vacancy|freelance).
private sealed record RulesSettings(
int MinLen,
IReadOnlyList StopPhrases,
diff --git a/src/core/Deal.Modules.Settings/Application/Models/MyPromptDto.cs b/src/core/Deal.Modules.Settings/Application/Models/MyPromptDto.cs
index b2937b6..c4ef1f7 100644
--- a/src/core/Deal.Modules.Settings/Application/Models/MyPromptDto.cs
+++ b/src/core/Deal.Modules.Settings/Application/Models/MyPromptDto.cs
@@ -1,7 +1,7 @@
namespace Deal.Modules.Settings.Application.Models;
///
-/// Элемент «Моих промптов»
+/// Элемент «Моих промптов».
///
/// Идентификатор промпта (начинается с pp_).
/// Название (как показывается пользователю).
diff --git a/src/core/Deal.Modules.Settings/Application/Models/PublicSettingsDto.cs b/src/core/Deal.Modules.Settings/Application/Models/PublicSettingsDto.cs
index 59ba100..435b8ae 100644
--- a/src/core/Deal.Modules.Settings/Application/Models/PublicSettingsDto.cs
+++ b/src/core/Deal.Modules.Settings/Application/Models/PublicSettingsDto.cs
@@ -1,7 +1,7 @@
namespace Deal.Modules.Settings.Application.Models;
///
-/// Публичный снимок настроек тенанта — тело ответа GET/PATCH /api/settings
+/// Публичный снимок настроек тенанта — тело ответа GET/PATCH /api/settings.
///
public sealed record PublicSettingsDto
{
diff --git a/src/core/Deal.Modules.Settings/Application/Models/SettingsDefaults.cs b/src/core/Deal.Modules.Settings/Application/Models/SettingsDefaults.cs
index e4c9781..5aecc27 100644
--- a/src/core/Deal.Modules.Settings/Application/Models/SettingsDefaults.cs
+++ b/src/core/Deal.Modules.Settings/Application/Models/SettingsDefaults.cs
@@ -122,7 +122,6 @@ public static class SettingsDefaults
///
public const bool BlockResumes = true;
- // ── Глобальные исключения (стоп-уровень, §5.14) ──
///
/// Дефолт «excludeKeywords»
@@ -257,7 +256,6 @@ public static class SettingsDefaults
///
public const bool DiscPaused = false;
- // Возвращает: Словарь: id провайдера → конфиг с пустым ключом, базовым URL и первой моделью.
private static IReadOnlyDictionary BuildDefaultAiConfigs()
{
var result = new Dictionary(AiProviders.All.Count);
diff --git a/src/core/Deal.Modules.Settings/Application/Models/SettingsKeys.cs b/src/core/Deal.Modules.Settings/Application/Models/SettingsKeys.cs
index f47dd32..4ea4d97 100644
--- a/src/core/Deal.Modules.Settings/Application/Models/SettingsKeys.cs
+++ b/src/core/Deal.Modules.Settings/Application/Models/SettingsKeys.cs
@@ -331,7 +331,7 @@ public static class SettingsKeys
///
/// Возвращает категорию публичного ключа или null, если ключ не в каталоге.
///
- /// Имя ключа (camelCase, как в api-map).
+ /// Имя ключа.
/// Категория ключа или null для неизвестного/внутреннего ключа.
public static SettingKind? FindPublicKind(string key)
{
diff --git a/src/core/Deal.Modules.Settings/Application/Registrars/SettingsModuleRegistrar.cs b/src/core/Deal.Modules.Settings/Application/Registrars/SettingsModuleRegistrar.cs
index 14d6eba..4d06ef9 100644
--- a/src/core/Deal.Modules.Settings/Application/Registrars/SettingsModuleRegistrar.cs
+++ b/src/core/Deal.Modules.Settings/Application/Registrars/SettingsModuleRegistrar.cs
@@ -12,8 +12,6 @@ public static class SettingsModuleRegistrar
///
/// Регистрирует сервисы модуля Settings в контейнере.
///
- /// Коллекция сервисов.
- /// Коллекция сервисов для цепочки вызовов.
public static IServiceCollection AddSettingsModule(this IServiceCollection services)
{
services.AddScoped();
diff --git a/src/core/Deal.Modules.Settings/Application/Services/RatesService.cs b/src/core/Deal.Modules.Settings/Application/Services/RatesService.cs
index e6bbf07..c32f5cd 100644
--- a/src/core/Deal.Modules.Settings/Application/Services/RatesService.cs
+++ b/src/core/Deal.Modules.Settings/Application/Services/RatesService.cs
@@ -73,8 +73,6 @@ public sealed class RatesService(
return true;
}
- // ct: Токен отмены.
- // пустой список — no-op. При сбое источника (кэш не записан) слушатели НЕ вызываются.
private async Task NotifyRatesChangedAsync(CancellationToken ct)
{
foreach (IRatesChangedListener listener in listeners)
@@ -166,10 +164,7 @@ public sealed class RatesService(
await store.SetAsync(SettingsKeys.RatesCache, JsonSerializer.Serialize(cache, JsonOptions), ct);
}
- // Нормализует значение настройки источника к известным source кэша (mock|cbr).
- // setting: Значение настройки rateSource.
private static string NormalizeSource(string setting) => setting == MockRateSource ? MockSource : CbrSource;
- // Возвращает: Количество миллисекунд с 1970-01-01 UTC.
private static long UtcNowMs() => DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
}
diff --git a/src/core/Deal.Modules.Settings/Application/Services/SettingsService.PatchMyPrompts.cs b/src/core/Deal.Modules.Settings/Application/Services/SettingsService.PatchMyPrompts.cs
index 9bd1bc2..8a766cd 100644
--- a/src/core/Deal.Modules.Settings/Application/Services/SettingsService.PatchMyPrompts.cs
+++ b/src/core/Deal.Modules.Settings/Application/Services/SettingsService.PatchMyPrompts.cs
@@ -8,8 +8,6 @@ namespace Deal.Modules.Settings.Application.Services;
// и чтение полей элемента: ReadTrimmedField/ReadPromptId). colState — passthrough в главном ApplyPatchAsync.
public sealed partial class SettingsService
{
- // array: JSON-массив из тела PATCH.
- // Возвращает: JSON-строка списка {id,name,description,prompt} (camelCase).
private static string SerializeCleanMyPrompts(JsonElement array)
{
var clean = new List();
diff --git a/src/core/Deal.Modules.Settings/Application/Services/SettingsService.PatchScalarKeys.cs b/src/core/Deal.Modules.Settings/Application/Services/SettingsService.PatchScalarKeys.cs
index 3cc21aa..d40fec3 100644
--- a/src/core/Deal.Modules.Settings/Application/Services/SettingsService.PatchScalarKeys.cs
+++ b/src/core/Deal.Modules.Settings/Application/Services/SettingsService.PatchScalarKeys.cs
@@ -7,9 +7,6 @@ namespace Deal.Modules.Settings.Application.Services;
// int/string-ключи и списки строк (ApplyIntKey/ApplyStringKey/SerializeStringList).
public sealed partial class SettingsService
{
- // body: Тело PATCH (не мутируется).
- // overrides: Сохранённые переопределения (для «другого конца» интервала).
- // Возвращает: Ключ → уже склампированное значение; ключи в результате пропускаются общим циклом.
private static Dictionary PrepareDelayClamps(Dictionary body, Dictionary overrides)
{
var result = new Dictionary(StringComparer.Ordinal);
@@ -122,8 +119,6 @@ public sealed partial class SettingsService
writes[key] = JsonSerializer.Serialize(text);
}
- // array: JSON-массив из тела PATCH.
- // Возвращает: JSON-строка массива строк.
private static string SerializeStringList(JsonElement array)
{
var items = new List();
diff --git a/src/core/Deal.Modules.Settings/Application/Services/SettingsService.PatchSecrets.cs b/src/core/Deal.Modules.Settings/Application/Services/SettingsService.PatchSecrets.cs
index 91c5a2d..2d42920 100644
--- a/src/core/Deal.Modules.Settings/Application/Services/SettingsService.PatchSecrets.cs
+++ b/src/core/Deal.Modules.Settings/Application/Services/SettingsService.PatchSecrets.cs
@@ -7,10 +7,6 @@ namespace Deal.Modules.Settings.Application.Services;
// apiKey), включая удаление переопределения, вернувшегося к дефолту (EqualsDefault).
public sealed partial class SettingsService
{
- // value: JSON-объект {id провайдера → конфиг} из тела PATCH.
- // overrides: Сохранённые переопределения.
- // writes: Накопитель записей (key → valueJson).
- // removals: Список ключей на удаление (переопределение вернулось к дефолту).
private void ApplyAiConfigsKey(
JsonElement value,
Dictionary overrides,
diff --git a/src/core/Deal.Modules.Settings/Application/Services/SettingsService.PublicForms.cs b/src/core/Deal.Modules.Settings/Application/Services/SettingsService.PublicForms.cs
index b6435d9..13c893a 100644
--- a/src/core/Deal.Modules.Settings/Application/Services/SettingsService.PublicForms.cs
+++ b/src/core/Deal.Modules.Settings/Application/Services/SettingsService.PublicForms.cs
@@ -4,8 +4,6 @@ namespace Deal.Modules.Settings.Application.Services;
public sealed partial class SettingsService
{
- // config: Сохранённая конфигурация (apiKey — enc: или пустая строка).
- // Возвращает: DTO с флагом keySet и маской keyMasked.
private AiConfigPublicDto ToPublic(AiConfigSetting config)
{
string plainKey = secretCipher.Decrypt(config.ApiKey);
@@ -16,9 +14,6 @@ public sealed partial class SettingsService
KeyMasked: MaskSecret(plainKey));
}
- // Маска СЕКРЕТА (apiKey ИИ): всегда скрывает, кроме пустого — короткие значения не раскрываются
- // value: Открытый секрет (не null).
- // Возвращает: Маскированная строка.
private static string MaskSecret(string value)
{
return value.Length switch
diff --git a/src/core/Deal.Modules.Settings/Application/Services/SettingsService.ReadMerge.cs b/src/core/Deal.Modules.Settings/Application/Services/SettingsService.ReadMerge.cs
index 61169bf..8446961 100644
--- a/src/core/Deal.Modules.Settings/Application/Services/SettingsService.ReadMerge.cs
+++ b/src/core/Deal.Modules.Settings/Application/Services/SettingsService.ReadMerge.cs
@@ -4,11 +4,8 @@ using Deal.Modules.Settings.Application.Models;
namespace Deal.Modules.Settings.Application.Services;
-// TryRead* и Merge* по категориям ключей (канон мягкого чтения для GET /api/settings).
public sealed partial class SettingsService
{
- // ct: Токен отмены.
- // Возвращает: Словарь: ключ → JsonElement (клоны, живут после вызова). Повреждённые JSON-строки пропускаются.
private async Task> LoadStoredOverridesAsync(CancellationToken ct)
{
var result = new Dictionary(StringComparer.Ordinal);
@@ -30,9 +27,6 @@ public sealed partial class SettingsService
return result;
}
- // element: JSON-значение.
- // value: Прочитанное число (диапазон long сжимается до int).
- // Возвращает: True — значение прочитано; иначе False (нечисловое — пропуск ключа).
private static bool TryReadInt(JsonElement element, out int value)
{
long wide;
@@ -63,9 +57,6 @@ public sealed partial class SettingsService
return true;
}
- // element: JSON-значение.
- // value: Прочитанное значение.
- // Возвращает: True — значение вида true/false.
private static bool TryReadBool(JsonElement element, out bool value)
{
if (element.ValueKind is JsonValueKind.True or JsonValueKind.False)
@@ -78,9 +69,6 @@ public sealed partial class SettingsService
return false;
}
- // element: JSON-значение.
- // text: Текст; null — если значение не скаляр (объект/массив/null).
- // Возвращает: True — значение скалярное.
private static bool TryReadText(JsonElement element, out string? text)
{
switch (element.ValueKind)
diff --git a/src/core/Deal.Modules.Settings/Application/Services/SettingsService.cs b/src/core/Deal.Modules.Settings/Application/Services/SettingsService.cs
index be6799b..8038b4f 100644
--- a/src/core/Deal.Modules.Settings/Application/Services/SettingsService.cs
+++ b/src/core/Deal.Modules.Settings/Application/Services/SettingsService.cs
@@ -84,7 +84,7 @@ public sealed partial class SettingsService(
///
/// Публичный снимок настроек
///
- /// Полный public-снимок §4.6 (тело GET/PATCH /api/settings).
+ /// Полный public-снимок (тело GET/PATCH /api/settings).
public async Task GetPublicAsync(CancellationToken ct)
{
Dictionary overrides = await LoadStoredOverridesAsync(ct);
@@ -94,7 +94,7 @@ public sealed partial class SettingsService(
///
/// Частичное обновление настроек
///
- /// Тело PATCH — произвольный JSON-объект из публичных ключей §4.6.
+ /// Тело PATCH — произвольный JSON-объект из публичных ключей.
/// Полный public-снимок после применения (как GET).
public async Task ApplyPatchAsync(Dictionary body, CancellationToken ct)
{
@@ -102,7 +102,6 @@ public sealed partial class SettingsService(
Dictionary overrides = await LoadStoredOverridesAsync(ct);
- // клампы 5..600 и swap при min > max; при одном конце — кламп относительно сохранённого другого.
Dictionary preparedDelays = PrepareDelayClamps(body, overrides);
var writes = new Dictionary(StringComparer.Ordinal);
@@ -180,7 +179,6 @@ public sealed partial class SettingsService(
await store.RemoveAsync(key, ct);
}
- // (реализация порта — ConversionRecomputer модуля Kanban; пустой список — no-op).
if (AffectsConversion(body))
{
await NotifyRatesChangedAsync(ct);
@@ -189,8 +187,6 @@ public sealed partial class SettingsService(
return await GetPublicAsync(ct);
}
- // body: Тело PATCH.
- // Возвращает: True — нужен пересчёт конверсий после применения.
private static bool AffectsConversion(Dictionary body)
{
return HasNonNull(body, SettingsKeys.TargetCurrency) || HasNonNull(body, SettingsKeys.ConversionOn);
@@ -205,7 +201,6 @@ public sealed partial class SettingsService(
return body.TryGetValue(key, out JsonElement value) && value.ValueKind != JsonValueKind.Null;
}
- // ct: Токен отмены.
private async Task NotifyRatesChangedAsync(CancellationToken ct)
{
foreach (IRatesChangedListener listener in listeners)
@@ -252,7 +247,6 @@ public sealed partial class SettingsService(
ResumeMarkers = MergeStringList(overrides, SettingsKeys.ResumeMarkers, SettingsDefaults.ResumeMarkers),
BlockResumes = MergeBool(overrides, SettingsKeys.BlockResumes, SettingsDefaults.BlockResumes),
- // Глобальные исключения (§5.14)
ExcludeKeywords = MergeStringList(overrides, SettingsKeys.ExcludeKeywords, SettingsDefaults.ExcludeKeywords),
ExcludeLocations = MergeStringList(overrides, SettingsKeys.ExcludeLocations, SettingsDefaults.ExcludeLocations),
ExcludeTypes = MergeStringList(overrides, SettingsKeys.ExcludeTypes, SettingsDefaults.ExcludeTypes),
diff --git a/src/core/Deal.Modules.Telegram/Application/DialogsService.cs b/src/core/Deal.Modules.Telegram/Application/DialogsService.cs
index 1bd52f8..0265b17 100644
--- a/src/core/Deal.Modules.Telegram/Application/DialogsService.cs
+++ b/src/core/Deal.Modules.Telegram/Application/DialogsService.cs
@@ -27,7 +27,7 @@ public sealed class DialogsService(
///
/// Список диалогов каталога для вкладки «Каналы».
///
- /// Диалоги (включённые мониторингом первыми) в форме §4.8.
+ /// Диалоги (включённые мониторингом первыми) в форме.
public Task> ListAsync(CancellationToken ct) => store.ListAsync(ct);
///
@@ -152,7 +152,6 @@ public sealed class DialogsService(
}
catch (Exception exception) when (exception is not OperationCanceledException || !ct.IsCancellationRequested)
{
- // канал остаётся неразобранным — следующий «Перечитать»/первое включение попробует снова.
logger.LogWarning(exception, "Перечитывание канала {DialogId} не удалось", dialogId);
}
}
@@ -186,8 +185,8 @@ public sealed class DialogsService(
/// Последние сообщения диалога для превью.
///
/// Id диалога.
- /// Сколько последних сообщений (1..50; api-map /dialogs/preview limit 1..50).
- /// Сообщения от новых к старым в форме §4.11 ({id, text, time, lead}).
+ /// Сколько последних сообщений.
+ /// Сообщения от новых к старым в форме ({id, text, time, lead}).
public async Task> PreviewAsync(
string dialogId,
int limit,
@@ -209,7 +208,6 @@ public sealed class DialogsService(
return await store.ListMessagesAsync(dialogId, limit, ct).ConfigureAwait(false);
}
- // lead свежих сообщений — по строке TgMessages «m_