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) } }