Закрепить reviewed routes и защиту бизнес-ответов

This commit is contained in:
2026-05-12 15:59:23 +03:00
parent b625f9af5b
commit 4fcf349894
47 changed files with 5341 additions and 416 deletions
@@ -8,6 +8,24 @@ describe("addressIntentResolver regression bridges", () => {
expect(result.intent).toBe("vat_liability_confirmed_for_tax_period");
});
it("detects VAT movement inspection wording with an explicit year", () => {
const result = resolveAddressIntent(
"\u043f\u043e\u043a\u0430\u0436\u0438 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u044f \u043f\u043e \u041d\u0414\u0421 \u0437\u0430 2020 \u043f\u043e \u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441"
);
expect(result.intent).toBe("vat_liability_confirmed_for_tax_period");
expect(result.reasons).toContain("vat_period_inspection_bridge_signal_detected");
});
it("detects canonical VAT charged-or-paid wording with an explicit year", () => {
const result = resolveAddressIntent(
"\u041a\u0430\u043a\u043e\u0439 \u041d\u0414\u0421 \u0431\u044b\u043b \u043d\u0430\u0447\u0438\u0441\u043b\u0435\u043d \u0438\u043b\u0438 \u0443\u043f\u043b\u0430\u0447\u0435\u043d \u0432 2020 \u0433\u043e\u0434\u0443 \u043f\u043e \u043a\u043e\u043c\u043f\u0430\u043d\u0438\u0438 \u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441?"
);
expect(result.intent).toBe("vat_liability_confirmed_for_tax_period");
expect(result.reasons).toContain("vat_liability_explicit_period_bridge_signal_detected");
});
it("detects payables snapshot wording in plain human form", () => {
const result = resolveAddressIntent("мы должны комуто денег на сегодня?");
@@ -1336,9 +1336,9 @@ describe("address compose stage utf8 headers", () => {
{ userMessage: "Сколько у нас заказчиков, поставщиков и смешанных контрагентов?" }
);
expect(reply.text).toContain("Роли контрагентов по активности:");
expect(reply.text).toContain("Заказчики (только customer-роль): 122.");
expect(reply.text).toContain("Поставщики (только supplier-роль): 71.");
expect(reply.text).toContain("Распределение ролей по активности:");
expect(reply.text).toContain("Заказчики с ролью покупателя: 122.");
expect(reply.text).toContain("Поставщики с ролью поставщика: 71.");
expect(reply.text).toContain("Смешанные (и покупатель, и поставщик): 23.");
expect(reply.text).not.toContain("Всего уникальных контрагентов в базе");
});
@@ -1391,7 +1391,7 @@ describe("address compose stage utf8 headers", () => {
{ userMessage: "скока поставщиков в базе" }
);
expect(reply.text).toContain("Поставщиков (только supplier-роль): 71.");
expect(reply.text).toContain("Поставщиков с ролью поставщика: 71.");
expect(reply.text).not.toContain("Роли контрагентов по активности:");
expect(reply.text).not.toContain("Всего уникальных контрагентов в базе");
});
@@ -1444,7 +1444,7 @@ describe("address compose stage utf8 headers", () => {
{ userMessage: "скок клиентов" }
);
expect(reply.text).toContain("Заказчиков (только customer-роль): 122.");
expect(reply.text).toContain("Заказчиков с ролью покупателя: 122.");
expect(reply.text).not.toContain("Роли контрагентов по активности:");
expect(reply.text).not.toContain("Всего уникальных контрагентов в базе");
});
@@ -1711,7 +1711,7 @@ describe("address compose stage utf8 headers", () => {
expect(reply.responseType).toBe("FACTUAL_LIST");
expect(reply.text).toContain("по максимальной сумме одной входящей операции");
expect(reply.text).toContain("1. Клиент Б | max single: 1200");
expect(reply.text).toContain("1. Клиент Б | максимальная разовая сумма: 1.200,00 ₽");
});
it("renders supplier payout list by operations count", () => {
@@ -2033,7 +2033,7 @@ describe("address compose stage utf8 headers", () => {
);
expect(reply.responseType).toBe("FACTUAL_SUMMARY");
expect(reply.text).toContain("Покрытие VAT-источников через MCP");
expect(reply.text).toContain("Покрытие VAT-источников в 1С");
expect(reply.text).toContain("Найдено VAT-объектов: 5");
expect(reply.text).toContain("РегистрНакопления.НДСПродажи");
});
@@ -2063,12 +2063,14 @@ describe("address compose stage utf8 headers", () => {
userMessage: "сколько платить ндс в налоговую за декабрь 2019",
periodFrom: "2019-10-01",
periodTo: "2019-12-31",
organizationHint: "ООО Альтернатива Плюс",
useRubCurrency: true
}
);
expect(reply.responseType).toBe("FACTUAL_SUMMARY");
expect(reply.text).toContain("Коротко: подтвержденный НДС к уплате за налоговый период");
expect(reply.text).toContain("Коротко: подтвержденный НДС к уплате за налоговый период по организации ООО Альтернатива Плюс");
expect(reply.text).toContain("- Организация: ООО Альтернатива Плюс.");
expect(reply.text).toContain("50.000,00 ₽");
expect(reply.semantics?.result_mode).toBe("confirmed_balance");
expect(reply.semantics?.balance_confirmed).toBe(true);
@@ -2214,7 +2216,7 @@ describe("address compose stage utf8 headers", () => {
);
expect(reply.responseType).toBe("FACTUAL_LIST");
expect(reply.text).toContain("Блок 2.1. MCP-проверка VAT-источников");
expect(reply.text).toContain("Блок 2.1. Проверка VAT-источников в 1С");
expect(reply.text).toContain("VAT-объектов в метаданных 1С: 3");
expect(reply.text).toContain("Источников с движениями до даты среза: 1");
expect(reply.text).toContain("РегистрНакопления.НДСНачисленный");
@@ -2247,7 +2249,7 @@ describe("address compose stage utf8 headers", () => {
);
expect(reply.responseType).toBe("FACTUAL_LIST");
expect(reply.text).toContain("Probe VAT-источников завершился ошибкой");
expect(reply.text).toContain("Дополнительная проверка VAT-источников завершилась ошибкой");
});
});
@@ -5099,13 +5101,13 @@ describe("address recipe catalog counterparty filtering", () => {
expect(aging.extracted_filters.limit).toBeUndefined();
});
it("selects customer value recipe and keeps top-20 default", () => {
it("selects customer value recipe and keeps expanded top-200 default", () => {
const selected = selectAddressRecipe("customer_revenue_and_payments", {});
expect(selected.selected_recipe).toBeTruthy();
const plan = buildAddressRecipePlan(selected.selected_recipe!, {});
expect(plan.recipe.recipe_id).toBe("address_customer_revenue_and_payments_v1");
expect(plan.limit).toBe(20);
expect(plan.limit).toBe(200);
expect(plan.query).toContain("ПоступлениеНаРасчетныйСчет");
expect(plan.query).toContain("БанкПоступление.ДоговорКонтрагента");
});
@@ -5138,13 +5140,13 @@ describe("address recipe catalog counterparty filtering", () => {
expect(plan.limit).toBe(1000);
});
it("selects supplier payouts recipe and keeps top-20 default", () => {
it("selects supplier payouts recipe and keeps expanded top-200 default", () => {
const selected = selectAddressRecipe("supplier_payouts_profile", {});
expect(selected.selected_recipe).toBeTruthy();
const plan = buildAddressRecipePlan(selected.selected_recipe!, {});
expect(plan.recipe.recipe_id).toBe("address_supplier_payouts_profile_v1");
expect(plan.limit).toBe(20);
expect(plan.limit).toBe(200);
expect(plan.query).toContain("СписаниеСРасчетногоСчета");
expect(plan.query).toContain("БанкСписание.ДоговорКонтрагента");
});
@@ -5343,6 +5345,26 @@ describe("address recipe catalog counterparty filtering", () => {
expect(plan.query).toContain("ПОДСТРОКА(ЕСТЬNULL(Остатки.Счет.Код, \"\"), 1, 2) = \"60\"");
});
it("builds debt due-date aging query without carrying noisy organization suffix", () => {
const selected = selectAddressRecipe("debt_due_date_aging_for_organization", {
as_of_date: "2020-12-31",
organization: "ООО Альтернатива Плюс на конец 2020 можно точно понять"
});
expect(selected.selected_recipe?.recipe_id).toBe("address_debt_due_date_aging_for_organization_v1");
const plan = buildAddressRecipePlan(selected.selected_recipe!, {
as_of_date: "2020-12-31",
organization: "ООО Альтернатива Плюс на конец 2020 можно точно понять"
});
expect(plan.query).toContain("УстановленСрокОплаты");
expect(plan.query).toContain('Наименование ПОДОБНО "%Альтернатива%"');
expect(plan.query).toContain('Наименование ПОДОБНО "%Плюс%"');
expect(plan.query).not.toContain('Наименование ПОДОБНО "%конец%"');
expect(plan.query).not.toContain('Наименование ПОДОБНО "%2020%"');
expect(plan.query).not.toContain('Наименование ПОДОБНО "%можно%"');
expect(plan.query).not.toContain('Наименование ПОДОБНО "%понять%"');
});
it("injects account condition into movements query for account snapshot", () => {
const filters = extractAddressFilters(
"Какой остаток по счету 60 на дату 2020-07-31",
@@ -180,6 +180,76 @@ describe("assistant MCP discovery answer adapter", () => {
expect(draft.must_not_claim).toContain("Do not present the confirmed movement rows as a complete movement universe.");
});
it("does not present bank-like ranked value-flow leaders as ordinary customers", () => {
const draft = buildAssistantMcpDiscoveryAnswerDraft({
pilot_status: "executed",
pilot_scope: "counterparty_ranked_value_flow_query_movements_v1",
dry_run: false,
mcp_execution_performed: true,
executed_primitives: ["query_movements"],
skipped_primitives: [],
probe_results: [],
evidence: {
evidence_status: "confirmed",
answer_permission: "confirmed_answer",
confirmed_facts: [],
inferred_facts: [],
unknown_facts: [],
query_limitations: [],
reason_codes: [],
query_plan: {}
},
source_rows_summary: "2 MCP movement rows fetched, 2 matched ranking scope",
derived_metadata_surface: null,
derived_entity_resolution: null,
derived_activity_period: null,
derived_value_flow: null,
derived_bidirectional_value_flow: null,
derived_business_overview: null,
derived_ranked_value_flow: {
organization_scope: "ООО Альтернатива Плюс",
period_scope: "2020",
value_flow_direction: "incoming_customer_revenue",
ranking_need: "top_desc",
aggregation_axis: "counterparty",
coverage_limited_by_probe_limit: false,
ranked_values: [
{
axis_value: "СБЕРБАНК, ПАО",
total_amount: 12792194.31,
total_amount_human_ru: "12 792 194,31 руб.",
rows_with_amount: 3,
rows_matched: 3,
first_movement_date: "2020-01-15",
latest_movement_date: "2020-12-20",
counterparty_role_hint: "bank_or_financial_institution"
},
{
axis_value: "Группа СВК",
total_amount: 12093465,
total_amount_human_ru: "12 093 465 руб.",
rows_with_amount: 2,
rows_matched: 2,
first_movement_date: "2020-02-15",
latest_movement_date: "2020-11-10",
counterparty_role_hint: "ordinary_counterparty"
}
],
inference_basis: "confirmed_1c_movement_rows_grouped_by_counterparty"
},
query_limitations: [],
reason_codes: ["pilot_derived_ranked_value_flow_from_confirmed_rows"]
} as any);
const confirmed = draft.confirmed_lines.join("\n");
expect(confirmed).toContain("Крупнейший входящий денежный источник СБЕРБАНК, ПАО");
expect(confirmed).toContain("не называю это клиентской выручкой");
expect(confirmed).not.toContain("Больше всего денег принёс контрагент СБЕРБАНК");
expect(draft.must_not_claim).toContain(
"Do not present bank-like counterparties as ordinary customers, suppliers, revenue, procurement dependency, or business quality evidence without payment-purpose/contract proof."
);
});
it("turns business overview multi-probe evidence into an analyst-safe draft", async () => {
const planner = planAssistantMcpDiscovery({
dataNeedGraph: {
@@ -355,6 +425,7 @@ describe("assistant MCP discovery answer adapter", () => {
{ rows: [] },
{ rows: [] },
{ rows: [] },
{ rows: [] },
{
rows: [
{ Период: "2020-01-15T00:00:00", Регистратор: "Поступление 1" },
@@ -429,6 +500,7 @@ describe("assistant MCP discovery answer adapter", () => {
{ rows: [{ Period: "2020-01-15T00:00:00", Amount: 120000, Counterparty: "Клиент А" }] },
{ rows: [{ Period: "2020-01-20T00:00:00", Amount: 50000, Counterparty: "Поставщик А" }] },
{ rows: [] },
{ rows: [] },
{
rows: [
{ Period: "2020-12-31T00:00:00", Amount: 100000, Counterparty: "Клиент А" }
@@ -528,6 +600,7 @@ describe("assistant MCP discovery answer adapter", () => {
{ rows: [] },
{ rows: [] },
{ rows: [] },
{ rows: [] },
{
rows: [
{ Period: "2020-12-31T00:00:00", Amount: 250000, Quantity: 10, Item: "Товар А" },
@@ -576,6 +649,73 @@ describe("assistant MCP discovery answer adapter", () => {
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("answers inventory reserve boundary questions direct-first before broad overview detail", async () => {
const planner = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
business_fact_family: "business_overview",
action_family: "inventory_reserve_boundary",
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", "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_overview",
asked_action_family: "inventory_reserve_boundary",
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020",
unsupported_but_understood_family: "inventory_reserve_liquidation_boundary"
}
});
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: [] },
{
rows: [
{ Period: "2020-12-31T00:00:00", Amount: 250000, Quantity: 10, 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(
"\u0442\u043e\u0447\u043d\u043e \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c \u0440\u0435\u0437\u0435\u0440\u0432"
);
expect(draft.headline).toContain("\u043d\u0435\u043b\u044c\u0437\u044f");
expect(draft.headline).toContain("staleness-risk proxy");
expect(draft.headline).not.toContain("бизнес-обзор");
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: {
@@ -796,7 +936,9 @@ describe("assistant MCP discovery answer adapter", () => {
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("Крупнейший входящий денежный источник");
expect(userText).toContain("не называю это клиентской выручкой");
expect(userText).not.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 \u0421\u0411\u0415\u0420\u0411\u0410\u041d\u041a");
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");
@@ -1409,7 +1551,7 @@ describe("assistant MCP discovery answer adapter", () => {
unsupported_but_understood_family: "counterparty_payouts_or_outflow"
}
});
const broadRows = Array.from({ length: 100 }, (_, index) => ({
const broadRows = Array.from({ length: 200 }, (_, index) => ({
Period: `2020-01-${String((index % 28) + 1).padStart(2, "0")}T00:00:00`,
Amount: 10,
Counterparty: "SVK"
@@ -26,6 +26,25 @@ function entryPointContract(overrides: Record<string, unknown> = {}) {
selected_chain_matches_top: true
}
},
route_candidate: {
schema_version: "assistant_mcp_route_candidate_v1",
policy_owner: "assistantMcpDiscoveryRuntimeBridge",
candidate_status: "ready_for_reviewed_execution",
selected_chain_id: "value_flow_ranking",
selected_chain_summary: "Rank value flow",
nearest_catalog_chain_template: "value_flow_ranking",
catalog_alignment_status: "selected_matches_top",
business_fact_family: "value_flow",
action_family: "turnover",
proof_expectation: "coverage_checked_fact",
required_axes: ["organization", "period"],
provided_axes: ["organization", "period"],
missing_axes: [],
executable_now: true,
enablement_reason: null,
recommended_next_action: "Execute through the reviewed runtime bridge and truth gate.",
forbidden_overclaim_flags: ["no_unchecked_fact_totals"]
},
answer_draft: {
answer_mode: "confirmed_with_bounded_inference"
}
@@ -54,6 +73,15 @@ describe("assistant MCP discovery debug attachment", () => {
expect(debug.mcp_discovery_catalog_chain_alignment_status).toBe("selected_matches_top");
expect(debug.mcp_discovery_catalog_chain_top_match).toBe("value_flow_ranking");
expect(debug.mcp_discovery_catalog_chain_selected_matches_top).toBe(true);
expect(debug.mcp_discovery_route_candidate_status).toBe("ready_for_reviewed_execution");
expect(debug.mcp_discovery_route_candidate_fact_family).toBe("value_flow");
expect(debug.mcp_discovery_route_candidate_action_family).toBe("turnover");
expect(debug.mcp_discovery_route_candidate_missing_axes).toEqual([]);
expect(debug.mcp_discovery_route_candidate_provided_axes).toEqual(["organization", "period"]);
expect(debug.mcp_discovery_route_candidate_executable_now).toBe(true);
expect(debug.mcp_discovery_route_candidate_next_action).toBe(
"Execute through the reviewed runtime bridge and truth gate."
);
expect(debug.mcp_discovery_answer_mode).toBe("confirmed_with_bounded_inference");
expect(debug.mcp_discovery_business_fact_answer_allowed).toBe(true);
expect(debug.mcp_discovery_user_facing_response_allowed).toBe(true);
@@ -76,6 +104,11 @@ describe("assistant MCP discovery debug attachment", () => {
expect(debug.mcp_discovery_catalog_chain_alignment_status).toBeNull();
expect(debug.mcp_discovery_catalog_chain_top_match).toBeNull();
expect(debug.mcp_discovery_catalog_chain_selected_matches_top).toBe(false);
expect(debug.mcp_discovery_route_candidate_v1).toBeNull();
expect(debug.mcp_discovery_route_candidate_status).toBeNull();
expect(debug.mcp_discovery_route_candidate_missing_axes).toEqual([]);
expect(debug.mcp_discovery_route_candidate_provided_axes).toEqual([]);
expect(debug.mcp_discovery_route_candidate_executable_now).toBe(false);
expect(debug.mcp_discovery_answer_mode).toBeNull();
expect(debug.mcp_discovery_business_fact_answer_allowed).toBe(false);
expect(debug.mcp_discovery_user_facing_response_allowed).toBe(false);
@@ -293,6 +293,80 @@ describe("assistant MCP discovery pilot executor", () => {
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(6);
});
it("marks bank-like counterparties in business-overview rankings before evidence wording", 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_overview",
asked_action_family: "broad_evaluation",
explicit_organization_scope: "ООО Альтернатива Плюс"
}
});
const deps = buildSequentialDeps([
{
rows: [
{ Period: "2020-01-15T00:00:00", Amount: 1200000, Counterparty: "СБЕРБАНК, ПАО" },
{ Period: "2020-02-15T00:00:00", Amount: 800000, Counterparty: "Группа СВК" }
]
},
{
rows: [
{ Period: "2020-01-20T00:00:00", Amount: 650000, Counterparty: "СБЕРБАНК, ПАО" },
{ Period: "2020-03-20T00:00:00", Amount: 50000, Counterparty: "ООО Поставщик" }
]
},
{ rows: [] },
{ rows: [] },
{ rows: [] },
{ rows: [] }
]);
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
expect(result.derived_business_overview?.top_customers[0]).toMatchObject({
axis_value: "СБЕРБАНК, ПАО",
counterparty_role_hint: "bank_or_financial_institution"
});
expect(result.derived_business_overview?.top_customers[1]).toMatchObject({
axis_value: "Группа СВК",
counterparty_role_hint: "ordinary_counterparty"
});
expect(result.derived_business_overview?.top_suppliers[0]).toMatchObject({
axis_value: "СБЕРБАНК, ПАО",
counterparty_role_hint: "bank_or_financial_institution"
});
const confirmedFacts = result.evidence.confirmed_facts.join("\n");
const inferredFacts = result.evidence.inferred_facts.join("\n");
expect(confirmedFacts).toContain("Крупнейший входящий денежный источник");
expect(confirmedFacts).toContain("Крупнейший небанковский входящий контрагент");
expect(confirmedFacts).toContain("Крупнейший получатель исходящих денег");
expect(confirmedFacts).not.toContain("Самый крупный подтвержденный клиент в проверенном срезе: СБЕРБАНК");
expect(confirmedFacts).not.toContain("Самый крупный подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: СБЕРБАНК");
expect(inferredFacts).toContain("outgoing cash concentration proxy");
});
it("adds a checked VAT/tax family to business overview only when an explicit period is available", async () => {
const planner = planAssistantMcpDiscovery({
dataNeedGraph: {
@@ -347,6 +421,7 @@ describe("assistant MCP discovery pilot executor", () => {
{ rows: [] },
{ rows: [] },
{ rows: [] },
{ rows: [] },
{
rows: [
{ Период: "2020-01-15T00:00:00", Регистратор: "Поступление 1" },
@@ -402,9 +477,9 @@ describe("assistant MCP discovery pilot executor", () => {
expect(result.reason_codes).toContain("pilot_derived_business_overview_tax_position_from_confirmed_rows");
expect(result.reason_codes).toContain("pilot_business_overview_trading_margin_query_mcp_executed");
expect(result.reason_codes).toContain("pilot_derived_business_overview_trading_margin_proxy_from_confirmed_rows");
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(13);
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(14);
const taxCall = deps.executeAddressMcpQuery.mock.calls[2]?.[0];
const tradingMarginCall = deps.executeAddressMcpQuery.mock.calls[9]?.[0];
const tradingMarginCall = deps.executeAddressMcpQuery.mock.calls[10]?.[0];
expect(String(taxCall?.query ?? "")).toContain("НДСЗаписиКнигиПродаж");
expect(String(taxCall?.query ?? "")).toContain("НДСЗаписиКнигиПокупок");
expect(String(tradingMarginCall?.query ?? "")).toContain("Документ.РеализацияТоваровУслуг.Товары");
@@ -453,6 +528,7 @@ describe("assistant MCP discovery pilot executor", () => {
{ rows: [] },
{ rows: [] },
{ rows: [] },
{ rows: [] },
{
rows: [
{ Period: "2020-03-01T00:00:00", Amount: 100000, Item: "Товар А", Counterparty: "Клиент А", AccountKt: "41.01" },
@@ -510,6 +586,7 @@ describe("assistant MCP discovery pilot executor", () => {
{ rows: [{ Period: "2020-01-15T00:00:00", Amount: 120000, Counterparty: "Клиент А" }] },
{ rows: [{ Period: "2020-01-20T00:00:00", Amount: 50000, Counterparty: "Поставщик А" }] },
{ rows: [] },
{ rows: [] },
{
rows: [
{ Period: "2020-12-31T00:00:00", Amount: 70000, Counterparty: "Клиент А" },
@@ -602,15 +679,107 @@ describe("assistant MCP discovery pilot executor", () => {
expect(result.reason_codes).toContain("pilot_derived_business_overview_open_settlement_quality_from_confirmed_rows");
expect(result.reason_codes).toContain("pilot_derived_business_overview_debt_age_signal_from_contract_dates");
expect(result.reason_codes).toContain("pilot_derived_business_overview_debt_staleness_risk_proxy_from_confirmed_rows");
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(13);
const receivablesCall = deps.executeAddressMcpQuery.mock.calls[3]?.[0];
const payablesCall = deps.executeAddressMcpQuery.mock.calls[4]?.[0];
const openContractsCall = deps.executeAddressMcpQuery.mock.calls[5]?.[0];
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(14);
const receivablesCall = deps.executeAddressMcpQuery.mock.calls[4]?.[0];
const payablesCall = deps.executeAddressMcpQuery.mock.calls[5]?.[0];
const openContractsCall = deps.executeAddressMcpQuery.mock.calls[6]?.[0];
expect(String(receivablesCall?.query ?? "")).toContain("62");
expect(String(payablesCall?.query ?? "")).toContain("60");
expect(String(openContractsCall?.query ?? "")).toContain("СуммаРазвернутыйОстатокКт");
});
it("checks debt due-date aging with contract payment terms before claiming 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: "debt_due_date_boundary",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: null,
ranking_need: null,
proof_expectation: "due_date_aging",
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_unchecked_overdue_claim"],
reason_codes: ["data_need_graph_built", "data_need_graph_family_business_overview"]
},
turnMeaning: {
asked_domain_family: "business_overview",
asked_action_family: "debt_due_date_boundary",
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020"
}
});
const deps = buildSequentialDeps([
{ rows: [] },
{ rows: [] },
{ rows: [] },
{ rows: [] },
{ rows: [] },
{ rows: [] },
{
rows: [
{ Period: "2020-12-31T00:00:00", Amount: 100000, Counterparty: "Клиент А", Contract: "Договор А от 10.02.2019" }
]
},
{
rows: [
{
Period: "2020-12-31T00:00:00",
Amount: 100000,
Counterparty: "Клиент А",
Contract: "Договор А от 10.02.2019",
ДокументРасчетов: "Реализация товаров от 10.03.2020",
УстановленСрокОплаты: false,
СрокОплаты: 0
}
]
},
{ rows: [] },
{ rows: [] },
{ rows: [] },
{ rows: [] },
{ rows: [] },
{ rows: [] },
{ rows: [] }
]);
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
expect(result.derived_business_overview?.debt_due_date_aging).toMatchObject({
as_of_date: "2020-12-31",
rows_with_amount: 1,
gross_open_amount: 100000,
rows_with_payment_terms: 0,
rows_without_payment_terms: 1,
overdue_rows: 0,
evidence_status: "no_payment_terms_configured"
});
expect(result.derived_business_overview?.missing_signal_families).not.toContain("debt_due_date_aging_quality");
expect(result.derived_business_overview?.missing_proof_families.map((item) => item.family)).not.toContain(
"debt_due_date_aging_quality"
);
expect(result.evidence.confirmed_facts.join("\n")).toContain("срок оплаты не установлен");
expect(result.evidence.confirmed_facts.join("\n")).toContain("Подтвержденной просрочки");
expect(result.evidence.unknown_facts.join("\n")).not.toContain("due-date aging этим бизнес-обзором не подтверждены");
expect(result.reason_codes).toContain("pilot_business_overview_debt_due_date_aging_query_mcp_executed");
expect(result.reason_codes).toContain("pilot_derived_business_overview_debt_due_date_aging_from_confirmed_rows");
expect(result.reason_codes).toContain("pilot_derived_business_overview_debt_due_date_aging_no_payment_terms_configured");
const dueDateCall = deps.executeAddressMcpQuery.mock.calls[7]?.[0];
expect(String(dueDateCall?.query ?? "")).toContain("УстановленСрокОплаты");
expect(String(dueDateCall?.query ?? "")).toContain("СрокОплаты");
});
it("adds a checked inventory-position family to business overview only as an as-of-date snapshot", async () => {
const planner = planAssistantMcpDiscovery({
dataNeedGraph: {
@@ -650,6 +819,7 @@ describe("assistant MCP discovery pilot executor", () => {
{ rows: [] },
{ rows: [] },
{ rows: [] },
{ rows: [] },
{
rows: [
{ Period: "2020-12-31T00:00:00", Amount: 250000, Quantity: 10, Item: "Товар А" },
@@ -729,8 +899,8 @@ describe("assistant MCP discovery pilot executor", () => {
expect(result.reason_codes).toContain("pilot_derived_business_overview_inventory_position_from_confirmed_rows");
expect(result.reason_codes).toContain("pilot_derived_business_overview_inventory_turnover_proxy_from_confirmed_rows");
expect(result.reason_codes).toContain("pilot_derived_business_overview_inventory_staleness_risk_proxy_from_confirmed_rows");
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(13);
const inventoryCall = deps.executeAddressMcpQuery.mock.calls[6]?.[0];
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(14);
const inventoryCall = deps.executeAddressMcpQuery.mock.calls[7]?.[0];
expect(inventoryCall?.account_scope).toContain("41.01");
});
@@ -1335,7 +1505,7 @@ describe("assistant MCP discovery pilot executor", () => {
unsupported_but_understood_family: "counterparty_payouts_or_outflow"
}
});
const rows = Array.from({ length: 100 }, (_, index) => ({
const rows = Array.from({ length: 200 }, (_, index) => ({
Period: `2020-01-${String((index % 28) + 1).padStart(2, "0")}T00:00:00`,
Amount: 10,
Counterparty: "SVK"
@@ -1359,7 +1529,7 @@ describe("assistant MCP discovery pilot executor", () => {
unsupported_but_understood_family: "counterparty_payouts_or_outflow"
}
});
const broadRows = Array.from({ length: 100 }, (_, index) => ({
const broadRows = Array.from({ length: 200 }, (_, index) => ({
Period: `2020-01-${String((index % 28) + 1).padStart(2, "0")}T00:00:00`,
Amount: 10,
Counterparty: "SVK"
@@ -1560,7 +1730,7 @@ describe("assistant MCP discovery pilot executor", () => {
unsupported_but_understood_family: "counterparty_bidirectional_value_flow_or_netting"
}
});
const outgoingBroadRows = Array.from({ length: 100 }, (_, index) => ({
const outgoingBroadRows = Array.from({ length: 200 }, (_, index) => ({
Period: `2020-01-${String((index % 28) + 1).padStart(2, "0")}T00:00:00`,
Amount: 10,
Counterparty: "SVK"
@@ -44,6 +44,177 @@ describe("assistant MCP discovery response candidate", () => {
expect(candidate.reply_text).not.toContain("primitive");
});
it("keeps inventory reserve boundary answers direct instead of compacting into a money overview", () => {
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
entryPoint({
turn_input: {
adapter_status: "ready",
turn_meaning_ref: {
asked_domain_family: "business_overview",
asked_action_family: "inventory_reserve_boundary",
unsupported_but_understood_family: "inventory_reserve_liquidation_boundary"
},
data_need_graph: {
business_fact_family: "business_overview",
ranking_need: null,
reason_codes: ["data_need_graph_family_business_overview"]
}
},
bridge: {
bridge_status: "answer_draft_ready",
user_facing_response_allowed: true,
business_fact_answer_allowed: true,
requires_user_clarification: false,
pilot: {
pilot_scope: "business_overview_route_template_v1",
derived_business_overview: {
period_scope: null,
incoming_customer_revenue: {
total_amount_human_ru: "157 192 981,43 руб.",
coverage_limited_by_probe_limit: true
},
outgoing_supplier_payout: {
total_amount_human_ru: "35 439 044,74 руб.",
coverage_limited_by_probe_limit: true
},
net_amount_human_ru: "121 753 936,69 руб.",
net_direction: "net_incoming"
}
},
answer_draft: {
answer_mode: "confirmed_with_bounded_inference",
headline:
"\u041a\u043e\u0440\u043e\u0442\u043a\u043e: \u0442\u043e\u0447\u043d\u043e \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c \u0440\u0435\u0437\u0435\u0440\u0432 \u043f\u043e\u0434 \u043d\u0435\u043b\u0438\u043a\u0432\u0438\u0434\u044b \u043f\u043e \u0442\u0435\u043a\u0443\u0449\u0438\u043c \u0434\u0430\u043d\u043d\u044b\u043c \u043d\u0435\u043b\u044c\u0437\u044f.",
confirmed_lines: ["Денежный обзор здесь не является ответом на складской резерв."],
inference_lines: [],
unknown_lines: [
"\u0420\u0435\u0437\u0435\u0440\u0432\u044b, \u0441\u043f\u0438\u0441\u0430\u043d\u0438\u044f \u0438 \u043b\u0438\u043a\u0432\u0438\u0434\u0430\u0446\u0438\u043e\u043d\u043d\u0430\u044f \u0441\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u044c \u0441\u043a\u043b\u0430\u0434\u0430 \u043d\u0435 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u044b."
],
limitation_lines: [],
next_step_line: null
}
}
})
);
expect(candidate.reply_text).toContain(
"\u0442\u043e\u0447\u043d\u043e \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c \u0440\u0435\u0437\u0435\u0440\u0432"
);
expect(candidate.reply_text).toContain("\u041f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u043d\u0443\u0436\u043d\u043e");
expect(candidate.reply_text).not.toContain("157 192 981");
expect(candidate.reply_text).not.toContain("лимит");
});
it("keeps profit/margin boundary answers direct instead of compacting into a money overview", () => {
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
entryPoint({
turn_input: {
adapter_status: "ready",
turn_meaning_ref: {
asked_domain_family: "business_overview",
asked_action_family: "profit_margin_boundary",
unsupported_but_understood_family: "profit_margin_boundary"
},
data_need_graph: {
business_fact_family: "business_overview",
ranking_need: null,
reason_codes: ["data_need_graph_family_business_overview"]
}
},
bridge: {
bridge_status: "answer_draft_ready",
user_facing_response_allowed: true,
business_fact_answer_allowed: true,
requires_user_clarification: false,
pilot: {
pilot_scope: "business_overview_route_template_v1",
derived_business_overview: {
incoming_customer_revenue: {
total_amount_human_ru: "47 628 853,03 руб."
},
outgoing_supplier_payout: {
total_amount_human_ru: "19 568 878,06 руб."
},
net_amount_human_ru: "28 059 974,97 руб.",
net_direction: "net_incoming"
}
},
answer_draft: {
answer_mode: "confirmed_with_bounded_inference",
headline:
"Коротко: нельзя точно подтвердить чистую прибыль и маржу по текущему срезу 1С; есть только bounded operating-flow/trading-margin proxy, не P&L и не бухгалтерский финансовый результат.",
confirmed_lines: ["Денежный обзор здесь не является ответом на чистую прибыль."],
inference_lines: [],
unknown_lines: ["Для точного P&L нужны себестоимость, расходы и закрытие периода."],
limitation_lines: [],
next_step_line: null
}
}
})
);
expect(candidate.reply_text).toContain("нельзя точно подтвердить чистую прибыль");
expect(candidate.reply_text).toContain("P&L");
expect(candidate.reply_text).toContain("себестоимости");
expect(candidate.reply_text).not.toContain("47 628 853");
});
it("keeps vendor-risk boundary answers direct instead of compacting into a money overview", () => {
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
entryPoint({
turn_input: {
adapter_status: "ready",
turn_meaning_ref: {
asked_domain_family: "business_overview",
asked_action_family: "vendor_risk_procurement_boundary",
unsupported_but_understood_family: "vendor_risk_procurement_boundary"
},
data_need_graph: {
business_fact_family: "business_overview",
ranking_need: null,
reason_codes: ["data_need_graph_family_business_overview"]
}
},
bridge: {
bridge_status: "answer_draft_ready",
user_facing_response_allowed: true,
business_fact_answer_allowed: true,
requires_user_clarification: false,
pilot: {
pilot_scope: "business_overview_route_template_v1",
derived_business_overview: {
outgoing_supplier_payout: {
total_amount_human_ru: "19 568 878,06 руб."
},
top_suppliers: [
{
axis_value: "СБЕРБАНК, ПАО",
total_amount_human_ru: "6 653 022,52 руб."
}
]
}
},
answer_draft: {
answer_mode: "confirmed_with_bounded_inference",
headline: "Коротко: точный риск зависимости от одного поставщика по текущим данным не подтвержден.",
confirmed_lines: [],
inference_lines: [],
unknown_lines: ["Vendor-risk route не подключен."],
limitation_lines: [],
next_step_line: null
}
}
})
);
expect(candidate.reply_text).toContain("риск зависимости");
expect(candidate.reply_text).toContain("outgoing cash concentration proxy");
expect(candidate.reply_text).toContain("банк/финансовая организация");
expect(candidate.reply_text).toContain("не доказанная зависимость от обычного поставщика");
expect(candidate.reply_text).not.toContain("крупнейший подтвержденный поставщик/получатель исходящих платежей: СБЕРБАНК");
expect(candidate.reply_text).not.toContain("операционное нетто");
});
it("uses a compact direct answer for business-overview top year questions", () => {
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
entryPoint({
@@ -114,13 +285,15 @@ describe("assistant MCP discovery response candidate", () => {
})
);
expect(candidate.reply_text).toContain("в доступном проверенном MCP-срезе");
expect(candidate.reply_text).toContain("в доступном проверенном срезе 1С");
expect(candidate.reply_text).toContain("лидирует 2015");
expect(candidate.reply_text).toContain("2015");
expect(candidate.reply_text).toContain("136 723 459,73 руб.");
expect(candidate.reply_text).toContain("не полный бухгалтерский рейтинг доходности");
expect(candidate.reply_text).toContain("не как чистую бухгалтерскую прибыль");
expect(candidate.reply_text).toContain("лимит выборки MCP");
expect(candidate.reply_text).toContain("проверка достигла лимита строк");
expect(candidate.reply_text).not.toContain("лимит выборки MCP");
expect(candidate.reply_text).not.toContain("MCP-срез");
expect(candidate.reply_text).not.toContain("Что подтверждено:");
expect(candidate.reply_text).not.toContain("Профиль операционной активности");
});
@@ -197,6 +370,76 @@ describe("assistant MCP discovery response candidate", () => {
expect(candidate.reply_text).not.toContain("Складской срез");
});
it("does not present bank-like incoming leaders as ordinary client revenue", () => {
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
entryPoint({
turn_input: {
adapter_status: "ready",
data_need_graph: {
business_fact_family: "business_overview",
ranking_need: null,
reason_codes: [
"data_need_graph_family_business_overview",
"data_need_graph_business_overview_direct_money_answer"
]
}
},
bridge: {
bridge_status: "answer_draft_ready",
user_facing_response_allowed: true,
business_fact_answer_allowed: true,
requires_user_clarification: false,
pilot: {
pilot_scope: "business_overview_route_template_v1",
derived_business_overview: {
period_scope: "2020",
incoming_customer_revenue: {
total_amount_human_ru: "24 885 659,31 руб.",
coverage_limited_by_probe_limit: false
},
outgoing_supplier_payout: {
total_amount_human_ru: "19 568 878,06 руб.",
coverage_limited_by_probe_limit: false
},
net_amount_human_ru: "5 316 781,25 руб.",
net_direction: "net_incoming",
top_customers: [
{
axis_value: "СБЕРБАНК, ПАО",
total_amount_human_ru: "12 792 194,31 руб.",
counterparty_role_hint: "bank_or_financial_institution"
},
{
axis_value: "Группа СВК",
total_amount_human_ru: "12 093 465 руб.",
counterparty_role_hint: "ordinary_counterparty"
}
],
top_suppliers: [],
yearly_breakdown: []
}
},
answer_draft: {
answer_mode: "confirmed_with_bounded_inference",
headline: "Ограниченный бизнес-обзор.",
confirmed_lines: [],
inference_lines: [],
unknown_lines: [],
limitation_lines: [],
next_step_line: null
}
}
})
);
const firstLine = candidate.reply_text?.split("\n")[0] ?? "";
expect(firstLine).toContain("крупнейший входящий денежный источник: СБЕРБАНК, ПАО");
expect(firstLine).toContain("не называю это клиентской выручкой");
expect(firstLine).toContain("крупнейший небанковский входящий контрагент: Группа СВК");
expect(candidate.reply_text).not.toContain("крупнейший источник входящих денег: СБЕРБАНК");
expect(candidate.reply_text).not.toContain("Самый крупный подтвержденный клиент");
});
it("mentions separate counterparty scope in company plus counterparty business summaries", () => {
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
entryPoint({
@@ -607,7 +850,7 @@ describe("assistant MCP discovery response candidate", () => {
requires_user_clarification: false,
answer_draft: {
answer_mode: "confirmed_with_bounded_inference",
headline: "По данным 1С найдены строки исходящих платежей/списаний; сумму можно называть только в рамках проверенного периода и найденных строк.",
headline: "По данным 1С найдены строки исходящих платежей/списаний; сумму можно называть только в рамках проверенного периода и найденных строк.",
confirmed_lines: ["1C supplier-payout rows were found for counterparty SVK"],
inference_lines: [
"Requested period coverage was recovered through monthly 1C value-flow probes after the broad probe hit the row limit"
@@ -661,7 +904,7 @@ describe("assistant MCP discovery response candidate", () => {
requires_user_clarification: false,
answer_draft: {
answer_mode: "confirmed_with_bounded_inference",
headline: "По данным 1С найдены строки движений; ответ ограничен проверенным периодом и найденными строками.",
headline: "По данным 1С найдены строки движений; ответ ограничен проверенным периодом и найденными строками.",
confirmed_lines: ["1C movement rows were found for counterparty SVK"],
inference_lines: ["Counterparty movement evidence is limited to confirmed 1C movement rows in the checked scope"],
unknown_lines: ["Full movement history outside the checked period is not proven by this MCP discovery pilot"],
@@ -672,9 +915,9 @@ describe("assistant MCP discovery response candidate", () => {
})
);
expect(candidate.reply_text).toContain("В 1С найдены строки движений по контрагенту SVK.");
expect(candidate.reply_text).toContain("Срез движений ограничен только подтвержденными строками движений");
expect(candidate.reply_text).toContain("Полный исторический срез движений вне проверенного периода этим поиском не подтвержден.");
expect(candidate.reply_text).toContain("В 1С найдены строки движений по контрагенту SVK.");
expect(candidate.reply_text).toContain("Срез движений ограничен только подтвержденными строками движений");
expect(candidate.reply_text).toContain("Полный исторический срез движений вне проверенного периода этим поиском не подтвержден.");
expect(candidate.reply_text).not.toContain("1C movement rows were found");
});
@@ -841,9 +1084,8 @@ describe("assistant MCP discovery response candidate", () => {
expect(candidate.reply_text).toContain(
"\u0412 1\u0421 \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u043d\u044b \u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0435 \u0438 \u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0435 \u0434\u0435\u043d\u0435\u0436\u043d\u044b\u0435 \u0441\u0442\u0440\u043e\u043a\u0438 \u0432 \u0437\u0430\u043f\u0440\u043e\u0448\u0435\u043d\u043d\u043e\u043c \u0441\u0440\u0435\u0437\u0435"
);
expect(candidate.reply_text).toContain(
"\u0417\u0430\u043f\u0440\u043e\u0448\u0435\u043d\u043d\u044b\u0439 \u043f\u0435\u0440\u0438\u043e\u0434 \u0443\u043f\u0435\u0440\u0441\u044f \u0432 \u043b\u0438\u043c\u0438\u0442 \u0441\u0442\u0440\u043e\u043a MCP"
);
expect(candidate.reply_text).toContain("Запрошенный период достиг лимита строк");
expect(candidate.reply_text).not.toContain("уперся в лимит строк MCP");
expect(candidate.reply_text).not.toContain(
"\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0441\u043a\u043e\u043c\u0443 \u043a\u043e\u043d\u0442\u0443\u0440\u0443"
);
@@ -621,7 +621,7 @@ describe("assistant MCP discovery response policy", () => {
answer_mode: "confirmed_with_bounded_inference",
headline: "Рейтинг по контрагентам построен по подтвержденным строкам 1С.",
confirmed_lines: [
"Больше всего денег принёс контрагент СБЕРБАНК, ПАО по организации ООО Альтернатива Плюс за период 2020: 12 792 194,31 руб. по 9 строкам с суммой."
"Крупнейший входящий денежный источник — СБЕРБАНК, ПАО по организации ООО Альтернатива Плюс за период 2020: 12 792 194,31 руб. по 9 строкам с суммой. Это банк/финансовая организация; без назначения платежа или договора не считаю это обычным клиентом или выручкой."
],
inference_lines: [
"Рейтинг по контрагентам по организации ООО Альтернатива Плюс за период 2020 рассчитан только по подтвержденным строкам 1С."
@@ -639,6 +639,7 @@ describe("assistant MCP discovery response policy", () => {
expect(result.decision).toBe("apply_candidate");
expect(result.reply_text).toContain("ООО Альтернатива Плюс");
expect(result.reply_text).toContain("2020");
expect(result.reply_text).toContain("не считаю это обычным клиентом");
expect(result.reason_codes).toContain("mcp_discovery_response_policy_semantic_conflict_allows_candidate_override");
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_aligned_factual_address_reply");
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_factual_address_continuation_target");
@@ -37,6 +37,22 @@ function buildBidirectionalDeps(
};
}
function buildSequentialDeps(results: Array<{ rows: Array<Record<string, unknown>>; 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 };
}
describe("assistant MCP discovery runtime bridge", () => {
it("composes planner, pilot executor, and answer draft without wiring the hot runtime", async () => {
const result = await runAssistantMcpDiscoveryRuntimeBridge({
@@ -150,8 +166,24 @@ describe("assistant MCP discovery runtime bridge", () => {
expect(result.loop_state.catalog_chain_template_matches[0]).toBe("value_flow_ranking");
expect(result.loop_state.catalog_chain_template_alignment.alignment_status).toBe("selected_matches_top");
expect(result.loop_state.catalog_chain_template_alignment.selected_chain_matches_top).toBe(true);
expect(result.route_candidate).toMatchObject({
schema_version: "assistant_mcp_route_candidate_v1",
candidate_status: "needs_user_scope",
selected_chain_id: "value_flow_ranking",
nearest_catalog_chain_template: "value_flow_ranking",
catalog_alignment_status: "selected_matches_top",
business_fact_family: "value_flow",
action_family: "turnover",
executable_now: false
});
expect(result.route_candidate.missing_axes).toContain("organization");
expect(result.route_candidate.provided_axes).toContain("aggregate_axis");
expect(result.route_candidate.recommended_next_action).toBe(
"Ask the user for the missing scope axes before MCP execution."
);
expect(result.reason_codes).toContain("planner_selected_chain_matches_catalog_top");
expect(result.reason_codes).toContain("runtime_bridge_loop_state_awaiting_clarification");
expect(result.reason_codes).toContain("runtime_bridge_route_candidate_needs_user_scope");
});
it("produces a bounded ranked value-flow answer when period and organization are known", async () => {
@@ -192,6 +224,12 @@ describe("assistant MCP discovery runtime bridge", () => {
axis_value: "СВК-А",
total_amount: 2100
});
expect(result.route_candidate).toMatchObject({
candidate_status: "ready_for_reviewed_execution",
selected_chain_id: "value_flow_ranking",
executable_now: true,
enablement_reason: null
});
expect(result.answer_draft.confirmed_lines.join("\n")).toContain("СВК-А");
});
@@ -437,6 +475,288 @@ describe("assistant MCP discovery runtime bridge", () => {
expect(userFacing).not.toContain("MCP discovery pilot");
});
it("marks exact business-overview proof gaps as route enablement instead of reviewed execution", async () => {
const deps = buildSequentialDeps([
{ rows: [{ Period: "2020-01-15T00:00:00", Amount: 120000, Counterparty: "Client A" }] },
{ rows: [{ Period: "2020-01-20T00:00:00", Amount: 50000, Counterparty: "Supplier A" }] },
{ rows: [] },
{ rows: [] },
{ rows: [] },
{ rows: [] },
{ rows: [] },
{
rows: [
{ Period: "2020-12-31T00:00:00", Amount: 250000, Quantity: 10, Item: "Widget A" },
{ Period: "2020-12-31T00:00:00", Amount: 50000, Quantity: 5, Item: "Widget B" }
]
},
{
rows: [
{ Period: "2020-01-10T00:00:00", Amount: 200000, Quantity: 8, Item: "Widget A" },
{ Period: "2020-11-01T00:00:00", Amount: 50000, Quantity: 2, Item: "Widget B" }
]
},
{
rows: [
{ Period: "2020-01-15T00:00:00", Registrator: "Purchase 1" },
{ Period: "2020-12-15T00:00:00", Registrator: "Purchase 2" }
]
},
{
rows: [
{ Period: "2020-03-01T00:00:00", Amount: 600000, Item: "Widget A", Counterparty: "Client A", AccountKt: "41.01" },
{ Period: "2020-02-01T00:00:00", Amount: 240000, Item: "Widget A", Counterparty: "Supplier A", AccountDt: "41.01" }
]
}
]);
const result = await runAssistantMcpDiscoveryRuntimeBridge({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
business_fact_family: "business_overview",
action_family: "inventory_reserve_boundary",
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",
"fetch_supporting_documents",
"probe_coverage",
"explain_evidence_basis"
],
forbidden_overclaim_flags: [
"no_raw_model_claims",
"no_unchecked_fact_totals",
"no_unchecked_business_health_claim"
],
reason_codes: ["data_need_graph_built", "data_need_graph_family_business_overview"]
},
turnMeaning: {
asked_domain_family: "business_overview",
asked_action_family: "inventory_reserve_boundary",
explicit_organization_scope: "OOO Alternative Plus",
explicit_date_scope: "2020"
},
deps
});
expect(result.bridge_status).toBe("answer_draft_ready");
expect(result.business_fact_answer_allowed).toBe(true);
expect(result.route_candidate).toMatchObject({
candidate_status: "needs_route_enablement",
selected_chain_id: "business_overview",
business_fact_family: "business_overview",
action_family: "inventory_reserve_boundary",
executable_now: false
});
expect(result.route_candidate.enablement_reason).toContain("inventory_reserve_liquidation_quality");
expect(result.route_candidate.enablement_reason).toContain(
"reviewed_inventory_quality_route_with_reserves_writeoffs_obsolescence_and_liquidation_value"
);
expect(result.route_candidate.forbidden_overclaim_flags).toContain(
"confirmed_obsolete_stock_reserve_writeoff_or_liquidation_value"
);
expect(result.reason_codes).toContain("runtime_bridge_route_candidate_needs_route_enablement");
});
it("promotes profit-margin boundary when accounting 90/91/99 proof is available", async () => {
const deps = buildSequentialDeps([
{ rows: [{ Period: "2020-01-15T00:00:00", Amount: 12012833.72, Counterparty: "Client A" }] },
{ rows: [{ Period: "2020-01-20T00:00:00", Amount: 6430415.34, Counterparty: "Supplier A" }] },
{ rows: [] },
{
rows: [
{ Period: "2020-12-31T00:00:00", Registrator: "ACC90_REVENUE_KT", Amount: 12012833.72 },
{ Period: "2020-12-31T00:00:00", Registrator: "ACC90_COST_DT", Amount: 6430415.34 },
{ Period: "2020-12-31T00:00:00", Registrator: "ACC90_SELLING_DT", Amount: 1715450.75 },
{ Period: "2020-12-31T00:00:00", Registrator: "ACC90_RESULT_TO_99_PROFIT", Amount: 3053486.79 },
{ Period: "2020-12-31T00:00:00", Registrator: "ACC90_RESULT_FROM_99_LOSS", Amount: 1188658.09 },
{ Period: "2020-12-31T00:00:00", Registrator: "ACC91_RESULT_FROM_99_LOSS", Amount: 9001644.55 },
{ Period: "2020-12-31T00:00:00", Registrator: "ACC84_TO99_LOSS_TRANSFER", Amount: 7136815.85 }
]
},
{ rows: [] },
{ rows: [] },
{ rows: [] },
{ rows: [] },
{ rows: [] },
{ rows: [{ Period: "2020-01-15T00:00:00", Registrator: "Purchase 1" }] },
{ rows: [] },
{ rows: [] },
{ rows: [] },
{ rows: [] }
]);
const result = await runAssistantMcpDiscoveryRuntimeBridge({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
business_fact_family: "business_overview",
action_family: "profit_margin_boundary",
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",
"fetch_supporting_documents",
"probe_coverage",
"explain_evidence_basis"
],
forbidden_overclaim_flags: [
"no_raw_model_claims",
"no_unchecked_fact_totals",
"no_unchecked_business_health_claim"
],
reason_codes: ["data_need_graph_built", "data_need_graph_family_business_overview"]
},
turnMeaning: {
asked_domain_family: "business_overview",
asked_action_family: "profit_margin_boundary",
explicit_organization_scope: "OOO Alternative Plus",
explicit_date_scope: "2020"
},
deps
});
const overview = result.pilot.derived_business_overview;
const missingFamilies = overview?.missing_proof_families.map((item) => item.family) ?? [];
const userFacing = [
result.answer_draft.headline,
...result.answer_draft.confirmed_lines,
...result.answer_draft.inference_lines,
...result.answer_draft.unknown_lines,
...result.answer_draft.limitation_lines,
result.answer_draft.next_step_line ?? ""
].join("\n");
expect(result.bridge_status).toBe("answer_draft_ready");
expect(result.business_fact_answer_allowed).toBe(true);
expect(result.route_candidate).toMatchObject({
candidate_status: "ready_for_reviewed_execution",
selected_chain_id: "business_overview",
business_fact_family: "business_overview",
action_family: "profit_margin_boundary",
executable_now: true
});
expect(overview?.accounting_financial_result).toMatchObject({
period_scope: "2020",
final_result_amount: -7136815.85,
final_result_direction: "loss",
final_transfer_basis: "account_99_to_84_period_close",
period_close_rows_with_amount: 4
});
expect(overview?.accounting_financial_result?.net_margin_to_revenue_pct).toBe(-59.41);
expect(missingFamilies).not.toContain("accounting_profit_margin");
expect(userFacing).toContain("90/91/99");
expect(userFacing).toContain("90.01");
expect(userFacing).toContain("7 136 815,85");
expect(userFacing).not.toContain("operating-flow/trading-margin proxy");
expect(result.reason_codes).toContain("pilot_derived_business_overview_accounting_financial_result_from_confirmed_rows");
expect(result.reason_codes).toContain("answer_contains_business_overview_accounting_financial_result");
expect(result.reason_codes).toContain("runtime_bridge_route_candidate_ready_for_reviewed_execution");
});
it("promotes debt due-date boundary after reviewed payment-term route returns a checked negative", async () => {
const deps = buildSequentialDeps([
{ rows: [] },
{ rows: [] },
{ rows: [] },
{ rows: [] },
{ rows: [] },
{ rows: [] },
{
rows: [
{ Period: "2020-12-31T00:00:00", Amount: 100000, Counterparty: "Клиент А", Contract: "Договор А" }
]
},
{
rows: [
{
Period: "2020-12-31T00:00:00",
Amount: 100000,
Counterparty: "Клиент А",
Contract: "Договор А",
ДокументРасчетов: "Реализация товаров от 10.03.2020",
УстановленСрокОплаты: false,
СрокОплаты: 0
}
]
},
{ rows: [] },
{ rows: [] },
{ rows: [] },
{ rows: [] },
{ rows: [] },
{ rows: [] },
{ rows: [] }
]);
const result = await runAssistantMcpDiscoveryRuntimeBridge({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
business_fact_family: "business_overview",
action_family: "debt_due_date_boundary",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: null,
ranking_need: null,
proof_expectation: "due_date_aging",
clarification_gaps: [],
decomposition_candidates: [
"collect_scoped_movements",
"aggregate_checked_amounts",
"fetch_supporting_documents",
"probe_coverage",
"explain_evidence_basis"
],
forbidden_overclaim_flags: [
"no_raw_model_claims",
"no_unchecked_overdue_claim"
],
reason_codes: ["data_need_graph_built", "data_need_graph_family_business_overview"]
},
turnMeaning: {
asked_domain_family: "business_overview",
asked_action_family: "debt_due_date_boundary",
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020"
},
deps
});
const userFacing = [
result.answer_draft.headline,
...result.answer_draft.confirmed_lines,
...result.answer_draft.unknown_lines
].join("\n");
expect(result.bridge_status).toBe("answer_draft_ready");
expect(result.route_candidate).toMatchObject({
candidate_status: "ready_for_reviewed_execution",
selected_chain_id: "business_overview",
business_fact_family: "business_overview",
action_family: "debt_due_date_boundary",
executable_now: true
});
expect(result.pilot.derived_business_overview?.debt_due_date_aging).toMatchObject({
evidence_status: "no_payment_terms_configured",
rows_without_payment_terms: 1,
overdue_rows: 0
});
expect(userFacing).toContain("срок оплаты не установлен");
expect(userFacing).toContain("Подтвержденной просрочки");
expect(result.reason_codes).toContain("runtime_bridge_route_candidate_ready_for_reviewed_execution");
expect(result.reason_codes).toContain("answer_contains_business_overview_debt_due_date_aging_no_payment_terms_configured");
});
it("bridges selected-item inventory provenance templates through exact document evidence", async () => {
const deps = buildDeps([
{
@@ -711,5 +1031,15 @@ describe("assistant MCP discovery runtime bridge", () => {
});
expect(result.loop_state.pending_axes).toEqual(["organization", "period"]);
expect(result.loop_state.explicit_entity_candidates).toEqual([]);
expect(result.route_candidate).toMatchObject({
candidate_status: "needs_user_scope",
selected_chain_id: "movement_evidence",
business_fact_family: "movement_evidence",
action_family: "list_movements",
proof_expectation: "clarification_required",
executable_now: false
});
expect(result.route_candidate.missing_axes).toEqual(["organization", "period"]);
expect(result.route_candidate.forbidden_overclaim_flags).toContain("no_unchecked_fact_totals");
});
});
@@ -1505,6 +1505,36 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.reason_codes).toContain("mcp_discovery_not_applicable_for_supported_exact_turn");
});
it("forces discovery over an exact ranking intent when organization scope is missing", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage:
"\u043a\u0430\u043a\u043e\u0439 \u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442 \u043f\u0440\u0438\u043d\u0435\u0441 \u0431\u043e\u043b\u044c\u0448\u0435 \u0432\u0441\u0435\u0433\u043e \u0434\u0435\u043d\u0435\u0433 \u0437\u0430 2020 \u0433\u043e\u0434?",
assistantTurnMeaning: {
asked_domain_family: "counterparty",
asked_action_family: "counterparty_value_or_turnover",
explicit_intent_candidate: "customer_revenue_and_payments",
explicit_entity_candidates: [{ type: "counterparty", value: "\u0433\u043e\u0434\u0443", source: "current_turn_loose_entity_tail" }],
explicit_date_scope: "2020",
stale_replay_forbidden: false
}
});
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.semantic_data_need).toBe("counterparty value-flow evidence");
expect(result.data_need_graph?.business_fact_family).toBe("value_flow");
expect(result.data_need_graph?.ranking_need).toBe("top_desc");
expect(result.data_need_graph?.clarification_gaps).toEqual(["organization"]);
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "counterparty_value",
asked_action_family: "counterparty_value_or_turnover",
explicit_date_scope: "2020",
stale_replay_forbidden: true
});
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.reason_codes).not.toContain("mcp_discovery_not_applicable_for_supported_exact_turn");
});
it("routes broad business evaluation into business overview discovery without metadata drift", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage:
@@ -1536,6 +1566,46 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.reason_codes).not.toContain("mcp_discovery_not_applicable_for_supported_exact_turn");
});
it("keeps inventory reserve questions as a bounded boundary check instead of generic overview", () => {
const orgName =
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage:
"\u041c\u043e\u0436\u043d\u043e \u043b\u0438 \u043f\u043e \u044d\u0442\u0438\u043c \u0434\u0430\u043d\u043d\u044b\u043c \u0442\u043e\u0447\u043d\u043e \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c \u0440\u0435\u0437\u0435\u0440\u0432 \u043f\u043e\u0434 \u043d\u0435\u043b\u0438\u043a\u0432\u0438\u0434\u044b \u043d\u0430 \u0441\u043a\u043b\u0430\u0434\u0435?",
assistantTurnMeaning: {
asked_domain_family: "business_summary",
asked_action_family: "broad_evaluation",
unsupported_but_understood_family: "broad_business_evaluation",
stale_replay_forbidden: true
},
followupContext: {
previous_discovery_pilot_scope: "business_overview_route_template_v1",
previous_filters: {
organization: orgName,
period_from: "2020-01-01",
period_to: "2020-12-31"
}
}
});
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.semantic_data_need).toBe("business overview evidence with bounded analyst interpretation");
expect(result.data_need_graph?.business_fact_family).toBe("business_overview");
expect(result.data_need_graph?.subject_candidates).toEqual([]);
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "business_overview",
asked_action_family: "inventory_reserve_boundary",
explicit_organization_scope: orgName,
explicit_date_scope: "2020",
unsupported_but_understood_family: "inventory_reserve_liquidation_boundary",
stale_replay_forbidden: true
});
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.reason_codes).toContain("mcp_discovery_broad_business_evaluation_route_candidate");
expect(result.reason_codes).toContain("mcp_discovery_data_need_graph_built");
});
it("lets raw business-overview wording override stale exact turnover meaning", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage:
@@ -2170,10 +2240,10 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.data_need_graph?.business_fact_family).toBe("business_overview");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation",
asked_action_family: "profit_margin_boundary",
explicit_organization_scope: orgName,
explicit_date_scope: "2020",
unsupported_but_understood_family: "broad_business_evaluation",
unsupported_but_understood_family: "profit_margin_boundary",
stale_replay_forbidden: true
});
expect(result.reason_codes).toContain("mcp_discovery_broad_business_evaluation_route_candidate");
@@ -2181,6 +2251,23 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.data_need_graph?.clarification_gaps).toEqual([]);
});
it("routes legal-entity profit and margin wording to the missing P&L proof-family boundary", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "по ООО Альтернатива Плюс за 2020 можно точно сказать чистую прибыль и маржу?"
});
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.data_need_graph?.business_fact_family).toBe("business_overview");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "business_overview",
asked_action_family: "profit_margin_boundary",
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020",
unsupported_but_understood_family: "profit_margin_boundary"
});
});
it("routes organization-level overdue debt wording to business overview instead of exact receivables recipes", () => {
const orgName = "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
const result = buildAssistantMcpDiscoveryTurnInput({
@@ -2201,10 +2288,10 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.data_need_graph?.business_fact_family).toBe("business_overview");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation",
asked_action_family: "debt_due_date_boundary",
explicit_organization_scope: orgName,
explicit_date_scope: "2020",
unsupported_but_understood_family: "broad_business_evaluation",
unsupported_but_understood_family: "debt_due_date_boundary",
stale_replay_forbidden: true
});
expect(result.reason_codes).toContain("mcp_discovery_broad_business_evaluation_route_candidate");
@@ -2232,10 +2319,10 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.data_need_graph?.business_fact_family).toBe("business_overview");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation",
asked_action_family: "inventory_reserve_boundary",
explicit_organization_scope: orgName,
explicit_date_scope: "2020",
unsupported_but_understood_family: "broad_business_evaluation",
unsupported_but_understood_family: "inventory_reserve_liquidation_boundary",
stale_replay_forbidden: true
});
expect(result.reason_codes).toContain("mcp_discovery_broad_business_evaluation_route_candidate");
@@ -2263,10 +2350,10 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.data_need_graph?.business_fact_family).toBe("business_overview");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation",
asked_action_family: "vendor_risk_procurement_boundary",
explicit_organization_scope: orgName,
explicit_date_scope: "2020",
unsupported_but_understood_family: "broad_business_evaluation",
unsupported_but_understood_family: "vendor_risk_procurement_boundary",
stale_replay_forbidden: true
});
expect(result.reason_codes).toContain("mcp_discovery_broad_business_evaluation_route_candidate");
@@ -2708,6 +2795,42 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.data_need_graph?.clarification_gaps).toEqual(["period"]);
});
it("routes explicit VAT movement wording to metadata-scoped movement evidence over exact VAT aggregate", () => {
const orgName =
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage:
"\u043f\u043e\u043a\u0430\u0436\u0438 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u044f \u043f\u043e \u041d\u0414\u0421 \u0437\u0430 2020 \u043f\u043e \u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441",
assistantTurnMeaning: {
explicit_intent_candidate: "vat_liability_confirmed_for_tax_period"
},
predecomposeContract: {
entities: { organization: orgName },
period: {
period_from: "2020-01-01",
period_to: "2020-12-31"
}
}
});
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.semantic_data_need).toBe("movement evidence");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "movements",
asked_action_family: "list_movements",
metadata_scope_hint: "\u041d\u0414\u0421",
subject_resolution_optional: true,
explicit_organization_scope: orgName,
explicit_date_scope: "2020",
unsupported_but_understood_family: "movement_evidence",
stale_replay_forbidden: true
});
expect(result.data_need_graph?.business_fact_family).toBe("movement_evidence");
expect(result.data_need_graph?.clarification_gaps).toEqual([]);
expect(result.reason_codes).toContain("mcp_discovery_vat_movement_evidence_signal_detected");
});
it("pivots a metadata-scoped subjectless movement retrieval into documents without inventing a counterparty", () => {
const orgName =
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
@@ -2867,6 +2990,62 @@ describe("assistant MCP discovery turn input adapter", () => {
});
});
it("prefers semantic organization over noisy raw organization tail for debt due-date overview", () => {
const orgName = "ООО Альтернатива Плюс";
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "по ООО Альтернатива Плюс на конец 2020 можно точно понять, какая дебиторка просрочена?",
predecomposeContract: {
entities: { counterparty: orgName, organization: orgName },
period: { period_from: "2020-01-01", period_to: "2020-12-31" }
}
});
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.semantic_data_need).toBe("business overview evidence with bounded analyst interpretation");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "business_overview",
asked_action_family: "debt_due_date_boundary",
explicit_organization_scope: orgName,
explicit_date_scope: "2020",
unsupported_but_understood_family: "debt_due_date_boundary",
stale_replay_forbidden: true
});
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
});
it("routes debt due-date boundary follow-up through the business overview lane", () => {
const orgName =
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage:
"\u0442\u043e \u0435\u0441\u0442\u044c \u043f\u0440\u043e\u0441\u0440\u043e\u0447\u043a\u0443 \u0434\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u043d\u0435\u043b\u044c\u0437\u044f, \u043a\u043e\u0440\u043e\u0442\u043a\u043e \u043f\u043e\u0447\u0435\u043c\u0443?",
followupContext: {
previous_discovery_pilot_scope: "business_overview_route_template_v1",
previous_filters: {
organization: orgName,
period_from: "2020-01-01",
period_to: "2020-12-31"
}
}
});
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.semantic_data_need).toBe("business overview evidence with bounded analyst interpretation");
expect(result.data_need_graph?.business_fact_family).toBe("business_overview");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "business_overview",
asked_action_family: "debt_due_date_boundary",
explicit_organization_scope: orgName,
explicit_date_scope: "2020",
unsupported_but_understood_family: "debt_due_date_boundary",
stale_replay_forbidden: true
});
expect(result.reason_codes).toContain("mcp_discovery_business_overview_continuation_from_followup_context");
expect(result.reason_codes).toContain("mcp_discovery_business_overview_debt_due_date_followup_boundary");
});
it("lets raw metadata scope override stale document evidence subject on topic switch", () => {
const orgName =
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
@@ -2946,6 +3125,44 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.reason_codes).toContain("mcp_discovery_business_overview_continuation_from_followup_context");
});
it("keeps short profit/loss follow-up on the accounting profit-margin boundary", () => {
const orgName =
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage:
"\u0430 \u044d\u0442\u043e \u043f\u0440\u0438\u0431\u044b\u043b\u044c \u0438\u043b\u0438 \u0443\u0431\u044b\u0442\u043e\u043a, \u043a\u043e\u0440\u043e\u0442\u043a\u043e?",
assistantTurnMeaning: {
asked_domain_family: "unknown",
asked_action_family: "unknown"
},
followupContext: {
previous_discovery_pilot_scope: "business_overview_route_template_v1",
previous_filters: {
organization: orgName,
period_from: "2020-01-01",
period_to: "2020-12-31"
}
}
});
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.semantic_data_need).toBe("business overview evidence with bounded analyst interpretation");
expect(result.data_need_graph?.business_fact_family).toBe("business_overview");
expect(result.data_need_graph?.subject_candidates).toEqual([]);
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "business_overview",
asked_action_family: "profit_margin_boundary",
explicit_organization_scope: orgName,
explicit_date_scope: "2020",
unsupported_but_understood_family: "profit_margin_boundary",
stale_replay_forbidden: true
});
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.reason_codes).toContain("mcp_discovery_business_overview_continuation_from_followup_context");
expect(result.reason_codes).toContain("mcp_discovery_business_overview_profit_margin_followup_boundary");
});
it("keeps detailed money-breakdown follow-up in business overview without pseudo counterparty anchors", () => {
const orgName =
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
@@ -557,6 +557,45 @@ describe("assistantRoutePolicy", () => {
expect(decision.orchestrationContract?.followup_context_detected).toBe(false);
});
it("keeps plain organization clarification in address lane for pending route-candidate scope", () => {
const orgName =
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
const policy = buildPolicy({
resolveAddressToolGateDecision: undefined
});
const decision = policy.resolveAssistantOrchestrationDecision({
rawUserMessage: orgName,
effectiveAddressUserMessage: orgName,
followupContext: {
previous_intent: "customer_revenue_and_payments",
target_intent: "customer_revenue_and_payments",
previous_discovery_pilot_scope: "counterparty_value_flow_query_movements_v1",
previous_discovery_loop_status: "awaiting_clarification",
previous_discovery_loop_selected_chain_id: "value_flow_ranking",
previous_discovery_loop_pending_axes: ["organization"],
previous_discovery_loop_asked_domain_family: "counterparty_value",
previous_discovery_loop_asked_action_family: "turnover",
previous_discovery_loop_unsupported_family: "counterparty_value_or_turnover"
},
llmPreDecomposeMeta: {
predecomposeContract: {
mode: "unsupported",
intent: "unknown",
entities: { organization: orgName },
semantics: { anchor_kind: "organization", anchor_value: orgName }
}
},
useMock: false
});
expect(decision.runAddressLane).toBe(true);
expect(decision.toolGateReason).toBe("followup_context_detected");
expect(decision.livingMode).toBe("address_data");
expect(decision.livingReason).toBe("address_lane_triggered");
expect(decision.orchestrationContract?.hard_meta_mode).toBeNull();
});
it("does not turn short entity follow-up into organization switch just because scope already has an active company", () => {
const policy = buildPolicy({
resolveAddressToolGateDecision: undefined,
@@ -1206,6 +1206,67 @@ describe("assistantTransitionPolicy", () => {
period_to: "2020-12-31"
});
});
it("carries business overview boundary context through short capability-shaped follow-up", () => {
const policy = buildPolicy({
findLastAddressAssistantItem: () => null,
shouldHandleAsAssistantCapabilityMetaQuery: () => true,
hasDataRetrievalRequestSignal: () => false,
hasAddressFollowupContextSignal: () => false
});
const organization = "ООО Альтернатива Плюс";
const carryover = policy.resolveAddressFollowupCarryoverContext(
"то есть просрочку доказать нельзя, коротко почему?",
[
{
role: "assistant",
text: "На 2020-12-31 подтвержденной просрочки нет: в договорах срок оплаты не установлен.",
debug: {
execution_lane: "living_chat",
mcp_discovery_response_applied: true,
assistant_active_organization: organization,
assistant_mcp_discovery_entry_point_v1: {
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
entry_status: "bridge_executed",
turn_input: {
turn_meaning_ref: {
asked_domain_family: "business_overview",
asked_action_family: "debt_due_date_boundary",
unsupported_but_understood_family: "debt_due_date_boundary",
explicit_organization_scope: organization,
explicit_date_scope: "2020"
}
},
bridge: {
bridge_status: "answer_draft_ready",
business_fact_answer_allowed: true,
pilot: {
pilot_scope: "business_overview_route_template_v1"
},
answer_draft: {
answer_mode: "confirmed_with_bounded_inference"
}
}
}
}
}
],
null,
null,
null
);
expect(carryover?.followupSelectionMode).toBe("carry_previous_intent");
expect(carryover?.followupContext?.previous_discovery_pilot_scope).toBe(
"business_overview_route_template_v1"
);
expect(carryover?.followupContext?.previous_filters).toMatchObject({
organization,
period_from: "2020-01-01",
period_to: "2020-12-31"
});
});
it("carries resolved entity candidates from grounded entity-resolution discovery into followup context", () => {
const policy = buildPolicy({
findLastAddressAssistantItem: () => null,
@@ -1506,6 +1567,67 @@ describe("assistantTransitionPolicy", () => {
);
});
it("treats a plain organization reply as continuation of a pending route-candidate organization scope", () => {
const orgName =
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
const policy = buildPolicy({
findLastAddressAssistantItem: () => ({
role: "assistant",
text: "\u041d\u0443\u0436\u043d\u043e \u0443\u0442\u043e\u0447\u043d\u0438\u0442\u044c \u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\u0438\u044e.",
debug: {
execution_lane: "living_chat",
detected_intent: "customer_revenue_and_payments",
mcp_discovery_response_applied: true,
assistant_mcp_discovery_entry_point_v1: {
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
entry_status: "bridge_executed",
bridge: {
bridge_status: "needs_clarification",
business_fact_answer_allowed: false,
pilot: {
pilot_scope: "counterparty_value_flow_query_movements_v1"
},
loop_state: {
schema_version: "assistant_mcp_discovery_loop_state_v1",
loop_status: "awaiting_clarification",
selected_chain_id: "value_flow_ranking",
pilot_scope: "counterparty_value_flow_query_movements_v1",
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
unsupported_but_understood_family: "counterparty_value_or_turnover",
ranking_need: "top_desc",
pending_axes: ["organization"],
provided_axes: ["aggregate_axis", "amount", "coverage_target"],
explicit_date_scope: "2020"
}
}
}
}
}),
hasAddressFollowupContextSignal: () => false
});
const carryover = policy.resolveAddressFollowupCarryoverContext(
orgName,
[],
null,
{
predecomposeContract: {
mode: "unsupported",
intent: "unknown",
entities: { organization: orgName },
semantics: { anchor_kind: "organization", anchor_value: orgName }
}
},
null
);
expect(carryover?.followupContext?.previous_discovery_loop_status).toBe("awaiting_clarification");
expect(carryover?.followupContext?.previous_discovery_loop_selected_chain_id).toBe("value_flow_ranking");
expect(carryover?.followupContext?.previous_discovery_loop_pending_axes).toEqual(["organization"]);
expect(carryover?.followupContext?.target_intent).toBe("customer_revenue_and_payments");
});
it("carries grounded metadata downstream route hints into followup context", () => {
const policy = buildPolicy({
findLastAddressAssistantItem: () => null,
@@ -114,6 +114,40 @@ describe("counterparty analytics reply builders", () => {
expect(reply.text).not.toContain("Топ-");
});
it("does not mistake a year period in top-counterparty wording for a top-year question", () => {
const reply = composeFactualReply(
"customer_revenue_and_payments",
[
{
period: "2020-03-01T00:00:00Z",
registrator: "Поступление 1",
account_dt: "",
account_kt: "",
amount: 500,
analytics: ["Клиент А", "Договор А-1"]
},
{
period: "2020-03-02T00:00:00Z",
registrator: "Поступление 2",
account_dt: "",
account_kt: "",
amount: 1200,
analytics: ["Клиент Б", "Договор Б-1"]
}
],
{
userMessage: "какой контрагент принес больше всего денег за 2020 год?",
periodFrom: "2020-01-01",
periodTo: "2020-12-31"
}
);
expect(reply.responseType).toBe("FACTUAL_LIST");
expect(reply.text).toContain("Клиент Б");
expect(reply.text).not.toContain("Клиент А");
expect(reply.text).not.toContain("1. 2020 |");
});
it("explains organization activity age as 1C activity rather than legal age", () => {
const reply = composeFactualReply(
"counterparty_activity_lifecycle",