using Deal.Contracts.Integrations.Models;
using Deal.Modules.Kanban.Application;
using Deal.Modules.Kanban.Application.Models;
using Deal.Modules.Pipeline.Application.Models;
using Deal.Modules.Pipeline.Application.Parse;
namespace Deal.Modules.Pipeline.Application;
///
/// Маппинг локального разбора в контрактный (pipeline.py _local_fields L718–798 → raw-словарь карточки).
///
///
/// Единый модульный способ превратить (LocalFieldsParser, Ruling 7) в форму
/// разбора, которую ест : бюджет нормализуется
/// (ai.py clean_budget L316–326), контакты квалифицируются (build_contacts
/// L389–421), блок «О заявке» — только legacy-суть (), тип — маркерная
/// гипотеза (is_vacancy_known=false), доска — inbox (board=null: «смысловые колонки до ИИ не
/// назначаем», python L954–958/L796–797). Маппинг используют локальные пути воркера (Ruling 8: aiEnabled=false,
/// сбой ИИ, ML-ветка с локальными полями) и адаптер LocalAiClassifier (Ruling 5 — эталон «ядро владельца +
/// тонкий адаптер»), поэтому он живёт в модуле, а не в Infrastructure: один источник истины для обеих сторон.
///
public static class AiCardMapper
{
///
/// Строит разбор карточки из локальных полей и исходного текста (форма классификатора Ruling 5).
///
///
/// Квалификация контактов идёт по строке с fallback-поиском в тексте
/// (python build_contacts L389–421, ≤6, боты/сервисные ссылки отбрасываются) — как при сборке карточки
/// CardComposer'ом, чтобы разбор и карточка не расходились. Вызывающий при необходимости перекрывает
/// поля через with (ML-ветка: доска/тип ML, доклад terms в стек; локальные пути — нет).
///
/// Локальные поля (заголовок/суть/стек/бюджет/контакты/признак найма).
/// Исходный текст сообщения (fallback-источник кандидатов контактов).
/// Контрактный разбор: бюджет нормализован, контакты квалифицированы, board=null, is_vacancy_known=false.
public static AiParsedCardDto FromLocal(LocalParsedFields fields, string text)
{
CardBudgetDto? normalized = BudgetNormalizer.Normalize(fields.Budget);
AiBudgetDto? budget = normalized is null ? null : new AiBudgetDto(normalized.From, normalized.To, normalized.Cur);
IReadOnlyList qualified = ContactsQualifier.Build(fields.Contacts, text);
var contacts = new List(qualified.Count);
foreach (CardContactDto contact in qualified)
{
contacts.Add(new AiContactDto(contact.Type, contact.Value));
}
return new AiParsedCardDto(
Title: fields.Title,
Company: null,
Format: null,
Task: null,
Requirements: null,
Plus: null,
Conditions: null,
Summary: fields.Summary,
Stack: fields.Stack,
Budget: budget,
Contacts: contacts,
IsVacancy: fields.IsVacancy,
IsVacancyKnown: false, // маркерная гипотеза — тип подтверждает только ИИ по контексту (python L796)
IsSpam: false,
Board: null); // смысловые колонки до ИИ не назначаем (python L797)
}
}