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" ); } function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpDiscoveryPilotExecutionContract): string { 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 (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 === "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 (pilot.evidence.confirmed_facts.length === 0) { claims.push("Do not claim a confirmed business fact when confirmed_facts is empty."); } return claims; } 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 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}`; } 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); const inferenceLines = derivedInferenceLine ? [derivedInferenceLine] : pilot.evidence.inferred_facts; const derivedValueLine = derivedValueFlowConfirmedLine(pilot); const confirmedLines = derivedValueLine ? [...pilot.evidence.confirmed_facts, derivedValueLine] : 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) }; }