Разбить модули Deal.Modules.* по назначению

Application проектов Discovery, Kanban, Pipeline, Settings, Tenants
разделён на Abstractions/Exceptions/Extensions/Models/Registrars/Services;
namespace приведён к путям, using потребителей мигрированы и
дедуплицированы (169 файлов), cref/FQN обновлены.
This commit is contained in:
Rustam Khalimov
2026-09-11 13:18:14 +03:00
parent 31c434ed93
commit cd0b3b606b
356 changed files with 14436 additions and 12906 deletions
@@ -0,0 +1,124 @@
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;
/// <summary>
/// Сервис карточек — единый домен карточки (дашборд и «Выбранные»): leads.py L151279 + L509551
/// и projects.py L103282 (этап 9).
/// </summary>
/// <remarks>
/// Чистый сервис модуля (без EF/HTTP): оркестрирует <see cref="ICardStore"/> (карточки/контейнеры/
/// комментарии/журнал), <see cref="ISettingsStore"/> (кэш курсов <c>ratesCache</c>, настройка напоминаний,
/// Ruling 3/7), <see cref="IMlClient"/> (обучающие сигналы move/trash/restore и счётчики counts) и
/// <see cref="IFileStorage"/> (объекты вложений карточки).
/// Полные карточки (JSON-поля, comments, human-метка time, matchHits) собирает адаптер (маппинг
/// строки → CardDto в хранилище); сервис добавляет поведение: валидации с фиксированными текстами прототипа,
/// пересчёт matchHits при размещении в доску (Ruling 2, ColumnRules), журнал CardMoves и push-сигналы ML,
/// операции пространства «Выбранные» (создание, патч, ссылки, файлы, перенос по стадии с историей,
/// напоминания) на той же сущности карточки.
/// Обучение ML идёт ВСЕГДА и синхронно — выключатель <c>mlEnabled</c> управляет только использованием ML в
/// пайплайне, а не записью действий пользователя (ml_client.py L6–7: «обучение идёт всегда»; Ruling 4).
/// 404-семантика «Карточка не найдена» выражается null-результатом методов; тексты 400 — константы класса.
/// C32: класс разделён на partial-файлы по темам (Operations/Helpers/Selected/Files/Reminders).
/// </remarks>
/// <param name="store">Единый порт хранилища карточек/контейнеров/журнала тенанта.</param>
/// <param name="settings">KV-хранилище настроек тенанта (курсы, напоминания).</param>
/// <param name="mlClient">Клиент ML: PushAsync — обучающий сигнал действия, StatusAsync — счётчики counts.</param>
/// <param name="storage">Файловое хранилище вложений карточки (объекты файлов).</param>
public sealed partial class CardsService
{
private readonly ICardStore _store;
private readonly ISettingsStore _settings;
private readonly IMlClient _mlClient;
private readonly IFileStorage _storage;
/// <summary>
/// Создаёт сервис карточек над портами модуля (поле-захват DI-зависимостей).
/// </summary>
/// <param name="store">Единый порт хранилища карточек/контейнеров/журнала тенанта.</param>
/// <param name="settings">KV-хранилище настроек тенанта (курсы, напоминания).</param>
/// <param name="mlClient">Клиент ML: PushAsync — обучающий сигнал действия, StatusAsync — счётчики counts.</param>
/// <param name="storage">Файловое хранилище вложений карточки (объекты файлов).</param>
public CardsService(ICardStore store, ISettingsStore settings, IMlClient mlClient, IFileStorage storage)
{
_store = store;
_settings = settings;
_mlClient = mlClient;
_storage = storage;
}
// ── Фиксированные строки прототипа (400-детали; leads.py L183184, L239240, dashboard_routes L241) ──
/// <summary>
/// 400 move: целевой контейнер не существует (или это «Неразобранное» — оно допустимо).
/// </summary>
public const string MoveTargetInvalidDetail = "Переносить можно только в существующий контейнер или в «Неразобранное»";
/// <summary>
/// 400 move: исходная колонка archive/trash — из них карточку выводит только restore_lead
/// (кнопка «Вернуть»): прямой перенос в доску миновал бы снятие метки «спам» при возврате из корзины
/// (Ruling 4, PushAsync(spam, 1.0) только в RestoreLeadAsync) и журнал restore.
/// </summary>
public const string MoveSourceRestrictedDetail = "Переносить из корзины, архива или «взятых в работу» нельзя — верните карточку на канбан";
/// <summary>
/// 400 clear-col: колонка не trash/archive (clear_col L239240).
/// </summary>
public const string ClearColInvalidDetail = "Очищать можно только корзину или архив";
/// <summary>
/// 400 комментарий: пустой текст после Trim (dashboard_routes L240241).
/// </summary>
public const string EmptyCommentDetail = "Пустой комментарий";
// ── Журнал CardMoves: действия (leads.py _log_learning L4044) ──────────
// Действие журнала: перенос на доску/в «Неразобранное» (_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";
/// <summary>
/// Автор комментария — «Вы» (свои комментарии, add_comment L262). Единственный источник строки
/// для комментариев карточки.
/// </summary>
public const string CommentAuthor = "Вы";
/// <summary>
/// Human-метка времени свежего комментария (add_comment L259265; «только что» = возраст < 1 мин).
/// Единственный источник строки: адаптер KanbanStore считает её в HumanAge.
/// </summary>
public const string JustNowLabel = "только что";
// Минимальная длина поискового запроса после Trim: q короче → пустой ответ (search L511512, 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;
}