Compare commits

...

4 Commits

22 changed files with 1104 additions and 246 deletions

View File

@ -1,10 +1,10 @@
{
"schema_version": "domain_scenario_pack_v1",
"pack_id": "active_phase107_mixed_planner_brain_dogfood_20260615",
"domain": "address_phase107_mixed_planner_brain_dogfood",
"runtime_domain": "planner_autonomy_mixed_boundary",
"title": "Mixed planner-brain dogfood replay",
"description": "Active slice for dogfooding the autonomous agent loop on a compact mixed planner path: living chat guard, counterparty alias grounding, follow-up money flow, net, documents, movement evidence, broad business evaluation, and off-domain recovery.",
"pack_id": "active_phase108_settlement_pivot_debt_boundary_20260615",
"domain": "address_phase108_settlement_pivot_debt_boundary",
"runtime_domain": "settlement_document_payment_contract_pivots",
"title": "Settlement pivot and debt boundary replay",
"description": "Active slice for dogfooding the autonomous agent loop on a settlement-heavy path: counterparty document root, repeated payments/contracts/documents pivots, year switch carryover, debt due-date boundary, and off-domain recovery.",
"status": "active",
"source_of_truth_policy": {
"purpose": "single mutable domain source for the current orchestration target",
@ -21,25 +21,27 @@
},
"analysis_context": {
"as_of_date": "2026-06-15",
"source": "phase107_after_phase106_profit_cashflow_margin_next_step",
"source": "phase108_after_phase107_mixed_planner_brain_dogfood",
"known_previous_anchors": [
"phase83_planner_brain_alignment_mix_20260603_live9",
"phase104_generic_role_tail_anchor_hygiene_20260615_p04_loop",
"phase106_profit_cashflow_margin_next_step_20260615_p03_loop"
"phase75_contracts_to_payments_year_switch",
"phase79_payments_to_contracts_to_documents_year_switch",
"phase81_payments_contracts_documents_back_to_payments_year_switch",
"phase94_debt_due_date_aging_reviewed_route",
"phase107_mixed_planner_brain_dogfood_20260615_p11_loop"
],
"why_now": "Phase83 passed in the legacy truth-harness format; this pack replays the highest-value mixed planner slice through the current autonomous pack-loop."
"why_now": "Phase107 hardened counterparty money-flow memory. Phase108 moves to settlement pivots: documents, payments, contracts, repeated pronoun reuse, year-switch carryover, and honest debt due-date boundaries."
},
"target_score": 88,
"bindings": {
"main_organization": "ООО Альтернатива Плюс",
"control_year": "2020",
"svk_counterparty": "Группа СВК"
"pivot_counterparty": "Жуковка 51",
"control_year": "2021",
"debt_boundary_year_end": "2020-12-31"
},
"issue_codes_under_test": [
"technical_garbage_in_answer",
"business_direct_answer_missing",
"business_next_step_missing",
"counterparty_value_flow_misrouted_to_company_profit",
"business_utility_gap"
],
"detectors_under_test": [
@ -48,62 +50,66 @@
"first_line_not_direct_answer",
"top_level_scaffold_before_answer",
"limited_answer_without_next_action",
"counterparty_value_flow_required_surface",
"counterparty_value_flow_profit_accounts_forbidden"
"business_answer_too_verbose_output"
],
"agent_audit_expectations": {
"semantic_answer_review_first": true,
"direct_answer_first": true,
"business_utility_required": true,
"selected_counterparty_memory_required": true,
"date_carryover_required": true,
"documents_and_movements_are_evidence_drilldowns": true,
"pivot_action_memory_required": true,
"year_switch_keeps_last_action_required": true,
"debt_due_date_boundary_must_be_honest": true,
"off_domain_living_chat_must_not_replay_business_context": true,
"technical_garbage_forbidden": true,
"do_not_accept_if": [
"a follow-up over 'нему' loses the selected counterparty",
"a payout or net follow-up loses the 2020 period",
"a counterparty received/paid/net question is answered as company profit",
"document or movement follow-up answers with a fresh clarification instead of reusing the selected counterparty",
"broad business evaluation leaks route ids, capability ids, runtime enums, or debug payload",
"a pronoun follow-up over 'по нему' loses Жуковка 51",
"the year switch asks again for a counterparty instead of reusing the active settlement object",
"a payment/contracts/documents pivot answers with a different object family without saying so",
"the debt due-date answer claims proven overdue debt without payment-term evidence",
"the final answer leaks route ids, capability ids, runtime enums, or debug payload",
"off-domain living chat is hijacked by stale 1C business context"
]
},
"scenarios": [
{
"scenario_id": "mixed_planner_counterparty_evidence_and_living_guard",
"title": "Counterparty memory, evidence drilldowns, business overview, and off-domain guard",
"description": "Validate a compact but cross-cutting planner path: chat sanity, loose counterparty alias grounding, follow-up money flow and net, documents/movements evidence, explicit broad company evaluation, and recovery into ordinary living chat.",
"scenario_id": "settlement_pivot_year_switch_and_debt_boundary",
"title": "Documents, payments, contracts, year switch, and debt boundary",
"description": "Validate repeated settlement pivots over the same counterparty and a debt due-date boundary canary in one session.",
"steps": [
{
"step_id": "step_01_human_smalltalk_sanity",
"title": "Living chat remains human and does not expose discovery internals",
"step_id": "step_01_documents_by_counterparty",
"title": "Open documents for the counterparty",
"node_role": "root",
"question": "привет, ты на связи?",
"question": "Покажи документы по Жуковке 51.",
"allowed_reply_types": [
"factual",
"factual_with_explanation",
"partial_coverage"
],
"semantic_tags": [
"human_answer",
"mcp_discovery_gate_sanity",
"meta_smalltalk"
"documents_by_counterparty",
"counterparty_root",
"pivot_seed",
"integrity_guard"
],
"required_answer_shape": "direct_answer_first",
"required_answer_patterns_any": [
"(?i)привет|на связи|готов|помочь"
"required_answer_patterns_all": [
"(?i)жуковк",
"(?i)документ|сч[её]т|акт|накладн|строк"
],
"forbidden_answer_patterns": [
"(?i)mcp|runtime_|query_documents|primitive|route_candidate|capability_id|snapshot_items|answer_object"
"(?i)route_candidate|primitive|planner_|catalog_|snapshot_items|answer_object|capability_id|mcp_discovery",
"(?i)уточните.*контрагент|какого контрагента"
],
"criticality": "info"
"criticality": "critical"
},
{
"step_id": "step_02_resolve_counterparty_alias",
"title": "Entity resolution grounds the checked 1C counterparty from a loose alias",
"question": "найди в 1С контрагента СВК",
"step_id": "step_02_payments_by_pronoun_followup",
"title": "Pivot from documents to payments",
"question": "А по нему платежи?",
"depends_on": [
"step_01_human_smalltalk_sanity"
"step_01_documents_by_counterparty"
],
"allowed_reply_types": [
"factual",
@ -111,139 +117,29 @@
"partial_coverage"
],
"semantic_tags": [
"entity_resolution",
"alias_grounding",
"followup_anchor",
"planner_catalog_alignment"
"payments_followup",
"counterparty_pronoun_resolution",
"document_to_payments_pivot",
"integrity_guard"
],
"required_answer_shape": "direct_answer_first",
"required_answer_patterns_all": [
"(?i)свк",
"(?i)контрагент"
],
"required_answer_patterns_any": [
"(?i)группа\\s+свк",
"(?i)каталог|найден|наиболее вероятн"
"(?i)жуковк|контрагент",
"(?i)плат[её]ж|операц|банк|поступлен|списан"
],
"forbidden_answer_patterns": [
"(?i)получили|заплатили|нетто|оборот|выручк|сумм(а|ы)",
"(?i)уточните.*контрагент|какого контрагента",
"(?i)документы по всем контрагентам",
"(?i)route_candidate|primitive|planner_|catalog_|snapshot_items|answer_object|capability_id|mcp_discovery"
],
"criticality": "critical"
},
{
"step_id": "step_03_incoming_by_resolved_entity",
"title": "Incoming value-flow follow-up reuses the resolved counterparty anchor",
"question": "сколько получили по нему за 2020 год",
"step_id": "step_03_contracts_after_payments_pivot",
"title": "Pivot from payments to contracts",
"question": "А по нему договоры?",
"depends_on": [
"step_02_resolve_counterparty_alias"
],
"allowed_reply_types": [
"factual_with_explanation",
"partial_coverage"
],
"semantic_tags": [
"entity_resolution",
"incoming_value_flow",
"followup_reuse",
"date_carryover",
"planner_catalog_alignment"
],
"required_answer_shape": "direct_answer_first",
"required_answer_patterns_all": [
"(?i)2020",
"(?i)получил|входящ|поступ",
"(?i)руб"
],
"required_answer_patterns_any": [
"(?i)группа\\s+свк",
"(?i)свк"
],
"forbidden_answer_patterns": [
"(?i)не найден контрагент|уточните, какого контрагента|по какому контрагенту",
"(?i)чист.*прибыл|90/91/99|финрезульт",
"(?i)route_candidate|primitive|planner_|catalog_|snapshot_items|answer_object|capability_id|mcp_discovery"
],
"criticality": "critical"
},
{
"step_id": "step_04_payout_switch_by_resolved_entity",
"title": "Outgoing payment follow-up keeps the same grounded counterparty and checked year",
"question": "а теперь сколько заплатили?",
"depends_on": [
"step_03_incoming_by_resolved_entity"
],
"allowed_reply_types": [
"factual_with_explanation",
"partial_coverage"
],
"semantic_tags": [
"entity_resolution",
"payout_switch",
"followup_reuse",
"date_carryover",
"planner_catalog_alignment"
],
"required_answer_shape": "direct_answer_first",
"required_answer_patterns_all": [
"(?i)2020",
"(?i)заплатил|исходящ|списан|платеж|платёж",
"(?i)руб"
],
"required_answer_patterns_any": [
"(?i)группа\\s+свк",
"(?i)свк"
],
"forbidden_answer_patterns": [
"(?i)не найден контрагент|уточните, какого контрагента|по какому контрагенту|за какой год",
"(?i)чист.*прибыл|90/91/99|финрезульт",
"(?i)route_candidate|primitive|planner_|catalog_|snapshot_items|answer_object|capability_id|mcp_discovery"
],
"criticality": "critical"
},
{
"step_id": "step_05_net_after_payout",
"title": "Net-flow follow-up reuses the same grounded counterparty and checked year after payout",
"question": "а какое нетто?",
"depends_on": [
"step_04_payout_switch_by_resolved_entity"
],
"allowed_reply_types": [
"factual_with_explanation",
"partial_coverage"
],
"semantic_tags": [
"entity_resolution",
"net_value_flow",
"followup_reuse",
"date_carryover",
"planner_catalog_alignment"
],
"required_answer_shape": "direct_answer_first",
"required_answer_patterns_all": [
"(?i)2020",
"(?i)нетто|сальдо|разниц",
"(?i)получ",
"(?i)заплат",
"(?i)руб"
],
"required_answer_patterns_any": [
"12[ .\\u00a0]?093[ .\\u00a0]?465|12093465",
"(?i)группа\\s+свк|свк"
],
"forbidden_answer_patterns": [
"(?i)не найден контрагент|уточните, какого контрагента|по какому контрагенту|за какой год",
"(?i)чист.*прибыл|90/91/99|финрезульт",
"(?i)route_candidate|primitive|planner_|catalog_|snapshot_items|answer_object|capability_id|mcp_discovery"
],
"criticality": "critical"
},
{
"step_id": "step_06_documents_after_net",
"title": "Document evidence follow-up keeps the grounded counterparty after the net answer",
"question": "а по документам?",
"depends_on": [
"step_05_net_after_payout"
"step_02_payments_by_pronoun_followup"
],
"allowed_reply_types": [
"factual",
@ -251,34 +147,28 @@
"partial_coverage"
],
"semantic_tags": [
"entity_resolution",
"document_evidence",
"value_flow_pivot",
"followup_reuse",
"planner_catalog_alignment"
"contracts_followup",
"second_pivot",
"counterparty_pronoun_resolution",
"integrity_guard"
],
"required_answer_shape": "direct_answer_first",
"required_answer_patterns_all": [
"(?i)документ|счет|счёт|накладн|акт"
],
"required_answer_patterns_any": [
"(?i)группа\\s+свк",
"(?i)свк",
"(?i)2020"
"(?i)жуковк|контрагент",
"(?i)договор|контракт|соглаш"
],
"forbidden_answer_patterns": [
"(?i)не найден контрагент|уточните, какого контрагента|по какому контрагенту",
"(?i)сколько получили|сколько заплатили|нетто",
"(?i)уточните.*контрагент|какого контрагента",
"(?i)route_candidate|primitive|planner_|catalog_|snapshot_items|answer_object|capability_id|mcp_discovery"
],
"criticality": "critical"
},
{
"step_id": "step_07_movements_after_documents",
"title": "Movement evidence follow-up keeps the grounded counterparty after the document answer",
"question": "а по движениям?",
"step_id": "step_04_documents_after_contracts_pivot",
"title": "Pivot back from contracts to documents",
"question": "А по нему документы?",
"depends_on": [
"step_06_documents_after_net"
"step_03_contracts_after_payments_pivot"
],
"allowed_reply_types": [
"factual",
@ -286,84 +176,168 @@
"partial_coverage"
],
"semantic_tags": [
"entity_resolution",
"movement_evidence",
"document_pivot",
"followup_reuse",
"planner_catalog_alignment"
"documents_followup",
"third_pivot",
"counterparty_pronoun_resolution",
"integrity_guard"
],
"required_answer_shape": "direct_answer_first",
"required_answer_patterns_all": [
"(?i)движени|операц|платеж|платёж|списан|поступ"
],
"required_answer_patterns_any": [
"(?i)группа\\s+свк",
"(?i)свк",
"(?i)2020"
"(?i)жуковк|контрагент",
"(?i)документ|сч[её]т|акт|накладн|строк"
],
"forbidden_answer_patterns": [
"(?i)не найден контрагент|уточните, какого контрагента|по какому контрагенту",
"(?i)сколько получили|сколько заплатили|нетто",
"(?i)уточните.*контрагент|какого контрагента",
"(?i)route_candidate|primitive|planner_|catalog_|snapshot_items|answer_object|capability_id|mcp_discovery"
],
"criticality": "critical"
},
{
"step_id": "step_08_broad_business_evaluation_explicit_org",
"title": "Broad business evaluation stays grounded and bounded after counterparty drilldowns",
"question": "Как ты оценишь деятельность компании {{bindings.main_organization}} по данным 1С?",
"step_id": "step_05_payments_after_documents_pivot",
"title": "Pivot back again to payments",
"question": "А по нему платежи?",
"depends_on": [
"step_07_movements_after_documents"
"step_04_documents_after_contracts_pivot"
],
"allowed_reply_types": [
"factual",
"factual_with_explanation",
"partial_coverage"
],
"semantic_tags": [
"broad_business_evaluation",
"grounded_summary",
"context_switch_from_counterparty_to_company"
"payments_followup",
"fourth_pivot",
"counterparty_pronoun_resolution",
"integrity_guard"
],
"required_answer_shape": "direct_answer_first",
"required_answer_patterns_all": [
"(?i)альтернатива",
"(?i)ограниченн|проверенн|не\\s+аудит|не\\s+аудитор",
"(?i)1с|подтвержд",
"(?i)денежн|долг|ндс|контрагент|операц"
"(?i)жуковк|контрагент",
"(?i)плат[её]ж|операц|банк|поступлен|списан"
],
"forbidden_answer_patterns": [
"(?i)^\\s*коротко:",
"(?i)чистая\\s+прибыль\\s+подтверждена|аудиторское\\s+заключение",
"(?i)уточните.*контрагент|какого контрагента",
"(?i)route_candidate|primitive|planner_|catalog_|snapshot_items|answer_object|capability_id|mcp_discovery"
],
"criticality": "critical"
},
{
"step_id": "step_06_year_switch_after_payments",
"title": "Year switch keeps counterparty and payment action",
"question": "А за 2021?",
"depends_on": [
"step_05_payments_after_documents_pivot"
],
"allowed_reply_types": [
"factual",
"factual_with_explanation",
"partial_coverage"
],
"semantic_tags": [
"year_switch_after_fourth_pivot",
"payments_followup",
"period_followup_reuse",
"integrity_guard"
],
"required_answer_shape": "direct_answer_first",
"required_answer_patterns_all": [
"(?i)2021",
"(?i)жуковк|контрагент",
"(?i)плат[её]ж|операц|банк|поступлен|списан"
],
"forbidden_answer_patterns": [
"(?i)уточните.*контрагент|какого контрагента|за какой год",
"(?i)метадан|схем|объект[а-я]* 1с",
"(?i)route_candidate|primitive|planner_|catalog_|snapshot_items|answer_object|capability_id|mcp_discovery"
],
"criticality": "critical"
},
{
"step_id": "step_07_debt_due_date_boundary",
"title": "Debt due-date ask stays honest about overdue evidence",
"question": "По ООО Альтернатива Плюс на конец 2020 можно точно понять, какая дебиторка просрочена?",
"allowed_reply_types": [
"factual",
"factual_with_explanation",
"partial_coverage"
],
"semantic_tags": [
"business_overview",
"debt_due_date_boundary",
"checked_negative",
"honest_boundary"
],
"required_answer_shape": "direct_answer_first",
"required_answer_patterns_all": [
"(?i)дебитор|долг|открыт.*расчет|остат",
"(?i)2020|2020-12-31|конец 2020",
"(?i)срок.*оплат|due[- ]?date|просроч",
"(?i)не установлен|не подтвержд|не доказ|нет подтвержденной просроч",
"(?i)1С|проверенн|найденн|подтвержд"
],
"forbidden_answer_patterns": [
"(?i)точно подтверждаю.*просроч",
"(?i)просроч.*подтвержден.*без.*срок",
"(?i)route_candidate|query_movements|primitive|planner_|catalog_|snapshot_items|answer_object|capability_id|mcp_discovery"
],
"criticality": "critical"
},
{
"step_id": "step_08_debt_boundary_short_followup",
"title": "Short follow-up keeps the debt due-date boundary",
"question": "То есть просрочку доказать нельзя, коротко почему?",
"depends_on": [
"step_07_debt_due_date_boundary"
],
"allowed_reply_types": [
"factual",
"factual_with_explanation",
"partial_coverage"
],
"semantic_tags": [
"context_carryover",
"debt_due_date_boundary",
"checked_negative",
"short_followup"
],
"required_answer_shape": "direct_answer_first",
"required_answer_patterns_all": [
"(?i)срок.*оплат|due[- ]?date|договор",
"(?i)не установлен|не хватает|не подтвержд|не доказ",
"(?i)просроч",
"(?i)2020|2020-12-31|конец 2020"
],
"forbidden_answer_patterns": [
"(?i)уточните организац|какую компанию",
"(?i)точно подтверждаю.*просроч",
"(?i)route_candidate|primitive|planner_|catalog_|snapshot_items|answer_object|capability_id|mcp_discovery"
],
"criticality": "critical"
},
{
"step_id": "step_09_off_domain_living_chat_not_hijacked",
"title": "Off-domain living chat is not hijacked by stale business context",
"question": "а чем капибара отличается от утки?",
"depends_on": [
"step_08_broad_business_evaluation_explicit_org"
],
"title": "Off-domain chat recovers after settlement and debt context",
"question": "Ок, а пингвины умеют летать?",
"allowed_reply_types": [
"factual",
"factual_with_explanation",
"partial_coverage"
],
"semantic_tags": [
"off_domain_living_chat",
"stale_replay_forbidden",
"context_boundary"
"context_boundary",
"human_answer"
],
"required_answer_shape": "direct_answer_first",
"required_answer_patterns_any": [
"(?i)капибар.*утк|утк.*капибар",
"(?i)млекопита|птиц|грызун"
"(?i)пингвин",
"(?i)не умеют летать|не летают|большинство.*не летает"
],
"forbidden_answer_patterns": [
"(?i)альтернатива|свк|контрагент|1с|поступлен|платеж|платёж|нетто",
"(?i)1С|контрагент|дебитор|договор|плат[её]ж|документ|Жуковк|Альтернатива",
"(?i)route_candidate|primitive|planner_|catalog_|snapshot_items|answer_object|capability_id|mcp_discovery"
],
"criticality": "warning"
"criticality": "info"
}
]
}

View File

@ -4317,11 +4317,10 @@ class AddressQueryService {
rowsMatched: expandedFilteredRows.length
});
const expandedFactual = (0, composeStage_1.composeFactualReply)(intent.intent, expandedFilteredRows, composeRuntimeOptions(expandedLimitFilters));
const expandedPrefix = `Период сохранен. Глубина live-выборки автоматически расширена до ${expandedPlan.limit} строк.`;
const expandedLimitations = [...filters.warnings, "query_limit_auto_expanded_for_anchor_recovery"];
const expandedReasons = [...baseReasons, "query_limit_auto_expanded_for_anchor_recovery"];
return buildFactualExecutionResult({
replyText: `${expandedPrefix}\n${expandedFactual.text}`,
replyText: expandedFactual.text,
responseType: expandedFactual.responseType,
responseSemantics: expandedFactual.semantics,
selectedRecipe: expandedSelection.selected_recipe.recipe_id,

View File

@ -582,7 +582,8 @@ function runAssistantAddressLaneResponseRuntime(input) {
mcp_discovery_response_candidate_v1: mcpDiscoveryResponsePolicy.candidate,
mcp_discovery_response_applied: mcpDiscoveryResponsePolicy.applied,
comparison_scope_response_augmentation_v1: comparisonScopeProofReply?.audit ?? null,
...buildAppliedMcpDiscoveryRoutePatch(debugWithResponseGuard, mcpDiscoveryResponsePolicy.applied)
...buildAppliedMcpDiscoveryRoutePatch(debugWithResponseGuard, mcpDiscoveryResponsePolicy.applied),
...(0, assistantMcpDiscoveryResponsePolicy_1.buildAppliedMcpDiscoveryTruthPatch)(mcpDiscoveryResponsePolicy, debugWithResponseGuard)
};
const finalization = finalizeAddressTurnSafe({
sessionId: input.sessionId,

View File

@ -142,7 +142,8 @@ function runAssistantDeepTurnResponseRuntime(input) {
...packagingRuntime.debug,
mcp_discovery_response_policy_v1: mcpDiscoveryResponsePolicy,
mcp_discovery_response_candidate_v1: mcpDiscoveryResponsePolicy.candidate,
mcp_discovery_response_applied: mcpDiscoveryResponsePolicy.applied
mcp_discovery_response_applied: mcpDiscoveryResponsePolicy.applied,
...(0, assistantMcpDiscoveryResponsePolicy_1.buildAppliedMcpDiscoveryTruthPatch)(mcpDiscoveryResponsePolicy, packagingRuntime.debug)
};
const assistantItem = mcpDiscoveryResponsePolicy.applied
? {

View File

@ -362,6 +362,7 @@ async function runAssistantLivingChatRuntime(input) {
mcp_discovery_response_policy_v1: mcpDiscoveryResponsePolicy,
mcp_discovery_response_candidate_v1: mcpDiscoveryResponsePolicy.candidate,
mcp_discovery_response_applied: mcpDiscoveryResponsePolicy.applied,
...(0, assistantMcpDiscoveryResponsePolicy_1.buildAppliedMcpDiscoveryTruthPatch)(mcpDiscoveryResponsePolicy, addressRuntimeMeta),
living_chat_script_guard_applied: livingChatScriptGuardApplied,
living_chat_script_guard_reason: livingChatScriptGuardReason,
living_chat_grounding_guard_applied: livingChatGroundingGuardApplied,

View File

@ -73,6 +73,18 @@ function requestsBusinessOverviewNextChecks(turnMeaning, graph) {
const text = normalizedTurnAndGraphText(turnMeaning, graph);
return /(?:что\s+(?:дальше\s+)?провер\w*|что\s+провер\w*\s+дальше|следующ\w*\s+шаг|в\s+первую\s+очередь\s+провер\w*|next\s+checks?|next\s+steps?)/iu.test(text);
}
function requestsShortDebtBoundaryFollowup(turnMeaning, graph) {
const text = normalizeQuestionText([
turnMeaning?.raw_message,
turnMeaning?.effective_message,
graph?.source_message,
graph?.question
].join(" "));
if (!/\u043f\u0440\u043e\u0441\u0440\u043e\u0447/iu.test(text)) {
return false;
}
return /(?:\u043a\u043e\u0440\u043e\u0442\u043a|\u043f\u043e\u0447\u0435\u043c\u0443|\u0434\u043e\u043a\u0430\u0437\p{L}*)/iu.test(text);
}
function requestsCompactCashflowAnswer(turnMeaning, graph) {
const text = normalizeQuestionText([
turnMeaning?.raw_message,
@ -902,6 +914,7 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
const debtDueDateBoundary = actionFamily === "debt_due_date_boundary" || unsupportedFamily === "debt_due_date_boundary";
const vendorRiskBoundary = actionFamily === "vendor_risk_procurement_boundary" || unsupportedFamily === "vendor_risk_procurement_boundary";
const inventoryReserveBoundary = actionFamily === "inventory_reserve_boundary" || unsupportedFamily === "inventory_reserve_liquidation_boundary";
const shortDebtBoundaryFollowup = debtDueDateBoundary && requestsShortDebtBoundaryFollowup(turnMeaning, graph);
const compactCashflowRequested = directMoneyOnlyAnswer && requestsCompactCashflowAnswer(turnMeaning, graph);
const cashflowPolarityRequested = compactCashflowRequested && requestsCashflowPolarityAnswer(turnMeaning, graph);
const directAccountingProfitRequested = requestsDirectAccountingProfitAnswer(turnMeaning, graph);
@ -1035,19 +1048,20 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
const rowsWithPaymentTerms = typeof dueDateAging.rows_with_payment_terms === "number" && Number.isFinite(dueDateAging.rows_with_payment_terms)
? dueDateAging.rows_with_payment_terms
: null;
const rowsWithAmount = typeof dueDateAging.rows_with_amount === "number" && Number.isFinite(dueDateAging.rows_with_amount)
? dueDateAging.rows_with_amount
: null;
const dueDateScopePrefix = organizationScope ? `по компании ${organizationScope} ` : "";
if (status === "confirmed_overdue") {
lines.push(`Коротко: ${dueDateScopePrefix}на ${asOfDate} подтвержденная просрочка есть: ${overdueAmount ?? "сумма не распознана"} по ${dueDateAging.overdue_rows ?? "найденным"} строкам.`);
lines.push("Основа ответа: открытые расчеты 60/62/76, договорный срок оплаты и дата расчетного документа; это проверка просрочки по срокам оплаты, а не просто возраст договора.");
}
else if (status === "no_payment_terms_configured") {
if (shortDebtBoundaryFollowup) {
lines.push(`Коротко: да, доказать просрочку ${dueDateScopePrefix}на ${asOfDate} нельзя, потому что в подтвержденных данных не установлен срок оплаты.`);
lines.push(`Открытый долг${grossAmount ? ` на ${grossAmount}` : ""} виден, но без due date это задолженность без доказанной договорной просрочки.`);
return joinBusinessReplyLines(lines);
}
lines.push(`Коротко: ${dueDateScopePrefix}на ${asOfDate} подтвержденной просрочки нет: открытые расчеты проверены${grossAmount ? ` на ${grossAmount}` : ""}, но в найденных договорах срок оплаты не установлен.`);
lines.push(rowsWithAmount !== null
? `Проверено строк с суммой: ${rowsWithAmount}. Без установленного срока оплаты нельзя честно назвать эти остатки просрочкой.`
: "Без установленного срока оплаты нельзя честно назвать эти остатки просрочкой.");
lines.push("Без установленного срока оплаты нельзя честно назвать эти остатки просрочкой.");
lines.push("Что проверить дальше: сроки оплаты и даты документов-оснований в договорах/расчетах; без них просрочку доказать нельзя.");
}
else if (status === "insufficient_due_date_basis") {
lines.push(`Коротко: ${dueDateScopePrefix}на ${asOfDate} просрочка не подтверждена: по строкам с установленным сроком оплаты не хватило даты расчетного документа.`);

View File

@ -1,9 +1,111 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ASSISTANT_MCP_DISCOVERY_RESPONSE_POLICY_SCHEMA_VERSION = void 0;
exports.buildAppliedMcpDiscoveryTruthPatch = buildAppliedMcpDiscoveryTruthPatch;
exports.applyAssistantMcpDiscoveryResponsePolicy = applyAssistantMcpDiscoveryResponsePolicy;
const assistantMcpDiscoveryResponseCandidate_1 = require("./assistantMcpDiscoveryResponseCandidate");
const assistantRuntimeContracts_1 = require("../types/assistantRuntimeContracts");
exports.ASSISTANT_MCP_DISCOVERY_RESPONSE_POLICY_SCHEMA_VERSION = "assistant_mcp_discovery_response_policy_v1";
function uniqueReasonCodes(values) {
const result = [];
values.forEach((value) => {
const text = toNonEmptyString(value);
if (!text) {
return;
}
const normalized = normalizeReasonCode(text);
if (normalized && !result.includes(normalized)) {
result.push(normalized);
}
});
return result;
}
function patchCoverageGateState(value, gate) {
const source = toRecordObject(value);
if (!source) {
return value;
}
const currentGate = toRecordObject(source.coverage_gate_state);
return {
...source,
coverage_gate_state: {
...(currentGate ?? {}),
coverage_status: gate.coverage_status,
evidence_grade: gate.evidence_grade,
grounding_status: gate.grounding_status,
truth_mode: gate.truth_mode,
carryover_eligibility: gate.carryover_eligibility,
reason_codes: gate.reason_codes
}
};
}
function buildAppliedMcpDiscoveryTruthPatch(policy, currentDebug) {
if (!policy.applied || !ALLOWED_CANDIDATE_STATUSES.has(policy.candidate.candidate_status)) {
return {};
}
const reasonCodes = uniqueReasonCodes([
...policy.reason_codes,
"mcp_discovery_response_policy_candidate_applied",
"mcp_discovery_response_candidate_guarded_truth_patch"
]);
const truthGate = {
schema_version: assistantRuntimeContracts_1.ASSISTANT_TRUTH_ANSWER_POLICY_RUNTIME_SCHEMA_VERSION,
policy_owner: "assistantTruthAnswerPolicyRuntimeAdapter",
coverage_status: "partial",
evidence_grade: "medium",
grounding_status: "partial",
truth_mode: "limited",
carryover_eligibility: "root_only",
reason_codes: reasonCodes,
source_truth_gate_status: "partial_supported",
blocked_or_limited_explanation: "mcp_discovery_guarded_bounded_answer"
};
const answerShape = {
schema_version: assistantRuntimeContracts_1.ASSISTANT_TRUTH_ANSWER_POLICY_RUNTIME_SCHEMA_VERSION,
policy_owner: "assistantTruthAnswerPolicyRuntimeAdapter",
answer_shape: "limited_with_reason",
reply_type: "partial_coverage",
capability_contract_id: null,
transition_contract_id: null,
may_state_confirmed_facts: true,
must_include_limitation: true,
may_power_followup: true,
required_sections: ["direct_answer", "evidence_window", "limitations"],
downgrade_only: true
};
const truthPolicy = {
schema_version: assistantRuntimeContracts_1.ASSISTANT_TRUTH_ANSWER_POLICY_RUNTIME_SCHEMA_VERSION,
policy_owner: "assistantTruthAnswerPolicyRuntimeAdapter",
truth_gate: truthGate,
answer_shape: answerShape
};
const debug = currentDebug ?? {};
const patch = {
assistant_truth_answer_policy_v1: truthPolicy,
coverage_gate_contract: truthGate,
answer_shape_contract: answerShape,
truth_gate_contract_status: "partial_supported",
truth_mode: "limited",
carryover_eligibility: "root_only",
answer_shape: "limited_with_reason"
};
const stateTransition = patchCoverageGateState(debug.assistant_state_transition_v1, truthGate);
if (stateTransition !== debug.assistant_state_transition_v1) {
patch.assistant_state_transition_v1 = stateTransition;
}
const stateTransitionContract = patchCoverageGateState(debug.state_transition_contract, truthGate);
if (stateTransitionContract !== debug.state_transition_contract) {
patch.state_transition_contract = stateTransitionContract;
}
const runtimeContract = toRecordObject(debug.assistant_runtime_contract_v1);
if (runtimeContract) {
patch.assistant_runtime_contract_v1 = {
...runtimeContract,
truth_gate_contract_status: "partial_supported"
};
}
return patch;
}
const ALLOWED_CANDIDATE_STATUSES = new Set([
"ready_for_guarded_use",
"checked_sources_only_candidate",

View File

@ -5278,11 +5278,10 @@ export class AddressQueryService {
expandedFilteredRows,
composeRuntimeOptions(expandedLimitFilters)
);
const expandedPrefix = `Период сохранен. Глубина live-выборки автоматически расширена до ${expandedPlan.limit} строк.`;
const expandedLimitations = [...filters.warnings, "query_limit_auto_expanded_for_anchor_recovery"];
const expandedReasons = [...baseReasons, "query_limit_auto_expanded_for_anchor_recovery"];
return buildFactualExecutionResult({
replyText: `${expandedPrefix}\n${expandedFactual.text}`,
replyText: expandedFactual.text,
responseType: expandedFactual.responseType,
responseSemantics: expandedFactual.semantics,
selectedRecipe: expandedSelection.selected_recipe.recipe_id,

View File

@ -10,7 +10,10 @@ import {
import { applyAssistantCapabilityBindingResponseGuard } from "./assistantCapabilityBindingResponseGuard";
import { attachAssistantCapabilityRuntimeBinding } from "./assistantCapabilityRuntimeBindingAdapter";
import { attachAssistantMcpDiscoveryDebug } from "./assistantMcpDiscoveryDebugAttachment";
import { applyAssistantMcpDiscoveryResponsePolicy } from "./assistantMcpDiscoveryResponsePolicy";
import {
applyAssistantMcpDiscoveryResponsePolicy,
buildAppliedMcpDiscoveryTruthPatch
} from "./assistantMcpDiscoveryResponsePolicy";
import { attachAssistantRuntimeContractShadow } from "./assistantRuntimeContractResolver";
import { attachAssistantStateTransition } from "./assistantStateTransitionRuntimeAdapter";
import { attachAssistantTruthAnswerPolicy } from "./assistantTruthAnswerPolicyRuntimeAdapter";
@ -712,7 +715,8 @@ export function runAssistantAddressLaneResponseRuntime<ResponseType = AssistantM
mcp_discovery_response_candidate_v1: mcpDiscoveryResponsePolicy.candidate,
mcp_discovery_response_applied: mcpDiscoveryResponsePolicy.applied,
comparison_scope_response_augmentation_v1: comparisonScopeProofReply?.audit ?? null,
...buildAppliedMcpDiscoveryRoutePatch(debugWithResponseGuard, mcpDiscoveryResponsePolicy.applied)
...buildAppliedMcpDiscoveryRoutePatch(debugWithResponseGuard, mcpDiscoveryResponsePolicy.applied),
...buildAppliedMcpDiscoveryTruthPatch(mcpDiscoveryResponsePolicy, debugWithResponseGuard)
};
const finalization = finalizeAddressTurnSafe({
sessionId: input.sessionId,

View File

@ -20,7 +20,10 @@ import {
finalizeAssistantDeepTurn,
type FinalizeAssistantDeepTurnInput
} from "./assistantDeepTurnFinalizeRuntimeAdapter";
import { applyAssistantMcpDiscoveryResponsePolicy } from "./assistantMcpDiscoveryResponsePolicy";
import {
applyAssistantMcpDiscoveryResponsePolicy,
buildAppliedMcpDiscoveryTruthPatch
} from "./assistantMcpDiscoveryResponsePolicy";
import type { ClaimBoundAnchorAudit, TargetedEvidenceAcquisitionAudit } from "./assistantClaimBoundEvidence";
import type { CompanyAnchorSet } from "./companyAnchorResolver";
import type {
@ -268,7 +271,11 @@ export function runAssistantDeepTurnResponseRuntime(
...packagingRuntime.debug,
mcp_discovery_response_policy_v1: mcpDiscoveryResponsePolicy,
mcp_discovery_response_candidate_v1: mcpDiscoveryResponsePolicy.candidate,
mcp_discovery_response_applied: mcpDiscoveryResponsePolicy.applied
mcp_discovery_response_applied: mcpDiscoveryResponsePolicy.applied,
...buildAppliedMcpDiscoveryTruthPatch(
mcpDiscoveryResponsePolicy,
packagingRuntime.debug as unknown as Record<string, unknown>
)
} as AssistantDebugPayload;
const assistantItem = mcpDiscoveryResponsePolicy.applied
? {

View File

@ -8,7 +8,10 @@ import {
} from "./assistantMemoryRecapPolicy";
import { resolveAssistantOrganizationAuthority } from "./assistantContinuityPolicy";
import { attachAssistantMcpDiscoveryDebug } from "./assistantMcpDiscoveryDebugAttachment";
import { applyAssistantMcpDiscoveryResponsePolicy } from "./assistantMcpDiscoveryResponsePolicy";
import {
applyAssistantMcpDiscoveryResponsePolicy,
buildAppliedMcpDiscoveryTruthPatch
} from "./assistantMcpDiscoveryResponsePolicy";
export interface AssistantLivingChatSessionScopeInput {
knownOrganizations?: unknown[];
@ -473,6 +476,7 @@ export async function runAssistantLivingChatRuntime(
mcp_discovery_response_policy_v1: mcpDiscoveryResponsePolicy,
mcp_discovery_response_candidate_v1: mcpDiscoveryResponsePolicy.candidate,
mcp_discovery_response_applied: mcpDiscoveryResponsePolicy.applied,
...buildAppliedMcpDiscoveryTruthPatch(mcpDiscoveryResponsePolicy, addressRuntimeMeta),
living_chat_script_guard_applied: livingChatScriptGuardApplied,
living_chat_script_guard_reason: livingChatScriptGuardReason,
living_chat_grounding_guard_applied: livingChatGroundingGuardApplied,

View File

@ -117,6 +117,22 @@ function requestsBusinessOverviewNextChecks(
);
}
function requestsShortDebtBoundaryFollowup(
turnMeaning: Record<string, unknown> | null,
graph: Record<string, unknown> | null
): boolean {
const text = normalizeQuestionText([
turnMeaning?.raw_message,
turnMeaning?.effective_message,
graph?.source_message,
graph?.question
].join(" "));
if (!/\u043f\u0440\u043e\u0441\u0440\u043e\u0447/iu.test(text)) {
return false;
}
return /(?:\u043a\u043e\u0440\u043e\u0442\u043a|\u043f\u043e\u0447\u0435\u043c\u0443|\u0434\u043e\u043a\u0430\u0437\p{L}*)/iu.test(text);
}
function requestsCompactCashflowAnswer(
turnMeaning: Record<string, unknown> | null,
graph: Record<string, unknown> | null
@ -1086,6 +1102,7 @@ function buildCompactBusinessOverviewReply(
actionFamily === "vendor_risk_procurement_boundary" || unsupportedFamily === "vendor_risk_procurement_boundary";
const inventoryReserveBoundary =
actionFamily === "inventory_reserve_boundary" || unsupportedFamily === "inventory_reserve_liquidation_boundary";
const shortDebtBoundaryFollowup = debtDueDateBoundary && requestsShortDebtBoundaryFollowup(turnMeaning, graph);
const compactCashflowRequested = directMoneyOnlyAnswer && requestsCompactCashflowAnswer(turnMeaning, graph);
const cashflowPolarityRequested = compactCashflowRequested && requestsCashflowPolarityAnswer(turnMeaning, graph);
const directAccountingProfitRequested = requestsDirectAccountingProfitAnswer(turnMeaning, graph);
@ -1259,10 +1276,6 @@ function buildCompactBusinessOverviewReply(
typeof dueDateAging.rows_with_payment_terms === "number" && Number.isFinite(dueDateAging.rows_with_payment_terms)
? dueDateAging.rows_with_payment_terms
: null;
const rowsWithAmount =
typeof dueDateAging.rows_with_amount === "number" && Number.isFinite(dueDateAging.rows_with_amount)
? dueDateAging.rows_with_amount
: null;
const dueDateScopePrefix = organizationScope ? `по компании ${organizationScope} ` : "";
if (status === "confirmed_overdue") {
lines.push(
@ -1270,13 +1283,23 @@ function buildCompactBusinessOverviewReply(
);
lines.push("Основа ответа: открытые расчеты 60/62/76, договорный срок оплаты и дата расчетного документа; это проверка просрочки по срокам оплаты, а не просто возраст договора.");
} else if (status === "no_payment_terms_configured") {
if (shortDebtBoundaryFollowup) {
lines.push(
`Коротко: да, доказать просрочку ${dueDateScopePrefix}на ${asOfDate} нельзя, потому что в подтвержденных данных не установлен срок оплаты.`
);
lines.push(
`Открытый долг${grossAmount ? ` на ${grossAmount}` : ""} виден, но без due date это задолженность без доказанной договорной просрочки.`
);
return joinBusinessReplyLines(lines);
}
lines.push(
`Коротко: ${dueDateScopePrefix}на ${asOfDate} подтвержденной просрочки нет: открытые расчеты проверены${grossAmount ? ` на ${grossAmount}` : ""}, но в найденных договорах срок оплаты не установлен.`
);
lines.push(
rowsWithAmount !== null
? `Проверено строк с суммой: ${rowsWithAmount}. Без установленного срока оплаты нельзя честно назвать эти остатки просрочкой.`
: "Без установленного срока оплаты нельзя честно назвать эти остатки просрочкой."
"Без установленного срока оплаты нельзя честно назвать эти остатки просрочкой."
);
lines.push(
"Что проверить дальше: сроки оплаты и даты документов-оснований в договорах/расчетах; без них просрочку доказать нельзя."
);
} else if (status === "insufficient_due_date_basis") {
lines.push(

View File

@ -4,6 +4,11 @@ import {
type AssistantMcpDiscoveryResponseCandidateStatus
} from "./assistantMcpDiscoveryResponseCandidate";
import type { AssistantMcpDiscoveryRuntimeEntryPointContract } from "./assistantMcpDiscoveryRuntimeEntryPoint";
import {
ASSISTANT_TRUTH_ANSWER_POLICY_RUNTIME_SCHEMA_VERSION,
type AssistantCoverageGateState,
type AssistantTruthAnswerPolicyRuntimeContract
} from "../types/assistantRuntimeContracts";
export const ASSISTANT_MCP_DISCOVERY_RESPONSE_POLICY_SCHEMA_VERSION =
"assistant_mcp_discovery_response_policy_v1" as const;
@ -31,6 +36,112 @@ export interface AssistantMcpDiscoveryResponsePolicyResult {
reason_codes: string[];
}
function uniqueReasonCodes(values: unknown[]): string[] {
const result: string[] = [];
values.forEach((value) => {
const text = toNonEmptyString(value);
if (!text) {
return;
}
const normalized = normalizeReasonCode(text);
if (normalized && !result.includes(normalized)) {
result.push(normalized);
}
});
return result;
}
function patchCoverageGateState(value: unknown, gate: AssistantCoverageGateState): unknown {
const source = toRecordObject(value);
if (!source) {
return value;
}
const currentGate = toRecordObject(source.coverage_gate_state);
return {
...source,
coverage_gate_state: {
...(currentGate ?? {}),
coverage_status: gate.coverage_status,
evidence_grade: gate.evidence_grade,
grounding_status: gate.grounding_status,
truth_mode: gate.truth_mode,
carryover_eligibility: gate.carryover_eligibility,
reason_codes: gate.reason_codes
}
};
}
export function buildAppliedMcpDiscoveryTruthPatch(
policy: AssistantMcpDiscoveryResponsePolicyResult,
currentDebug?: Record<string, unknown> | null
): Record<string, unknown> {
if (!policy.applied || !ALLOWED_CANDIDATE_STATUSES.has(policy.candidate.candidate_status)) {
return {};
}
const reasonCodes = uniqueReasonCodes([
...policy.reason_codes,
"mcp_discovery_response_policy_candidate_applied",
"mcp_discovery_response_candidate_guarded_truth_patch"
]);
const truthGate: AssistantTruthAnswerPolicyRuntimeContract["truth_gate"] = {
schema_version: ASSISTANT_TRUTH_ANSWER_POLICY_RUNTIME_SCHEMA_VERSION,
policy_owner: "assistantTruthAnswerPolicyRuntimeAdapter",
coverage_status: "partial",
evidence_grade: "medium",
grounding_status: "partial",
truth_mode: "limited",
carryover_eligibility: "root_only",
reason_codes: reasonCodes,
source_truth_gate_status: "partial_supported",
blocked_or_limited_explanation: "mcp_discovery_guarded_bounded_answer"
};
const answerShape: AssistantTruthAnswerPolicyRuntimeContract["answer_shape"] = {
schema_version: ASSISTANT_TRUTH_ANSWER_POLICY_RUNTIME_SCHEMA_VERSION,
policy_owner: "assistantTruthAnswerPolicyRuntimeAdapter",
answer_shape: "limited_with_reason",
reply_type: "partial_coverage",
capability_contract_id: null,
transition_contract_id: null,
may_state_confirmed_facts: true,
must_include_limitation: true,
may_power_followup: true,
required_sections: ["direct_answer", "evidence_window", "limitations"],
downgrade_only: true
};
const truthPolicy: AssistantTruthAnswerPolicyRuntimeContract = {
schema_version: ASSISTANT_TRUTH_ANSWER_POLICY_RUNTIME_SCHEMA_VERSION,
policy_owner: "assistantTruthAnswerPolicyRuntimeAdapter",
truth_gate: truthGate,
answer_shape: answerShape
};
const debug = currentDebug ?? {};
const patch: Record<string, unknown> = {
assistant_truth_answer_policy_v1: truthPolicy,
coverage_gate_contract: truthGate,
answer_shape_contract: answerShape,
truth_gate_contract_status: "partial_supported",
truth_mode: "limited",
carryover_eligibility: "root_only",
answer_shape: "limited_with_reason"
};
const stateTransition = patchCoverageGateState(debug.assistant_state_transition_v1, truthGate);
if (stateTransition !== debug.assistant_state_transition_v1) {
patch.assistant_state_transition_v1 = stateTransition;
}
const stateTransitionContract = patchCoverageGateState(debug.state_transition_contract, truthGate);
if (stateTransitionContract !== debug.state_transition_contract) {
patch.state_transition_contract = stateTransitionContract;
}
const runtimeContract = toRecordObject(debug.assistant_runtime_contract_v1);
if (runtimeContract) {
patch.assistant_runtime_contract_v1 = {
...runtimeContract,
truth_gate_contract_status: "partial_supported"
};
}
return patch;
}
const ALLOWED_CANDIDATE_STATUSES = new Set<AssistantMcpDiscoveryResponseCandidateStatus>([
"ready_for_guarded_use",
"checked_sources_only_candidate",

View File

@ -235,6 +235,24 @@ describe("assistant deep turn response runtime adapter", () => {
expect(runtime.response.assistant_reply).toContain("Discovery answer");
expect(runtime.debug?.mcp_discovery_response_applied).toBe(true);
expect(runtime.debug?.truth_gate_contract_status).toBe("partial_supported");
expect(runtime.debug?.truth_mode).toBe("limited");
expect(runtime.debug?.coverage_gate_contract).toEqual(
expect.objectContaining({
coverage_status: "partial",
evidence_grade: "medium",
grounding_status: "partial",
truth_mode: "limited",
source_truth_gate_status: "partial_supported"
})
);
expect(runtime.debug?.answer_shape_contract).toEqual(
expect.objectContaining({
answer_shape: "limited_with_reason",
reply_type: "partial_coverage",
may_state_confirmed_facts: true
})
);
expect(runFinalizeDeepTurn).toHaveBeenCalledWith(
expect.objectContaining({
assistantReply: expect.stringContaining("Discovery answer"),

View File

@ -523,6 +523,116 @@ describe("assistant MCP discovery response candidate", () => {
expect(candidate.reply_text).not.toContain("руб..");
});
it("keeps debt due-date boundary answers useful and hides row counters", () => {
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
entryPoint({
turn_input: {
adapter_status: "ready",
turn_meaning_ref: {
raw_message: "По ООО Альтернатива Плюс на конец 2020 можно точно понять, какая дебиторка просрочена?",
effective_message: "Проверить просроченную дебиторку ООО Альтернатива Плюс на конец 2020.",
asked_domain_family: "business_overview",
asked_action_family: "debt_due_date_boundary",
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020-12-31"
},
data_need_graph: {
business_fact_family: "business_overview",
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: {
debt_due_date_aging: {
evidence_status: "no_payment_terms_configured",
as_of_date: "2020-12-31",
gross_open_amount_human_ru: "35 472 380,36 руб.",
rows_with_amount: 46
}
}
},
answer_draft: {
answer_mode: "confirmed_with_bounded_inference",
headline: "Debt boundary.",
confirmed_lines: [],
inference_lines: [],
unknown_lines: [],
limitation_lines: [],
next_step_line: null
}
}
})
);
expect(candidate.reply_text).toContain("35 472 380,36 руб.");
expect(candidate.reply_text).toContain("срок оплаты не установлен");
expect(candidate.reply_text).toContain("нельзя честно назвать эти остатки просрочкой");
expect(candidate.reply_text).toContain("Что проверить дальше");
expect(candidate.reply_text).not.toContain("Проверено строк");
expect(candidate.reply_text).not.toContain("строк с суммой");
});
it("answers short debt boundary follow-ups directly instead of replaying the full debt packet", () => {
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
entryPoint({
turn_input: {
adapter_status: "ready",
turn_meaning_ref: {
raw_message: "То есть просрочку доказать нельзя, коротко почему?",
effective_message: "Коротко объяснить, почему просрочку нельзя доказать.",
asked_domain_family: "business_overview",
asked_action_family: "debt_due_date_boundary",
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020-12-31"
},
data_need_graph: {
business_fact_family: "business_overview",
question: "То есть просрочку доказать нельзя, коротко почему?",
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: {
debt_due_date_aging: {
evidence_status: "no_payment_terms_configured",
as_of_date: "2020-12-31",
gross_open_amount_human_ru: "35 472 380,36 руб.",
rows_with_amount: 46
}
}
},
answer_draft: {
answer_mode: "confirmed_with_bounded_inference",
headline: "Debt boundary.",
confirmed_lines: [],
inference_lines: [],
unknown_lines: [],
limitation_lines: [],
next_step_line: null
}
}
})
);
expect(candidate.reply_text?.split("\n")[0]).toContain("доказать просрочку");
expect(candidate.reply_text?.split("\n")[0]).toContain("нельзя");
expect(candidate.reply_text).toContain("due date");
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({

View File

@ -1,4 +1,69 @@
[
{
"generation_id": "gen-ag06152044-9f689d",
"created_at": "2026-06-15T20:44:38+00:00",
"mode": "saved_user_sessions",
"title": "AGENT | Phase108 settlement pivots and debt due-date boundary",
"count": 8,
"domain": "address_phase108_settlement_pivot_debt_boundary",
"questions": [
"Покажи документы по Жуковке 51.",
"А по нему платежи?",
"А по нему договоры?",
"А по нему документы?",
"А за 2021?",
"По ООО Альтернатива Плюс на конец 2020 можно точно понять, какая дебиторка просрочена?",
"То есть просрочку доказать нельзя, коротко почему?",
"Ок, а пингвины умеют летать?"
],
"generated_by": "codex_agent",
"saved_case_set_file": "assistant_autogen_saved_user_sessions_20260615204438_gen-ag06152044-9f689d.json",
"context": {
"llm_provider": null,
"model": null,
"assistant_prompt_version": "address_query_runtime_v1",
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"autogen_personality_id": null,
"autogen_personality_prompt": null,
"source_session_id": null,
"saved_session_file": "assistant_saved_session_20260615204438_gen-ag06152044-9f689d.json",
"saved_case_set_kind": "agent_semantic_scenario",
"agent_run": true,
"agent_focus": "settlement pivots + debt due-date boundary + agent validation",
"architecture_phase": "assistant-stage1 phase108",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\active_domain_contract.json",
"scenario_id": null,
"semantic_tags": [
"business_overview",
"checked_negative",
"context_boundary",
"context_carryover",
"contracts_followup",
"counterparty_pronoun_resolution",
"counterparty_root",
"debt_due_date_boundary",
"document_to_payments_pivot",
"documents_by_counterparty",
"documents_followup",
"fourth_pivot",
"honest_boundary",
"human_answer",
"integrity_guard",
"off_domain_living_chat",
"payments_followup",
"period_followup_reuse",
"pivot_seed",
"second_pivot",
"short_followup",
"third_pivot",
"year_switch_after_fourth_pivot"
],
"validation_status": "accepted_domain_pack_loop",
"validated_run_dir": "artifacts\\domain_runs\\phase108_settlement_pivot_debt_boundary_20260615_p05_loop",
"saved_after_validated_replay": true
}
},
{
"generation_id": "gen-ag06151952-5d8f12",
"created_at": "2026-06-15T19:52:40+00:00",

View File

@ -0,0 +1,237 @@
{
"saved_at": "2026-06-15T20:44:38+00:00",
"generation_id": "gen-ag06152044-9f689d",
"mode": "saved_user_sessions",
"title": "AGENT | Phase108 settlement pivots and debt due-date boundary",
"agent_run": true,
"questions": [
"Покажи документы по Жуковке 51.",
"А по нему платежи?",
"А по нему договоры?",
"А по нему документы?",
"А за 2021?",
"По ООО Альтернатива Плюс на конец 2020 можно точно понять, какая дебиторка просрочена?",
"То есть просрочку доказать нельзя, коротко почему?",
"Ок, а пингвины умеют летать?"
],
"metadata": {
"assistant_prompt_version": "address_query_runtime_v1",
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "settlement pivots + debt due-date boundary + agent validation",
"architecture_phase": "assistant-stage1 phase108",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\active_domain_contract.json",
"scenario_id": null,
"semantic_tags": [
"business_overview",
"checked_negative",
"context_boundary",
"context_carryover",
"contracts_followup",
"counterparty_pronoun_resolution",
"counterparty_root",
"debt_due_date_boundary",
"document_to_payments_pivot",
"documents_by_counterparty",
"documents_followup",
"fourth_pivot",
"honest_boundary",
"human_answer",
"integrity_guard",
"off_domain_living_chat",
"payments_followup",
"period_followup_reuse",
"pivot_seed",
"second_pivot",
"short_followup",
"third_pivot",
"year_switch_after_fourth_pivot"
],
"validation_status": "accepted_domain_pack_loop",
"validated_run_dir": "artifacts\\domain_runs\\phase108_settlement_pivot_debt_boundary_20260615_p05_loop",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_domain_pack_loop",
"validated_run_dir": "artifacts\\domain_runs\\phase108_settlement_pivot_debt_boundary_20260615_p05_loop",
"final_status": "accepted",
"loop_id": "phase108_settlement_pivot_debt_boundary_20260615_p05_loop",
"target_score": 88,
"iterations_ran": 1,
"quality_score": 92,
"repair_target_count": 0,
"repair_target_severity_counts": {
"P0": 0,
"P1": 0,
"P2": 0
},
"accepted_gate": true,
"effective_runtime": {
"manifest_path": "artifacts\\domain_runs\\phase108_settlement_pivot_debt_boundary_20260615_p05_loop\\effective_runtime.json",
"runner": "domain_case_loop.run-pack-loop",
"git_sha": "e9f0ef6b4129e1e30d010c8ef9272afd742629fe",
"backend_url": "http://127.0.0.1:8787",
"mcp_proxy_url": "http://127.0.0.1:6003",
"llm_provider": "local",
"llm_model": "unsloth/qwen3-30b-a3b-instruct-2507",
"temperature": 0.8,
"max_output_tokens": 900,
"prompt_version": "normalizer_v2_0_2",
"prompt_source": "file",
"prompt_hash": "f36e36a5e491cd24511b380e0c0059cb01aea5e2af738fb9f01671cd291735c1",
"prompt_registry_status": "pass"
},
"saved_after_validated_replay": true
}
},
"source_session_id": null,
"session": {
"session_id": null,
"mode": "agent_semantic_run",
"items": [
{
"message_id": "agent-user-001",
"role": "user",
"text": "Покажи документы по Жуковке 51.",
"created_at": "2026-06-15T20:44:38+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-002",
"role": "user",
"text": "А по нему платежи?",
"created_at": "2026-06-15T20:44:38+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-003",
"role": "user",
"text": "А по нему договоры?",
"created_at": "2026-06-15T20:44:38+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-004",
"role": "user",
"text": "А по нему документы?",
"created_at": "2026-06-15T20:44:38+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-005",
"role": "user",
"text": "А за 2021?",
"created_at": "2026-06-15T20:44:38+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-006",
"role": "user",
"text": "По ООО Альтернатива Плюс на конец 2020 можно точно понять, какая дебиторка просрочена?",
"created_at": "2026-06-15T20:44:38+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-007",
"role": "user",
"text": "То есть просрочку доказать нельзя, коротко почему?",
"created_at": "2026-06-15T20:44:38+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-008",
"role": "user",
"text": "Ок, а пингвины умеют летать?",
"created_at": "2026-06-15T20:44:38+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
}
],
"agent_run": true,
"metadata": {
"assistant_prompt_version": "address_query_runtime_v1",
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "settlement pivots + debt due-date boundary + agent validation",
"architecture_phase": "assistant-stage1 phase108",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\active_domain_contract.json",
"scenario_id": null,
"semantic_tags": [
"business_overview",
"checked_negative",
"context_boundary",
"context_carryover",
"contracts_followup",
"counterparty_pronoun_resolution",
"counterparty_root",
"debt_due_date_boundary",
"document_to_payments_pivot",
"documents_by_counterparty",
"documents_followup",
"fourth_pivot",
"honest_boundary",
"human_answer",
"integrity_guard",
"off_domain_living_chat",
"payments_followup",
"period_followup_reuse",
"pivot_seed",
"second_pivot",
"short_followup",
"third_pivot",
"year_switch_after_fourth_pivot"
],
"validation_status": "accepted_domain_pack_loop",
"validated_run_dir": "artifacts\\domain_runs\\phase108_settlement_pivot_debt_boundary_20260615_p05_loop",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_domain_pack_loop",
"validated_run_dir": "artifacts\\domain_runs\\phase108_settlement_pivot_debt_boundary_20260615_p05_loop",
"final_status": "accepted",
"loop_id": "phase108_settlement_pivot_debt_boundary_20260615_p05_loop",
"target_score": 88,
"iterations_ran": 1,
"quality_score": 92,
"repair_target_count": 0,
"repair_target_severity_counts": {
"P0": 0,
"P1": 0,
"P2": 0
},
"accepted_gate": true,
"effective_runtime": {
"manifest_path": "artifacts\\domain_runs\\phase108_settlement_pivot_debt_boundary_20260615_p05_loop\\effective_runtime.json",
"runner": "domain_case_loop.run-pack-loop",
"git_sha": "e9f0ef6b4129e1e30d010c8ef9272afd742629fe",
"backend_url": "http://127.0.0.1:8787",
"mcp_proxy_url": "http://127.0.0.1:6003",
"llm_provider": "local",
"llm_model": "unsloth/qwen3-30b-a3b-instruct-2507",
"temperature": 0.8,
"max_output_tokens": 900,
"prompt_version": "normalizer_v2_0_2",
"prompt_source": "file",
"prompt_hash": "f36e36a5e491cd24511b380e0c0059cb01aea5e2af738fb9f01671cd291735c1",
"prompt_registry_status": "pass"
},
"saved_after_validated_replay": true
}
}
}
}

View File

@ -0,0 +1,49 @@
{
"suite_id": "assistant_saved_session_gen-ag06152044-9f689d",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_suite_v0_1",
"generated_at": "2026-06-15T20:44:38+00:00",
"generation_id": "gen-ag06152044-9f689d",
"mode": "saved_user_sessions",
"title": "AGENT | Phase108 settlement pivots and debt due-date boundary",
"domain": "address_phase108_settlement_pivot_debt_boundary",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "agent_saved_user_sessions",
"title": "AGENT | Phase108 settlement pivots and debt due-date boundary",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "Покажи документы по Жуковке 51."
},
{
"user_message": "А по нему платежи?"
},
{
"user_message": "А по нему договоры?"
},
{
"user_message": "А по нему документы?"
},
{
"user_message": "А за 2021?"
},
{
"user_message": "По ООО Альтернатива Плюс на конец 2020 можно точно понять, какая дебиторка просрочена?"
},
{
"user_message": "То есть просрочку доказать нельзя, коротко почему?"
},
{
"user_message": "Ок, а пингвины умеют летать?"
}
]
}
]
}

View File

@ -518,12 +518,24 @@ def evaluate_limited_next_action(
limited_outputs = 0
for output in outputs:
text = current_answer_text_for_output(output)
step_state_path = output.get("step_state_path")
step_state = read_json_object(step_state_path) if isinstance(step_state_path, Path) else {}
review = step_state.get("business_first_review") if isinstance(step_state.get("business_first_review"), dict) else {}
review_limited = review.get("limited_answer_detected")
if review_limited is False:
continue
limited_match = first_pattern_search(limited_patterns, text)
if not limited_match:
if review_limited is not True and not limited_match:
continue
limited_outputs += 1
if not first_pattern_search(next_action_patterns, text):
failures.append({"path": output["repo_path"], "limited_match": limited_match.group(0)[:240]})
next_action_present = review.get("next_action_present") is True or bool(first_pattern_search(next_action_patterns, text))
if not next_action_present:
evidence = {"path": output["repo_path"]}
if limited_match:
evidence["limited_match"] = limited_match.group(0)[:240]
else:
evidence["limited_match"] = "business_first_review.limited_answer_detected"
failures.append(evidence)
status = "fail" if failures else "pass"
if failures:
message = "limited answer has no next action"

View File

@ -260,6 +260,10 @@ GUARDED_INSUFFICIENCY_PRIMARY_MARKERS = (
"\u043d\u0435 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0430",
"\u043d\u0435\u043b\u044c\u0437\u044f \u0447\u0435\u0441\u0442\u043d\u043e \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0434",
"\u043d\u0435\u043b\u044c\u0437\u044f \u043a\u043e\u0440\u0440\u0435\u043a\u0442\u043d\u043e \u043e\u043f\u0440\u0435\u0434\u0435\u043b",
"\u043d\u0435\u043b\u044c\u0437\u044f \u0434\u043e\u043a\u0430\u0437\u0430\u0442\u044c",
"\u043d\u0435 \u0434\u043e\u043a\u0430\u0437\u0430\u0442\u044c",
"\u043d\u0435 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d \u0441\u0440\u043e\u043a \u043e\u043f\u043b\u0430\u0442\u044b",
"\u043d\u0435\u0442 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u043e\u0433\u043e \u0441\u0440\u043e\u043a\u0430 \u043e\u043f\u043b\u0430\u0442\u044b",
"\u0447\u0435\u0441\u0442\u043d\u043e \u043f\u043e\u0441\u0447\u0438\u0442\u0430\u0442\u044c \u043d\u0435\u043b\u044c\u0437\u044f",
"\u043d\u0435\u0442 \u0434\u043e\u0441\u0442\u0430\u0442\u043e\u0447\u043d\u043e\u0439 \u0431\u0430\u0437\u044b",
"\u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u043d\u043e\u0439 \u0441\u0435\u0431\u0435\u0441\u0442\u043e\u0438\u043c",
@ -275,6 +279,9 @@ GUARDED_INSUFFICIENCY_LIMITATION_MARKERS = (
"\u043d\u0435 \u0434\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0435\u0442 \u043e\u0441\u0442\u0430\u0442\u043e\u043a",
"\u043d\u0435 \u0444\u0438\u043d\u0430\u043b\u044c\u043d\u044b\u0439 \u0440\u0435\u0435\u0441\u0442\u0440",
"\u0433\u0440\u0430\u043d\u0438\u0446\u0430 \u043e\u0442\u0432\u0435\u0442\u0430",
"due date",
"\u0441\u0440\u043e\u043a \u043e\u043f\u043b\u0430\u0442\u044b",
"\u0434\u043e\u0433\u043e\u0432\u043e\u0440\u043d\u043e\u0439 \u043f\u0440\u043e\u0441\u0440\u043e\u0447",
"\u0440\u0430\u0441\u0447\u0435\u0442\u043d\u0443\u044e \u0431\u0430\u0437\u0443",
"\u0440\u0430\u0441\u0447\u0451\u0442\u043d\u0443\u044e \u0431\u0430\u0437\u0443",
)
@ -2798,6 +2805,26 @@ def has_guarded_insufficiency_language(answer_text: str) -> bool:
return has_unconfirmed_marker and has_limitation_marker
def is_validated_guarded_mcp_discovery_insufficiency(state: dict[str, Any]) -> bool:
if state.get("mcp_discovery_response_applied") is not True:
return False
if str(state.get("mcp_discovery_response_candidate_status") or "").strip() != "ready_for_guarded_use":
return False
if str(state.get("mcp_discovery_route_candidate_status") or "").strip() != "ready_for_reviewed_execution":
return False
if str(state.get("mcp_discovery_execution_handoff_can_use_guarded_response") or "").casefold() not in {"true", "1"}:
return False
if str(state.get("truth_gate_contract_status") or "").strip() != "partial_supported":
return False
if str(state.get("truth_mode") or "").strip() not in GUARDED_INSUFFICIENCY_TRUTH_MODES:
return False
if str(state.get("answer_shape") or "").strip() not in GUARDED_INSUFFICIENCY_ANSWER_SHAPES:
return False
if str(state.get("evidence_grade") or "").strip() not in {"weak", "medium", "strong"}:
return False
return bool(str(state.get("assistant_text") or "").strip())
def is_validated_guarded_insufficiency_answer(
state: dict[str, Any],
execution_status: str,
@ -2808,13 +2835,14 @@ def is_validated_guarded_insufficiency_answer(
return False
if violations:
return False
if state.get("mcp_discovery_response_applied") is True:
mcp_discovery_guarded = is_validated_guarded_mcp_discovery_insufficiency(state)
if state.get("mcp_discovery_response_applied") is True and not mcp_discovery_guarded:
return False
if str(state.get("reply_type") or "").strip() not in {"factual", "factual_with_explanation", "partial_coverage"}:
return False
if str(state.get("fallback_type") or "").strip() not in {"", "none"}:
if not mcp_discovery_guarded and str(state.get("fallback_type") or "").strip() not in {"", "none"}:
return False
if str(state.get("mcp_call_status") or "").strip() != "matched_non_empty":
if not mcp_discovery_guarded and str(state.get("mcp_call_status") or "").strip() != "matched_non_empty":
return False
result_mode = str(state.get("result_mode") or "").strip()
truth_mode = str(state.get("truth_mode") or "").strip()
@ -2825,7 +2853,7 @@ def is_validated_guarded_insufficiency_answer(
or answer_shape in GUARDED_INSUFFICIENCY_ANSWER_SHAPES
or state.get("balance_confirmed") is False
)
if not has_limited_evidence_mode:
if not (has_limited_evidence_mode or mcp_discovery_guarded):
return False
if not has_guarded_insufficiency_language(str(state.get("assistant_text") or "")):
return False

View File

@ -160,6 +160,65 @@ class AgentDetectorRunnerTests(unittest.TestCase):
self.assertEqual(results["summary"]["status"], "pass")
self.assertEqual(results["results"][0]["message"], "limited answer includes a next action")
def test_limited_next_action_ignores_non_limited_off_domain_can_not_phrase(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)
artifact_dir = root / "run"
step_dir = artifact_dir / "scenarios" / "chat" / "steps" / "s01"
write_text(step_dir / "output.md", "Нет, пингвины не могут летать.")
write_json(
step_dir / "step_state.json",
{
"business_first_review": {
"limited_answer_detected": False,
"next_action_present": False,
}
},
)
registry_path = root / "detector_registry.json"
issue_catalog_path = root / "issue_catalog.json"
write_json(
registry_path,
{
"schema_version": "agent_detector_registry_v1",
"detectors": {
"limited_answer_without_next_action": {
"kind": "answer_text_required_when_limited",
"automation_level": "semi_automatic",
"description": "Limited answers need a next action.",
"issue_codes": ["business_next_step_missing"],
"inputs": ["output.md"],
"check": {
"limited_patterns": ["(?i)не могу"],
"required_next_action_patterns_any": ["(?i)что проверить дальше"],
},
}
},
},
)
write_json(
issue_catalog_path,
{
"schema_version": "agent_issue_catalog_v1",
"issues": {
"business_next_step_missing": {
"detectors": ["limited_answer_without_next_action"],
}
},
},
)
results = runner.build_detector_results(
artifact_dir,
issue_codes=["business_next_step_missing"],
registry_path=registry_path,
issue_catalog_path=issue_catalog_path,
include_default_global=False,
)
self.assertEqual(results["summary"]["status"], "pass")
self.assertEqual(results["results"][0]["message"], "answer is not limited; next action requirement not triggered")
def test_candidate_forbidden_regex_is_limited_to_evidence_path(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp)

View File

@ -1617,6 +1617,46 @@ class DomainCaseLoopStepStateTests(unittest.TestCase):
self.assertTrue(step_state["guarded_insufficiency_validated"])
self.assertEqual(step_state["acceptance_status"], "validated")
def test_guarded_mcp_discovery_debt_boundary_followup_validates(self) -> None:
answer_text = (
"Да, доказать просрочку по компании ООО Альтернатива Плюс на 2020-12-31 нельзя, "
"потому что в подтвержденных данных не установлен срок оплаты.\n"
"Открытый долг на 35 472 380,36 руб. виден, но без due date это задолженность "
"без доказанной договорной просрочки."
)
step_state = dcl.validate_step_contract(
{
"execution_status": "partial",
"reply_type": "partial_coverage",
"required_answer_shape": "direct_answer_first",
"required_answer_patterns_all": [
"(?i)срок.*оплат|due[- ]?date|договор",
"(?i)не установлен|не хватает|не подтвержд|не доказ",
"(?i)просроч",
"(?i)2020|2020-12-31|конец 2020",
],
"fallback_type": "partial",
"truth_gate_contract_status": "partial_supported",
"truth_mode": "limited",
"answer_shape": "limited_with_reason",
"evidence_grade": "medium",
"mcp_discovery_response_applied": True,
"mcp_discovery_response_candidate_status": "ready_for_guarded_use",
"mcp_discovery_route_candidate_status": "ready_for_reviewed_execution",
"mcp_discovery_execution_handoff_can_use_guarded_response": True,
"assistant_text": answer_text,
"actual_direct_answer": (
"Да, доказать просрочку по компании ООО Альтернатива Плюс на 2020-12-31 нельзя, "
"потому что в подтвержденных данных не установлен срок оплаты."
),
"top_non_empty_lines": answer_text.splitlines(),
}
)
self.assertNotIn("required_answer_patterns_all_missing", step_state["violated_invariants"])
self.assertTrue(step_state["guarded_insufficiency_validated"])
self.assertEqual(step_state["acceptance_status"], "validated")
def test_expected_clarification_partial_answer_validates(self) -> None:
answer_text = (
"Для рейтинга прибыльности номенклатуры нужен период.\n\n"