ARCH: добить all-time и period-first clarification loops

This commit is contained in:
dctouch 2026-04-23 13:00:54 +03:00
parent 053a3bf157
commit 8a1ae696dc
16 changed files with 397 additions and 26 deletions

View File

@ -0,0 +1,70 @@
{
"schema_version": "domain_truth_harness_spec_v1",
"scenario_id": "address_truth_harness_phase47_multi_hop_open_net_period_first_clarification_loop",
"domain": "address_phase47_multi_hop_open_net_period_first_clarification_loop",
"title": "Phase 47 multi-hop open net period-first clarification loop",
"description": "Targeted AGENT replay for Big Block F where an open-scope net question asks for both organization and period, then keeps the same net loop after a period-only clarification, and finally answers after the second clarification provides the organization.",
"bindings": {},
"steps": [
{
"step_id": "step_01_open_net_requires_org_and_period",
"title": "Open net question asks for both organization and period",
"question": "Какое нетто по деньгам: сколько получили и сколько заплатили?",
"allowed_reply_types": ["clarification_required", "partial_coverage", "factual_with_explanation"],
"required_answer_patterns_all": [
"(?i)уточн|нужно",
"(?i)организац",
"(?i)период"
],
"forbidden_answer_patterns": [
"(?i)уточните контрагента",
"(?i)не найден контрагент",
"(?i)по какому контрагенту",
"(?i)не найдено контрагента"
],
"criticality": "critical",
"semantic_tags": ["open_scope_net", "multi_hop_clarification", "organization_scope", "period_scope", "bounded_autonomy"]
},
{
"step_id": "step_02_period_only_clarification_keeps_same_net_loop",
"title": "Period-only clarification preserves the same net loop and asks only for the organization",
"question": "за 2020 год",
"allowed_reply_types": ["clarification_required", "partial_coverage"],
"required_answer_patterns_all": [
"(?i)уточн|нужно",
"(?i)организац"
],
"forbidden_answer_patterns": [
"(?i)период",
"(?i)уточните контрагента",
"(?i)не найден контрагент"
],
"criticality": "critical",
"semantic_tags": ["open_scope_net", "multi_hop_clarification", "period_followup_reuse", "bounded_autonomy"]
},
{
"step_id": "step_03_org_clarification_completes_same_net_loop",
"title": "Organization clarification completes the same net loop and yields a bounded net answer",
"question": "по ООО Альтернатива Плюс",
"allowed_reply_types": ["factual_with_explanation", "partial_coverage"],
"required_answer_patterns_all": [
"(?i)2020",
"(?i)получ|входящ|заплат|исходящ|нетто",
"(?i)руб"
],
"required_answer_patterns_any": [
"(?i)альтернатива",
"(?i)проверенн|найденн",
"(?i)разниц|нетто"
],
"forbidden_answer_patterns": [
"(?i)уточните организацию",
"(?i)уточните период",
"(?i)уточните контрагента",
"(?i)не найден контрагент"
],
"criticality": "critical",
"semantic_tags": ["open_scope_net", "multi_hop_clarification", "organization_followup_reuse", "bounded_autonomy"]
}
]
}

View File

@ -0,0 +1,69 @@
{
"schema_version": "domain_truth_harness_spec_v1",
"scenario_id": "address_truth_harness_phase48_multi_hop_open_total_all_time_clarification_loop",
"domain": "address_phase48_multi_hop_open_total_all_time_clarification_loop",
"title": "Phase 48 multi-hop open total all-time clarification loop",
"description": "Targeted AGENT replay for Big Block F where an open-scope incoming-total question asks for both organization and period, then keeps the same total loop after an organization-only clarification, and finally accepts 'за все время' as the period clarification that clears the remaining gap and produces a bounded answer.",
"bindings": {},
"steps": [
{
"step_id": "step_01_open_total_requires_org_and_period",
"title": "Open total question asks for both organization and period",
"question": "Сколько вообще входящих денег было?",
"allowed_reply_types": ["clarification_required", "partial_coverage", "factual_with_explanation"],
"required_answer_patterns_all": [
"(?i)уточн|нужно",
"(?i)организац",
"(?i)период"
],
"forbidden_answer_patterns": [
"(?i)уточните контрагента",
"(?i)не найден контрагент",
"(?i)по какому контрагенту",
"(?i)не найдено контрагента"
],
"criticality": "critical",
"semantic_tags": ["open_scope_total", "multi_hop_clarification", "organization_scope", "period_scope", "bounded_autonomy"]
},
{
"step_id": "step_02_org_only_clarification_keeps_same_total_loop",
"title": "Organization-only clarification preserves the same total loop and asks only for the period",
"question": "по ООО Альтернатива Плюс",
"allowed_reply_types": ["clarification_required", "partial_coverage"],
"required_answer_patterns_all": [
"(?i)уточн|нужно",
"(?i)период"
],
"forbidden_answer_patterns": [
"(?i)организац",
"(?i)уточните контрагента",
"(?i)не найден контрагент"
],
"criticality": "critical",
"semantic_tags": ["open_scope_total", "multi_hop_clarification", "organization_followup_reuse", "bounded_autonomy"]
},
{
"step_id": "step_03_all_time_clarification_completes_same_total_loop",
"title": "All-time clarification clears the remaining period gap and yields a bounded total answer",
"question": "за все время",
"allowed_reply_types": ["factual_with_explanation", "partial_coverage"],
"required_answer_patterns_all": [
"(?i)входящ|получ",
"(?i)руб"
],
"required_answer_patterns_any": [
"(?i)все время|доступное время|проверенн",
"(?i)альтернатива",
"(?i)найденн"
],
"forbidden_answer_patterns": [
"(?i)уточните организацию",
"(?i)уточните период",
"(?i)уточните контрагента",
"(?i)не найден контрагент"
],
"criticality": "critical",
"semantic_tags": ["open_scope_total", "all_time_scope", "multi_hop_clarification", "bounded_autonomy"]
}
]
}

View File

@ -65,7 +65,13 @@ const PRIMITIVE_CONTRACTS = [
supported_fact_families: ["value_flow", "movement_evidence"], supported_fact_families: ["value_flow", "movement_evidence"],
supported_action_families: ["turnover", "payout", "net_value_flow", "list_movements"], supported_action_families: ["turnover", "payout", "net_value_flow", "list_movements"],
planning_tags: ["movement", "comparison", "ranking", "aggregation", "monthly_aggregation"], planning_tags: ["movement", "comparison", "ranking", "aggregation", "monthly_aggregation"],
required_axes_any_of: [["period", "account"], ["period", "counterparty"], ["period", "organization"]], required_axes_any_of: [
["period", "account"],
["period", "counterparty"],
["period", "organization"],
["all_time_scope", "counterparty"],
["all_time_scope", "organization"]
],
optional_axes: ["contract", "document", "amount", "item", "warehouse"], optional_axes: ["contract", "document", "amount", "item", "warehouse"],
output_fact_kinds: ["movement_rows", "turnover", "balance_delta"], output_fact_kinds: ["movement_rows", "turnover", "balance_delta"],
evidence_floor: "rows_matched", evidence_floor: "rows_matched",
@ -93,7 +99,13 @@ const PRIMITIVE_CONTRACTS = [
supported_fact_families: ["value_flow"], supported_fact_families: ["value_flow"],
supported_action_families: ["turnover", "payout", "net_value_flow"], supported_action_families: ["turnover", "payout", "net_value_flow"],
planning_tags: ["aggregation", "ranking", "monthly_aggregation"], planning_tags: ["aggregation", "ranking", "monthly_aggregation"],
required_axes_any_of: [["aggregate_axis", "period"], ["aggregate_axis", "counterparty"], ["aggregate_axis", "account"]], required_axes_any_of: [
["aggregate_axis", "period"],
["aggregate_axis", "counterparty"],
["aggregate_axis", "account"],
["aggregate_axis", "counterparty", "all_time_scope"],
["aggregate_axis", "organization", "all_time_scope"]
],
optional_axes: ["organization", "contract", "document", "amount"], optional_axes: ["organization", "contract", "document", "amount"],
output_fact_kinds: ["aggregate_totals", "ranked_axis_values"], output_fact_kinds: ["aggregate_totals", "ranked_axis_values"],
evidence_floor: "rows_matched", evidence_floor: "rows_matched",

View File

@ -46,7 +46,6 @@ function isInternalMechanicsLine(value) {
text.includes("runtime_") || text.includes("runtime_") ||
text.includes("planner_") || text.includes("planner_") ||
text.includes("catalog_") || text.includes("catalog_") ||
text.includes("mcp discovery") ||
text.includes("needs more scope before execution") || text.includes("needs more scope before execution") ||
text.includes("mcp_execution_performed")); text.includes("mcp_execution_performed"));
} }

View File

@ -100,10 +100,10 @@ function hasOpenScopeOneSidedValueTotalHint(rawUtterance, action) {
return false; return false;
} }
if (action === "turnover") { if (action === "turnover") {
return /(?:\bсколько\s+(?:мы\s+)?(?:получили|получено|входящих(?:\s+денег)?|поступлений|денег\s+пришло)\b|(?:сумма|объем)\s+(?:входящих|поступлений)|поступлений\s+за\b)/iu.test(rawUtterance); return /(?:\bсколько\s+(?:(?:вообще|всего|реально)\s+){0,2}(?:мы\s+)?(?:получили|получено|входящих(?:\s+денег)?(?:\s+было)?|поступлений|денег\s+пришло)\b|(?:сумма|объем)\s+(?:входящих|поступлений)|поступлений\s+за\b)/iu.test(rawUtterance);
} }
if (action === "payout") { if (action === "payout") {
return /(?:\bсколько\s+(?:мы\s+)?(?:заплатили|выплатили|потратили|исходящих(?:\s+денег)?|платежей|списаний)\b|(?:сумма|объем)\s+(?:исходящих|платежей|списаний)|(?:платежей|списаний)\s+за\b)/iu.test(rawUtterance); return /(?:\bсколько\s+(?:(?:вообще|всего|реально)\s+){0,2}(?:мы\s+)?(?:заплатили|выплатили|потратили|исходящих(?:\s+денег)?(?:\s+было)?|платежей(?:\s+было)?|списаний(?:\s+было)?)\b|(?:сумма|объем)\s+(?:исходящих|платежей|списаний)|(?:платежей|списаний)\s+за\b)/iu.test(rawUtterance);
} }
return false; return false;
} }
@ -112,10 +112,10 @@ function hasOpenScopeOneSidedValueTotalHintUtf8Safe(rawUtterance, action) {
return false; return false;
} }
if (action === "turnover") { if (action === "turnover") {
return /(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e\s+(?:\u043c\u044b\s+)?(?:\u043f\u043e\u043b\u0443\u0447\u0438\u043b\u0438|\u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043e|\u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0445(?:\s+\u0434\u0435\u043d\u0435\u0433)?|\u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d\u0438\u0439|\u0434\u0435\u043d\u0435\u0433\s+\u043f\u0440\u0438\u0448\u043b\u043e)|(?:\u0441\u0443\u043c\u043c\u0430|\u043e\u0431\u044a\u0435\u043c)\s+(?:\u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0445|\u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d\u0438\u0439)|\u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d\u0438\u0439\s+\u0437\u0430)/u.test(rawUtterance); return /(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e\s+(?:(?:\u0432\u043e\u043e\u0431\u0449\u0435|\u0432\u0441\u0435\u0433\u043e|\u0440\u0435\u0430\u043b\u044c\u043d\u043e)\s+){0,2}(?:\u043c\u044b\s+)?(?:\u043f\u043e\u043b\u0443\u0447\u0438\u043b\u0438|\u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043e|\u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0445(?:\s+\u0434\u0435\u043d\u0435\u0433)?(?:\s+\u0431\u044b\u043b\u043e)?|\u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d\u0438\u0439|\u0434\u0435\u043d\u0435\u0433\s+\u043f\u0440\u0438\u0448\u043b\u043e)|(?:\u0441\u0443\u043c\u043c\u0430|\u043e\u0431\u044a\u0435\u043c)\s+(?:\u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0445|\u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d\u0438\u0439)|\u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d\u0438\u0439\s+\u0437\u0430)/u.test(rawUtterance);
} }
if (action === "payout") { if (action === "payout") {
return /(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e\s+(?:\u043c\u044b\s+)?(?:\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u043b\u0438|\u0432\u044b\u043f\u043b\u0430\u0442\u0438\u043b\u0438|\u043f\u043e\u0442\u0440\u0430\u0442\u0438\u043b\u0438|\u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0445(?:\s+\u0434\u0435\u043d\u0435\u0433)?|\u043f\u043b\u0430\u0442\u0435\u0436\u0435\u0439|\u0441\u043f\u0438\u0441\u0430\u043d\u0438\u0439)|(?:\u0441\u0443\u043c\u043c\u0430|\u043e\u0431\u044a\u0435\u043c)\s+(?:\u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0445|\u043f\u043b\u0430\u0442\u0435\u0436\u0435\u0439|\u0441\u043f\u0438\u0441\u0430\u043d\u0438\u0439)|(?:\u043f\u043b\u0430\u0442\u0435\u0436\u0435\u0439|\u0441\u043f\u0438\u0441\u0430\u043d\u0438\u0439)\s+\u0437\u0430)/u.test(rawUtterance); return /(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e\s+(?:(?:\u0432\u043e\u043e\u0431\u0449\u0435|\u0432\u0441\u0435\u0433\u043e|\u0440\u0435\u0430\u043b\u044c\u043d\u043e)\s+){0,2}(?:\u043c\u044b\s+)?(?:\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u043b\u0438|\u0432\u044b\u043f\u043b\u0430\u0442\u0438\u043b\u0438|\u043f\u043e\u0442\u0440\u0430\u0442\u0438\u043b\u0438|\u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0445(?:\s+\u0434\u0435\u043d\u0435\u0433)?(?:\s+\u0431\u044b\u043b\u043e)?|\u043f\u043b\u0430\u0442\u0435\u0436\u0435\u0439(?:\s+\u0431\u044b\u043b\u043e)?|\u0441\u043f\u0438\u0441\u0430\u043d\u0438\u0439(?:\s+\u0431\u044b\u043b\u043e)?)|(?:\u0441\u0443\u043c\u043c\u0430|\u043e\u0431\u044a\u0435\u043c)\s+(?:\u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0445|\u043f\u043b\u0430\u0442\u0435\u0436\u0435\u0439|\u0441\u043f\u0438\u0441\u0430\u043d\u0438\u0439)|(?:\u043f\u043b\u0430\u0442\u0435\u0436\u0435\u0439|\u0441\u043f\u0438\u0441\u0430\u043d\u0438\u0439)\s+\u0437\u0430)/u.test(rawUtterance);
} }
return false; return false;
} }

View File

@ -58,6 +58,11 @@ function addScopeAxes(axes, meaning) {
pushUnique(axes, "period"); pushUnique(axes, "period");
} }
} }
function addTimeScopeAxes(axes, dataNeedGraph) {
if (dataNeedGraph?.time_scope_need === "all_time_scope") {
pushUnique(axes, "all_time_scope");
}
}
function includesAny(text, tokens) { function includesAny(text, tokens) {
return tokens.some((token) => text.includes(token)); return tokens.some((token) => text.includes(token));
} }
@ -276,6 +281,7 @@ function recipeFor(input) {
const axes = []; const axes = [];
const requestedAggregationAxis = aggregationAxis(meaning); const requestedAggregationAxis = aggregationAxis(meaning);
addScopeAxes(axes, meaning); addScopeAxes(axes, meaning);
addTimeScopeAxes(axes, dataNeedGraph);
if (graphClarificationGaps.includes("lane_family_choice")) { if (graphClarificationGaps.includes("lane_family_choice")) {
pushUnique(axes, "lane_family_choice"); pushUnique(axes, "lane_family_choice");
return { return {

View File

@ -56,7 +56,6 @@ function hasInternalMechanics(value) {
text.includes("planner_") || text.includes("planner_") ||
text.includes("catalog_") || text.includes("catalog_") ||
text.includes("select ") || text.includes("select ") ||
text.includes("mcp discovery") ||
text.includes("needs more scope before execution") || text.includes("needs more scope before execution") ||
text.includes("mcp_execution_performed")); text.includes("mcp_execution_performed"));
} }

View File

@ -768,14 +768,27 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
? null ? null
: predecomposeEntities.counterparty; : predecomposeEntities.counterparty;
const predecomposeDateScope = collectDateScope(predecomposeContract); const predecomposeDateScope = collectDateScope(predecomposeContract);
const periodClarificationFollowupApplicable = Boolean(followupSeed.domain &&
followupSeed.loopStatus === "awaiting_clarification" &&
followupSeed.loopPendingAxes.includes("period") &&
!rawLifecycleSignal &&
!rawMetadataSignal &&
(rawAllTimeScopeSignal ||
explicitDateScopeLiteralDetected ||
rawDateScope ||
relativeCurrentDateHintDetected ||
(predecomposeDateScope && !isImplicitCurrentDateScope(predecomposeDateScope))));
const followupDiscoverySeedApplicable = Boolean(followupSeed.domain && const followupDiscoverySeedApplicable = Boolean(followupSeed.domain &&
!rawLifecycleSignal && !rawLifecycleSignal &&
!rawValueFlowSignal && !rawMetadataSignal &&
(monthlyAggregationSignal || (periodClarificationFollowupApplicable ||
explicitDateScopeLiteralDetected || (!rawValueFlowSignal &&
predecomposeDateScope || (monthlyAggregationSignal ||
explicitOrganizationScopeSignal || rawAllTimeScopeSignal ||
organizationClarificationFollowupApplicable)); explicitDateScopeLiteralDetected ||
predecomposeDateScope ||
explicitOrganizationScopeSignal ||
organizationClarificationFollowupApplicable))));
const metadataFollowupSeedApplicable = Boolean(followupSeed.domain === "metadata" && const metadataFollowupSeedApplicable = Boolean(followupSeed.domain === "metadata" &&
!rawLifecycleSignal && !rawLifecycleSignal &&
!rawValueFlowSignal && !rawValueFlowSignal &&
@ -1197,6 +1210,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
groundedValueFlowFollowupApplicable, groundedValueFlowFollowupApplicable,
forceDiscoveryOverExplicitIntent: Boolean(entityResolutionClarificationCandidate) || forceDiscoveryOverExplicitIntent: Boolean(entityResolutionClarificationCandidate) ||
organizationClarificationFollowupApplicable || organizationClarificationFollowupApplicable ||
periodClarificationFollowupApplicable ||
metadataAmbiguityLaneClarificationApplicable || metadataAmbiguityLaneClarificationApplicable ||
metadataGroundedMovementLaneApplicable || metadataGroundedMovementLaneApplicable ||
metadataGroundedDocumentLaneApplicable || metadataGroundedDocumentLaneApplicable ||
@ -1249,6 +1263,9 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
if (organizationClarificationFollowupApplicable) { if (organizationClarificationFollowupApplicable) {
pushReason(reasonCodes, "mcp_discovery_organization_clarification_followup_from_followup_context"); pushReason(reasonCodes, "mcp_discovery_organization_clarification_followup_from_followup_context");
} }
if (periodClarificationFollowupApplicable) {
pushReason(reasonCodes, "mcp_discovery_period_clarification_followup_from_followup_context");
}
if (payoutSignal) { if (payoutSignal) {
pushReason(reasonCodes, "mcp_discovery_payout_signal_detected"); pushReason(reasonCodes, "mcp_discovery_payout_signal_detected");
} }

View File

@ -98,7 +98,13 @@ const PRIMITIVE_CONTRACTS: AssistantMcpCatalogPrimitiveContract[] = [
supported_fact_families: ["value_flow", "movement_evidence"], supported_fact_families: ["value_flow", "movement_evidence"],
supported_action_families: ["turnover", "payout", "net_value_flow", "list_movements"], supported_action_families: ["turnover", "payout", "net_value_flow", "list_movements"],
planning_tags: ["movement", "comparison", "ranking", "aggregation", "monthly_aggregation"], planning_tags: ["movement", "comparison", "ranking", "aggregation", "monthly_aggregation"],
required_axes_any_of: [["period", "account"], ["period", "counterparty"], ["period", "organization"]], required_axes_any_of: [
["period", "account"],
["period", "counterparty"],
["period", "organization"],
["all_time_scope", "counterparty"],
["all_time_scope", "organization"]
],
optional_axes: ["contract", "document", "amount", "item", "warehouse"], optional_axes: ["contract", "document", "amount", "item", "warehouse"],
output_fact_kinds: ["movement_rows", "turnover", "balance_delta"], output_fact_kinds: ["movement_rows", "turnover", "balance_delta"],
evidence_floor: "rows_matched", evidence_floor: "rows_matched",
@ -126,7 +132,13 @@ const PRIMITIVE_CONTRACTS: AssistantMcpCatalogPrimitiveContract[] = [
supported_fact_families: ["value_flow"], supported_fact_families: ["value_flow"],
supported_action_families: ["turnover", "payout", "net_value_flow"], supported_action_families: ["turnover", "payout", "net_value_flow"],
planning_tags: ["aggregation", "ranking", "monthly_aggregation"], planning_tags: ["aggregation", "ranking", "monthly_aggregation"],
required_axes_any_of: [["aggregate_axis", "period"], ["aggregate_axis", "counterparty"], ["aggregate_axis", "account"]], required_axes_any_of: [
["aggregate_axis", "period"],
["aggregate_axis", "counterparty"],
["aggregate_axis", "account"],
["aggregate_axis", "counterparty", "all_time_scope"],
["aggregate_axis", "organization", "all_time_scope"]
],
optional_axes: ["organization", "contract", "document", "amount"], optional_axes: ["organization", "contract", "document", "amount"],
output_fact_kinds: ["aggregate_totals", "ranked_axis_values"], output_fact_kinds: ["aggregate_totals", "ranked_axis_values"],
evidence_floor: "rows_matched", evidence_floor: "rows_matched",

View File

@ -153,12 +153,12 @@ function hasOpenScopeOneSidedValueTotalHint(rawUtterance: string, action: string
return false; return false;
} }
if (action === "turnover") { if (action === "turnover") {
return /(?:\bсколько\s+(?:мы\s+)?(?:получили|получено|входящих(?:\s+денег)?|поступлений|денег\s+пришло)\b|(?:сумма|объем)\s+(?:входящих|поступлений)|поступлений\s+за\b)/iu.test( return /(?:\bсколько\s+(?:(?:вообще|всего|реально)\s+){0,2}(?:мы\s+)?(?:получили|получено|входящих(?:\s+денег)?(?:\s+было)?|поступлений|денег\s+пришло)\b|(?:сумма|объем)\s+(?:входящих|поступлений)|поступлений\s+за\b)/iu.test(
rawUtterance rawUtterance
); );
} }
if (action === "payout") { if (action === "payout") {
return /(?:\bсколько\s+(?:мы\s+)?(?:заплатили|выплатили|потратили|исходящих(?:\s+денег)?|платежей|списаний)\b|(?:сумма|объем)\s+(?:исходящих|платежей|списаний)|(?:платежей|списаний)\s+за\b)/iu.test( return /(?:\bсколько\s+(?:(?:вообще|всего|реально)\s+){0,2}(?:мы\s+)?(?:заплатили|выплатили|потратили|исходящих(?:\s+денег)?(?:\s+было)?|платежей(?:\s+было)?|списаний(?:\s+было)?)\b|(?:сумма|объем)\s+(?:исходящих|платежей|списаний)|(?:платежей|списаний)\s+за\b)/iu.test(
rawUtterance rawUtterance
); );
} }
@ -170,12 +170,12 @@ function hasOpenScopeOneSidedValueTotalHintUtf8Safe(rawUtterance: string, action
return false; return false;
} }
if (action === "turnover") { if (action === "turnover") {
return /(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e\s+(?:\u043c\u044b\s+)?(?:\u043f\u043e\u043b\u0443\u0447\u0438\u043b\u0438|\u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043e|\u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0445(?:\s+\u0434\u0435\u043d\u0435\u0433)?|\u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d\u0438\u0439|\u0434\u0435\u043d\u0435\u0433\s+\u043f\u0440\u0438\u0448\u043b\u043e)|(?:\u0441\u0443\u043c\u043c\u0430|\u043e\u0431\u044a\u0435\u043c)\s+(?:\u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0445|\u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d\u0438\u0439)|\u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d\u0438\u0439\s+\u0437\u0430)/u.test( return /(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e\s+(?:(?:\u0432\u043e\u043e\u0431\u0449\u0435|\u0432\u0441\u0435\u0433\u043e|\u0440\u0435\u0430\u043b\u044c\u043d\u043e)\s+){0,2}(?:\u043c\u044b\s+)?(?:\u043f\u043e\u043b\u0443\u0447\u0438\u043b\u0438|\u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043e|\u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0445(?:\s+\u0434\u0435\u043d\u0435\u0433)?(?:\s+\u0431\u044b\u043b\u043e)?|\u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d\u0438\u0439|\u0434\u0435\u043d\u0435\u0433\s+\u043f\u0440\u0438\u0448\u043b\u043e)|(?:\u0441\u0443\u043c\u043c\u0430|\u043e\u0431\u044a\u0435\u043c)\s+(?:\u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0445|\u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d\u0438\u0439)|\u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d\u0438\u0439\s+\u0437\u0430)/u.test(
rawUtterance rawUtterance
); );
} }
if (action === "payout") { if (action === "payout") {
return /(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e\s+(?:\u043c\u044b\s+)?(?:\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u043b\u0438|\u0432\u044b\u043f\u043b\u0430\u0442\u0438\u043b\u0438|\u043f\u043e\u0442\u0440\u0430\u0442\u0438\u043b\u0438|\u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0445(?:\s+\u0434\u0435\u043d\u0435\u0433)?|\u043f\u043b\u0430\u0442\u0435\u0436\u0435\u0439|\u0441\u043f\u0438\u0441\u0430\u043d\u0438\u0439)|(?:\u0441\u0443\u043c\u043c\u0430|\u043e\u0431\u044a\u0435\u043c)\s+(?:\u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0445|\u043f\u043b\u0430\u0442\u0435\u0436\u0435\u0439|\u0441\u043f\u0438\u0441\u0430\u043d\u0438\u0439)|(?:\u043f\u043b\u0430\u0442\u0435\u0436\u0435\u0439|\u0441\u043f\u0438\u0441\u0430\u043d\u0438\u0439)\s+\u0437\u0430)/u.test( return /(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e\s+(?:(?:\u0432\u043e\u043e\u0431\u0449\u0435|\u0432\u0441\u0435\u0433\u043e|\u0440\u0435\u0430\u043b\u044c\u043d\u043e)\s+){0,2}(?:\u043c\u044b\s+)?(?:\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u043b\u0438|\u0432\u044b\u043f\u043b\u0430\u0442\u0438\u043b\u0438|\u043f\u043e\u0442\u0440\u0430\u0442\u0438\u043b\u0438|\u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0445(?:\s+\u0434\u0435\u043d\u0435\u0433)?(?:\s+\u0431\u044b\u043b\u043e)?|\u043f\u043b\u0430\u0442\u0435\u0436\u0435\u0439(?:\s+\u0431\u044b\u043b\u043e)?|\u0441\u043f\u0438\u0441\u0430\u043d\u0438\u0439(?:\s+\u0431\u044b\u043b\u043e)?)|(?:\u0441\u0443\u043c\u043c\u0430|\u043e\u0431\u044a\u0435\u043c)\s+(?:\u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0445|\u043f\u043b\u0430\u0442\u0435\u0436\u0435\u0439|\u0441\u043f\u0438\u0441\u0430\u043d\u0438\u0439)|(?:\u043f\u043b\u0430\u0442\u0435\u0436\u0435\u0439|\u0441\u043f\u0438\u0441\u0430\u043d\u0438\u0439)\s+\u0437\u0430)/u.test(
rawUtterance rawUtterance
); );
} }

View File

@ -150,6 +150,15 @@ function addScopeAxes(axes: string[], meaning: AssistantMcpDiscoveryTurnMeaningR
} }
} }
function addTimeScopeAxes(
axes: string[],
dataNeedGraph: AssistantMcpDiscoveryDataNeedGraphContract | null | undefined
): void {
if (dataNeedGraph?.time_scope_need === "all_time_scope") {
pushUnique(axes, "all_time_scope");
}
}
function includesAny(text: string, tokens: string[]): boolean { function includesAny(text: string, tokens: string[]): boolean {
return tokens.some((token) => text.includes(token)); return tokens.some((token) => text.includes(token));
} }
@ -417,6 +426,7 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
const axes: string[] = []; const axes: string[] = [];
const requestedAggregationAxis = aggregationAxis(meaning); const requestedAggregationAxis = aggregationAxis(meaning);
addScopeAxes(axes, meaning); addScopeAxes(axes, meaning);
addTimeScopeAxes(axes, dataNeedGraph);
if (graphClarificationGaps.includes("lane_family_choice")) { if (graphClarificationGaps.includes("lane_family_choice")) {
pushUnique(axes, "lane_family_choice"); pushUnique(axes, "lane_family_choice");

View File

@ -1053,15 +1053,30 @@ export function buildAssistantMcpDiscoveryTurnInput(
? null ? null
: predecomposeEntities.counterparty; : predecomposeEntities.counterparty;
const predecomposeDateScope = collectDateScope(predecomposeContract); const predecomposeDateScope = collectDateScope(predecomposeContract);
const periodClarificationFollowupApplicable = Boolean(
followupSeed.domain &&
followupSeed.loopStatus === "awaiting_clarification" &&
followupSeed.loopPendingAxes.includes("period") &&
!rawLifecycleSignal &&
!rawMetadataSignal &&
(rawAllTimeScopeSignal ||
explicitDateScopeLiteralDetected ||
rawDateScope ||
relativeCurrentDateHintDetected ||
(predecomposeDateScope && !isImplicitCurrentDateScope(predecomposeDateScope)))
);
const followupDiscoverySeedApplicable = Boolean( const followupDiscoverySeedApplicable = Boolean(
followupSeed.domain && followupSeed.domain &&
!rawLifecycleSignal && !rawLifecycleSignal &&
!rawValueFlowSignal && !rawMetadataSignal &&
(monthlyAggregationSignal || (periodClarificationFollowupApplicable ||
explicitDateScopeLiteralDetected || (!rawValueFlowSignal &&
predecomposeDateScope || (monthlyAggregationSignal ||
explicitOrganizationScopeSignal || rawAllTimeScopeSignal ||
organizationClarificationFollowupApplicable) explicitDateScopeLiteralDetected ||
predecomposeDateScope ||
explicitOrganizationScopeSignal ||
organizationClarificationFollowupApplicable)))
); );
const metadataFollowupSeedApplicable = Boolean( const metadataFollowupSeedApplicable = Boolean(
followupSeed.domain === "metadata" && followupSeed.domain === "metadata" &&
@ -1563,6 +1578,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
forceDiscoveryOverExplicitIntent: forceDiscoveryOverExplicitIntent:
Boolean(entityResolutionClarificationCandidate) || Boolean(entityResolutionClarificationCandidate) ||
organizationClarificationFollowupApplicable || organizationClarificationFollowupApplicable ||
periodClarificationFollowupApplicable ||
metadataAmbiguityLaneClarificationApplicable || metadataAmbiguityLaneClarificationApplicable ||
metadataGroundedMovementLaneApplicable || metadataGroundedMovementLaneApplicable ||
metadataGroundedDocumentLaneApplicable || metadataGroundedDocumentLaneApplicable ||
@ -1616,6 +1632,9 @@ export function buildAssistantMcpDiscoveryTurnInput(
if (organizationClarificationFollowupApplicable) { if (organizationClarificationFollowupApplicable) {
pushReason(reasonCodes, "mcp_discovery_organization_clarification_followup_from_followup_context"); pushReason(reasonCodes, "mcp_discovery_organization_clarification_followup_from_followup_context");
} }
if (periodClarificationFollowupApplicable) {
pushReason(reasonCodes, "mcp_discovery_period_clarification_followup_from_followup_context");
}
if (payoutSignal) { if (payoutSignal) {
pushReason(reasonCodes, "mcp_discovery_payout_signal_detected"); pushReason(reasonCodes, "mcp_discovery_payout_signal_detected");
} }

View File

@ -71,6 +71,17 @@ describe("assistant MCP catalog index", () => {
expect(primitives).toEqual(["resolve_entity_reference", "query_documents", "probe_coverage"]); expect(primitives).toEqual(["resolve_entity_reference", "query_documents", "probe_coverage"]);
}); });
it("treats all-time organization-scoped value-flow as catalog-compatible without inventing a fake period axis", () => {
const primitives = searchAssistantMcpCatalogPrimitivesByFactAxis({
business_fact_family: "value_flow",
action_family: "turnover",
has_subject_candidates: false,
required_axes: ["organization", "all_time_scope", "aggregate_axis", "amount", "coverage_target"]
});
expect(primitives).toEqual(["query_movements", "aggregate_by_axis", "probe_coverage"]);
});
it("can search reviewed primitives directly from a confirmed document metadata surface", () => { it("can search reviewed primitives directly from a confirmed document metadata surface", () => {
const primitives = searchAssistantMcpCatalogPrimitivesByMetadataSurface({ const primitives = searchAssistantMcpCatalogPrimitivesByMetadataSurface({
downstream_route_family: "document_evidence", downstream_route_family: "document_evidence",
@ -150,6 +161,25 @@ describe("assistant MCP catalog index", () => {
]); ]);
}); });
it("marks an all-time organization-scoped value-flow plan as catalog-compatible without requiring an explicit period", () => {
const plan = buildAssistantMcpDiscoveryPlan({
semanticDataNeed: "organization-scoped value-flow evidence",
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_organization_scope: "ООО Альтернатива Плюс"
},
proposedPrimitives: ["query_movements", "aggregate_by_axis", "probe_coverage"],
requiredAxes: ["organization", "all_time_scope", "aggregate_axis", "amount", "coverage_target"]
});
const review = reviewAssistantMcpDiscoveryPlanAgainstCatalog(plan);
expect(review.review_status).toBe("catalog_compatible");
expect(review.reason_codes).toContain("catalog_plan_compatible");
expect(review.missing_axes_by_primitive).toEqual({});
});
it("preserves source-summary evidence floors for metadata and coverage primitives", () => { it("preserves source-summary evidence floors for metadata and coverage primitives", () => {
expect(getAssistantMcpCatalogPrimitive("inspect_1c_metadata").evidence_floor).toBe("source_summary"); expect(getAssistantMcpCatalogPrimitive("inspect_1c_metadata").evidence_floor).toBe("source_summary");
expect(getAssistantMcpCatalogPrimitive("probe_coverage").evidence_floor).toBe("source_summary"); expect(getAssistantMcpCatalogPrimitive("probe_coverage").evidence_floor).toBe("source_summary");

View File

@ -182,6 +182,29 @@ describe("assistant MCP discovery data need graph", () => {
expect(result.reason_codes).toContain("data_need_graph_open_scope_total_needs_organization"); expect(result.reason_codes).toContain("data_need_graph_open_scope_total_needs_organization");
}); });
it("treats colloquial open incoming total wording with filler words as an open-scope ask rather than a missing subject", () => {
const result = buildAssistantMcpDiscoveryDataNeedGraph({
semanticDataNeed: "counterparty value-flow evidence",
rawUtterance: "сколько вообще входящих денег было?",
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover"
}
});
expect(result.business_fact_family).toBe("value_flow");
expect(result.subject_candidates).toEqual([]);
expect(result.clarification_gaps).toEqual(["organization", "period"]);
expect(result.proof_expectation).toBe("clarification_required");
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");
expect(result.reason_codes).toContain("data_need_graph_open_scope_total_needs_organization");
});
it("treats all-time open-scope totals as an open-ended period rather than a missing period", () => { it("treats all-time open-scope totals as an open-ended period rather than a missing period", () => {
const result = buildAssistantMcpDiscoveryDataNeedGraph({ const result = buildAssistantMcpDiscoveryDataNeedGraph({
semanticDataNeed: "counterparty value-flow evidence", semanticDataNeed: "counterparty value-flow evidence",

View File

@ -779,4 +779,48 @@ describe("assistant MCP discovery planner", () => {
expect(result.reason_codes).toContain("planner_selected_open_scope_value_flow_total_from_data_need_graph"); expect(result.reason_codes).toContain("planner_selected_open_scope_value_flow_total_from_data_need_graph");
expect(result.selected_chain_id).not.toBe("entity_resolution"); expect(result.selected_chain_id).not.toBe("entity_resolution");
}); });
it("treats all-time organization-scoped open totals as execution-ready without reintroducing a fake period gap", () => {
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: "all_time_scope",
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",
"data_need_graph_all_time_scope_hint"
]
},
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
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",
"all_time_scope",
"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");
});
}); });

View File

@ -1500,6 +1500,27 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.data_need_graph?.reason_codes).toContain("data_need_graph_open_scope_total_needs_organization"); expect(result.data_need_graph?.reason_codes).toContain("data_need_graph_open_scope_total_needs_organization");
}); });
it("treats colloquial open incoming total wording with filler words as an open-scope ask that needs organization and period", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "сколько вообще входящих денег было?"
});
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",
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?.clarification_gaps).toEqual(["organization", "period"]);
expect(result.data_need_graph?.reason_codes).toContain("data_need_graph_open_scope_total_without_subject");
expect(result.data_need_graph?.reason_codes).toContain("data_need_graph_open_scope_total_needs_organization");
});
it("resumes an open-scope incoming total from follow-up context when the user clarifies only the organization", () => { it("resumes an open-scope incoming total from follow-up context when the user clarifies only the organization", () => {
const orgName = "ООО Альтернатива Плюс"; const orgName = "ООО Альтернатива Плюс";
const result = buildAssistantMcpDiscoveryTurnInput({ const result = buildAssistantMcpDiscoveryTurnInput({
@ -1564,6 +1585,46 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.data_need_graph?.reason_codes).toContain("data_need_graph_all_time_scope_hint"); expect(result.data_need_graph?.reason_codes).toContain("data_need_graph_all_time_scope_hint");
}); });
it("resumes an open-scope total clarification loop from saved state when the user resolves the pending period with all-time wording", () => {
const orgName = "ООО Альтернатива Плюс";
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "за все время",
assistantTurnMeaning: {
explicit_intent_candidate: "customer_revenue_and_payments"
},
followupContext: {
previous_discovery_loop_status: "awaiting_clarification",
previous_discovery_loop_selected_chain_id: "value_flow",
previous_discovery_loop_pending_axes: ["period"],
previous_discovery_loop_provided_axes: ["organization", "aggregate_axis", "amount", "coverage_target"],
previous_discovery_loop_asked_domain_family: "counterparty_value",
previous_discovery_loop_asked_action_family: "turnover",
previous_discovery_loop_unsupported_family: "counterparty_value_or_turnover",
previous_filters: {
organization: orgName
}
}
});
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.reason_codes).toContain("mcp_discovery_all_time_scope_signal_detected");
expect(result.reason_codes).toContain("mcp_discovery_period_clarification_followup_from_followup_context");
expect(result.reason_codes).not.toContain("mcp_discovery_not_applicable_for_supported_exact_turn");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_organization_scope: orgName,
unsupported_but_understood_family: "counterparty_value_or_turnover",
stale_replay_forbidden: true
});
expect(result.turn_meaning_ref?.explicit_date_scope).toBeUndefined();
expect(result.data_need_graph?.clarification_gaps).toEqual([]);
expect(result.data_need_graph?.time_scope_need).toBe("all_time_scope");
expect(result.data_need_graph?.reason_codes).toContain("data_need_graph_open_scope_total_without_subject");
expect(result.data_need_graph?.reason_codes).toContain("data_need_graph_all_time_scope_hint");
});
it("resumes an open-scope ranking from follow-up context when the user clarifies only the organization", () => { it("resumes an open-scope ranking from follow-up context when the user clarifies only the organization", () => {
const orgName = "ООО Альтернатива Плюс"; const orgName = "ООО Альтернатива Плюс";
const result = buildAssistantMcpDiscoveryTurnInput({ const result = buildAssistantMcpDiscoveryTurnInput({