import { describe, expect, it, vi } from "vitest"; import { buildAssistantMcpDiscoveryAnswerDraft } from "../src/services/assistantMcpDiscoveryAnswerAdapter"; import { executeAssistantMcpDiscoveryPilot } from "../src/services/assistantMcpDiscoveryPilotExecutor"; import { planAssistantMcpDiscovery } from "../src/services/assistantMcpDiscoveryPlanner"; function buildDeps(rows: Array>, error: string | null = null) { return { executeAddressMcpQuery: vi.fn(async () => ({ fetched_rows: rows.length, matched_rows: error ? 0 : rows.length, raw_rows: rows, rows: error ? [] : rows, error })) }; } function buildSequentialDeps(results: Array<{ rows: Array>; error?: string | null }>) { const executeAddressMcpQuery = vi.fn(async () => { const next = results.shift() ?? { rows: [] }; const rows = next.rows; const error = next.error ?? null; return { fetched_rows: rows.length, matched_rows: error ? 0 : rows.length, raw_rows: rows, rows: error ? [] : rows, error }; }); return { executeAddressMcpQuery }; } function buildCustomQueryDeps(result: { fetched_rows: number; matched_rows: number; rows: Array>; raw_rows?: Array>; error?: string | null; }) { return { executeAddressMcpQuery: vi.fn(async () => ({ fetched_rows: result.fetched_rows, matched_rows: result.matched_rows, rows: result.rows, raw_rows: result.raw_rows ?? result.rows, error: result.error ?? null })) }; } function buildMetadataDeps(rows: Array>, error: string | null = null) { return { executeAddressMcpMetadata: vi.fn(async () => ({ fetched_rows: error ? 0 : rows.length, raw_rows: error ? [] : rows, rows: error ? [] : rows, error })) }; } describe("assistant MCP discovery answer adapter", () => { it("turns confirmed lifecycle evidence into a human-safe bounded answer draft", async () => { const planner = planAssistantMcpDiscovery({ turnMeaning: { asked_domain_family: "counterparty_lifecycle", asked_action_family: "activity_duration", explicit_entity_candidates: ["SVK"] } }); const pilot = await executeAssistantMcpDiscoveryPilot( planner, buildDeps([{ Период: "2020-01-15T00:00:00", Регистратор: "CP_CUSTOMER_ACTIVITY_FIRST", Контрагент: "SVK" }]) ); const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot); expect(draft.answer_mode).toBe("confirmed_with_bounded_inference"); expect(draft.internal_mechanics_allowed).toBe(false); expect(draft.headline).toContain("подтвержденная активность"); expect(draft.confirmed_lines[0]).toContain("SVK"); expect(draft.confirmed_lines[0]).toContain("matched_rows=1"); expect(draft.inference_lines[0]).toContain("меньше месяца"); expect(draft.inference_lines.join("\n")).toContain("Первая найденная активность: 2020-01-15"); expect(draft.inference_lines.join("\n")).toContain("не юридически подтвержденный возраст регистрации"); expect(pilot.evidence.inferred_facts).toContain( "Activity window is bounded by first=2020-01-15, latest=2020-01-15, matched_rows=1" ); expect(pilot.evidence.inferred_facts).toContain("Activity-window inference is not legal registration age"); expect(draft.unknown_lines).toContain("Legal registration date is not proven by this MCP discovery pilot"); expect(draft.unknown_lines).toContain( "Business activity before the first confirmed 1C activity row is not proven by this MCP discovery pilot" ); expect(draft.must_not_claim).toContain("Do not present inferred activity duration as a formally confirmed legal fact."); expect(draft.reason_codes).toContain("answer_contains_unknown_fact_boundary"); }); it("uses checked-sources mode when MCP failed and avoids confirmed facts", async () => { const planner = planAssistantMcpDiscovery({ turnMeaning: { asked_domain_family: "counterparty_lifecycle", asked_action_family: "activity_duration", explicit_entity_candidates: ["SVK"] } }); const pilot = await executeAssistantMcpDiscoveryPilot(planner, buildDeps([], "MCP fetch failed: timeout")); const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot); expect(draft.answer_mode).toBe("checked_sources_only"); expect(draft.confirmed_lines).toEqual([]); expect(draft.limitation_lines).toContain("Доступ к 1С во время проверки оборвался; подтвержденные строки не получены."); expect(draft.limitation_lines).not.toContain("MCP fetch failed: timeout"); expect(draft.next_step_line).toContain("доступа к 1С"); expect(draft.next_step_line).not.toContain("MCP"); expect(draft.must_not_claim).toContain("Do not claim a confirmed business fact when confirmed_facts is empty."); }); it("turns generic document evidence into a bounded document answer draft", async () => { const planner = planAssistantMcpDiscovery({ turnMeaning: { asked_domain_family: "documents", asked_action_family: "list_documents", explicit_entity_candidates: ["SVK"], explicit_date_scope: "2020", unsupported_but_understood_family: "document_evidence" } }); const pilot = await executeAssistantMcpDiscoveryPilot( planner, buildDeps([{ Period: "2020-01-15T00:00:00", Counterparty: "SVK", Registrar: "Doc1" }]) ); const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot); expect(draft.answer_mode).toBe("confirmed_with_bounded_inference"); expect(draft.headline).toContain("документ"); expect(draft.headline).toContain("2020"); expect(draft.headline).toContain("SVK"); expect(draft.confirmed_lines).toContain("В 1С найдены строки документов по контрагенту SVK за 2020."); expect(draft.inference_lines).toContain( "Срез документов по контрагенту SVK за 2020 ограничен только подтвержденными строками документов, найденными этим поиском." ); expect(draft.unknown_lines).toContain( "Полный исторический срез документов по контрагенту SVK вне периода 2020 этим поиском не подтвержден." ); expect(draft.must_not_claim).toContain("Do not claim full document history outside the checked period."); }); it("turns generic movement evidence into a bounded movement answer draft", async () => { const planner = planAssistantMcpDiscovery({ turnMeaning: { asked_domain_family: "movements", asked_action_family: "list_movements", explicit_entity_candidates: ["SVK"], explicit_date_scope: "2020", unsupported_but_understood_family: "movement_evidence" } }); const pilot = await executeAssistantMcpDiscoveryPilot( planner, buildDeps([{ Period: "2020-01-15T00:00:00", Counterparty: "SVK", Registrar: "Move1" }]) ); const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot); expect(draft.answer_mode).toBe("confirmed_with_bounded_inference"); expect(draft.headline).toContain("движени"); expect(draft.headline).toContain("2020"); expect(draft.headline).toContain("SVK"); expect(draft.confirmed_lines).toContain("В 1С найдены строки движений по контрагенту SVK за 2020."); expect(draft.inference_lines).toContain( "Срез движений по контрагенту SVK за 2020 ограничен только подтвержденными строками движений, найденными этим поиском." ); expect(draft.unknown_lines).toContain( "Полный исторический срез движений по контрагенту SVK вне периода 2020 этим поиском не подтвержден." ); expect(draft.must_not_claim).toContain("Do not claim full movement history outside the checked period."); expect(draft.must_not_claim).toContain("Do not present the confirmed movement rows as a complete movement universe."); }); it("turns business overview multi-probe evidence into an analyst-safe draft", async () => { const planner = planAssistantMcpDiscovery({ dataNeedGraph: { schema_version: "assistant_data_need_graph_v1", policy_owner: "assistantMcpDiscoveryDataNeedGraph", subject_candidates: [], business_fact_family: "business_overview", action_family: "broad_evaluation", aggregation_need: null, time_scope_need: "all_time_scope", comparison_need: null, ranking_need: null, proof_expectation: "bounded_inference", clarification_gaps: [], decomposition_candidates: [ "collect_scoped_movements", "aggregate_checked_amounts", "aggregate_ranked_axis_values", "fetch_supporting_documents", "probe_coverage", "explain_evidence_basis" ], forbidden_overclaim_flags: ["no_raw_model_claims", "no_profit_or_margin_claim_without_evidence"], reason_codes: ["data_need_graph_built", "data_need_graph_family_business_overview"] }, turnMeaning: { asked_domain_family: "business_summary", asked_action_family: "broad_evaluation", explicit_organization_scope: "ООО Альтернатива Плюс", unsupported_but_understood_family: "broad_business_evaluation" } }); const pilot = await executeAssistantMcpDiscoveryPilot( planner, buildSequentialDeps([ { rows: [ { Period: "2020-01-15T00:00:00", Amount: 120000, Counterparty: "Клиент А" }, { Period: "2020-02-15T00:00:00", Amount: 80000, Counterparty: "Клиент Б" }, { Period: "2021-03-15T00:00:00", Amount: 220000, Counterparty: "Клиент А" } ] }, { rows: [ { Period: "2020-01-20T00:00:00", Amount: 150000, Counterparty: "Поставщик А" }, { Period: "2021-03-20T00:00:00", Amount: 50000, Counterparty: "Поставщик Б" } ] }, { rows: [ { Период: "2020-01-15T00:00:00", Регистратор: "Поступление 1" }, { Период: "2020-12-15T00:00:00", Регистратор: "Поступление 2" } ] }, { rows: [ { Period: "2020-01-01T00:00:00", Registrator: "DOC_TYPE_DOCS", AccountDt: "Списание с расчетного счета", Amount: 12 }, { Period: "2020-01-01T00:00:00", Registrator: "DOC_TYPE_DOCS", AccountDt: "Поступление на расчетный счет", Amount: 8 }, { Period: "2020-01-01T00:00:00", Registrator: "SECTION_DT_OPS", AccountDt: "60", Amount: 9 }, { Period: "2020-01-01T00:00:00", Registrator: "SECTION_KT_OPS", AccountDt: "62", Amount: 6 } ] }, { rows: [ { Period: "2020-01-01T00:00:00", Registrator: "CP_TOTAL", Amount: 412 }, { Period: "2020-01-01T00:00:00", Registrator: "CP_CUSTOMER_ACTIVE", Amount: 145 }, { Period: "2020-01-01T00:00:00", Registrator: "CP_SUPPLIER_ACTIVE", Amount: 94 }, { Period: "2020-01-01T00:00:00", Registrator: "CP_MIXED_ACTIVE", Amount: 23 }, { Period: "2020-01-01T00:00:00", Registrator: "CP_ACTIVE_UNION", Amount: 216 } ] }, { rows: [ { Period: "2020-01-01T00:00:00", Registrator: "CT_TOTAL", Amount: 520 }, { Period: "2020-01-01T00:00:00", Registrator: "CT_USED", Amount: 148 } ] } ]) ); const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot); expect(draft.answer_mode).toBe("confirmed_with_bounded_inference"); expect(draft.headline).toContain("бизнес-обзор"); expect(draft.confirmed_lines.join("\n")).toContain("Входящие поступления"); expect(draft.confirmed_lines.join("\n")).toContain("Самый крупный подтвержденный клиент"); expect(draft.confirmed_lines.join("\n")).toContain("Самый крупный подтвержденный поставщик"); expect(draft.confirmed_lines.join("\n")).toContain("Годовая раскладка операционного денежного потока"); expect(draft.confirmed_lines.join("\n")).toContain("Профиль операционной активности"); expect(draft.confirmed_lines.join("\n")).toContain("ведущий тип документов Списание с расчетного счета"); expect(draft.confirmed_lines.join("\n")).toContain("ведущий раздел учета 60"); expect(draft.confirmed_lines.join("\n")).toContain("Профиль контрагентской базы"); expect(draft.confirmed_lines.join("\n")).toContain("412 контрагентов в базе"); expect(draft.confirmed_lines.join("\n")).toContain("Договорной профиль"); expect(draft.confirmed_lines.join("\n")).toContain("148 из 520 договоров используются"); expect(draft.inference_lines.join("\n")).toContain("Аналитический вывод по оборотам"); expect(draft.inference_lines.join("\n")).toContain("Концентрация входящего потока"); expect(draft.inference_lines.join("\n")).toContain("Концентрация исходящего потока"); expect(draft.inference_lines.join("\n")).toContain("Годовая динамика по проверенным строкам"); expect(draft.inference_lines.join("\n")).toContain("2021"); expect(draft.inference_lines.join("\n")).toContain("Сводный LLM-аудит"); expect(draft.inference_lines.join("\n")).toContain("не прибыль и не маржа"); expect(draft.unknown_lines.join("\n")).toContain("Прибыль и маржа"); expect(draft.unknown_lines.join("\n")).toContain("Налоговая/VAT-позиция"); expect(draft.next_step_line).toContain("прибыль/маржу"); expect(draft.must_not_claim).toContain("Do not present business overview cash-flow spread as profit or margin."); expect(draft.must_not_claim).toContain("Do not present business overview yearly operating-flow breakdown as profit, financial result, or a complete annual P&L."); expect(draft.must_not_claim).toContain("Do not present business overview supplier concentration as vendor-risk audit, procurement quality, or full expense structure."); expect(draft.must_not_claim).toContain("Do not present business overview document/account-section activity profile as process quality, accounting correctness, or completeness of all 1C activity."); expect(draft.must_not_claim).toContain("Do not present business overview counterparty or contract profile as CRM quality, counterparty due diligence, contract-risk audit, or legal completeness."); expect(draft.must_not_claim).toContain("Do not present business overview missing proof families as checked, executed, or confirmed routes."); expect(draft.reason_codes).toContain("answer_contains_business_overview"); expect(draft.reason_codes).toContain("answer_contains_business_overview_supplier_concentration"); expect(draft.reason_codes).toContain("answer_contains_business_overview_yearly_operating_breakdown"); expect(draft.reason_codes).toContain("answer_contains_business_overview_document_activity_profile"); expect(draft.reason_codes).toContain("answer_contains_business_overview_counterparty_profile"); expect(draft.reason_codes).toContain("answer_contains_business_overview_contract_usage_profile"); expect(draft.reason_codes).toContain("answer_contains_business_overview_missing_proof_ledger"); expect(draft.reason_codes).toContain("answer_contains_business_overview_analyst_synthesis"); }); it("surfaces checked VAT/tax position in business overview without treating it as profit", async () => { const planner = planAssistantMcpDiscovery({ dataNeedGraph: { schema_version: "assistant_data_need_graph_v1", policy_owner: "assistantMcpDiscoveryDataNeedGraph", subject_candidates: [], business_fact_family: "business_overview", action_family: "broad_evaluation", aggregation_need: null, time_scope_need: "explicit_period", comparison_need: null, ranking_need: null, proof_expectation: "bounded_inference", clarification_gaps: [], decomposition_candidates: [ "collect_scoped_movements", "aggregate_checked_amounts", "aggregate_ranked_axis_values", "fetch_supporting_documents", "probe_coverage", "explain_evidence_basis" ], forbidden_overclaim_flags: ["no_raw_model_claims", "no_profit_or_margin_claim_without_evidence"], reason_codes: ["data_need_graph_built", "data_need_graph_family_business_overview"] }, turnMeaning: { asked_domain_family: "business_summary", asked_action_family: "broad_evaluation", explicit_organization_scope: "ООО Альтернатива Плюс", explicit_date_scope: "2020", unsupported_but_understood_family: "broad_business_evaluation" } }); const pilot = await executeAssistantMcpDiscoveryPilot( planner, buildSequentialDeps([ { rows: [{ Period: "2020-01-15T00:00:00", Amount: 120000, Counterparty: "Клиент А" }] }, { rows: [{ Period: "2020-01-20T00:00:00", Amount: 50000, Counterparty: "Поставщик А" }] }, { rows: [ { Регистратор: "VAT_BOOK_SALES", СчетДт: "68.02", Сумма: 40000 }, { Регистратор: "VAT_BOOK_PURCHASES", СчетДт: "19", Сумма: 12000 } ] }, { rows: [] }, { rows: [] }, { rows: [] }, { rows: [] }, { rows: [] }, { rows: [ { Период: "2020-01-15T00:00:00", Регистратор: "Поступление 1" }, { Период: "2020-12-15T00:00:00", Регистратор: "Поступление 2" } ] }, { rows: [ { Period: "2020-03-01T00:00:00", Amount: 200000, Item: "Товар А", Counterparty: "Клиент А", СчетКт: "41.01" }, { Period: "2020-02-01T00:00:00", Amount: 120000, Item: "Товар А", Counterparty: "Поставщик А", СчетДт: "41.01" } ] } ]) ); const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot); expect(draft.headline).toContain("НДС-позиция"); expect(draft.headline).toContain("торговый margin proxy"); expect(draft.confirmed_lines.join("\n")).toContain("НДС-позиция за 2020"); expect(draft.confirmed_lines.join("\n")).toContain("нетто к уплате 28 000 руб."); expect(draft.confirmed_lines.join("\n")).toContain("Торговый margin proxy за 2020"); expect(draft.confirmed_lines.join("\n")).toContain("валовый спред proxy 80 000 руб."); expect(draft.confirmed_lines.join("\n")).toContain("не чистая прибыль"); expect(draft.inference_lines.join("\n")).toContain("торговый спред proxy 80 000 руб."); expect(draft.inference_lines.join("\n")).toContain("не прибыль и не маржа"); expect(draft.unknown_lines.join("\n")).toContain("Чистая прибыль"); expect(draft.unknown_lines.join("\n")).not.toContain("Налоговая/VAT-позиция"); expect(draft.next_step_line).toContain("чистую прибыль"); expect(draft.reason_codes).toContain("answer_contains_business_overview_tax_position"); expect(draft.reason_codes).toContain("answer_contains_business_overview_trading_margin_proxy"); expect(draft.must_not_claim).toContain("Do not present business overview cash-flow spread as profit or margin."); expect(draft.must_not_claim).toContain("Do not present business overview trading-margin proxy as clean profit, accounting financial result, or exact cost-of-sales margin."); }); it("surfaces checked debt-position and open-settlement quality without treating them as overdue debt", async () => { const planner = planAssistantMcpDiscovery({ dataNeedGraph: { schema_version: "assistant_data_need_graph_v1", policy_owner: "assistantMcpDiscoveryDataNeedGraph", subject_candidates: [], business_fact_family: "business_overview", action_family: "broad_evaluation", aggregation_need: null, time_scope_need: "explicit_period", comparison_need: null, ranking_need: null, proof_expectation: "bounded_inference", clarification_gaps: [], decomposition_candidates: [ "collect_scoped_movements", "aggregate_checked_amounts", "aggregate_ranked_axis_values", "fetch_supporting_documents", "probe_coverage", "explain_evidence_basis" ], forbidden_overclaim_flags: ["no_raw_model_claims", "no_profit_or_margin_claim_without_evidence"], reason_codes: ["data_need_graph_built", "data_need_graph_family_business_overview"] }, turnMeaning: { asked_domain_family: "business_summary", asked_action_family: "broad_evaluation", explicit_organization_scope: "ООО Альтернатива Плюс", explicit_date_scope: "2020", unsupported_but_understood_family: "broad_business_evaluation" } }); const pilot = await executeAssistantMcpDiscoveryPilot( planner, buildSequentialDeps([ { rows: [{ Period: "2020-01-15T00:00:00", Amount: 120000, Counterparty: "Клиент А" }] }, { rows: [{ Period: "2020-01-20T00:00:00", Amount: 50000, Counterparty: "Поставщик А" }] }, { rows: [] }, { rows: [ { Period: "2020-12-31T00:00:00", Amount: 100000, Counterparty: "Клиент А" } ] }, { rows: [ { Period: "2020-12-31T00:00:00", Amount: 40000, Counterparty: "Поставщик А" } ] }, { rows: [ { Period: "2020-12-31T00:00:00", Amount: 100000, Counterparty: "Клиент А", Contract: "Договор А от 10.02.2019" }, { Period: "2020-12-31T00:00:00", Amount: 50000, Counterparty: "Поставщик А", Contract: "Договор Б от 15.03.2020" } ] }, { rows: [] }, { rows: [] }, { rows: [ { Period: "2020-01-15T00:00:00", Registrator: "Поступление 1" }, { Period: "2020-12-15T00:00:00", Registrator: "Поступление 2" } ] } ]) ); const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot); expect(draft.headline).toContain("долговой срез"); expect(draft.headline).toContain("качество открытых расчетов"); expect(draft.headline).toContain("возрастной сигнал открытых расчетов"); expect(draft.headline).toContain("staleness risk proxy открытых расчетов"); expect(draft.headline).toContain("договорные сроки оплаты/due-date просрочка"); expect(draft.confirmed_lines.join("\n")).toContain("Долговой срез на 2020-12-31"); expect(draft.confirmed_lines.join("\n")).toContain("Качество открытых расчетов на 2020-12-31"); expect(draft.confirmed_lines.join("\n")).toContain("Возрастной сигнал открытых расчетов"); expect(draft.confirmed_lines.join("\n")).toContain("не due-date анализ"); expect(draft.confirmed_lines.join("\n")).toContain("Staleness risk proxy открытых расчетов"); expect(draft.confirmed_lines.join("\n")).toContain("высокая зона внимания"); expect(draft.confirmed_lines.join("\n")).toContain("не due-date aging"); expect(draft.confirmed_lines.join("\n")).toContain("нетто"); expect(draft.inference_lines.join("\n")).toContain("Риски и контуры внимания"); expect(draft.inference_lines.join("\n")).toContain("самый старый договорный возрастной сигнал"); expect(draft.inference_lines.join("\n")).toContain("staleness risk proxy открытых расчетов"); expect(draft.inference_lines.join("\n")).toContain("Сводный LLM-аудит"); expect(draft.unknown_lines.join("\n")).toContain("due-date"); expect(draft.reason_codes).toContain("answer_contains_business_overview_debt_position"); expect(draft.reason_codes).toContain("answer_contains_business_overview_open_settlement_quality"); expect(draft.reason_codes).toContain("answer_contains_business_overview_debt_age_signal"); expect(draft.reason_codes).toContain("answer_contains_business_overview_debt_staleness_risk_proxy"); expect(draft.reason_codes).toContain("answer_contains_business_overview_analyst_synthesis"); expect(draft.must_not_claim).toContain("Do not present a debt-position snapshot as debt aging, overdue debt, or credit-quality analysis."); expect(draft.must_not_claim).toContain("Do not present open-settlement concentration as contractual due-date aging or confirmed overdue debt."); expect(draft.must_not_claim).toContain("Do not present business overview debt staleness risk proxy as confirmed overdue debt, contractual delinquency, credit risk, or due-date aging."); }); it("surfaces checked inventory-position snapshot in business overview without treating it as warehouse liquidity", async () => { const planner = planAssistantMcpDiscovery({ dataNeedGraph: { schema_version: "assistant_data_need_graph_v1", policy_owner: "assistantMcpDiscoveryDataNeedGraph", subject_candidates: [], business_fact_family: "business_overview", action_family: "broad_evaluation", aggregation_need: null, time_scope_need: "explicit_period", comparison_need: null, ranking_need: null, proof_expectation: "bounded_inference", clarification_gaps: [], decomposition_candidates: [ "collect_scoped_movements", "aggregate_checked_amounts", "aggregate_ranked_axis_values", "fetch_supporting_documents", "probe_coverage", "explain_evidence_basis" ], forbidden_overclaim_flags: ["no_raw_model_claims", "no_profit_or_margin_claim_without_evidence"], reason_codes: ["data_need_graph_built", "data_need_graph_family_business_overview"] }, turnMeaning: { asked_domain_family: "business_summary", asked_action_family: "broad_evaluation", explicit_organization_scope: "ООО Тест", explicit_date_scope: "2020", unsupported_but_understood_family: "broad_business_evaluation" } }); const pilot = await executeAssistantMcpDiscoveryPilot( planner, buildSequentialDeps([ { rows: [{ Period: "2020-01-15T00:00:00", Amount: 120000, Counterparty: "Клиент А" }] }, { rows: [{ Period: "2020-01-20T00:00:00", Amount: 50000, Counterparty: "Поставщик А" }] }, { rows: [] }, { rows: [] }, { rows: [] }, { rows: [] }, { rows: [ { Period: "2020-12-31T00:00:00", Amount: 250000, Quantity: 10, Item: "Товар А" }, { Period: "2020-12-31T00:00:00", Amount: 50000, Quantity: 5, Item: "Товар Б" } ] }, { rows: [ { Period: "2020-01-10T00:00:00", Amount: 200000, Quantity: 8, Item: "Товар А" } ] }, { rows: [{ Period: "2020-01-15T00:00:00", Registrator: "Поступление 1" }] }, { rows: [ { Period: "2020-03-01T00:00:00", Amount: 600000, Item: "Товар А", Counterparty: "Клиент А", AccountKt: "41.01" }, { Period: "2020-02-01T00:00:00", Amount: 240000, Item: "Товар А", Counterparty: "Поставщик А", AccountDt: "41.01" } ] } ]) ); const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot); expect(draft.headline).toContain("складской срез"); expect(draft.headline).toContain("оборотный proxy склада"); expect(draft.headline).toContain("staleness risk proxy склада"); expect(draft.headline).toContain("резервы/списания/ликвидационная стоимость склада"); expect(draft.headline).not.toContain("полноценная складская ликвидность"); expect(draft.confirmed_lines.join("\n")).toContain("Складской срез на 2020-12-31"); expect(draft.confirmed_lines.join("\n")).toContain("Товар А"); expect(draft.confirmed_lines.join("\n")).toContain("Оборотный proxy склада за 2020"); expect(draft.confirmed_lines.join("\n")).toContain("sales-to-stock ratio 2x"); expect(draft.confirmed_lines.join("\n")).toContain("Staleness risk proxy склада"); expect(draft.confirmed_lines.join("\n")).toContain("зона наблюдения"); expect(draft.inference_lines.join("\n")).toContain("оборотный proxy склада"); expect(draft.inference_lines.join("\n")).toContain("staleness risk proxy склада"); expect(draft.unknown_lines.join("\n")).toContain("Резервы"); expect(draft.next_step_line).toContain("НДС/налоговую позицию за явный период"); expect(draft.next_step_line).toContain("качество открытых расчетов"); expect(draft.next_step_line).toContain("резервы, списания, неликвидность и ликвидационную стоимость склада"); expect(draft.reason_codes).toContain("answer_contains_business_overview_inventory_position"); expect(draft.reason_codes).toContain("answer_contains_business_overview_inventory_turnover_proxy"); expect(draft.reason_codes).toContain("answer_contains_business_overview_inventory_staleness_risk_proxy"); expect(draft.must_not_claim).toContain("Do not present an inventory snapshot or purchase-date aging signal as turnover, obsolescence, liquidation value, or full inventory health."); expect(draft.must_not_claim).toContain("Do not present business overview inventory turnover proxy as full inventory liquidity, FIFO turnover, obsolescence analysis, or liquidation value."); expect(draft.must_not_claim).toContain("Do not present business overview inventory staleness risk proxy as confirmed obsolete stock, reserve, write-off, or liquidation value."); }); it("renders metadata-scoped movement all-time follow-up as an all-time bounded answer", async () => { const planner = planAssistantMcpDiscovery({ dataNeedGraph: { schema_version: "assistant_data_need_graph_v1", policy_owner: "assistantMcpDiscoveryDataNeedGraph", subject_candidates: [], business_fact_family: "movement_evidence", action_family: "list_movements", aggregation_need: null, time_scope_need: "all_time_scope", comparison_need: null, ranking_need: null, proof_expectation: "coverage_checked_fact", clarification_gaps: [], decomposition_candidates: ["collect_scoped_movements", "probe_coverage"], forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"], reason_codes: ["data_need_graph_built", "data_need_graph_all_time_scope_hint"] }, turnMeaning: { asked_domain_family: "movements", asked_action_family: "list_movements", explicit_entity_candidates: [], explicit_organization_scope: "ООО Альтернатива Плюс", unsupported_but_understood_family: "movement_evidence" } }); const pilot = await executeAssistantMcpDiscoveryPilot( planner, buildCustomQueryDeps({ fetched_rows: 100, matched_rows: 0, rows: [], raw_rows: [{ Period: "2020-06-30T00:00:00", Organization: "ООО Альтернатива Плюс", Registrar: "Move1" }] }) ); const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot); expect(draft.answer_mode).toBe("bounded_inference_only"); expect(draft.headline).toContain("движени"); expect(draft.headline).toContain("все доступное время"); expect(draft.headline).not.toContain("за 2020"); expect(draft.inference_lines.join("\n")).not.toContain("за 2020"); }); it("keeps bounded-only movement answers tied to the resolved entity and checked period", async () => { const planner = planAssistantMcpDiscovery({ turnMeaning: { asked_domain_family: "movements", asked_action_family: "list_movements", explicit_entity_candidates: ["Группа СВК"], explicit_date_scope: "2020", unsupported_but_understood_family: "movement_evidence" } }); const pilot = await executeAssistantMcpDiscoveryPilot( planner, buildCustomQueryDeps({ fetched_rows: 100, matched_rows: 0, rows: [], raw_rows: [{ Period: "2020-06-30T00:00:00", Counterparty: "Группа СВК", Registrar: "Move1" }] }) ); const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot); expect(draft.answer_mode).toBe("bounded_inference_only"); expect(draft.headline).toContain("движени"); expect(draft.headline).toContain("Группа СВК"); expect(draft.headline).toContain("2020"); expect(draft.inference_lines).toContain( "По движениям по контрагенту Группа СВК за 2020 удалось проверить только ограниченный срез 1С; подтвержденных строк движений этим поиском не найдено." ); expect(draft.unknown_lines).toContain( "Полный исторический срез движений по контрагенту Группа СВК вне периода 2020 этим поиском не подтвержден." ); }); it("asks for clarification when discovery did not execute due to missing scope", async () => { const planner = planAssistantMcpDiscovery({ turnMeaning: { asked_domain_family: "counterparty_value", asked_action_family: "turnover", explicit_entity_candidates: ["SVK"] } }); const pilot = await executeAssistantMcpDiscoveryPilot(planner, buildDeps([])); const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot); expect(draft.answer_mode).toBe("needs_clarification"); expect(draft.headline).toBe("Нужно уточнить контекст перед поиском в 1С."); expect(draft.next_step_line).toContain("Уточните контрагента"); expect(draft.must_not_claim).toContain("Do not claim rows were checked when mcp_execution_performed=false."); }); it("asks for organization rather than counterparty when a ranked value-flow ask already has the period", async () => { const planner = planAssistantMcpDiscovery({ dataNeedGraph: { schema_version: "assistant_data_need_graph_v1", policy_owner: "assistantMcpDiscoveryDataNeedGraph", subject_candidates: [], business_fact_family: "value_flow", action_family: "turnover", aggregation_need: null, time_scope_need: "explicit_period", comparison_need: null, ranking_need: "top_desc", proof_expectation: "coverage_checked_fact", clarification_gaps: [], decomposition_candidates: ["collect_scoped_movements", "aggregate_ranked_axis_values", "probe_coverage"], forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"], reason_codes: ["data_need_graph_built", "data_need_graph_ranking_top_desc"] }, turnMeaning: { asked_domain_family: "counterparty_value", asked_action_family: "turnover", explicit_date_scope: "2020" } }); const pilot = await executeAssistantMcpDiscoveryPilot(planner, buildDeps([])); const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot); expect(draft.answer_mode).toBe("needs_clarification"); expect(draft.headline).toContain("рейтинг"); expect(draft.next_step_line).toContain("организацию"); expect(draft.next_step_line).not.toContain("Уточните контрагента"); }); it("asks for both organization and period when an open ranking still misses both axes", async () => { const planner = planAssistantMcpDiscovery({ dataNeedGraph: { schema_version: "assistant_data_need_graph_v1", policy_owner: "assistantMcpDiscoveryDataNeedGraph", subject_candidates: [], business_fact_family: "value_flow", action_family: "turnover", aggregation_need: null, time_scope_need: "period_required", comparison_need: null, ranking_need: "top_desc", proof_expectation: "clarification_required", clarification_gaps: ["organization", "period"], decomposition_candidates: ["collect_scoped_movements", "aggregate_ranked_axis_values", "probe_coverage"], forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"], reason_codes: [ "data_need_graph_built", "data_need_graph_ranking_top_desc", "data_need_graph_open_scope_total_needs_organization", "data_need_graph_has_clarification_gaps" ] }, turnMeaning: { asked_domain_family: "counterparty_value", asked_action_family: "turnover", seeded_ranking_need: "top_desc" } }); const pilot = await executeAssistantMcpDiscoveryPilot(planner, buildDeps([])); const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot); expect(draft.answer_mode).toBe("needs_clarification"); expect(draft.headline).toContain("период"); expect(draft.headline).toContain("организац"); expect(draft.next_step_line).toContain("период"); expect(draft.next_step_line).toContain("организац"); }); it("renders confirmed ranked value-flow without raw technical evidence lines", async () => { const planner = planAssistantMcpDiscovery({ dataNeedGraph: { schema_version: "assistant_data_need_graph_v1", policy_owner: "assistantMcpDiscoveryDataNeedGraph", subject_candidates: [], business_fact_family: "value_flow", action_family: "turnover", aggregation_need: null, time_scope_need: "explicit_period", comparison_need: null, ranking_need: "top_desc", proof_expectation: "coverage_checked_fact", clarification_gaps: [], decomposition_candidates: ["collect_scoped_movements", "aggregate_ranked_axis_values", "probe_coverage"], forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"], reason_codes: ["data_need_graph_built", "data_need_graph_ranking_top_desc"] }, turnMeaning: { asked_domain_family: "counterparty_value", asked_action_family: "turnover", explicit_organization_scope: "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441", explicit_date_scope: "2020", unsupported_but_understood_family: "counterparty_value_or_turnover" } }); const pilot = await executeAssistantMcpDiscoveryPilot( planner, buildDeps([ { Period: "2020-01-15T00:00:00", Amount: 12000, Counterparty: "\u0421\u0411\u0415\u0420\u0411\u0410\u041d\u041a, \u041f\u0410\u041e", Organization: "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441" }, { Period: "2020-02-20T00:00:00", Amount: 5000, Counterparty: "\u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a", Organization: "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441" } ]) ); const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot); const userText = [...draft.confirmed_lines, ...draft.inference_lines, ...draft.unknown_lines].join("\n"); expect(draft.answer_mode).toBe("confirmed_with_bounded_inference"); expect(draft.confirmed_lines).toHaveLength(1); expect(userText).toContain("\u0411\u043e\u043b\u044c\u0448\u0435 \u0432\u0441\u0435\u0433\u043e \u0434\u0435\u043d\u0435\u0433 \u043f\u0440\u0438\u043d\u0451\u0441 \u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442"); expect(userText).toContain("\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441"); expect(userText).not.toContain("1C incoming value-flow"); expect(userText).not.toContain("Full ranking outside"); expect(draft.unknown_lines[0]).toContain("\u041f\u043e\u043b\u043d\u044b\u0439 \u0440\u0435\u0439\u0442\u0438\u043d\u0433"); }); it("does not overclaim a comparative ranking when only one counterparty is present", async () => { const planner = planAssistantMcpDiscovery({ dataNeedGraph: { schema_version: "assistant_data_need_graph_v1", policy_owner: "assistantMcpDiscoveryDataNeedGraph", subject_candidates: [], business_fact_family: "value_flow", action_family: "turnover", aggregation_need: null, time_scope_need: "explicit_period", comparison_need: null, ranking_need: "top_desc", proof_expectation: "coverage_checked_fact", clarification_gaps: [], decomposition_candidates: ["collect_scoped_movements", "aggregate_ranked_axis_values", "probe_coverage"], forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"], reason_codes: ["data_need_graph_built", "data_need_graph_ranking_top_desc"] }, turnMeaning: { asked_domain_family: "counterparty_value", asked_action_family: "turnover", explicit_organization_scope: "ООО Альтернатива Плюс", explicit_date_scope: "2021", seeded_ranking_need: "top_desc" } }); const pilot = await executeAssistantMcpDiscoveryPilot( planner, buildDeps([ { Period: "2021-01-15T00:00:00", Amount: 8560025, Counterparty: "Группа СВК", Organization: "ООО Альтернатива Плюс" } ]) ); const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot); const text = draft.confirmed_lines.join("\n"); expect(text).toContain("найден один контрагент"); expect(text).toContain("Группа СВК"); expect(text).toContain("не полноценный сравнительный рейтинг"); expect(text).not.toContain("Больше всего денег принёс"); }); it("asks for both organization and period when an open total still misses both axes", async () => { const planner = planAssistantMcpDiscovery({ dataNeedGraph: { schema_version: "assistant_data_need_graph_v1", policy_owner: "assistantMcpDiscoveryDataNeedGraph", subject_candidates: [], business_fact_family: "value_flow", action_family: "turnover", aggregation_need: null, time_scope_need: "period_required", comparison_need: null, ranking_need: null, proof_expectation: "clarification_required", clarification_gaps: ["organization", "period"], decomposition_candidates: ["collect_scoped_movements", "probe_coverage"], forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"], reason_codes: [ "data_need_graph_built", "data_need_graph_open_scope_total_without_subject", "data_need_graph_open_scope_total_needs_organization", "data_need_graph_has_clarification_gaps" ] }, turnMeaning: { asked_domain_family: "counterparty_value", asked_action_family: "turnover" } }); const pilot = await executeAssistantMcpDiscoveryPilot(planner, buildDeps([])); const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot); const periodToken = "\u043f\u0435\u0440\u0438\u043e\u0434"; const organizationToken = "\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446"; const counterpartyToken = "\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442"; expect(draft.answer_mode).toBe("needs_clarification"); expect(draft.headline).toContain(periodToken); expect(draft.headline).toContain(organizationToken); expect(draft.next_step_line).toContain(periodToken); expect(draft.next_step_line).toContain(organizationToken); expect(draft.next_step_line).not.toContain(counterpartyToken); expect(draft.unknown_lines).toEqual([]); expect(draft.limitation_lines).toEqual([]); }); it("asks for organization rather than counterparty on open bidirectional comparison when only the period is known", async () => { const planner = planAssistantMcpDiscovery({ dataNeedGraph: { schema_version: "assistant_data_need_graph_v1", policy_owner: "assistantMcpDiscoveryDataNeedGraph", subject_candidates: [], business_fact_family: "value_flow", action_family: "net_value_flow", aggregation_need: null, time_scope_need: "explicit_period", comparison_need: "incoming_vs_outgoing", ranking_need: null, proof_expectation: "coverage_checked_fact", clarification_gaps: [], decomposition_candidates: ["collect_incoming_movements", "collect_outgoing_movements", "probe_coverage"], forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"], reason_codes: ["data_need_graph_built", "data_need_graph_comparison_incoming_vs_outgoing"] }, turnMeaning: { asked_domain_family: "counterparty_value", asked_action_family: "net_value_flow", explicit_date_scope: "2020" } }); const pilot = await executeAssistantMcpDiscoveryPilot(planner, buildDeps([])); const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot); expect(draft.answer_mode).toBe("needs_clarification"); expect(draft.headline).toContain("входящий и исходящий"); expect(draft.next_step_line).toContain("организацию"); expect(draft.next_step_line).not.toContain("Уточните контрагента"); }); it("asks for an explicit lane choice when mixed metadata ambiguity cannot continue on a neutral follow-up", async () => { const planner = planAssistantMcpDiscovery({ turnMeaning: { asked_domain_family: "metadata", asked_action_family: "resolve_next_lane", explicit_entity_candidates: ["SVK"], explicit_date_scope: "2020", unsupported_but_understood_family: "metadata_lane_choice_clarification" } }); const pilot = await executeAssistantMcpDiscoveryPilot(planner, buildDeps([])); const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot); expect(draft.answer_mode).toBe("needs_clarification"); expect(draft.headline).toContain("контуров"); expect(draft.next_step_line).toContain("по документам"); expect(draft.next_step_line).toContain("по движениям/регистрам"); expect(draft.must_not_claim).toContain("Do not claim rows were checked when mcp_execution_performed=false."); }); it("keeps metadata lane-choice clarification human-facing when planner selects it from data-need graph", async () => { const planner = planAssistantMcpDiscovery({ dataNeedGraph: { schema_version: "assistant_data_need_graph_v1", policy_owner: "assistantMcpDiscoveryDataNeedGraph", subject_candidates: ["НДС"], business_fact_family: "metadata_surface", action_family: "resolve_next_lane", aggregation_need: null, time_scope_need: null, comparison_need: null, ranking_need: null, proof_expectation: "supporting_evidence", clarification_gaps: ["lane_family_choice"], decomposition_candidates: [], forbidden_overclaim_flags: ["no_raw_model_claims"], reason_codes: ["data_need_graph_built", "data_need_graph_requires_lane_family_choice"] }, turnMeaning: { asked_domain_family: "metadata", asked_action_family: "resolve_next_lane", explicit_entity_candidates: ["НДС"], unsupported_but_understood_family: "metadata_lane_choice_clarification" } }); const pilot = await executeAssistantMcpDiscoveryPilot(planner, buildDeps([])); const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot); expect(draft.answer_mode).toBe("needs_clarification"); expect(draft.headline).toContain("контуров"); expect(draft.next_step_line).toContain("по документам"); expect(draft.next_step_line).toContain("по движениям/регистрам"); expect(draft.next_step_line).not.toContain("Уточните контрагента"); }); it("keeps movement clarification anchored to the chosen lane after metadata ambiguity was resolved", async () => { const planner = planAssistantMcpDiscovery({ turnMeaning: { asked_domain_family: "movements", asked_action_family: "list_movements", explicit_entity_candidates: ["НДС"], unsupported_but_understood_family: "movement_evidence" } }); const pilot = await executeAssistantMcpDiscoveryPilot(planner, buildDeps([])); const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot); expect(draft.answer_mode).toBe("needs_clarification"); expect(draft.headline).toContain("движениям/регистрам"); expect(draft.headline).toContain("НДС"); expect(draft.headline).toContain("период"); expect(draft.next_step_line).toContain("движениям/регистрам"); expect(draft.next_step_line).toContain("НДС"); expect(draft.next_step_line).toContain("период"); }); it("turns resolved entity grounding into a human-safe entity search answer draft", async () => { const planner = planAssistantMcpDiscovery({ turnMeaning: { asked_domain_family: "entity_resolution", asked_action_family: "search_business_entity", explicit_entity_candidates: ["Группа СВК"], unsupported_but_understood_family: "entity_resolution" } }); const pilot = await executeAssistantMcpDiscoveryPilot( planner, buildDeps([ { Counterparty: "Группа СВК", CounterpartyRef: "Ref-1" }, { Counterparty: "СВК Логистика", CounterpartyRef: "Ref-2" } ]) ); const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot); expect(draft.answer_mode).toBe("confirmed_with_bounded_inference"); expect(draft.headline).toContain("вероятный контрагент"); expect(draft.confirmed_lines.join("\n")).toContain("Группа СВК"); expect(draft.inference_lines.join("\n")).toContain("заземление сущности"); expect(draft.next_step_line).toContain("искать документы, движения или денежный поток"); expect(draft.must_not_claim).toContain( "Do not present catalog grounding as confirmed business activity, turnover, or document evidence." ); }); it("asks for clarification when entity grounding stays ambiguous", async () => { const planner = planAssistantMcpDiscovery({ turnMeaning: { asked_domain_family: "entity_resolution", asked_action_family: "search_business_entity", explicit_entity_candidates: ["СВК"], unsupported_but_understood_family: "entity_resolution" } }); const pilot = await executeAssistantMcpDiscoveryPilot( planner, buildDeps([ { Counterparty: "СВК-А", CounterpartyRef: "Ref-1" }, { Counterparty: "СВК-Б", CounterpartyRef: "Ref-2" } ]) ); const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot); expect(draft.answer_mode).toBe("needs_clarification"); expect(draft.headline).toContain("несколько похожих контрагентов"); expect(draft.inference_lines.join("\n")).toContain("СВК-А"); expect(draft.inference_lines.join("\n")).toContain("1. СВК-А"); expect(draft.inference_lines.join("\n")).toContain("2. СВК-Б"); expect(draft.next_step_line).toContain("какой именно контрагент нужен"); expect(draft.next_step_line).toContain("1. СВК-А"); expect(draft.next_step_line).toContain("2. СВК-Б"); expect(draft.next_step_line).toContain("номером варианта"); }); it.skip("keeps entity search honest when no catalog candidate was confirmed", async () => { const planner = planAssistantMcpDiscovery({ turnMeaning: { asked_domain_family: "entity_resolution", asked_action_family: "search_business_entity", explicit_entity_candidates: ["Несуществующий Контрагент"], unsupported_but_understood_family: "entity_resolution" } }); const pilot = await executeAssistantMcpDiscoveryPilot( planner, buildDeps([{ Counterparty: "Группа СВК", CounterpartyRef: "Ref-1" }]) ); const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot); expect(draft.answer_mode).toBe("checked_sources_only"); expect(draft.headline).toContain("точный контрагент пока не подтвержден"); expect(draft.unknown_lines).toContain( 'No counterparty matching "Несуществующий Контрагент" was confirmed in the checked 1C catalog slice' ); expect(draft.next_step_line).toContain("Дайте точное название или ИНН"); }); it("turns metadata surface evidence into a human-safe metadata answer draft", async () => { const planner = planAssistantMcpDiscovery({ turnMeaning: { asked_domain_family: "metadata", asked_action_family: "inspect_documents", explicit_entity_candidates: ["НДС"] } }); const pilot = await executeAssistantMcpDiscoveryPilot( planner, buildMetadataDeps([ { FullName: "Документ.СчетФактураВыданный", MetaType: "Документ", attributes: [{ Name: "Дата" }, { Name: "Организация" }] } ]) ); const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot); const confirmedText = draft.confirmed_lines.join("\n"); expect(draft.answer_mode).toBe("confirmed_with_bounded_inference"); expect(draft.headline).toContain("схеме 1С"); expect(confirmedText).toContain("В схеме 1С"); expect(confirmedText).toContain("Документ.СчетФактураВыданный"); expect(confirmedText).toContain("Для следующего шага подходят"); expect(confirmedText).toContain("Дата"); expect(draft.inference_lines.join("\n")).toContain("контур документов"); expect(draft.next_step_line).toContain("типу «Документ»"); expect(confirmedText).not.toContain("Confirmed 1C metadata surface"); expect(confirmedText).not.toContain("Metadata surface family scores"); expect(draft.must_not_claim).toContain("Do not present metadata surface as confirmed business data rows."); expect(draft.must_not_claim).toContain("Do not present the inferred next checked lane as already executed data retrieval."); }); it("uses a distinct human headline for catalog drilldown instead of repeating a generic metadata overview", async () => { const planner = planAssistantMcpDiscovery({ metadataSurface: { selected_entity_set: "Catalog", selected_surface_objects: ["Catalog.Counterparties"], downstream_route_family: "catalog_drilldown", route_family_selection_basis: "selected_entity_set", recommended_next_primitive: "drilldown_related_objects", ambiguity_detected: false, ambiguity_entity_sets: [] }, turnMeaning: { asked_domain_family: "metadata", asked_action_family: "inspect_catalog", unsupported_but_understood_family: "schema_surface" } }); const pilot = await executeAssistantMcpDiscoveryPilot( planner, buildMetadataDeps([ { FullName: "Catalog.Counterparties", MetaType: "Catalog", attributes: [{ Name: "Description" }] } ]) ); const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot); expect(draft.answer_mode).toBe("confirmed_with_bounded_inference"); expect(draft.headline).toContain("углубиться"); expect(draft.headline).toContain("справочников"); expect(draft.headline).not.toContain("catalog drilldown"); }); it("renders catalog metadata without leaking internal surface scoring", async () => { const planner = planAssistantMcpDiscovery({ turnMeaning: { asked_domain_family: "metadata", asked_action_family: "inspect_catalog", explicit_entity_candidates: ["контрагент"], unsupported_but_understood_family: "1c_metadata_surface" } }); const pilot = await executeAssistantMcpDiscoveryPilot( planner, buildMetadataDeps([ { FullName: "Справочник.ДоговорыКонтрагентов", MetaType: "Справочник" }, { FullName: "Справочник.Контрагенты", MetaType: "Справочник" } ]) ); const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot); const userText = [ draft.headline, ...draft.confirmed_lines, ...draft.inference_lines, ...draft.unknown_lines, draft.next_step_line ?? "" ].join("\n"); expect(userText).toContain("Справочник.ДоговорыКонтрагентов"); expect(userText).toContain("Справочник.Контрагенты"); expect(userText).toContain("Детальный список полей"); expect(userText).not.toContain("Confirmed 1C metadata surface"); expect(userText).not.toContain("Metadata surface family scores"); expect(userText).not.toContain("surface «"); expect(userText).not.toContain("catalog drilldown"); }); it("keeps metadata answer honest when schema surface stays ambiguous", async () => { const planner = planAssistantMcpDiscovery({ turnMeaning: { asked_domain_family: "metadata", asked_action_family: "inspect_fields", explicit_entity_candidates: ["НДС"] } }); const pilot = await executeAssistantMcpDiscoveryPilot( planner, buildMetadataDeps([ { FullName: "Документ.СчетФактураВыданный", MetaType: "Документ", attributes: [{ Name: "Дата" }] }, { FullName: "РегистрНакопления.НДСПокупок", MetaType: "РегистрНакопления", resources: [{ Name: "СуммаНДС" }] } ]) ); const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot); expect(draft.headline).toContain("конкурирующих контуров"); expect(draft.inference_lines.join("\n")).toContain("несколько возможных контуров"); expect(draft.unknown_lines).toContain( "Exact downstream metadata surface remains ambiguous across: Документ, РегистрНакопления" ); expect(draft.next_step_line).toContain("Документ, РегистрНакопления"); }); it("turns value-flow evidence into a bounded turnover answer draft", async () => { const planner = planAssistantMcpDiscovery({ turnMeaning: { asked_domain_family: "counterparty_value", asked_action_family: "turnover", explicit_entity_candidates: ["SVK"], explicit_date_scope: "2020" } }); const pilot = await executeAssistantMcpDiscoveryPilot( planner, buildDeps([ { Period: "2020-01-15T00:00:00", Amount: 1250, Counterparty: "SVK" }, { Period: "2020-02-20T00:00:00", Amount: "2 500,50", Counterparty: "SVK" } ]) ); const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot); const confirmedText = draft.confirmed_lines.join("\n"); expect(draft.answer_mode).toBe("confirmed_with_bounded_inference"); expect(draft.headline).toContain("входящих денежных поступлений"); expect(confirmedText).toContain("3 750,50 руб."); expect(confirmedText).toContain("входящих денежных поступлений"); expect(confirmedText).toContain("2020-01-15"); expect(confirmedText).toContain("2020-02-20"); expect(draft.unknown_lines).toContain("Full turnover outside the checked period is not proven by this MCP discovery pilot"); expect(draft.must_not_claim).toContain("Do not claim full all-time turnover unless the checked period and coverage prove it."); expect(draft.limitation_lines.join("\n")).not.toContain("pilot_"); }); it("turns supplier payout evidence into a bounded outgoing payment answer draft", async () => { const planner = planAssistantMcpDiscovery({ turnMeaning: { asked_domain_family: "counterparty_value", asked_action_family: "payout", explicit_entity_candidates: ["SVK"], explicit_date_scope: "2020", unsupported_but_understood_family: "counterparty_payouts_or_outflow" } }); const pilot = await executeAssistantMcpDiscoveryPilot( planner, buildDeps([ { Period: "2020-03-15T00:00:00", Amount: 4100, Counterparty: "SVK" }, { Period: "2020-04-20T00:00:00", Amount: "900,25", Counterparty: "SVK" } ]) ); const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot); const confirmedText = draft.confirmed_lines.join("\n"); expect(draft.answer_mode).toBe("confirmed_with_bounded_inference"); expect(draft.headline).toContain("исходящих платежей"); expect(confirmedText).toContain("исходящих платежей/списаний"); expect(confirmedText).toContain("5 000,25 руб."); expect(draft.inference_lines.join("\n")).toContain("supplier-payout total"); expect(draft.unknown_lines).toContain("Full supplier-payout amount outside the checked period is not proven by this MCP discovery pilot"); }); it("turns bidirectional value-flow evidence into a bounded net cash answer draft", async () => { const planner = planAssistantMcpDiscovery({ turnMeaning: { asked_domain_family: "counterparty_value", asked_action_family: "net_value_flow", explicit_entity_candidates: ["SVK"], explicit_date_scope: "2020", unsupported_but_understood_family: "counterparty_bidirectional_value_flow_or_netting" } }); const pilot = await executeAssistantMcpDiscoveryPilot( planner, buildSequentialDeps([ { rows: [ { Period: "2020-01-15T00:00:00", Amount: 10000, Counterparty: "SVK" }, { Period: "2020-02-20T00:00:00", Amount: "2 500,50", Counterparty: "SVK" } ] }, { rows: [{ Period: "2020-03-10T00:00:00", Amount: 4000, Counterparty: "SVK" }] } ]) ); const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot); const confirmedText = draft.confirmed_lines.join("\n"); expect(draft.answer_mode).toBe("confirmed_with_bounded_inference"); expect(draft.headline).toContain("входящих и исходящих денежных движений"); expect(confirmedText).toContain("получили 12 500,50 руб."); expect(confirmedText).toContain("заплатили 4 000 руб."); expect(confirmedText).toContain("нетто в нашу сторону: 8 500,50 руб."); expect(draft.inference_lines.join("\n")).toContain("net value-flow"); expect(draft.unknown_lines).toContain("Full bidirectional value-flow outside the checked period is not proven by this MCP discovery pilot"); expect(draft.must_not_claim).toContain("Do not present a derived sum as a legal/accounting final total outside the checked 1C rows."); }); it("renders monthly bidirectional breakdown lines when the turn explicitly asked by month", async () => { const planner = planAssistantMcpDiscovery({ turnMeaning: { asked_domain_family: "counterparty_value", asked_action_family: "net_value_flow", asked_aggregation_axis: "month", explicit_entity_candidates: ["SVK"], explicit_date_scope: "2020", unsupported_but_understood_family: "counterparty_bidirectional_value_flow_or_netting" } }); const pilot = await executeAssistantMcpDiscoveryPilot( planner, buildSequentialDeps([ { rows: [ { Period: "2020-01-15T00:00:00", Amount: 10000, Counterparty: "SVK" }, { Period: "2020-02-20T00:00:00", Amount: "2 500,50", Counterparty: "SVK" } ] }, { rows: [ { Period: "2020-01-10T00:00:00", Amount: 4000, Counterparty: "SVK" }, { Period: "2020-02-11T00:00:00", Amount: 1000, Counterparty: "SVK" } ] } ]) ); const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot); const confirmedText = draft.confirmed_lines.join("\n"); expect(draft.answer_mode).toBe("confirmed_with_bounded_inference"); expect(draft.headline).toContain("помесяч"); expect(confirmedText).toContain("Помесячно: янв 2020"); expect(confirmedText).toContain("получили 10 000 руб."); expect(confirmedText).toContain("заплатили 4 000 руб."); expect(confirmedText).toContain("Помесячно: фев 2020"); expect(confirmedText).toContain("нетто в нашу сторону 1 500,50 руб."); expect(draft.reason_codes).toContain("answer_contains_monthly_breakdown"); }); it("keeps recovered yearly coverage out of the unknown block and explains the recovery as bounded inference", async () => { const planner = planAssistantMcpDiscovery({ turnMeaning: { asked_domain_family: "counterparty_value", asked_action_family: "payout", explicit_entity_candidates: ["SVK"], explicit_date_scope: "2020", unsupported_but_understood_family: "counterparty_payouts_or_outflow" } }); const broadRows = Array.from({ length: 100 }, (_, index) => ({ Period: `2020-01-${String((index % 28) + 1).padStart(2, "0")}T00:00:00`, Amount: 10, Counterparty: "SVK" })); const monthlyResults = Array.from({ length: 12 }, (_, index) => ({ rows: [ { Period: `2020-${String(index + 1).padStart(2, "0")}-05T00:00:00`, Amount: (index + 1) * 100, Counterparty: "SVK" } ] })); const pilot = await executeAssistantMcpDiscoveryPilot( planner, buildSequentialDeps([{ rows: broadRows }, ...monthlyResults]) ); const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot); expect(draft.inference_lines).toContain( "Requested period coverage was recovered through monthly 1C value-flow probes" ); expect(draft.unknown_lines).not.toContain( "Complete requested-period coverage is not proven by the available checked rows" ); }); it("does not leak primitive names or query text into user-facing lines", async () => { const planner = planAssistantMcpDiscovery({ turnMeaning: { asked_domain_family: "counterparty_lifecycle", asked_action_family: "activity_duration", explicit_entity_candidates: ["SVK"] } }); const pilot = await executeAssistantMcpDiscoveryPilot( planner, buildDeps([{ Период: "2020-01-15T00:00:00", Регистратор: "CP_CUSTOMER_ACTIVITY_FIRST", Контрагент: "SVK" }]) ); const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot); const userFacing = [ draft.headline, ...draft.confirmed_lines, ...draft.inference_lines, ...draft.unknown_lines, ...draft.limitation_lines, draft.next_step_line ?? "" ].join("\n"); expect(userFacing).not.toContain("query_documents"); expect(userFacing).not.toContain("SELECT"); expect(userFacing).not.toContain("ВЫБРАТЬ"); expect(userFacing).not.toContain("primitive"); }); it("verbalizes activity duration from first and latest confirmed 1C rows", async () => { const planner = planAssistantMcpDiscovery({ turnMeaning: { asked_domain_family: "counterparty_lifecycle", asked_action_family: "activity_duration", explicit_entity_candidates: ["SVK"] } }); const pilot = await executeAssistantMcpDiscoveryPilot( planner, buildDeps([ { Period: "2020-01-15T00:00:00", Counterparty: "SVK" }, { Period: "2023-12-20T00:00:00", Counterparty: "SVK" } ]) ); const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot); const inferenceText = draft.inference_lines.join("\n"); expect(inferenceText).toContain("3 года 11 месяцев"); expect(inferenceText).toContain("2020-01-15"); expect(inferenceText).toContain("2023-12-20"); expect(inferenceText).toContain("не юридически подтвержденный возраст регистрации"); expect(pilot.evidence.inferred_facts).toContain( "Activity window is bounded by first=2020-01-15, latest=2023-12-20, matched_rows=2" ); expect(draft.reason_codes).toContain("pilot_derived_activity_period_from_confirmed_rows"); }); it("keeps not-found entity search user-facing lines in Russian", async () => { const planner = planAssistantMcpDiscovery({ turnMeaning: { asked_domain_family: "entity_resolution", asked_action_family: "search_business_entity", explicit_entity_candidates: ["\u041d\u0435\u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0439 \u041a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442"], unsupported_but_understood_family: "entity_resolution" } }); const pilot = await executeAssistantMcpDiscoveryPilot( planner, buildDeps([{ Counterparty: "\u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a", CounterpartyRef: "Ref-1" }]) ); const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot); const unknownText = draft.unknown_lines.join("\n"); expect(draft.answer_mode).toBe("checked_sources_only"); expect(unknownText).toContain("\u043d\u0435 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d \u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442"); expect(unknownText).toContain("\u041d\u0435\u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0439 \u041a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442"); expect(unknownText).toContain("\u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u044b, \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u044f \u0438 \u0434\u0435\u043d\u0435\u0436\u043d\u044b\u0435 \u043f\u043e\u043a\u0430\u0437\u0430\u0442\u0435\u043b\u0438"); }); });