using Deal.Contracts.Integrations; using Deal.Contracts.Integrations.Models; using Deal.Modules.Kanban.Application.ColumnRules; using Deal.Modules.Kanban.Application.Models; using Deal.Modules.Settings.Application.Abstractions; using Deal.Modules.Settings.Application.Models; using Deal.Modules.Settings.Application.Registrars; using Deal.Modules.Settings.Application.Services; // Алиас: статический класс ColumnRules лежит в одноимённом пространстве имён — внутри пространства имён // Deal.Modules.Kanban.Application имя ColumnRules резолвится в пространство (CS0234), нужен явный алиас. using KanbanColumnRules = Deal.Modules.Kanban.Application.ColumnRules.ColumnRules; using Deal.Modules.Kanban.Application.Abstractions; using Deal.Modules.Kanban.Application.Extensions; using Deal.Modules.Kanban.Application.Registrars; namespace Deal.Modules.Kanban.Application.Services; /// /// Сервис карточек — единый домен карточки (дашборд и «Выбранные»): leads.py L151–279 + L509–551 /// и projects.py L103–282 (этап 9). /// /// /// Чистый сервис модуля (без EF/HTTP): оркестрирует (карточки/контейнеры/ /// комментарии/журнал), (кэш курсов ratesCache, настройка напоминаний, /// Ruling 3/7), (обучающие сигналы move/trash/restore и счётчики counts) и /// (объекты вложений карточки). /// Полные карточки (JSON-поля, comments, human-метка time, matchHits) собирает адаптер (маппинг /// строки → CardDto в хранилище); сервис добавляет поведение: валидации с фиксированными текстами прототипа, /// пересчёт matchHits при размещении в доску (Ruling 2, ColumnRules), журнал CardMoves и push-сигналы ML, /// операции пространства «Выбранные» (создание, патч, ссылки, файлы, перенос по стадии с историей, /// напоминания) на той же сущности карточки. /// Обучение ML идёт ВСЕГДА и синхронно — выключатель mlEnabled управляет только использованием ML в /// пайплайне, а не записью действий пользователя (ml_client.py L6–7: «обучение идёт всегда»; Ruling 4). /// 404-семантика «Карточка не найдена» выражается null-результатом методов; тексты 400 — константы класса. /// C32: класс разделён на partial-файлы по темам (Operations/Helpers/Selected/Files/Reminders). /// /// Единый порт хранилища карточек/контейнеров/журнала тенанта. /// KV-хранилище настроек тенанта (курсы, напоминания). /// Клиент ML: PushAsync — обучающий сигнал действия, StatusAsync — счётчики counts. /// Файловое хранилище вложений карточки (объекты файлов). public sealed partial class CardsService { private readonly ICardStore _store; private readonly ISettingsStore _settings; private readonly IMlClient _mlClient; private readonly IFileStorage _storage; /// /// Создаёт сервис карточек над портами модуля (поле-захват DI-зависимостей). /// /// Единый порт хранилища карточек/контейнеров/журнала тенанта. /// KV-хранилище настроек тенанта (курсы, напоминания). /// Клиент ML: PushAsync — обучающий сигнал действия, StatusAsync — счётчики counts. /// Файловое хранилище вложений карточки (объекты файлов). public CardsService( ICardStore store, ISettingsStore settings, IMlClient mlClient, IFileStorage storage) { _store = store; _settings = settings; _mlClient = mlClient; _storage = storage; } // ── Фиксированные строки прототипа (400-детали; leads.py L183–184, L239–240, dashboard_routes L241) ── /// /// 400 move: целевой контейнер не существует (или это «Неразобранное» — оно допустимо). /// public const string MoveTargetInvalidDetail = "Переносить можно только в существующий контейнер или в «Неразобранное»"; /// /// 400 move: исходная колонка archive/trash — из них карточку выводит только restore_lead /// (кнопка «Вернуть»): прямой перенос в доску миновал бы снятие метки «спам» при возврате из корзины /// (Ruling 4, PushAsync(spam, −1.0) только в RestoreLeadAsync) и журнал restore. /// public const string MoveSourceRestrictedDetail = "Переносить из корзины, архива или «взятых в работу» нельзя — верните карточку на канбан"; /// /// 400 clear-col: колонка не trash/archive (clear_col L239–240). /// public const string ClearColInvalidDetail = "Очищать можно только корзину или архив"; /// /// 400 комментарий: пустой текст после Trim (dashboard_routes L240–241). /// public const string EmptyCommentDetail = "Пустой комментарий"; // ── Журнал CardMoves: действия (leads.py _log_learning L40–44) ────────── // Действие журнала: перенос на доску/в «Неразобранное» (_move L174). private const string ActionMove = "move"; // Действие журнала: в корзину (_move action='trash' L196). private const string ActionTrash = "trash"; // Действие журнала: возврат из архива/корзины (restore_lead L216). private const string ActionRestore = "restore"; // Действие журнала: добавлен комментарий (add_comment L264). private const string ActionComment = "comment"; /// /// Автор комментария — «Вы» (свои комментарии, add_comment L262). Единственный источник строки /// для комментариев карточки. /// public const string CommentAuthor = "Вы"; /// /// Human-метка времени свежего комментария (add_comment L259–265; «только что» = возраст < 1 мин). /// Единственный источник строки: адаптер KanbanStore считает её в HumanAge. /// public const string JustNowLabel = "только что"; // Минимальная длина поискового запроса после Trim: q короче → пустой ответ (search L511–512, Ruling 6). private const int MinSearchQueryLength = 2; // Ограничение результатов поиска: не больше 12 карточек (search L509, Ruling 6). private const int SearchLimit = 12; // Вес сигнала пользователя: действие = истина (ml_client.py USER_WEIGHT L26, Ruling 4). private const double PushWeightUser = 1.0; // Вес снятия метки: возврат из корзины (restore_lead L221, delta=-1.0). private const double PushWeightUnlearn = -1.0; }