Закрепить reviewed routes и защиту бизнес-ответов
This commit is contained in:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user