Deal — единая кодовая база
ci / build-test (push) Canceled after 0s

SaaS-мониторинг Telegram: ядро (модули Cards/Kanban/Pipeline/Tenants/Settings/
Discovery, Api, Infrastructure), сервисы telegram/ai/ml/storage, фронт Vue,
контракты и grpc-hosting, деплой-конфиги (dev/prod/observability/CI-раннер),
Gitea Actions CI, документация (ТЗ, техдок, api-map, код-стайл, планы, бэклог).

Текущее состояние: все этапы роадмапа 0–12 закрыты, сборка 5 sln 0/0,
тесты 1340/130/52/38/9 зелёные.
This commit is contained in:
Rustam Khalimov
2026-09-11 23:56:47 +03:00
commit 27c7831910
1383 changed files with 158436 additions and 0 deletions
@@ -0,0 +1,114 @@
using System.Globalization;
using Deal.Modules.Kanban.Application.Models;
namespace Deal.Modules.Kanban.Application.ColumnRules;
/// <summary>
/// Человекочитаемое описание правил колонки для note/подсказки.
/// </summary>
public static class RulesDescriber
{
/// <summary>
/// Описывает правила колонки.
/// </summary>
/// <param name="rules">Правила колонки (null/пустые → «без правил (решает ИИ/ML)»).</param>
/// <returns>Строка-расшифровка для UI/промпта.</returns>
public static string Describe(ContainerRulesDto? rules)
{
if (rules is null)
{
return NoRulesText;
}
var parts = new List<string>();
if (rules.Direction.Count > 0)
{
parts.Add($"направление: {JoinLimited(rules.Direction, DirectionLimit)}");
}
if (rules.Stack.Count > 0)
{
parts.Add($"стек: {JoinLimited(rules.Stack, TermsLimit)}");
}
if (rules.Keywords.Count > 0)
{
parts.Add($"слова: {JoinLimited(rules.Keywords, TermsLimit)}");
}
if (rules.Grade.Count > 0)
{
parts.Add($"грейд: {JoinLimited(rules.Grade, TermsLimit)}");
}
if (rules.Levels is { Count: > 0 })
{
parts.Add($"уровень: {JoinLimited(rules.Levels, TermsLimit)}");
}
if (rules.Locations is { Count: > 0 })
{
parts.Add($"локация: {JoinLimited(rules.Locations, TermsLimit)}");
}
if (rules.Types is { Count: > 0 })
{
parts.Add($"тип: {JoinLimited(rules.Types, TermsLimit)}");
}
if (rules.Exclude.Count > 0)
{
parts.Add($"исключено: {JoinLimited(rules.Exclude, TermsLimit)}");
}
AddRangePart(parts, rules.Budget, "бюджет");
AddRangePart(parts, rules.Prices, "цена");
if (parts.Count == 0)
{
return NoRulesText;
}
string mode = string.Equals(rules.Mode, "any", StringComparison.OrdinalIgnoreCase)
? AnyModePrefix
: AllModePrefix;
return mode + " · " + string.Join("; ", parts);
}
private const string NoRulesText = "без правил (решает ИИ/ML)";
private const string AllModePrefix = "все условия";
// Префикс режима «любое из условий».
private const string AnyModePrefix = "любое из условий";
private const int DirectionLimit = 6;
private const int TermsLimit = 8;
// Добавляет в описание диапазон группы (бюджет/цена), если задана хотя бы одна граница.
// parts: Накопитель частей описания.
// range: Диапазон группы (null — группа выключена).
// title: Подпись группы («бюджет»/«цена»).
private static void AddRangePart(
List<string> parts,
BudgetRangeDto? range,
string title)
{
if (range is null || (range.From is null && range.To is null))
{
return;
}
string lo = range.From is not null ? range.From.Value.ToString(CultureInfo.InvariantCulture) : string.Empty;
string hi = range.To is not null ? range.To.Value.ToString(CultureInfo.InvariantCulture) : string.Empty;
string cur = range.Cur ?? string.Empty;
parts.Add($"{title}: {lo}{hi} {cur}".Replace(" ", "").Replace(" ", ""));
}
private static string JoinLimited(IReadOnlyList<string> terms, int limit)
{
int count = Math.Min(terms.Count, limit);
return string.Join(", ", terms.Take(count));
}
}