diff --git a/docs/orchestration/hm_business_overview_cashflow_limits_20260525.json b/docs/orchestration/hm_business_overview_cashflow_limits_20260525.json new file mode 100644 index 0000000..29d6167 --- /dev/null +++ b/docs/orchestration/hm_business_overview_cashflow_limits_20260525.json @@ -0,0 +1,83 @@ +{ + "schema_version": "domain_truth_harness_spec_v1", + "scenario_id": "hm_business_overview_cashflow_limits_20260525", + "domain": "assistant_stage1_business_overview", + "title": "Hm business overview cashflow limit honesty", + "description": "Focused live replay for the Hm business overview chain: all-time earnings wording must not present limited cashflow probes as confirmed all-time accounting profit.", + "bindings": {}, + "steps": [ + { + "step_id": "s01_top_year", + "title": "Top revenue year", + "question": "\u043a\u0430\u043a\u043e\u0439 \u0443 \u041e\u041e\u041e \"\u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441\" \u0441\u0430\u043c\u044b\u0439 \u0434\u043e\u0445\u043e\u0434\u043d\u044b\u0439 \u0433\u043e\u0434", + "allowed_reply_types": [ + "factual", + "factual_with_explanation", + "partial_coverage" + ], + "required_direct_answer_patterns_any": [ + "2015", + "\u0432\u0445\u043e\u0434\u044f\u0449" + ], + "forbidden_answer_patterns": [ + "\u041a\u043e\u0440\u043e\u0442\u043a\u043e:", + "capability_id", + "mcp_discovery" + ], + "criticality": "critical", + "semantic_tags": [ + "business_overview", + "yearly_ranking" + ] + }, + { + "step_id": "s02_earnings_2017", + "title": "2017 accounting earnings", + "question": "\u0430 \u0437\u0430 2017 \u043c\u044b \u0441\u043a\u043e\u043a \u0437\u0430\u0440\u0430\u0431\u043e\u0442\u0430\u043b\u0438?", + "allowed_reply_types": [ + "factual", + "factual_with_explanation" + ], + "required_direct_answer_patterns_any": [ + "\u0443\u0447\u0435\u0442\u043d\u0430\u044f \u043f\u0440\u0438\u0431\u044b\u043b\u044c", + "116 938,23" + ], + "forbidden_answer_patterns": [ + "\u041a\u043e\u0440\u043e\u0442\u043a\u043e:", + "capability_id", + "mcp_discovery" + ], + "criticality": "critical", + "semantic_tags": [ + "business_overview", + "accounting_result" + ] + }, + { + "step_id": "s03_all_time_earnings_limit", + "title": "All-time earnings must disclose limited cashflow coverage", + "question": "\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0432\u043e\u043e\u0431\u0449\u0435 \u0434\u0435\u043d\u0435\u0433 \u043c\u044b \u0437\u0430\u0440\u0430\u0431\u043e\u0442\u0430\u043b\u0438 \u0437\u0430 \u0432\u0441\u0435 \u0432\u0440\u0435\u043c\u044f?", + "allowed_reply_types": [ + "factual", + "factual_with_explanation" + ], + "required_answer_patterns_all": [ + "\u0442\u043e\u0447\u043d\u0443\u044e \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u043f\u0440\u0438\u0431\u044b\u043b\u044c", + "\u043d\u0435 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0430\u044e", + "\u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u043d", + "\u043d\u0435 \u0441\u0447\u0438\u0442\u0430\u044e \u043f\u043e\u043b\u043d\u044b\u043c \u0431\u0443\u0445\u0433\u0430\u043b\u0442\u0435\u0440\u0441\u043a\u0438\u043c \u043e\u0431\u043e\u0440\u043e\u0442\u043e\u043c" + ], + "forbidden_answer_patterns": [ + "\u041a\u043e\u0440\u043e\u0442\u043a\u043e:", + "capability_id", + "mcp_discovery" + ], + "criticality": "critical", + "semantic_tags": [ + "business_overview", + "coverage_honesty", + "all_time_cashflow" + ] + } + ] +} diff --git a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryResponseCandidate.js b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryResponseCandidate.js index 8a33af0..7350070 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryResponseCandidate.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryResponseCandidate.js @@ -905,10 +905,16 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) { const signedNetDisplay = cashflowPolarityRequested && netDisplay && !String(netDisplay).trim().startsWith("-") ? `+${netDisplay}` : netDisplay; - const polarityLead = cashflowPolarityRequested - ? `\u041a\u043e\u0440\u043e\u0442\u043a\u043e: \u043f\u043e \u0434\u0435\u043d\u044c\u0433\u0430\u043c \u043f\u043b\u044e\u0441. ` - : "\u041a\u043e\u0440\u043e\u0442\u043a\u043e: "; + const limitedEarningsAnswer = Boolean(earningsAmountRequested && !accountingFinancialResultForEarnings && limitLine); + const polarityLead = limitedEarningsAnswer + ? "\u041a\u043e\u0440\u043e\u0442\u043a\u043e: \u0442\u043e\u0447\u043d\u0443\u044e \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u043f\u0440\u0438\u0431\u044b\u043b\u044c \u0437\u0430 \u044d\u0442\u043e \u043e\u043a\u043d\u043e \u043d\u0435 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0430\u044e; \u0432 \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u043d\u043e\u043c \u0434\u0435\u043d\u0435\u0436\u043d\u043e\u043c \u0441\u0440\u0435\u0437\u0435 " + : cashflowPolarityRequested + ? `\u041a\u043e\u0440\u043e\u0442\u043a\u043e: \u043f\u043e \u0434\u0435\u043d\u044c\u0433\u0430\u043c \u043f\u043b\u044e\u0441. ` + : "\u041a\u043e\u0440\u043e\u0442\u043a\u043e: "; lines.push(`${polarityLead}${organizationPrefix}${period} \u043f\u043e\u043b\u0443\u0447\u0438\u043b\u0438 ${incomingAmount ?? "0 \u0440\u0443\u0431."}; \u0437\u0430\u043f\u043b\u0430\u0442\u0438\u043b\u0438/\u0441\u043f\u0438\u0441\u0430\u043b\u0438 ${outgoingAmount ?? "0 \u0440\u0443\u0431."}; \u0434\u0435\u043d\u0435\u0436\u043d\u043e\u0435 \u043d\u0435\u0442\u0442\u043e ${signedNetDisplay}.`); + if (limitLine) { + lines.push(limitLine); + } lines.push(cashflowPolarityRequested ? "\u042d\u0442\u043e \u0434\u0435\u043d\u0435\u0436\u043d\u044b\u0439 \u043f\u043e\u0442\u043e\u043a \u043f\u043e \u043d\u0430\u0439\u0434\u0435\u043d\u043d\u044b\u043c \u0441\u0442\u0440\u043e\u043a\u0430\u043c 1\u0421, \u043d\u0435 \u0431\u0443\u0445\u0433\u0430\u043b\u0442\u0435\u0440\u0441\u043a\u0438\u0439 \u0444\u0438\u043d\u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442." : "\u042d\u0442\u043e \u0434\u0435\u043d\u0435\u0436\u043d\u044b\u0439 \u043f\u043e\u0442\u043e\u043a \u043f\u043e \u043d\u0430\u0439\u0434\u0435\u043d\u043d\u044b\u043c \u0441\u0442\u0440\u043e\u043a\u0430\u043c 1\u0421, \u043d\u0435 \u0447\u0438\u0441\u0442\u0430\u044f \u043f\u0440\u0438\u0431\u044b\u043b\u044c \u0438 \u043d\u0435 \u0431\u0443\u0445\u0433\u0430\u043b\u0442\u0435\u0440\u0441\u043a\u0438\u0439 \u0444\u0438\u043d\u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442."); diff --git a/llm_normalizer/backend/src/services/assistantMcpDiscoveryResponseCandidate.ts b/llm_normalizer/backend/src/services/assistantMcpDiscoveryResponseCandidate.ts index 0e4c6db..433d560 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryResponseCandidate.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryResponseCandidate.ts @@ -1096,12 +1096,18 @@ function buildCompactBusinessOverviewReply( cashflowPolarityRequested && netDisplay && !String(netDisplay).trim().startsWith("-") ? `+${netDisplay}` : netDisplay; - const polarityLead = cashflowPolarityRequested - ? `\u041a\u043e\u0440\u043e\u0442\u043a\u043e: \u043f\u043e \u0434\u0435\u043d\u044c\u0433\u0430\u043c \u043f\u043b\u044e\u0441. ` - : "\u041a\u043e\u0440\u043e\u0442\u043a\u043e: "; + const limitedEarningsAnswer = Boolean(earningsAmountRequested && !accountingFinancialResultForEarnings && limitLine); + const polarityLead = limitedEarningsAnswer + ? "\u041a\u043e\u0440\u043e\u0442\u043a\u043e: \u0442\u043e\u0447\u043d\u0443\u044e \u0443\u0447\u0435\u0442\u043d\u0443\u044e \u043f\u0440\u0438\u0431\u044b\u043b\u044c \u0437\u0430 \u044d\u0442\u043e \u043e\u043a\u043d\u043e \u043d\u0435 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0430\u044e; \u0432 \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u043d\u043e\u043c \u0434\u0435\u043d\u0435\u0436\u043d\u043e\u043c \u0441\u0440\u0435\u0437\u0435 " + : cashflowPolarityRequested + ? `\u041a\u043e\u0440\u043e\u0442\u043a\u043e: \u043f\u043e \u0434\u0435\u043d\u044c\u0433\u0430\u043c \u043f\u043b\u044e\u0441. ` + : "\u041a\u043e\u0440\u043e\u0442\u043a\u043e: "; lines.push( `${polarityLead}${organizationPrefix}${period} \u043f\u043e\u043b\u0443\u0447\u0438\u043b\u0438 ${incomingAmount ?? "0 \u0440\u0443\u0431."}; \u0437\u0430\u043f\u043b\u0430\u0442\u0438\u043b\u0438/\u0441\u043f\u0438\u0441\u0430\u043b\u0438 ${outgoingAmount ?? "0 \u0440\u0443\u0431."}; \u0434\u0435\u043d\u0435\u0436\u043d\u043e\u0435 \u043d\u0435\u0442\u0442\u043e ${signedNetDisplay}.` ); + if (limitLine) { + lines.push(limitLine); + } lines.push( cashflowPolarityRequested ? "\u042d\u0442\u043e \u0434\u0435\u043d\u0435\u0436\u043d\u044b\u0439 \u043f\u043e\u0442\u043e\u043a \u043f\u043e \u043d\u0430\u0439\u0434\u0435\u043d\u043d\u044b\u043c \u0441\u0442\u0440\u043e\u043a\u0430\u043c 1\u0421, \u043d\u0435 \u0431\u0443\u0445\u0433\u0430\u043b\u0442\u0435\u0440\u0441\u043a\u0438\u0439 \u0444\u0438\u043d\u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442." diff --git a/llm_normalizer/backend/tests/assistantMcpDiscoveryResponseCandidate.test.ts b/llm_normalizer/backend/tests/assistantMcpDiscoveryResponseCandidate.test.ts index ec0e4b1..52f8f14 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryResponseCandidate.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryResponseCandidate.test.ts @@ -850,6 +850,72 @@ describe("assistant MCP discovery response candidate", () => { expect(candidate.reply_text).not.toContain("wide overview"); }); + it("marks all-time earnings wording as limited when cashflow probes hit row limits", () => { + const candidate = buildAssistantMcpDiscoveryResponseCandidate( + entryPoint({ + turn_input: { + adapter_status: "ready", + turn_meaning_ref: { + raw_message: "сколько вообще денег мы заработали за все время?", + asked_action_family: "broad_evaluation", + unsupported_but_understood_family: "broad_business_evaluation", + explicit_organization_scope: "ООО Альтернатива Плюс" + }, + 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: { + incoming_customer_revenue: { + total_amount_human_ru: "285 819 547,57 руб.", + coverage_limited_by_probe_limit: true + }, + outgoing_supplier_payout: { + total_amount_human_ru: "137 963 720,37 руб.", + coverage_limited_by_probe_limit: true + }, + net_amount_human_ru: "147 855 827,20 руб.", + net_direction: "net_incoming", + top_customers: [], + top_suppliers: [], + accounting_financial_result: null, + coverage_limited_by_probe_limit: true + } + }, + answer_draft: { + answer_mode: "confirmed_with_bounded_inference", + headline: "wide overview should not leak", + confirmed_lines: [], + inference_lines: [], + unknown_lines: [], + limitation_lines: [], + next_step_line: null + } + } + }) + ); + + expect(candidate.reply_text).toContain("точную учетную прибыль"); + expect(candidate.reply_text).toContain("не подтверждаю"); + expect(candidate.reply_text).toContain("ограниченном денежном срезе"); + expect(candidate.reply_text).toContain("285 819 547,57"); + expect(candidate.reply_text).toContain("147 855 827,20"); + expect(candidate.reply_text).toContain("не считаю полным бухгалтерским оборотом"); + expect(candidate.reply_text).not.toContain("wide overview"); + }); + it("keeps broad overview layers when direct-money reason is present", () => { const candidate = buildAssistantMcpDiscoveryResponseCandidate( entryPoint({