diff --git a/docs/orchestration/address_truth_harness_phase34_open_scope_value_flow_totals.json b/docs/orchestration/address_truth_harness_phase34_open_scope_value_flow_totals.json new file mode 100644 index 0000000..c81c0aa --- /dev/null +++ b/docs/orchestration/address_truth_harness_phase34_open_scope_value_flow_totals.json @@ -0,0 +1,56 @@ +{ + "schema_version": "domain_truth_harness_spec_v1", + "scenario_id": "address_truth_harness_phase34_open_scope_value_flow_totals", + "domain": "address_phase34_open_scope_value_flow_totals", + "title": "Phase 34 open-scope value-flow totals replay", + "description": "Targeted AGENT replay for Big Block D where organization-scoped incoming/outgoing money totals must be understood as bounded open-scope value-flow questions rather than as missing-counterparty fact asks.", + "bindings": {}, + "steps": [ + { + "step_id": "step_01_incoming_total_for_org", + "title": "Raw organization-scoped incoming wording produces a bounded incoming total without inventing a counterparty", + "question": "Сколько входящих денег за 2020 год по ООО Альтернатива Плюс?", + "allowed_reply_types": ["factual_with_explanation", "partial_coverage"], + "required_answer_patterns_all": [ + "(?i)2020", + "(?i)входящ|получ|поступ", + "(?i)руб" + ], + "required_answer_patterns_any": [ + "(?i)альтернатива", + "(?i)проверенн|найденн" + ], + "forbidden_answer_patterns": [ + "(?i)уточните контрагента", + "(?i)не найден контрагент", + "(?i)по какому контрагенту", + "(?i)не найдено контрагента" + ], + "criticality": "critical", + "semantic_tags": ["value_flow_total", "incoming", "open_scope", "organization_scoped", "bounded_autonomy"] + }, + { + "step_id": "step_02_outgoing_total_for_org", + "title": "Raw organization-scoped outgoing wording produces a bounded payout total without inventing a counterparty", + "question": "Сколько исходящих денег за 2020 год по ООО Альтернатива Плюс?", + "allowed_reply_types": ["factual_with_explanation", "partial_coverage"], + "required_answer_patterns_all": [ + "(?i)2020", + "(?i)исходящ|заплат|списан|платеж", + "(?i)руб" + ], + "required_answer_patterns_any": [ + "(?i)альтернатива", + "(?i)проверенн|найденн" + ], + "forbidden_answer_patterns": [ + "(?i)уточните контрагента", + "(?i)не найден контрагент", + "(?i)по какому контрагенту", + "(?i)не найдено контрагента" + ], + "criticality": "critical", + "semantic_tags": ["value_flow_total", "outgoing", "open_scope", "organization_scoped", "bounded_autonomy"] + } + ] +} diff --git a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryDataNeedGraph.js b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryDataNeedGraph.js index 71b2ff9..b7c652c 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryDataNeedGraph.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryDataNeedGraph.js @@ -85,11 +85,17 @@ function comparisonNeedFor(action) { } return null; } +function supportsOrganizationScopedOpenTotal(action) { + return action === "turnover" || action === "payout"; +} function allowsOpenScopeWithoutSubject(input) { if (input.family !== "value_flow") { return false; } - return Boolean(input.rankingNeed || input.comparisonNeed === "incoming_vs_outgoing"); + if (input.rankingNeed || input.comparisonNeed === "incoming_vs_outgoing") { + return true; + } + return Boolean(input.organizationScope && supportsOrganizationScopedOpenTotal(input.action)); } function rankingNeedFromRawUtterance(value) { const text = lower(value); @@ -147,6 +153,12 @@ function decompositionCandidatesFor(input) { pushUnique(result, "probe_coverage"); return result; } + if (input.openScopeWithoutSubject) { + pushUnique(result, "collect_scoped_movements"); + pushUnique(result, input.aggregationNeed === "by_month" ? "aggregate_by_month" : "aggregate_checked_amounts"); + pushUnique(result, "probe_coverage"); + return result; + } pushUnique(result, "resolve_entity_reference"); if (input.action === "net_value_flow") { pushUnique(result, "collect_incoming_movements"); @@ -204,6 +216,7 @@ function buildAssistantMcpDiscoveryDataNeedGraph(input) { const rawUtterance = lower(input.rawUtterance); const aggregationAxis = lower(turnMeaning?.asked_aggregation_axis); const explicitDateScope = toNonEmptyString(turnMeaning?.explicit_date_scope); + const explicitOrganizationScope = toNonEmptyString(turnMeaning?.explicit_organization_scope); const subjectCandidates = (turnMeaning?.explicit_entity_candidates ?? []) .map((item) => toNonEmptyString(item)) .filter((item) => Boolean(item)); @@ -219,6 +232,8 @@ function buildAssistantMcpDiscoveryDataNeedGraph(input) { const openScopeWithoutSubject = subjectCandidates.length === 0 && allowsOpenScopeWithoutSubject({ family: businessFactFamily, + action, + organizationScope: explicitOrganizationScope, comparisonNeed, rankingNeed }); @@ -261,6 +276,9 @@ function buildAssistantMcpDiscoveryDataNeedGraph(input) { if (comparisonNeed) { pushReason(reasonCodes, `data_need_graph_comparison_${comparisonNeed}`); } + if (openScopeWithoutSubject && !rankingNeed && !comparisonNeed) { + pushReason(reasonCodes, "data_need_graph_open_scope_total_without_subject"); + } if (clarificationGaps.length > 0) { pushReason(reasonCodes, "data_need_graph_has_clarification_gaps"); } diff --git a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPlanner.js b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPlanner.js index 9ac8ab2..24f9d6a 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPlanner.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPlanner.js @@ -86,6 +86,7 @@ function recipeFor(input) { const graphAction = lower(dataNeedGraph?.action_family); const graphAggregation = lower(dataNeedGraph?.aggregation_need); const graphClarificationGaps = (dataNeedGraph?.clarification_gaps ?? []).map((item) => lower(item)); + const organizationScope = toNonEmptyString(meaning?.explicit_organization_scope); const combined = `${domain} ${action} ${unsupported}`.trim(); const axes = []; const requestedAggregationAxis = aggregationAxis(meaning); @@ -132,6 +133,24 @@ function recipeFor(input) { : "planner_selected_top_ranked_value_flow_from_data_need_graph" }; } + if (!hasSubjectCandidates(dataNeedGraph) && organizationScope) { + pushUnique(axes, "aggregate_axis"); + pushUnique(axes, "amount"); + pushUnique(axes, "coverage_target"); + if (requestedAggregationAxis === "month" || graphAggregation === "by_month") { + pushUnique(axes, "calendar_month"); + } + return { + semanticDataNeed: "organization-scoped value-flow evidence", + chainId: "value_flow", + chainSummary: "Query scoped movements for the checked period and organization without a preselected counterparty, aggregate checked amounts, then probe coverage before answering a bounded total.", + primitives: ["query_movements", "aggregate_by_axis", "probe_coverage"], + axes, + reason: requestedAggregationAxis === "month" || graphAggregation === "by_month" + ? "planner_selected_monthly_open_scope_value_flow_total_from_data_need_graph" + : "planner_selected_open_scope_value_flow_total_from_data_need_graph" + }; + } pushUnique(axes, "aggregate_axis"); pushUnique(axes, "amount"); pushUnique(axes, "coverage_target"); diff --git a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryTurnInputAdapter.js b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryTurnInputAdapter.js index 62204a4..15bbbef 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryTurnInputAdapter.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryTurnInputAdapter.js @@ -305,6 +305,14 @@ function hasBidirectionalValueFlowSignal(text) { function hasValueRankingSignal(text) { return /(?:кто\s+больше\s+всего.*ден[её]г|больше\s+всего.*ден[её]г|прин[её]с.*ден[её]г|сам(?:ый|ая|ое|ые).*(?:доходн|прибыльн)|most.*money|highest\s+(?:revenue|payment))/iu.test(text); } +function hasOrganizationScopeSignal(text) { + return /(?:\bРѕРѕРѕ\b|\bРёРї\b|\bао\b|\bпао\b|\bзао\b|\bllc\b|\binc\b|\bcorp\b|\bcompany\b|\borganization\b|\borganisation\b|организац|компан)/iu.test(text); +} +function hasOrganizationScopeSignalUtf8(text) { + return (/(? toNonEmptyString(item)) .filter((item): item is string => Boolean(item)); @@ -291,6 +307,8 @@ export function buildAssistantMcpDiscoveryDataNeedGraph( subjectCandidates.length === 0 && allowsOpenScopeWithoutSubject({ family: businessFactFamily, + action, + organizationScope: explicitOrganizationScope, comparisonNeed, rankingNeed }); @@ -332,6 +350,9 @@ export function buildAssistantMcpDiscoveryDataNeedGraph( if (comparisonNeed) { pushReason(reasonCodes, `data_need_graph_comparison_${comparisonNeed}`); } + if (openScopeWithoutSubject && !rankingNeed && !comparisonNeed) { + pushReason(reasonCodes, "data_need_graph_open_scope_total_without_subject"); + } if (clarificationGaps.length > 0) { pushReason(reasonCodes, "data_need_graph_has_clarification_gaps"); } diff --git a/llm_normalizer/backend/src/services/assistantMcpDiscoveryPlanner.ts b/llm_normalizer/backend/src/services/assistantMcpDiscoveryPlanner.ts index 04cea8e..a5cff6d 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryPlanner.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryPlanner.ts @@ -153,6 +153,7 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe { const graphAction = lower(dataNeedGraph?.action_family); const graphAggregation = lower(dataNeedGraph?.aggregation_need); const graphClarificationGaps = (dataNeedGraph?.clarification_gaps ?? []).map((item) => lower(item)); + const organizationScope = toNonEmptyString(meaning?.explicit_organization_scope); const combined = `${domain} ${action} ${unsupported}`.trim(); const axes: string[] = []; const requestedAggregationAxis = aggregationAxis(meaning); @@ -201,7 +202,27 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe { reason: dataNeedGraph.ranking_need === "bottom_asc" ? "planner_selected_bottom_ranked_value_flow_from_data_need_graph" - : "planner_selected_top_ranked_value_flow_from_data_need_graph" + : "planner_selected_top_ranked_value_flow_from_data_need_graph" + }; + } + if (!hasSubjectCandidates(dataNeedGraph) && organizationScope) { + pushUnique(axes, "aggregate_axis"); + pushUnique(axes, "amount"); + pushUnique(axes, "coverage_target"); + if (requestedAggregationAxis === "month" || graphAggregation === "by_month") { + pushUnique(axes, "calendar_month"); + } + return { + semanticDataNeed: "organization-scoped value-flow evidence", + chainId: "value_flow", + chainSummary: + "Query scoped movements for the checked period and organization without a preselected counterparty, aggregate checked amounts, then probe coverage before answering a bounded total.", + primitives: ["query_movements", "aggregate_by_axis", "probe_coverage"], + axes, + reason: + requestedAggregationAxis === "month" || graphAggregation === "by_month" + ? "planner_selected_monthly_open_scope_value_flow_total_from_data_need_graph" + : "planner_selected_open_scope_value_flow_total_from_data_need_graph" }; } pushUnique(axes, "aggregate_axis"); diff --git a/llm_normalizer/backend/src/services/assistantMcpDiscoveryTurnInputAdapter.ts b/llm_normalizer/backend/src/services/assistantMcpDiscoveryTurnInputAdapter.ts index 2d681bf..ecf2ed8 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryTurnInputAdapter.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryTurnInputAdapter.ts @@ -422,6 +422,20 @@ function hasValueRankingSignal(text: string): boolean { ); } +function hasOrganizationScopeSignal(text: string): boolean { + return /(?:\bРѕРѕРѕ\b|\bРёРї\b|\bао\b|\bпао\b|\bзао\b|\bllc\b|\binc\b|\bcorp\b|\bcompany\b|\borganization\b|\borganisation\b|организац|компан)/iu.test( + text + ); +} + +function hasOrganizationScopeSignalUtf8(text: string): boolean { + return ( + /(? { ]); expect(result.reason_codes).toContain("data_need_graph_comparison_incoming_vs_outgoing"); }); + it("treats organization-scoped incoming totals as an open-scope value need rather than a missing-subject fact ask", () => { + const result = buildAssistantMcpDiscoveryDataNeedGraph({ + semanticDataNeed: "counterparty value-flow evidence", + rawUtterance: "сколько входящих денег за 2020 год по ООО Альтернатива Плюс?", + turnMeaning: { + asked_domain_family: "counterparty_value", + asked_action_family: "turnover", + explicit_date_scope: "2020", + explicit_organization_scope: "ООО Альтернатива Плюс" + } + }); + + expect(result.business_fact_family).toBe("value_flow"); + expect(result.comparison_need).toBeNull(); + expect(result.ranking_need).toBeNull(); + expect(result.clarification_gaps).toEqual([]); + expect(result.decomposition_candidates).toEqual([ + "collect_scoped_movements", + "aggregate_checked_amounts", + "probe_coverage" + ]); + expect(result.reason_codes).toContain("data_need_graph_open_scope_total_without_subject"); + }); }); diff --git a/llm_normalizer/backend/tests/assistantMcpDiscoveryPlanner.test.ts b/llm_normalizer/backend/tests/assistantMcpDiscoveryPlanner.test.ts index 7aebeab..5b58d04 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryPlanner.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryPlanner.test.ts @@ -440,4 +440,38 @@ describe("assistant MCP discovery planner", () => { expect(result.selected_chain_summary).toContain("resolve the most relevant 1C reference"); expect(result.proposed_primitives).toEqual(["search_business_entity", "resolve_entity_reference", "probe_coverage"]); }); + it("keeps organization-scoped one-sided value-flow totals executable without forcing a counterparty", () => { + const result = planAssistantMcpDiscovery({ + dataNeedGraph: { + schema_version: "assistant_data_need_graph_v1", + policy_owner: "assistantMcpDiscoveryDataNeedGraph", + subject_candidates: [], + business_fact_family: "value_flow", + action_family: "turnover", + aggregation_need: null, + time_scope_need: "explicit_period", + comparison_need: null, + ranking_need: null, + proof_expectation: "coverage_checked_fact", + clarification_gaps: [], + decomposition_candidates: ["collect_scoped_movements", "aggregate_checked_amounts", "probe_coverage"], + forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"], + reason_codes: ["data_need_graph_built", "data_need_graph_open_scope_total_without_subject"] + }, + turnMeaning: { + asked_domain_family: "counterparty_value", + asked_action_family: "turnover", + explicit_date_scope: "2020", + explicit_organization_scope: "ООО Альтернатива Плюс" + } + }); + + expect(result.planner_status).toBe("ready_for_execution"); + expect(result.semantic_data_need).toBe("organization-scoped value-flow evidence"); + expect(result.selected_chain_id).toBe("value_flow"); + expect(result.proposed_primitives).toEqual(["query_movements", "aggregate_by_axis", "probe_coverage"]); + expect(result.required_axes).toEqual(["organization", "period", "aggregate_axis", "amount", "coverage_target"]); + expect(result.catalog_review.review_status).toBe("catalog_compatible"); + expect(result.reason_codes).toContain("planner_selected_open_scope_value_flow_total_from_data_need_graph"); + }); }); diff --git a/llm_normalizer/backend/tests/assistantMcpDiscoveryRuntimeBridge.test.ts b/llm_normalizer/backend/tests/assistantMcpDiscoveryRuntimeBridge.test.ts index 0f58d07..4b2f3d1 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryRuntimeBridge.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryRuntimeBridge.test.ts @@ -275,4 +275,45 @@ describe("assistant MCP discovery runtime bridge", () => { expect(userFacing).not.toContain("runtime_bridge"); expect(userFacing).not.toContain("primitive"); }); + it("produces a bounded one-sided value-flow answer for an organization-scoped total without inventing a counterparty", async () => { + const result = await runAssistantMcpDiscoveryRuntimeBridge({ + dataNeedGraph: { + schema_version: "assistant_data_need_graph_v1", + policy_owner: "assistantMcpDiscoveryDataNeedGraph", + subject_candidates: [], + business_fact_family: "value_flow", + action_family: "turnover", + aggregation_need: null, + time_scope_need: "explicit_period", + comparison_need: null, + ranking_need: null, + proof_expectation: "coverage_checked_fact", + clarification_gaps: [], + decomposition_candidates: ["collect_scoped_movements", "aggregate_checked_amounts", "probe_coverage"], + forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"], + reason_codes: ["data_need_graph_built", "data_need_graph_open_scope_total_without_subject"] + }, + turnMeaning: { + asked_domain_family: "counterparty_value", + asked_action_family: "turnover", + explicit_date_scope: "2020", + explicit_organization_scope: "ООО Альтернатива Плюс" + }, + deps: buildDeps([ + { Period: "2020-01-10T00:00:00", Amount: 3200, Counterparty: "Клиент-А" }, + { Period: "2020-05-22T00:00:00", Amount: 1800, Counterparty: "Клиент-Б" } + ]) + }); + + expect(result.bridge_status).toBe("answer_draft_ready"); + expect(result.business_fact_answer_allowed).toBe(true); + expect(result.planner.selected_chain_id).toBe("value_flow"); + expect(result.pilot.derived_value_flow).toMatchObject({ + counterparty: null, + period_scope: "2020", + total_amount: 5000 + }); + expect(result.answer_draft.confirmed_lines.join("\n")).toContain("5 000"); + expect(result.answer_draft.confirmed_lines.join("\n")).not.toContain("контрагенту"); + }); }); diff --git a/llm_normalizer/backend/tests/assistantMcpDiscoveryRuntimeEntryPoint.test.ts b/llm_normalizer/backend/tests/assistantMcpDiscoveryRuntimeEntryPoint.test.ts index 838ace7..f2f78ee 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryRuntimeEntryPoint.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryRuntimeEntryPoint.test.ts @@ -353,4 +353,59 @@ describe("assistant MCP discovery runtime entry point", () => { expect(result.turn_input.data_need_graph?.subject_candidates).toEqual([]); expect(result.bridge?.planner.selected_chain_id).toBe("value_flow_comparison"); }); + it("runs raw organization-scoped incoming totals as an open value-flow chain without inventing a counterparty", async () => { + const result = await runAssistantMcpDiscoveryRuntimeEntryPoint({ + userMessage: "сколько входящих денег за 2020 год по ООО Альтернатива Плюс?", + predecomposeContract: { + entities: { organization: "ООО Альтернатива Плюс" }, + period: { period_from: "2020-01-01", period_to: "2020-12-31" } + }, + deps: buildDeps([ + { Period: "2020-01-15T00:00:00", Amount: 2500, Counterparty: "Клиент-А" }, + { Period: "2020-06-20T00:00:00", Amount: 1000, Counterparty: "Клиент-Б" } + ]) + }); + + expect(result.entry_status).toBe("bridge_executed"); + expect(result.discovery_attempted).toBe(true); + expect(result.turn_input.turn_meaning_ref).toMatchObject({ + asked_domain_family: "counterparty_value", + asked_action_family: "turnover", + explicit_organization_scope: "ООО Альтернатива Плюс", + explicit_date_scope: "2020" + }); + expect(result.turn_input.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined(); + expect(result.turn_input.data_need_graph?.subject_candidates).toEqual([]); + expect(result.bridge?.planner.selected_chain_id).toBe("value_flow"); + expect(result.bridge?.pilot.pilot_scope).toBe("counterparty_value_flow_query_movements_v1"); + expect(result.bridge?.answer_draft.confirmed_lines.join("\n")).toContain("входящ"); + }); + + it("runs raw organization-scoped outgoing totals as an open payout chain without inventing a counterparty", async () => { + const orgName = "ООО Альтернатива Плюс"; + const result = await runAssistantMcpDiscoveryRuntimeEntryPoint({ + userMessage: "сколько исходящих денег за 2020 год по ООО Альтернатива Плюс?", + predecomposeContract: { + entities: { counterparty: orgName, organization: orgName }, + period: { period_from: "2020-01-01", period_to: "2020-12-31" } + }, + deps: buildDeps([ + { Period: "2020-02-18T00:00:00", Amount: 900, Counterparty: "Поставщик-А" }, + { Period: "2020-08-07T00:00:00", Amount: 300, Counterparty: "Поставщик-Б" } + ]) + }); + + expect(result.entry_status).toBe("bridge_executed"); + expect(result.discovery_attempted).toBe(true); + expect(result.turn_input.turn_meaning_ref).toMatchObject({ + asked_domain_family: "counterparty_value", + asked_action_family: "payout", + explicit_organization_scope: orgName, + explicit_date_scope: "2020" + }); + expect(result.turn_input.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined(); + expect(result.bridge?.planner.selected_chain_id).toBe("value_flow"); + expect(result.bridge?.pilot.pilot_scope).toBe("counterparty_supplier_payout_query_movements_v1"); + expect(result.bridge?.answer_draft.confirmed_lines.join("\n")).toContain("исход"); + }); }); diff --git a/llm_normalizer/backend/tests/assistantMcpDiscoveryTurnInputAdapter.test.ts b/llm_normalizer/backend/tests/assistantMcpDiscoveryTurnInputAdapter.test.ts index 375398b..c9e3fad 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryTurnInputAdapter.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryTurnInputAdapter.test.ts @@ -1377,4 +1377,49 @@ describe("assistant MCP discovery turn input adapter", () => { expect(result.data_need_graph?.comparison_need).toBe("incoming_vs_outgoing"); expect(result.reason_codes).not.toContain("mcp_discovery_counterparty_from_predecompose"); }); + it("keeps organization-scoped incoming totals in an open value-flow lane without inventing a counterparty", () => { + const result = buildAssistantMcpDiscoveryTurnInput({ + userMessage: "сколько входящих денег за 2020 год по ООО Альтернатива Плюс?", + predecomposeContract: { + entities: { organization: "ООО Альтернатива Плюс" }, + 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("counterparty value-flow evidence"); + expect(result.turn_meaning_ref).toMatchObject({ + asked_domain_family: "counterparty_value", + asked_action_family: "turnover", + explicit_organization_scope: "ООО Альтернатива Плюс", + explicit_date_scope: "2020", + unsupported_but_understood_family: "counterparty_value_or_turnover", + stale_replay_forbidden: true + }); + expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined(); + expect(result.data_need_graph?.subject_candidates).toEqual([]); + expect(result.data_need_graph?.reason_codes).toContain("data_need_graph_open_scope_total_without_subject"); + }); + + it("does not treat mirrored organization/counterparty predecompose as a real subject for organization-scoped payouts", () => { + 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.turn_meaning_ref).toMatchObject({ + asked_domain_family: "counterparty_value", + asked_action_family: "payout", + explicit_organization_scope: orgName, + explicit_date_scope: "2020" + }); + expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined(); + expect(result.data_need_graph?.subject_candidates).toEqual([]); + }); });