import type { AssistantMcpDiscoveryPilotExecutionContract } from "./assistantMcpDiscoveryPilotExecutor"; export const ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION = "assistant_mcp_discovery_answer_draft_v1" as const; export type AssistantMcpDiscoveryAnswerMode = | "confirmed_with_bounded_inference" | "bounded_inference_only" | "checked_sources_only" | "needs_clarification" | "blocked"; export interface AssistantMcpDiscoveryAnswerDraftContract { schema_version: typeof ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION; policy_owner: "assistantMcpDiscoveryAnswerAdapter"; answer_mode: AssistantMcpDiscoveryAnswerMode; headline: string; confirmed_lines: string[]; inference_lines: string[]; unknown_lines: string[]; limitation_lines: string[]; next_step_line: string | null; internal_mechanics_allowed: false; must_not_claim: string[]; reason_codes: string[]; } function normalizeReasonCode(value: string): string | null { const normalized = value .trim() .replace(/[^\p{L}\p{N}_.:-]+/gu, "_") .replace(/^_+|_+$/g, "") .toLowerCase(); return normalized.length > 0 ? normalized.slice(0, 120) : null; } function pushReason(target: string[], value: string): void { const normalized = normalizeReasonCode(value); if (normalized && !target.includes(normalized)) { target.push(normalized); } } function uniqueStrings(values: string[]): string[] { const result: string[] = []; for (const value of values) { const text = String(value ?? "").trim(); if (text && !result.includes(text)) { result.push(text); } } return result; } function isInternalMechanicsLine(value: string): boolean { const text = value.toLowerCase(); return ( text.includes("primitive") || text.includes("query_documents") || text.includes("query_movements") || text.includes("resolve_entity_reference") || text.includes("probe_coverage") || text.includes("explain_evidence_basis") || text.includes("pilot_only_executes") || text.includes("pilot_") || text.includes("runtime_") || text.includes("planner_") || text.includes("catalog_") ); } function userFacingLimitations(values: string[]): string[] { return uniqueStrings(values).filter((value) => !isInternalMechanicsLine(value)); } function modeFor(pilot: AssistantMcpDiscoveryPilotExecutionContract): AssistantMcpDiscoveryAnswerMode { if (pilot.pilot_status === "blocked") { return "blocked"; } if (pilot.pilot_status === "skipped_needs_clarification") { return "needs_clarification"; } if (pilot.evidence.answer_permission === "confirmed_answer") { return "confirmed_with_bounded_inference"; } if (pilot.evidence.answer_permission === "bounded_inference") { return "bounded_inference_only"; } return "checked_sources_only"; } function isValueFlowPilot(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean { return ( pilot.pilot_scope === "counterparty_value_flow_query_movements_v1" || pilot.pilot_scope === "counterparty_supplier_payout_query_movements_v1" || pilot.pilot_scope === "counterparty_bidirectional_value_flow_query_movements_v1" ); } function isDocumentPilot(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean { return pilot.pilot_scope === "counterparty_document_evidence_query_documents_v1"; } function isMetadataPilot(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean { return pilot.pilot_scope === "metadata_inspection_v1"; } function metadataRouteFamilyLabelRu( routeFamily: "document_evidence" | "movement_evidence" | "catalog_drilldown" | null ): string | null { if (routeFamily === "document_evidence") { return "контур документов"; } if (routeFamily === "movement_evidence") { return "контур движений/регистров"; } if (routeFamily === "catalog_drilldown") { return "контур справочников и связанных объектов"; } return null; } function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpDiscoveryPilotExecutionContract): string { const askedMonthlyBreakdown = pilot.derived_bidirectional_value_flow?.aggregation_axis === "month" || pilot.derived_value_flow?.aggregation_axis === "month"; if (pilot.derived_metadata_surface && mode === "confirmed_with_bounded_inference") { if (pilot.derived_metadata_surface.ambiguity_detected) { return "По метаданным 1С найдены конкурирующие schema-поверхности; перед следующим шагом нужно удержать неоднозначность явно."; } if (pilot.derived_metadata_surface.downstream_route_family) { return "По метаданным 1С найдена схема и заземлена вероятная поверхность для следующего безопасного шага."; } return "По метаданным 1С найдена доступная схема для дальнейшего безопасного поиска."; } if (askedMonthlyBreakdown && pilot.derived_bidirectional_value_flow && mode === "confirmed_with_bounded_inference") { return "По данным 1С найдены строки входящих и исходящих денежных движений; нетто и помесячная раскладка могут называться только как расчет по найденным строкам и проверенному периоду."; } if (askedMonthlyBreakdown && pilot.derived_value_flow && mode === "confirmed_with_bounded_inference") { if (pilot.derived_value_flow.value_flow_direction === "outgoing_supplier_payout") { return "По данным 1С найдены строки исходящих платежей/списаний; сумму и помесячную раскладку можно называть только в рамках проверенного периода и найденных строк."; } return "По данным 1С найдены строки денежных движений; сумму и помесячную раскладку можно называть только в рамках проверенного периода и найденных строк."; } if (pilot.derived_bidirectional_value_flow && mode === "confirmed_with_bounded_inference") { return "По данным 1С найдены строки входящих и исходящих денежных движений; нетто можно называть только как расчет по найденным строкам и проверенному периоду."; } if (pilot.derived_value_flow && mode === "confirmed_with_bounded_inference") { if (pilot.derived_value_flow.value_flow_direction === "outgoing_supplier_payout") { return "По данным 1С найдены строки исходящих платежей/списаний; сумму можно называть только в рамках проверенного периода и найденных строк."; } return "По данным 1С найдены строки денежных движений; сумму можно называть только в рамках проверенного периода и найденных строк."; } if (isDocumentPilot(pilot) && mode === "confirmed_with_bounded_inference") { return "По данным 1С найдены строки документов; ответ ограничен проверенным периодом и найденными строками."; } if (mode === "confirmed_with_bounded_inference") { return "По данным 1С есть подтвержденная активность; длительность можно оценивать только как вывод из этих строк."; } if (mode === "bounded_inference_only") { return "Точный факт не подтвержден, но есть ограниченная оценка по найденной активности в 1С."; } if (mode === "needs_clarification") { return "Нужно уточнить контекст перед поиском в 1С."; } if (mode === "blocked") { return "Поиск в 1С заблокирован runtime-политикой до выполнения."; } return "Я проверил доступный контур, но подтвержденного факта для ответа не получил."; } function nextStepFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null { if (mode === "needs_clarification") { return "Уточните контрагента, период или организацию, и я смогу выполнить проверку по 1С."; } if (mode === "confirmed_with_bounded_inference" && pilot.derived_metadata_surface) { const surface = pilot.derived_metadata_surface; if (surface.ambiguity_detected && surface.ambiguity_entity_sets.length > 0) { return `Следующим шагом лучше сузить surface до одного семейства: ${surface.ambiguity_entity_sets.join(", ")}.`; } const routeLabel = metadataRouteFamilyLabelRu(surface.downstream_route_family); if (surface.selected_entity_set && routeLabel) { return `Следующим шагом могу пойти в ${routeLabel} по surface «${surface.selected_entity_set}» и уже искать подтвержденные данные, а не только схему.`; } } if (mode === "checked_sources_only" && pilot.query_limitations.length > 0) { return "Можно повторить проверку после восстановления MCP-доступа или сузить вопрос до конкретного контрагента/периода."; } if (mode === "blocked") { return "Нужно сначала снять policy/blocking причину, иначе данные 1С использовать нельзя."; } return null; } function buildMustNotClaim(pilot: AssistantMcpDiscoveryPilotExecutionContract): string[] { const claims = [ "Do not expose MCP primitive names, query text, debug ids, or internal execution mechanics in the user answer.", "Do not claim rows were checked when mcp_execution_performed=false." ]; if (pilot.pilot_scope === "counterparty_lifecycle_query_documents_v1") { claims.push("Do not claim legal registration age unless a legal registration source is confirmed."); claims.push("Do not present inferred activity duration as a formally confirmed legal fact."); } if (isValueFlowPilot(pilot)) { claims.push("Do not claim full all-time turnover unless the checked period and coverage prove it."); claims.push("Do not present a derived sum as a legal/accounting final total outside the checked 1C rows."); } if (isDocumentPilot(pilot)) { claims.push("Do not claim full document history outside the checked period."); claims.push("Do not present the confirmed document rows as a complete document universe."); } if (isMetadataPilot(pilot)) { claims.push("Do not present metadata surface as confirmed business data rows."); claims.push("Do not claim a document/register exists outside the checked metadata probe results."); claims.push("Do not present the inferred next checked lane as already executed data retrieval."); } if (pilot.evidence.confirmed_facts.length === 0) { claims.push("Do not claim a confirmed business fact when confirmed_facts is empty."); } return claims; } const RU_MONTH_LABELS_SHORT = [ "янв", "фев", "мар", "апр", "май", "июн", "июл", "авг", "сен", "окт", "ноя", "дек" ] as const; function monthLabelRu(monthBucket: string): string { const match = monthBucket.match(/^(\d{4})-(\d{2})$/); if (!match) { return monthBucket; } const monthIndex = Number(match[2]) - 1; const label = RU_MONTH_LABELS_SHORT[monthIndex] ?? match[2]; return `${label} ${match[1]}`; } function netLabelRu(netDirection: "net_incoming" | "net_outgoing" | "balanced"): string { if (netDirection === "net_incoming") { return "нетто в нашу сторону"; } if (netDirection === "net_outgoing") { return "нетто исходящее"; } return "нетто нулевое"; } function derivedActivityInferenceLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null { const period = pilot.derived_activity_period; if (!period) { return null; } return [ `По подтвержденным строкам активности в 1С период взаимодействия можно оценить примерно как ${period.duration_human_ru}.`, `Первая найденная активность: ${period.first_activity_date}; последняя найденная активность: ${period.latest_activity_date}.`, "Это вывод по данным 1С, а не юридически подтвержденный возраст регистрации." ].join(" "); } function derivedMetadataConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null { const surface = pilot.derived_metadata_surface; if (!surface) { return null; } const scope = surface.metadata_scope ? ` по области "${surface.metadata_scope}"` : ""; const entitySets = surface.available_entity_sets.length > 0 ? ` Типы объектов: ${surface.available_entity_sets.join(", ")}.` : ""; const objects = surface.matched_objects.length > 0 ? ` Найденные объекты: ${surface.matched_objects.slice(0, 8).join(", ")}.` : ""; const selectedEntitySet = surface.selected_entity_set ? ` Выбранное family: ${surface.selected_entity_set}.` : ""; const selectedObjects = surface.selected_surface_objects.length > 0 ? ` Выбранные surface-объекты: ${surface.selected_surface_objects.slice(0, 6).join(", ")}.` : ""; const fields = surface.available_fields.length > 0 ? ` Доступные поля/секции: ${surface.available_fields.slice(0, 12).join(", ")}.` : ""; return `Подтвержденная metadata-поверхность 1С${scope}: ${surface.matched_rows} строк metadata-ответа.${entitySets}${objects}${selectedEntitySet}${selectedObjects}${fields}`.replace(/\s+/g, " ").trim(); } function derivedMetadataInferenceLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null { const surface = pilot.derived_metadata_surface; if (!surface) { return null; } if (surface.ambiguity_detected && surface.ambiguity_entity_sets.length > 0) { return `По подтвержденной metadata-поверхности видно несколько конкурирующих family: ${surface.ambiguity_entity_sets.join(", ")}. Следующий data-lane пока нельзя выбрать без явного сужения.`; } const routeLabel = metadataRouteFamilyLabelRu(surface.downstream_route_family); if (!surface.selected_entity_set || !routeLabel) { return null; } return `По подтвержденной metadata-поверхности следующий проверяемый шаг можно ограниченно оценить как ${routeLabel} через family «${surface.selected_entity_set}». Это еще не выполненный data-fetch, а только grounded выбор следующего контура.`; } function derivedValueFlowConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null { const flow = pilot.derived_value_flow; if (!flow) { return null; } const counterparty = flow.counterparty ? ` по контрагенту ${flow.counterparty}` : ""; const period = flow.period_scope ? ` за период ${flow.period_scope}` : " в проверенном окне"; const movementLabel = flow.value_flow_direction === "outgoing_supplier_payout" ? "исходящих платежей/списаний" : "денежных движений"; const totalLabel = flow.value_flow_direction === "outgoing_supplier_payout" ? "сумма исходящих платежей/списаний составляет" : "сумма составляет"; const caveat = flow.value_flow_direction === "outgoing_supplier_payout" ? "Это расчет по найденным строкам 1С, а не подтверждение полного объема платежей вне проверенного окна." : "Это расчет по найденным строкам 1С, а не подтверждение полного оборота вне проверенного окна."; const dates = flow.first_movement_date && flow.latest_movement_date ? ` Первая найденная дата движения: ${flow.first_movement_date}; последняя: ${flow.latest_movement_date}.` : ""; const limitCaveat = flow.coverage_limited_by_probe_limit ? " Лимит строк проверки достигнут; полный запрошенный период может быть покрыт не полностью." : ""; return `По найденным строкам ${movementLabel} в 1С${counterparty}${period} ${totalLabel} ${flow.total_amount_human_ru} Учтено строк с суммой: ${flow.rows_with_amount} из ${flow.rows_matched}.${dates}${limitCaveat} ${caveat}`; } function derivedValueFlowMonthlyLines(pilot: AssistantMcpDiscoveryPilotExecutionContract): string[] { const flow = pilot.derived_value_flow; if (!flow || flow.aggregation_axis !== "month" || flow.monthly_breakdown.length === 0) { return []; } return flow.monthly_breakdown.map((bucket) => { const monthLabel = monthLabelRu(bucket.month_bucket); if (flow.value_flow_direction === "outgoing_supplier_payout") { return `Помесячно: ${monthLabel} — заплатили ${bucket.total_amount_human_ru} по ${bucket.rows_with_amount} строкам с суммой`; } return `Помесячно: ${monthLabel} — получили ${bucket.total_amount_human_ru} по ${bucket.rows_with_amount} строкам с суммой`; }); } function sideDateRange(first: string | null, latest: string | null): string { if (first && latest) { return ` первая дата ${first}, последняя ${latest}`; } return " даты движения не выделены"; } function derivedBidirectionalValueFlowConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null { const flow = pilot.derived_bidirectional_value_flow; if (!flow) { return null; } const counterparty = flow.counterparty ? ` по контрагенту ${flow.counterparty}` : ""; const period = flow.period_scope ? ` за период ${flow.period_scope}` : " в проверенном окне"; const incoming = flow.incoming_customer_revenue; const outgoing = flow.outgoing_supplier_payout; const netLabel = flow.net_direction === "net_incoming" ? "нетто в нашу сторону" : flow.net_direction === "net_outgoing" ? "нетто исходящий" : "нетто нулевое"; const limitCaveat = flow.coverage_limited_by_probe_limit ? " Лимит строк проверки достигнут хотя бы по одной стороне; полный запрошенный период может быть покрыт не полностью." : ""; return [ `По найденным строкам 1С${counterparty}${period}: получили ${incoming.total_amount_human_ru} по входящим движениям, заплатили ${outgoing.total_amount_human_ru} по исходящим платежам/списаниям.`, `Расчетное ${netLabel}: ${flow.net_amount_human_ru}`, `Входящие строки с суммой: ${incoming.rows_with_amount} из ${incoming.rows_matched};${sideDateRange(incoming.first_movement_date, incoming.latest_movement_date)}.`, `Исходящие строки с суммой: ${outgoing.rows_with_amount} из ${outgoing.rows_matched};${sideDateRange(outgoing.first_movement_date, outgoing.latest_movement_date)}.`, `${limitCaveat} Это расчет по найденным строкам 1С, а не подтверждение полного сальдо вне проверенного окна.` ] .join(" ") .replace(/\s+/g, " ") .trim(); } function derivedBidirectionalValueFlowMonthlyLines(pilot: AssistantMcpDiscoveryPilotExecutionContract): string[] { const flow = pilot.derived_bidirectional_value_flow; if (!flow || flow.aggregation_axis !== "month" || flow.monthly_breakdown.length === 0) { return []; } return flow.monthly_breakdown.map( (bucket) => `Помесячно: ${monthLabelRu(bucket.month_bucket)} — получили ${bucket.incoming_total_amount_human_ru}, заплатили ${bucket.outgoing_total_amount_human_ru}, ${netLabelRu(bucket.net_direction)} ${bucket.net_amount_human_ru}` ); } export function buildAssistantMcpDiscoveryAnswerDraft( pilot: AssistantMcpDiscoveryPilotExecutionContract ): AssistantMcpDiscoveryAnswerDraftContract { const mode = modeFor(pilot); const reasonCodes = [...pilot.reason_codes, ...pilot.evidence.reason_codes]; pushReason(reasonCodes, `answer_mode_${mode}`); if (pilot.evidence.unknown_facts.length > 0) { pushReason(reasonCodes, "answer_contains_unknown_fact_boundary"); } if (pilot.evidence.inferred_facts.length > 0) { pushReason(reasonCodes, "answer_contains_bounded_inference"); } const derivedInferenceLine = derivedActivityInferenceLine(pilot) ?? derivedMetadataInferenceLine(pilot); const inferenceLines = derivedInferenceLine ? [derivedInferenceLine] : pilot.evidence.inferred_facts; const derivedMetadataLine = derivedMetadataConfirmedLine(pilot); const derivedValueLine = derivedBidirectionalValueFlowConfirmedLine(pilot) ?? derivedValueFlowConfirmedLine(pilot); const monthlyConfirmedLines = derivedBidirectionalValueFlowMonthlyLines(pilot).length > 0 ? derivedBidirectionalValueFlowMonthlyLines(pilot) : derivedValueFlowMonthlyLines(pilot); if (monthlyConfirmedLines.length > 0) { pushReason(reasonCodes, "answer_contains_monthly_breakdown"); } const confirmedLines = derivedValueLine ? [...pilot.evidence.confirmed_facts, derivedValueLine, ...monthlyConfirmedLines] : derivedMetadataLine ? [...pilot.evidence.confirmed_facts, derivedMetadataLine] : pilot.evidence.confirmed_facts; return { schema_version: ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION, policy_owner: "assistantMcpDiscoveryAnswerAdapter", answer_mode: mode, headline: headlineFor(mode, pilot), confirmed_lines: uniqueStrings(confirmedLines), inference_lines: uniqueStrings(inferenceLines), unknown_lines: uniqueStrings(pilot.evidence.unknown_facts), limitation_lines: userFacingLimitations([...pilot.query_limitations, ...pilot.evidence.query_limitations]), next_step_line: nextStepFor(mode, pilot), internal_mechanics_allowed: false, must_not_claim: buildMustNotClaim(pilot), reason_codes: uniqueStrings(reasonCodes) }; }