Убрать неиспользуемые using по код-стайлу

Прогон dotnet format (IDE0005) по 4 решениям: удалены лишние using,
оставшиеся после миграции namespace (676 файлов).
This commit is contained in:
Rustam Khalimov
2026-09-11 13:30:57 +03:00
parent 5bfa92a4ac
commit e8b9fab860
675 changed files with 47248 additions and 49911 deletions
@@ -1,257 +1,246 @@
using System.Globalization;
using System.Text.Json;
using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
using Deal.Modules.Cards.Application.Abstractions;
using Deal.Modules.Cards.Application.Dtos;
using Deal.Modules.Cards.Application.Models;
using Deal.Modules.Kanban.Application.Abstractions;
using Deal.Modules.Kanban.Application.Extensions;
using Deal.Modules.Kanban.Application.Models;
using Deal.Modules.Kanban.Application.Registrars;
using Deal.Modules.Kanban.Application.Services;
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 лежит в одноимённом пространстве имён (см. CardsService) —
// внутри Deal.Modules.Kanban.Application имя ColumnRules резолвится в пространство.
using KanbanColumnRules = Deal.Modules.Kanban.Application.ColumnRules.ColumnRules;
using Deal.Infrastructure.Integrations.Abstractions;
using Deal.Infrastructure.Integrations.Exceptions;
using Deal.Infrastructure.Integrations.Extensions;
using Deal.Infrastructure.Integrations.Models;
using Deal.Infrastructure.Integrations.Options;
namespace Deal.Infrastructure.Integrations.Services;
/// <summary>
/// Адаптер ИИ-предложений колонок/ключей — детерминированная эвристика этапа 3 (Ruling 3, план Task 14).
/// </summary>
/// <remarks>
/// Реализует порт <see cref="IColumnSuggester"/> поверх порта <see cref="ICardStore"/> и чистого ядра
/// <see cref="SuggestHeuristics"/> (модуль Kanban): читает «Неразобранное» (ListInboxWithSourceAsync),
/// считает группы слов-тем и создаёт доски suggested=true (RulesJson {mode:"any", keywords:[…]},
/// note-обоснование, цвет/позицию даёт ContainersService) и раскладывает карточки (is_new=TRUE,
/// prev_col='inbox', matchHits по правилам доски — Ruling 2). Причины отказов — детерминированные
/// строки прототипа/Ruling 3: «мало карточек в «Неразобранном» (нужно от 6)», «похожие колонки уже
/// есть или нечего сгруппировать»; кулдаун повторов — KV-ключ <see cref="SettingsKeys.LastSuggestAt"/>
/// (прототип COOLDOWN_S L51 + «недавно предлагали — подождите» L95). Журнал CardMoves/ML-сигналы при
/// раскладке НЕ пишутся (suggest.py _assign_ids L220239 — это не действие пользователя, а предложение).
/// Suggest-keywords читает карточки вне trash/archive (suggest_domain_keywords L172178).
/// </remarks>
/// <param name="store">Порт хранилища (карточки «Неразобранного», переносы в колонки-доски).</param>
/// <param name="settings">KV-хранилище настроек тенанта (кулдаун lastSuggestAt, как KEY suggest.py L52).</param>
/// <param name="containersService">Сервис контейнеров: список существующих и создание suggested-колонок с дефолтами.</param>
public sealed class LocalColumnSuggester(
ICardStore store,
ISettingsStore settings,
ContainersService containersService) : IColumnSuggester
{
// ── Кулдаун повторов (suggest.py COOLDOWN_S L51; KEY lastSuggestAt L52) ──
// Как часто можно переспрашивать ИИ-предложения: 20 минут (COOLDOWN_S = 20 * 60, L51).
private const long CooldownSeconds = 20 * 60;
// ── Детерминированные причины (Ruling 3; строки прототипа suggest.py) ──
// Кулдаун: повторный вызов слишком рано (suggest.py L95 «недавно предлагали — подождите»).
private const string CooldownReason = "недавно предлагали — подождите";
// Мало карточек в «Неразобранном»: {0} — порог MIN_INBOX (suggest.py L102).
private const string TooFewCardsReasonFormat = "мало карточек в «Неразобранном» (нужно от {0})";
// Групп не вышло: темы похожи на существующие доски или карточкам нечего разделить (L159).
private const string NothingGroupedReason = "похожие колонки уже есть или нечего сгруппировать";
// Мало карточек для ключей: нужно хотя бы 3 (suggest_domain_keywords L178).
private const string KeywordsTooFewReason = "мало карточек — сначала накопите заявки (нужно хотя бы 3)";
// Повторяющихся слов-маркеров не нашлось (suggest_domain_keywords L187, текст прототипа).
private const string KeywordsEmptyReason = "ИИ не смог выделить ключи — попробуйте ещё раз";
// Режим правил колонки-предложения: «любое из условий» (suggest.py _rules_for L68 mode: any).
private const string RulesModeAny = "any";
/// <inheritdoc />
public async Task<SuggestColumnsResultDto> SuggestColumnsAsync(CancellationToken ct)
{
if (await WithinCooldownAsync(ct))
{
return new SuggestColumnsResultDto(Ok: false, Created: 0, Reason: CooldownReason, Cooldown: true);
}
IReadOnlyList<CardDto> inbox = await store.ListInboxWithSourceAsync(ct);
if (inbox.Count < SuggestHeuristics.MinInbox)
{
return new SuggestColumnsResultDto(
Ok: false,
Created: 0,
Reason: string.Format(TooFewCardsReasonFormat, SuggestHeuristics.MinInbox),
Cooldown: false);
}
// Существующие (suggested=false) колонки: похожие темы не предлагаем (suggest.py L105, L138139).
IReadOnlyList<ContainerDto> containers = await containersService.ListAsync(ContainerSpaces.Dashboard, ct);
IReadOnlyList<string> existingNames = containers
.Where(container => !container.Suggested)
.Select(container => container.Name)
.ToList();
IReadOnlyList<SuggestedColumnPlan> plans = SuggestHeuristics.PlanColumns(inbox, existingNames);
if (plans.Count == 0)
{
return new SuggestColumnsResultDto(Ok: false, Created: 0, Reason: NothingGroupedReason, Cooldown: false);
}
int created = await StoreSuggestedColumnsAsync(inbox, plans, ct);
if (created == 0)
{
// Все колонки откатаны: карточки групп разобраны между чтением и раскладкой (suggest.py L153156).
return new SuggestColumnsResultDto(Ok: false, Created: 0, Reason: NothingGroupedReason, Cooldown: false);
}
await WriteLastSuggestAtAsync(ct);
return new SuggestColumnsResultDto(Ok: true, Created: created, Reason: null, Cooldown: false);
}
/// <inheritdoc />
public async Task<SuggestKeywordsResultDto> SuggestKeywordsAsync(CancellationToken ct)
{
// Выборка ключей — как suggest_domain_keywords L172176: карточки вне trash/archive с текстом,
// свежие 40 (ListCardsAsync(null) = «все, кроме taken», ORDER BY received_at DESC).
IReadOnlyList<CardDto> cards = await store.ListCardsAsync(new CardsQuery(null), ct);
List<string> texts = cards
.Where(card => card.Col != CardIds.Trash
&& card.Col != CardIds.Archive
&& card.SourceMsg.Trim().Length > 0)
.Take(SuggestHeuristics.KeywordsSampleLimit)
.Select(card => card.SourceMsg.Trim())
.ToList();
if (texts.Count < SuggestHeuristics.MinKeywordsSample)
{
return new SuggestKeywordsResultDto(Ok: false, Keywords: null, Reason: KeywordsTooFewReason);
}
IReadOnlyList<string> keywords = SuggestHeuristics.SuggestDomainKeywords(texts);
if (keywords.Count == 0)
{
return new SuggestKeywordsResultDto(Ok: false, Keywords: null, Reason: KeywordsEmptyReason);
}
return new SuggestKeywordsResultDto(Ok: true, Keywords: keywords, Reason: null);
}
// Создаёт доски-предложения по планам и раскладывает карточки (suggest.py L129156).
// inbox: Снимок «Неразобранного» (карточки планов берутся из него).
// plans: Планы колонок (SuggestHeuristics.PlanColumns, ≤4).
// ct: Токен отмены.
// Возвращает: Сколько досок реально создано (0 — все откатаны из-за разобранных карточек).
// Каждая доска — suggested=true c правилами {mode:"any", keywords:[тема]} и note-обоснованием.
// Перед раскладкой перечитывается «Неразобранное»: карточки, ушедшие из inbox между снимком и
// раскладкой (пользователь/тик), пропускаются — 1:1 со страховкой _assign_ids L231233. Если в
// колонку не легло ни одной карточки, пустая доска-предложение откатывается (_rollback_suggested
// L242248). matchHits считаются по правилам созданной доски (Ruling 2); журнал/ML не пишутся.
private async Task<int> StoreSuggestedColumnsAsync(
IReadOnlyList<CardDto> inbox,
IReadOnlyList<SuggestedColumnPlan> plans,
CancellationToken ct)
{
// Свежий снимок inbox — страховка «карточку уже разобрали» (suggest.py _assign_ids L231233).
HashSet<string> inboxIds = (await store.ListInboxWithSourceAsync(ct))
.Select(card => card.Id)
.ToHashSet(StringComparer.Ordinal);
Dictionary<string, string> textByCardId = inbox
.ToDictionary(card => card.Id, card => card.SourceMsg, StringComparer.Ordinal);
int created = 0;
foreach (SuggestedColumnPlan plan in plans)
{
var rules = new ContainerRulesDto(
Mode: RulesModeAny,
Direction: Array.Empty<string>(),
Keywords: [plan.Word],
Stack: Array.Empty<string>(),
Grade: Array.Empty<string>(),
Exclude: Array.Empty<string>(),
Budget: null);
ContainerDto container = await containersService.CreateAsync(new ContainerCreateDto(
Name: plan.Name,
Description: string.Empty,
Color: null,
Space: ContainerSpaces.Dashboard,
Kind: ContainerKinds.Board,
Suggested: true,
Rules: rules,
Note: plan.Note), ct);
int placed = 0;
foreach (string cardId in plan.CardIds)
{
if (!inboxIds.Contains(cardId))
{
continue; // карточка уже разобрана другим предложением/пользователем (L231–233)
}
IReadOnlyList<MatchHitDto> hits = KanbanColumnRules.ComputeHits(rules, textByCardId[cardId]);
await store.UpdateColumnAsync(new CardColumnUpdateDto(
CardId: cardId,
Col: container.Id,
IsNew: true,
PrevCol: CardIds.Inbox,
ArchivedAt: null,
MatchHits: hits), ct);
placed++;
}
if (placed == 0)
{
// Ничего не легло — пустое предложение не нужно (suggest.py L152156).
await containersService.DeleteAsync(container.Id, ct);
continue;
}
created++;
}
return created;
}
// Сработал ли кулдаун: с последнего успешного предложения прошло меньше 20 минут.
// Повреждённое/отсутствующее значение lastSuggestAt — кулдауна нет (как прототип: значение
// пишется только после успеха, L160–161; битый KV — дефолт «никогда»).
// ct: Токен отмены.
// Возвращает: True — повторный вызов слишком рано (ответ {ok:false, reason, cooldown:true}).
private async Task<bool> WithinCooldownAsync(CancellationToken ct)
{
SettingValue? row = await settings.GetAsync(SettingsKeys.LastSuggestAt, ct);
if (row is null)
{
return false;
}
try
{
using JsonDocument document = JsonDocument.Parse(row.ValueJson);
if (document.RootElement.ValueKind != JsonValueKind.Number)
{
return false;
}
long lastSuggestAt = document.RootElement.GetInt64();
return DateTimeOffset.UtcNow.ToUnixTimeSeconds() - lastSuggestAt < CooldownSeconds;
}
catch (JsonException)
{
return false; // повреждённое значение не должно блокировать предложения
}
}
// Записывает метку успешного предложения (suggest.py L160: set_setting(KEY, time.time())).
// ct: Токен отмены.
private Task WriteLastSuggestAtAsync(CancellationToken ct) =>
settings.SetAsync(
SettingsKeys.LastSuggestAt,
DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(CultureInfo.InvariantCulture),
ct);
}
using System.Globalization;
using System.Text.Json;
using Deal.Contracts.Integrations.Abstractions;
using Deal.Contracts.Integrations.Models;
using Deal.Modules.Cards.Application.Models;
using Deal.Modules.Kanban.Application.Abstractions;
using Deal.Modules.Kanban.Application.Models;
using Deal.Modules.Kanban.Application.Services;
using Deal.Modules.Settings.Application.Abstractions;
using Deal.Modules.Settings.Application.Models;
// Алиас: статический класс ColumnRules лежит в одноимённом пространстве имён (см. CardsService) —
// внутри Deal.Modules.Kanban.Application имя ColumnRules резолвится в пространство.
using KanbanColumnRules = Deal.Modules.Kanban.Application.ColumnRules.ColumnRules;
namespace Deal.Infrastructure.Integrations.Services;
/// <summary>
/// Адаптер ИИ-предложений колонок/ключей — детерминированная эвристика этапа 3 (Ruling 3, план Task 14).
/// </summary>
/// <remarks>
/// Реализует порт <see cref="IColumnSuggester"/> поверх порта <see cref="ICardStore"/> и чистого ядра
/// <see cref="SuggestHeuristics"/> (модуль Kanban): читает «Неразобранное» (ListInboxWithSourceAsync),
/// считает группы слов-тем и создаёт доски suggested=true (RulesJson {mode:"any", keywords:[…]},
/// note-обоснование, цвет/позицию даёт ContainersService) и раскладывает карточки (is_new=TRUE,
/// prev_col='inbox', matchHits по правилам доски — Ruling 2). Причины отказов — детерминированные
/// строки прототипа/Ruling 3: «мало карточек в «Неразобранном» (нужно от 6)», «похожие колонки уже
/// есть или нечего сгруппировать»; кулдаун повторов — KV-ключ <see cref="SettingsKeys.LastSuggestAt"/>
/// (прототип COOLDOWN_S L51 + «недавно предлагали — подождите» L95). Журнал CardMoves/ML-сигналы при
/// раскладке НЕ пишутся (suggest.py _assign_ids L220239 — это не действие пользователя, а предложение).
/// Suggest-keywords читает карточки вне trash/archive (suggest_domain_keywords L172178).
/// </remarks>
/// <param name="store">Порт хранилища (карточки «Неразобранного», переносы в колонки-доски).</param>
/// <param name="settings">KV-хранилище настроек тенанта (кулдаун lastSuggestAt, как KEY suggest.py L52).</param>
/// <param name="containersService">Сервис контейнеров: список существующих и создание suggested-колонок с дефолтами.</param>
public sealed class LocalColumnSuggester(
ICardStore store,
ISettingsStore settings,
ContainersService containersService) : IColumnSuggester
{
// ── Кулдаун повторов (suggest.py COOLDOWN_S L51; KEY lastSuggestAt L52) ──
// Как часто можно переспрашивать ИИ-предложения: 20 минут (COOLDOWN_S = 20 * 60, L51).
private const long CooldownSeconds = 20 * 60;
// ── Детерминированные причины (Ruling 3; строки прототипа suggest.py) ──
// Кулдаун: повторный вызов слишком рано (suggest.py L95 «недавно предлагали — подождите»).
private const string CooldownReason = "недавно предлагали — подождите";
// Мало карточек в «Неразобранном»: {0} — порог MIN_INBOX (suggest.py L102).
private const string TooFewCardsReasonFormat = "мало карточек в «Неразобранном» (нужно от {0})";
// Групп не вышло: темы похожи на существующие доски или карточкам нечего разделить (L159).
private const string NothingGroupedReason = "похожие колонки уже есть или нечего сгруппировать";
// Мало карточек для ключей: нужно хотя бы 3 (suggest_domain_keywords L178).
private const string KeywordsTooFewReason = "мало карточек — сначала накопите заявки (нужно хотя бы 3)";
// Повторяющихся слов-маркеров не нашлось (suggest_domain_keywords L187, текст прототипа).
private const string KeywordsEmptyReason = "ИИ не смог выделить ключи — попробуйте ещё раз";
// Режим правил колонки-предложения: «любое из условий» (suggest.py _rules_for L68 mode: any).
private const string RulesModeAny = "any";
/// <inheritdoc />
public async Task<SuggestColumnsResultDto> SuggestColumnsAsync(CancellationToken ct)
{
if (await WithinCooldownAsync(ct))
{
return new SuggestColumnsResultDto(Ok: false, Created: 0, Reason: CooldownReason, Cooldown: true);
}
IReadOnlyList<CardDto> inbox = await store.ListInboxWithSourceAsync(ct);
if (inbox.Count < SuggestHeuristics.MinInbox)
{
return new SuggestColumnsResultDto(
Ok: false,
Created: 0,
Reason: string.Format(TooFewCardsReasonFormat, SuggestHeuristics.MinInbox),
Cooldown: false);
}
// Существующие (suggested=false) колонки: похожие темы не предлагаем (suggest.py L105, L138139).
IReadOnlyList<ContainerDto> containers = await containersService.ListAsync(ContainerSpaces.Dashboard, ct);
IReadOnlyList<string> existingNames = containers
.Where(container => !container.Suggested)
.Select(container => container.Name)
.ToList();
IReadOnlyList<SuggestedColumnPlan> plans = SuggestHeuristics.PlanColumns(inbox, existingNames);
if (plans.Count == 0)
{
return new SuggestColumnsResultDto(Ok: false, Created: 0, Reason: NothingGroupedReason, Cooldown: false);
}
int created = await StoreSuggestedColumnsAsync(inbox, plans, ct);
if (created == 0)
{
// Все колонки откатаны: карточки групп разобраны между чтением и раскладкой (suggest.py L153156).
return new SuggestColumnsResultDto(Ok: false, Created: 0, Reason: NothingGroupedReason, Cooldown: false);
}
await WriteLastSuggestAtAsync(ct);
return new SuggestColumnsResultDto(Ok: true, Created: created, Reason: null, Cooldown: false);
}
/// <inheritdoc />
public async Task<SuggestKeywordsResultDto> SuggestKeywordsAsync(CancellationToken ct)
{
// Выборка ключей — как suggest_domain_keywords L172176: карточки вне trash/archive с текстом,
// свежие 40 (ListCardsAsync(null) = «все, кроме taken», ORDER BY received_at DESC).
IReadOnlyList<CardDto> cards = await store.ListCardsAsync(new CardsQuery(null), ct);
List<string> texts = cards
.Where(card => card.Col != CardIds.Trash
&& card.Col != CardIds.Archive
&& card.SourceMsg.Trim().Length > 0)
.Take(SuggestHeuristics.KeywordsSampleLimit)
.Select(card => card.SourceMsg.Trim())
.ToList();
if (texts.Count < SuggestHeuristics.MinKeywordsSample)
{
return new SuggestKeywordsResultDto(Ok: false, Keywords: null, Reason: KeywordsTooFewReason);
}
IReadOnlyList<string> keywords = SuggestHeuristics.SuggestDomainKeywords(texts);
if (keywords.Count == 0)
{
return new SuggestKeywordsResultDto(Ok: false, Keywords: null, Reason: KeywordsEmptyReason);
}
return new SuggestKeywordsResultDto(Ok: true, Keywords: keywords, Reason: null);
}
// Создаёт доски-предложения по планам и раскладывает карточки (suggest.py L129156).
// inbox: Снимок «Неразобранного» (карточки планов берутся из него).
// plans: Планы колонок (SuggestHeuristics.PlanColumns, ≤4).
// ct: Токен отмены.
// Возвращает: Сколько досок реально создано (0 — все откатаны из-за разобранных карточек).
// Каждая доска — suggested=true c правилами {mode:"any", keywords:[тема]} и note-обоснованием.
// Перед раскладкой перечитывается «Неразобранное»: карточки, ушедшие из inbox между снимком и
// раскладкой (пользователь/тик), пропускаются — 1:1 со страховкой _assign_ids L231233. Если в
// колонку не легло ни одной карточки, пустая доска-предложение откатывается (_rollback_suggested
// L242248). matchHits считаются по правилам созданной доски (Ruling 2); журнал/ML не пишутся.
private async Task<int> StoreSuggestedColumnsAsync(
IReadOnlyList<CardDto> inbox,
IReadOnlyList<SuggestedColumnPlan> plans,
CancellationToken ct)
{
// Свежий снимок inbox — страховка «карточку уже разобрали» (suggest.py _assign_ids L231233).
HashSet<string> inboxIds = (await store.ListInboxWithSourceAsync(ct))
.Select(card => card.Id)
.ToHashSet(StringComparer.Ordinal);
Dictionary<string, string> textByCardId = inbox
.ToDictionary(card => card.Id, card => card.SourceMsg, StringComparer.Ordinal);
int created = 0;
foreach (SuggestedColumnPlan plan in plans)
{
var rules = new ContainerRulesDto(
Mode: RulesModeAny,
Direction: Array.Empty<string>(),
Keywords: [plan.Word],
Stack: Array.Empty<string>(),
Grade: Array.Empty<string>(),
Exclude: Array.Empty<string>(),
Budget: null);
ContainerDto container = await containersService.CreateAsync(new ContainerCreateDto(
Name: plan.Name,
Description: string.Empty,
Color: null,
Space: ContainerSpaces.Dashboard,
Kind: ContainerKinds.Board,
Suggested: true,
Rules: rules,
Note: plan.Note), ct);
int placed = 0;
foreach (string cardId in plan.CardIds)
{
if (!inboxIds.Contains(cardId))
{
continue; // карточка уже разобрана другим предложением/пользователем (L231–233)
}
IReadOnlyList<MatchHitDto> hits = KanbanColumnRules.ComputeHits(rules, textByCardId[cardId]);
await store.UpdateColumnAsync(new CardColumnUpdateDto(
CardId: cardId,
Col: container.Id,
IsNew: true,
PrevCol: CardIds.Inbox,
ArchivedAt: null,
MatchHits: hits), ct);
placed++;
}
if (placed == 0)
{
// Ничего не легло — пустое предложение не нужно (suggest.py L152156).
await containersService.DeleteAsync(container.Id, ct);
continue;
}
created++;
}
return created;
}
// Сработал ли кулдаун: с последнего успешного предложения прошло меньше 20 минут.
// Повреждённое/отсутствующее значение lastSuggestAt — кулдауна нет (как прототип: значение
// пишется только после успеха, L160–161; битый KV — дефолт «никогда»).
// ct: Токен отмены.
// Возвращает: True — повторный вызов слишком рано (ответ {ok:false, reason, cooldown:true}).
private async Task<bool> WithinCooldownAsync(CancellationToken ct)
{
SettingValue? row = await settings.GetAsync(SettingsKeys.LastSuggestAt, ct);
if (row is null)
{
return false;
}
try
{
using JsonDocument document = JsonDocument.Parse(row.ValueJson);
if (document.RootElement.ValueKind != JsonValueKind.Number)
{
return false;
}
long lastSuggestAt = document.RootElement.GetInt64();
return DateTimeOffset.UtcNow.ToUnixTimeSeconds() - lastSuggestAt < CooldownSeconds;
}
catch (JsonException)
{
return false; // повреждённое значение не должно блокировать предложения
}
}
// Записывает метку успешного предложения (suggest.py L160: set_setting(KEY, time.time())).
// ct: Токен отмены.
private Task WriteLastSuggestAtAsync(CancellationToken ct) =>
settings.SetAsync(
SettingsKeys.LastSuggestAt,
DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(CultureInfo.InvariantCulture),
ct);
}