ARCH: закрепить open-scope continuity для org clarification

This commit is contained in:
dctouch 2026-04-22 21:58:01 +03:00
parent 94e537210c
commit 8a0a4f0922
19 changed files with 981 additions and 29 deletions

View File

@ -0,0 +1,75 @@
{
"schema_version": "domain_truth_harness_spec_v1",
"scenario_id": "address_truth_harness_phase35_open_scope_org_clarification",
"domain": "address_phase35_open_scope_org_clarification",
"title": "Phase 35 open-scope organization clarification replay",
"description": "Targeted AGENT replay for Big Block D where a generic one-sided money total must ask for organization, then continue the same bounded open-scope value-flow chain after the user clarifies only the organization.",
"bindings": {},
"steps": [
{
"step_id": "step_01_generic_incoming_total_requires_organization",
"title": "Generic incoming total asks for organization instead of inventing a counterparty",
"question": "Сколько входящих денег за 2020 год?",
"allowed_reply_types": ["clarification_required", "partial_coverage"],
"required_answer_patterns_all": [
"(?i)уточн|нужно",
"(?i)организац"
],
"forbidden_answer_patterns": [
"(?i)уточните контрагента",
"(?i)не найден контрагент",
"(?i)по какому контрагенту",
"(?i)не найдено контрагента"
],
"criticality": "critical",
"semantic_tags": ["value_flow_total", "incoming", "open_scope", "organization_clarification", "bounded_autonomy"]
},
{
"step_id": "step_02_org_clarification_resumes_incoming_total",
"title": "Organization-only clarification resumes the same open-scope incoming total chain",
"question": "по ООО Альтернатива Плюс",
"allowed_reply_types": ["factual_with_explanation", "partial_coverage"],
"required_answer_patterns_all": [
"(?i)2020",
"(?i)входящ|получ|поступ",
"(?i)руб"
],
"required_answer_patterns_any": [
"(?i)альтернатива",
"(?i)проверенн|найденн"
],
"forbidden_answer_patterns": [
"(?i)уточните контрагента",
"(?i)не найден контрагент",
"(?i)по какому контрагенту",
"(?i)не найдено контрагента"
],
"criticality": "critical",
"semantic_tags": ["value_flow_total", "incoming", "organization_followup_reuse", "bounded_autonomy"]
},
{
"step_id": "step_03_outgoing_followup_reuses_org_and_period",
"title": "Short outgoing follow-up reuses the same organization and period without reintroducing a counterparty",
"question": "а исходящих?",
"allowed_reply_types": ["factual_with_explanation", "partial_coverage"],
"required_answer_patterns_all": [
"(?i)2020",
"(?i)исходящ|заплат|списан|платеж",
"(?i)руб"
],
"required_answer_patterns_any": [
"(?i)альтернатива",
"(?i)проверенн|найденн"
],
"forbidden_answer_patterns": [
"(?i)уточните контрагента",
"(?i)не найден контрагент",
"(?i)по какому контрагенту",
"(?i)не найдено контрагента",
"(?i)уточните организацию"
],
"criticality": "critical",
"semantic_tags": ["value_flow_total", "outgoing", "organization_followup_reuse", "bounded_autonomy"]
}
]
}

View File

@ -0,0 +1,99 @@
{
"schema_version": "domain_truth_harness_spec_v1",
"scenario_id": "address_truth_harness_phase36_open_scope_year_switch_after_org_clarification",
"domain": "address_phase36_open_scope_year_switch_after_org_clarification",
"title": "Phase 36 open-scope year switch after organization clarification",
"description": "Targeted AGENT replay for Big Block D where an open-scope value-flow total asks for organization, then preserves the same organization and money-flow contour across a short year-switch follow-up and a one-sided outgoing follow-up.",
"bindings": {},
"steps": [
{
"step_id": "step_01_generic_incoming_total_requires_organization",
"title": "Generic incoming total asks for organization before execution",
"question": "Сколько входящих денег за 2020 год?",
"allowed_reply_types": ["clarification_required", "partial_coverage"],
"required_answer_patterns_all": [
"(?i)уточн|нужно",
"(?i)организац"
],
"forbidden_answer_patterns": [
"(?i)уточните контрагента",
"(?i)не найден контрагент",
"(?i)по какому контрагенту",
"(?i)не найдено контрагента"
],
"criticality": "critical",
"semantic_tags": ["value_flow_total", "incoming", "open_scope", "organization_clarification", "bounded_autonomy"]
},
{
"step_id": "step_02_org_clarification_resumes_incoming_total",
"title": "Organization-only clarification resumes the same incoming total for 2020",
"question": "по ООО Альтернатива Плюс",
"allowed_reply_types": ["factual_with_explanation", "partial_coverage"],
"required_answer_patterns_all": [
"(?i)2020",
"(?i)входящ|получ|поступ",
"(?i)руб"
],
"required_answer_patterns_any": [
"(?i)альтернатива",
"(?i)проверенн|найденн"
],
"forbidden_answer_patterns": [
"(?i)уточните контрагента",
"(?i)не найден контрагент",
"(?i)по какому контрагенту",
"(?i)не найдено контрагента"
],
"criticality": "critical",
"semantic_tags": ["value_flow_total", "incoming", "organization_followup_reuse", "bounded_autonomy"]
},
{
"step_id": "step_03_year_switch_reuses_org_and_axis",
"title": "Short year-switch follow-up keeps the same organization and incoming contour",
"question": "а за 2021?",
"allowed_reply_types": ["factual_with_explanation", "partial_coverage"],
"required_answer_patterns_all": [
"(?i)2021",
"(?i)входящ|получ|поступ",
"(?i)руб"
],
"required_answer_patterns_any": [
"(?i)альтернатива",
"(?i)проверенн|найденн"
],
"forbidden_answer_patterns": [
"(?i)уточните контрагента",
"(?i)не найден контрагент",
"(?i)по какому контрагенту",
"(?i)не найдено контрагента",
"(?i)уточните организацию"
],
"criticality": "critical",
"semantic_tags": ["value_flow_total", "incoming", "year_switch", "organization_followup_reuse", "bounded_autonomy"]
},
{
"step_id": "step_04_outgoing_followup_reuses_org_and_new_year",
"title": "Outgoing follow-up keeps the same organization and already switched year",
"question": "а исходящих?",
"allowed_reply_types": ["factual_with_explanation", "partial_coverage"],
"required_answer_patterns_all": [
"(?i)2021",
"(?i)исходящ|заплат|списан|платеж",
"(?i)руб"
],
"required_answer_patterns_any": [
"(?i)альтернатива",
"(?i)проверенн|найденн"
],
"forbidden_answer_patterns": [
"(?i)уточните контрагента",
"(?i)не найден контрагент",
"(?i)по какому контрагенту",
"(?i)не найдено контрагента",
"(?i)уточните организацию"
],
"criticality": "critical",
"semantic_tags": ["value_flow_total", "outgoing", "year_switch", "organization_followup_reuse", "bounded_autonomy"]
}
]
}

View File

@ -0,0 +1,77 @@
{
"schema_version": "domain_truth_harness_spec_v1",
"scenario_id": "address_truth_harness_phase37_open_scope_comparison_org_clarification",
"domain": "address_phase37_open_scope_comparison_org_clarification",
"title": "Phase 37 open-scope comparison organization clarification",
"description": "Targeted AGENT replay for Big Block D where an open-scope incoming-vs-outgoing comparison must ask for organization, then resume the same comparison contour after an organization-only clarification and preserve it across a short year switch.",
"bindings": {},
"steps": [
{
"step_id": "step_01_open_scope_comparison_requires_organization",
"title": "Generic incoming-vs-outgoing comparison asks for organization first",
"question": "Что больше за 2020 год: входящих денег или исходящих?",
"allowed_reply_types": ["clarification_required", "partial_coverage"],
"required_answer_patterns_all": [
"(?i)уточн|нужно",
"(?i)организац"
],
"forbidden_answer_patterns": [
"(?i)уточните контрагента",
"(?i)не найден контрагент",
"(?i)по какому контрагенту",
"(?i)не найдено контрагента"
],
"criticality": "critical",
"semantic_tags": ["value_flow_comparison", "open_scope", "organization_clarification", "bounded_autonomy"]
},
{
"step_id": "step_02_org_clarification_resumes_comparison",
"title": "Organization-only clarification resumes the same comparison contour for 2020",
"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": ["value_flow_comparison", "organization_followup_reuse", "bounded_autonomy"]
},
{
"step_id": "step_03_year_switch_reuses_org_and_comparison",
"title": "Short year-switch follow-up keeps the same organization and comparison contour",
"question": "а за 2021?",
"allowed_reply_types": ["factual_with_explanation", "partial_coverage"],
"required_answer_patterns_all": [
"(?i)2021",
"(?i)входящ|исходящ|получ|заплат|списан|платеж",
"(?i)руб"
],
"required_answer_patterns_any": [
"(?i)больше|меньше|превыш|разниц",
"(?i)альтернатива",
"(?i)проверенн|найденн"
],
"forbidden_answer_patterns": [
"(?i)уточните контрагента",
"(?i)не найден контрагент",
"(?i)по какому контрагенту",
"(?i)не найдено контрагента",
"(?i)уточните организацию"
],
"criticality": "critical",
"semantic_tags": ["value_flow_comparison", "year_switch", "organization_followup_reuse", "bounded_autonomy"]
}
]
}

View File

@ -127,6 +127,14 @@ function explicitDateScope(pilot) {
const normalized = value.trim();
return normalized.length > 0 ? normalized : null;
}
function explicitOrganizationScope(pilot) {
const value = pilot.evidence.query_plan.turn_meaning_ref?.explicit_organization_scope;
if (typeof value !== "string") {
return null;
}
const normalized = value.trim();
return normalized.length > 0 ? normalized : null;
}
function documentOrMovementScopeRu(pilot) {
const entity = firstEntityCandidate(pilot);
const period = explicitDateScope(pilot);
@ -151,6 +159,12 @@ function isBidirectionalValueFlowComparisonClarification(pilot) {
return (pilot.reason_codes.includes("planner_selected_bidirectional_value_flow_comparison_from_data_need_graph") ||
pilot.dry_run.reason_codes.includes("planner_selected_bidirectional_value_flow_comparison_from_data_need_graph"));
}
function isOpenScopeValueFlowClarification(pilot) {
return (pilot.reason_codes.includes("planner_selected_open_scope_value_flow_total_from_data_need_graph") ||
pilot.reason_codes.includes("planner_selected_monthly_open_scope_value_flow_total_from_data_need_graph") ||
pilot.dry_run.reason_codes.includes("planner_selected_open_scope_value_flow_total_from_data_need_graph") ||
pilot.dry_run.reason_codes.includes("planner_selected_monthly_open_scope_value_flow_total_from_data_need_graph"));
}
function isDocumentLaneClarification(pilot) {
return (isDocumentPilot(pilot) ||
pilot.reason_codes.includes("planner_selected_document_recipe") ||
@ -172,6 +186,16 @@ function dryRunMissingAxis(pilot, axis) {
return pilot.dry_run.execution_steps.some((step) => step.missing_axis_options.some((option) => option.includes(axis)));
}
function clarificationNeedRu(pilot) {
const organizationScopedOpenTotal = pilot.reason_codes.includes("data_need_graph_open_scope_total_needs_organization") ||
pilot.dry_run.reason_codes.includes("data_need_graph_open_scope_total_needs_organization") ||
pilot.reason_codes.includes("planner_requires_organization_scope_from_data_need_graph") ||
pilot.dry_run.reason_codes.includes("planner_requires_organization_scope_from_data_need_graph");
if (organizationScopedOpenTotal) {
return {
subject: "\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\u0438\u044e",
verb: "\u043d\u0443\u0436\u043d\u043e"
};
}
const hasCounterparty = dryRunHasAxis(pilot, "counterparty");
const hasAccount = dryRunHasAxis(pilot, "account");
const needsPeriod = dryRunMissingAxis(pilot, "period");
@ -188,9 +212,16 @@ function clarificationNeedRu(pilot) {
return { subject: "контекст проверки", verb: "нужно" };
}
function clarificationNextStepLine(pilot, laneLabel) {
const organizationScopedOpenTotal = pilot.reason_codes.includes("data_need_graph_open_scope_total_needs_organization") ||
pilot.dry_run.reason_codes.includes("data_need_graph_open_scope_total_needs_organization") ||
pilot.reason_codes.includes("planner_requires_organization_scope_from_data_need_graph") ||
pilot.dry_run.reason_codes.includes("planner_requires_organization_scope_from_data_need_graph");
const needsPeriod = dryRunMissingAxis(pilot, "period");
const needsOrganization = dryRunMissingAxis(pilot, "organization");
const scopeSuffix = laneScopeSuffix(pilot);
if (organizationScopedOpenTotal && !needsPeriod) {
return `Уточните организацию, и я продолжу поиск по ${laneLabel}${scopeSuffix} в 1С.`;
}
if (needsPeriod && needsOrganization) {
return `Уточните период и организацию, и я продолжу поиск по ${laneLabel}${scopeSuffix} в 1С.`;
}
@ -298,6 +329,10 @@ function headlineFor(mode, pilot) {
const need = clarificationNeedRu(pilot);
return `Могу посчитать ranking по денежному потоку между контрагентами, но для bounded поиска в 1С ${need.verb} ${need.subject}.`;
}
if (mode === "needs_clarification" && isOpenScopeValueFlowClarification(pilot)) {
const need = clarificationNeedRu(pilot);
return `Могу посчитать общий денежный поток в проверяемом окне, но для bounded поиска в 1С ${need.verb} ${need.subject}.`;
}
if (mode === "needs_clarification") {
return "Нужно уточнить контекст перед поиском в 1С.";
}
@ -337,6 +372,9 @@ function nextStepFor(mode, pilot) {
if (mode === "needs_clarification" && isRankedValueFlowClarification(pilot)) {
return clarificationNextStepLine(pilot, "ranking-поиску между контрагентами");
}
if (mode === "needs_clarification" && isOpenScopeValueFlowClarification(pilot)) {
return clarificationNextStepLine(pilot, "денежному потоку");
}
if (mode === "needs_clarification") {
return "Уточните контрагента, период или организацию, и я смогу выполнить проверку по 1С.";
}
@ -541,6 +579,8 @@ function derivedValueFlowConfirmedLine(pilot) {
if (!flow) {
return null;
}
const organizationScope = explicitOrganizationScope(pilot);
const organization = organizationScope ? ` по организации ${organizationScope}` : "";
const counterparty = flow.counterparty ? ` по контрагенту ${flow.counterparty}` : "";
const period = flow.period_scope ? ` за период ${flow.period_scope}` : " в проверенном окне";
const movementLabel = flow.value_flow_direction === "outgoing_supplier_payout"

View File

@ -85,6 +85,30 @@ function comparisonNeedFor(action) {
}
return null;
}
function hasOpenScopeOneSidedValueTotalHint(rawUtterance, action) {
if (!rawUtterance) {
return false;
}
if (action === "turnover") {
return /(?:\bсколько\s+(?:мы\s+)?(?:получили|получено|входящих(?:\s+денег)?|поступлений|денег\s+пришло)\b|(?:сумма|объем)\s+(?:входящих|поступлений)|поступлений\s+за\b)/iu.test(rawUtterance);
}
if (action === "payout") {
return /(?:\bсколько\s+(?:мы\s+)?(?:заплатили|выплатили|потратили|исходящих(?:\s+денег)?|платежей|списаний)\b|(?:сумма|объем)\s+(?:исходящих|платежей|списаний)|(?:платежей|списаний)\s+за\b)/iu.test(rawUtterance);
}
return false;
}
function hasOpenScopeOneSidedValueTotalHintUtf8Safe(rawUtterance, action) {
if (!rawUtterance) {
return false;
}
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);
}
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 false;
}
function supportsOrganizationScopedOpenTotal(action) {
return action === "turnover" || action === "payout";
}
@ -95,7 +119,7 @@ function allowsOpenScopeWithoutSubject(input) {
if (input.rankingNeed || input.comparisonNeed === "incoming_vs_outgoing") {
return true;
}
return Boolean(input.organizationScope && supportsOrganizationScopedOpenTotal(input.action));
return Boolean(supportsOrganizationScopedOpenTotal(input.action) && (input.organizationScope || input.oneSidedOpenScopeTotalHint));
}
function rankingNeedFromRawUtterance(value) {
const text = lower(value);
@ -229,19 +253,30 @@ function buildAssistantMcpDiscoveryDataNeedGraph(input) {
const aggregationNeed = aggregationNeedFor(aggregationAxis);
const comparisonNeed = comparisonNeedFor(action);
const rankingNeed = rankingNeedFromRawUtterance(rawUtterance);
const oneSidedOpenScopeTotalHint = hasOpenScopeOneSidedValueTotalHintUtf8Safe(rawUtterance, action);
const openScopeWithoutSubject = subjectCandidates.length === 0 &&
allowsOpenScopeWithoutSubject({
family: businessFactFamily,
action,
organizationScope: explicitOrganizationScope,
comparisonNeed,
rankingNeed
rankingNeed,
oneSidedOpenScopeTotalHint
});
const clarificationGaps = [];
if (unsupported === "metadata_lane_choice_clarification" || action === "resolve_next_lane") {
pushUnique(clarificationGaps, "lane_family_choice");
}
if (subjectCandidates.length === 0 && businessFactFamily !== "schema_surface" && !openScopeWithoutSubject) {
if (subjectCandidates.length === 0 &&
businessFactFamily === "value_flow" &&
openScopeWithoutSubject &&
!rankingNeed &&
!comparisonNeed &&
oneSidedOpenScopeTotalHint &&
!explicitOrganizationScope) {
pushUnique(clarificationGaps, "organization");
}
else if (subjectCandidates.length === 0 && businessFactFamily !== "schema_surface" && !openScopeWithoutSubject) {
pushUnique(clarificationGaps, "subject");
}
const timeScopeNeed = timeScopeNeedFor({
@ -279,6 +314,9 @@ function buildAssistantMcpDiscoveryDataNeedGraph(input) {
if (openScopeWithoutSubject && !rankingNeed && !comparisonNeed) {
pushReason(reasonCodes, "data_need_graph_open_scope_total_without_subject");
}
if (clarificationGaps.includes("organization")) {
pushReason(reasonCodes, "data_need_graph_open_scope_total_needs_organization");
}
if (clarificationGaps.length > 0) {
pushReason(reasonCodes, "data_need_graph_has_clarification_gaps");
}

View File

@ -41,6 +41,9 @@ function hasEntity(meaning) {
function hasSubjectCandidates(graph) {
return (graph?.subject_candidates.length ?? 0) > 0;
}
function hasReasonCode(graph, reasonCode) {
return (graph?.reason_codes ?? []).includes(reasonCode);
}
function aggregationAxis(meaning) {
return toNonEmptyString(meaning?.asked_aggregation_axis)?.toLowerCase() ?? null;
}
@ -87,6 +90,9 @@ function recipeFor(input) {
const graphAggregation = lower(dataNeedGraph?.aggregation_need);
const graphClarificationGaps = (dataNeedGraph?.clarification_gaps ?? []).map((item) => lower(item));
const organizationScope = toNonEmptyString(meaning?.explicit_organization_scope);
const openScopeTotalWithoutSubject = graphFactFamily === "value_flow" &&
!hasSubjectCandidates(dataNeedGraph) &&
hasReasonCode(dataNeedGraph, "data_need_graph_open_scope_total_without_subject");
const combined = `${domain} ${action} ${unsupported}`.trim();
const axes = [];
const requestedAggregationAxis = aggregationAxis(meaning);
@ -133,7 +139,8 @@ function recipeFor(input) {
: "planner_selected_top_ranked_value_flow_from_data_need_graph"
};
}
if (!hasSubjectCandidates(dataNeedGraph) && organizationScope) {
if (openScopeTotalWithoutSubject) {
pushUnique(axes, "organization");
pushUnique(axes, "aggregate_axis");
pushUnique(axes, "amount");
pushUnique(axes, "coverage_target");
@ -354,7 +361,27 @@ function planAssistantMcpDiscovery(input) {
maxProbeCount: budgetOverride.maxProbeCount
});
const review = (0, assistantMcpCatalogIndex_1.reviewAssistantMcpDiscoveryPlanAgainstCatalog)(plan);
const plannerStatus = statusFrom(plan, review);
const organizationClarificationRequired = (dataNeedGraph?.clarification_gaps ?? []).includes("organization") &&
!toNonEmptyString(input.turnMeaning?.explicit_organization_scope);
const adjustedReview = organizationClarificationRequired && recipe.primitives.includes("query_movements")
? {
...review,
review_status: "needs_more_axes",
missing_axes_by_primitive: {
...review.missing_axes_by_primitive,
query_movements: review.missing_axes_by_primitive.query_movements?.length
? review.missing_axes_by_primitive.query_movements
: [["organization"]]
},
reason_codes: review.reason_codes.includes("catalog_requires_organization_scope_from_data_need_graph")
? review.reason_codes
: [...review.reason_codes, "catalog_requires_organization_scope_from_data_need_graph"]
}
: review;
const plannerStatus = organizationClarificationRequired ? "needs_clarification" : statusFrom(plan, adjustedReview);
if (organizationClarificationRequired) {
pushReason(reasonCodes, "planner_requires_organization_scope_from_data_need_graph");
}
if (plannerStatus === "ready_for_execution") {
pushReason(reasonCodes, "planner_ready_for_guarded_mcp_execution");
}
@ -375,7 +402,7 @@ function planAssistantMcpDiscovery(input) {
proposed_primitives: recipe.primitives,
required_axes: recipe.axes,
discovery_plan: plan,
catalog_review: review,
catalog_review: adjustedReview,
reason_codes: reasonCodes
};
}

View File

@ -138,6 +138,19 @@ function readDiscoveryTurnMeaning(entryPoint) {
const turnInput = toRecordObject(entryPoint?.turn_input);
return toRecordObject(turnInput?.turn_meaning_ref);
}
function readDiscoveryDataNeedGraph(entryPoint) {
const turnInput = toRecordObject(entryPoint?.turn_input);
return toRecordObject(turnInput?.data_need_graph);
}
function isOpenScopeValueFlowWithoutSubject(entryPoint) {
const graph = readDiscoveryDataNeedGraph(entryPoint);
const businessFactFamily = toNonEmptyString(graph?.business_fact_family);
const subjectCandidates = Array.isArray(graph?.subject_candidates) ? graph.subject_candidates : [];
const reasonCodes = Array.isArray(graph?.reason_codes) ? graph.reason_codes : [];
return (businessFactFamily === "value_flow" &&
subjectCandidates.length === 0 &&
reasonCodes.some((reason) => toNonEmptyString(reason) === "data_need_graph_open_scope_total_without_subject"));
}
function readTruthAnswerShape(input) {
const directShape = toRecordObject(input.addressRuntimeMeta?.answer_shape_contract);
if (directShape) {
@ -197,6 +210,9 @@ function hasAlignedFactualAddressReply(input, entryPoint) {
if (!hasEffectivelyFactualAddressReply(input)) {
return false;
}
if (hasSemanticConflictWithDiscoveryTurnMeaning(input, entryPoint)) {
return false;
}
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
return isDetectedIntentAlignedWithTurnMeaning(detectedIntent, readDiscoveryTurnMeaning(entryPoint));
}
@ -218,6 +234,10 @@ function hasSemanticConflictWithDiscoveryTurnMeaning(input, entryPoint) {
if (!detectedIntent || (!askedDomain && !askedAction && !unsupportedFamily)) {
return false;
}
if (detectedIntent === "customer_revenue_and_payments" &&
isOpenScopeValueFlowWithoutSubject(entryPoint)) {
return true;
}
return !isDetectedIntentAlignedWithTurnMeaning(detectedIntent, turnMeaning);
}
function hasMatchedFactualAddressContinuationTarget(input, entryPoint) {

View File

@ -313,6 +313,17 @@ function hasOrganizationScopeSignalUtf8(text) {
/\b(?:llc|inc|corp|company|organization|organisation)\b/iu.test(text) ||
/(?:\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446|\u043a\u043e\u043c\u043f\u0430\u043d)/iu.test(text));
}
function extractOrganizationScopeFromRawText(value) {
const text = toNonEmptyString(value);
if (!text) {
return null;
}
const match = text.match(/(?:^|[\s,;:])(?:\u043f\u043e|for|in|within)?\s*((?:\u041e\u041e\u041e|\u0418\u041f|\u0410\u041e|\u041f\u0410\u041e|\u0417\u0410\u041e|LLC|Inc|LTD|Corp)\s+[^\n,.;:!?]+)/u);
if (!match?.[1]) {
return null;
}
return toNonEmptyString(match[1]);
}
function hasMonthlyAggregationSignal(text) {
return /(?:\u043f\u043e\s+\u043c\u0435\u0441\u044f\u0446\u0430\u043c|\u043f\u043e\u043c\u0435\u0441\u044f\u0447\u043d\u043e|\u0435\u0436\u0435\u043c\u0435\u0441\u044f\u0447\u043d\u043e|month\s+by\s+month|by\s+month|monthly)/iu.test(text);
}
@ -590,12 +601,25 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
const explicitIntentCandidate = toNonEmptyString(assistantTurnMeaning?.explicit_intent_candidate);
const assistantTurnMeaningDateScope = toNonEmptyString(assistantTurnMeaning?.explicit_date_scope);
const assistantTurnMeaningOrganizationScope = toNonEmptyString(assistantTurnMeaning?.explicit_organization_scope);
const rawOrganizationMentionSignal = hasOrganizationScopeSignalUtf8(rawText);
const rawOrganizationScope = extractOrganizationScopeFromRawText(rawUserText ?? rawEffectiveText ?? rawSignalSourceText);
const explicitOrganizationScopeSignal = Boolean(rawOrganizationMentionSignal &&
(rawOrganizationScope ?? predecomposeEntities.organization ?? assistantTurnMeaningOrganizationScope));
const organizationClarificationFollowupApplicable = Boolean(followupSeed.domain === "counterparty_value" &&
!followupSeed.counterparty &&
rawOrganizationMentionSignal &&
(rawOrganizationScope || followupSeed.organization) &&
!rawLifecycleSignal &&
!rawValueFlowSignal &&
!rawMetadataSignal);
const rawOpenScopeValueFlowOrganizationSignal = Boolean(rawValueFlowSignal &&
!rawBidirectionalValueFlowSignal &&
hasOrganizationScopeSignalUtf8(rawText) &&
(predecomposeEntities.organization ?? assistantTurnMeaningOrganizationScope));
explicitOrganizationScopeSignal);
const predecomposeOrganizationMirrorsCounterparty = sameScopedName(predecomposeEntities.counterparty, predecomposeEntities.organization);
const organizationMirrorsPredecomposeCounterparty = Boolean((rawBidirectionalValueFlowSignal || hasValueRankingSignal(rawText) || rawOpenScopeValueFlowOrganizationSignal) &&
const organizationMirrorsPredecomposeCounterparty = Boolean((rawBidirectionalValueFlowSignal ||
hasValueRankingSignal(rawText) ||
rawOpenScopeValueFlowOrganizationSignal ||
explicitOrganizationScopeSignal) &&
(sameScopedName(predecomposeEntities.counterparty, assistantTurnMeaningOrganizationScope) ||
predecomposeOrganizationMirrorsCounterparty));
const normalizedPredecomposeCounterparty = organizationMirrorsPredecomposeCounterparty
@ -605,7 +629,11 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
const followupDiscoverySeedApplicable = Boolean(followupSeed.domain &&
!rawLifecycleSignal &&
!rawValueFlowSignal &&
(monthlyAggregationSignal || explicitDateScopeLiteralDetected || predecomposeDateScope));
(monthlyAggregationSignal ||
explicitDateScopeLiteralDetected ||
predecomposeDateScope ||
explicitOrganizationScopeSignal ||
organizationClarificationFollowupApplicable));
const metadataFollowupSeedApplicable = Boolean(followupSeed.domain === "metadata" &&
!rawLifecycleSignal &&
!rawValueFlowSignal &&
@ -844,13 +872,17 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
Boolean(bidirectionalValueFlowSignal ||
hasValueRankingSignal(rawText) ||
rawOpenScopeValueFlowOrganizationSignal ||
explicitOrganizationScopeSignal ||
followupSeed.organization);
if (openScopeValueFlowWithoutCounterparty && !valueFlowOrganizationStaysScope) {
pushUnique(entityCandidates, predecomposeEntities.organization);
pushUnique(entityCandidates, followupSeed.organization);
}
const explicitOrganizationScope = valueFlowOrganizationStaysScope || !openScopeValueFlowWithoutCounterparty
? predecomposeEntities.organization ?? assistantTurnMeaningOrganizationScope ?? followupSeed.organization
? rawOrganizationScope ??
predecomposeEntities.organization ??
assistantTurnMeaningOrganizationScope ??
followupSeed.organization
: null;
if (valueFlowOrganizationStaysScope && explicitOrganizationScope) {
for (let index = entityCandidates.length - 1; index >= 0; index -= 1) {
@ -976,6 +1008,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
metadataGroundedDocumentLaneApplicable ||
groundedValueFlowFollowupApplicable,
forceDiscoveryOverExplicitIntent: Boolean(entityResolutionClarificationCandidate) ||
organizationClarificationFollowupApplicable ||
metadataAmbiguityLaneClarificationApplicable ||
metadataGroundedMovementLaneApplicable ||
metadataGroundedDocumentLaneApplicable ||
@ -1025,6 +1058,9 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
if (entityResolutionClarificationCandidate) {
pushReason(reasonCodes, "mcp_discovery_entity_resolution_clarification_candidate_selected");
}
if (organizationClarificationFollowupApplicable) {
pushReason(reasonCodes, "mcp_discovery_organization_clarification_followup_from_followup_context");
}
if (payoutSignal) {
pushReason(reasonCodes, "mcp_discovery_payout_signal_detected");
}

View File

@ -174,6 +174,15 @@ function explicitDateScope(pilot: AssistantMcpDiscoveryPilotExecutionContract):
return normalized.length > 0 ? normalized : null;
}
function explicitOrganizationScope(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
const value = pilot.evidence.query_plan.turn_meaning_ref?.explicit_organization_scope;
if (typeof value !== "string") {
return null;
}
const normalized = value.trim();
return normalized.length > 0 ? normalized : null;
}
function documentOrMovementScopeRu(pilot: AssistantMcpDiscoveryPilotExecutionContract): string {
const entity = firstEntityCandidate(pilot);
const period = explicitDateScope(pilot);
@ -210,6 +219,15 @@ function isBidirectionalValueFlowComparisonClarification(
);
}
function isOpenScopeValueFlowClarification(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
return (
pilot.reason_codes.includes("planner_selected_open_scope_value_flow_total_from_data_need_graph") ||
pilot.reason_codes.includes("planner_selected_monthly_open_scope_value_flow_total_from_data_need_graph") ||
pilot.dry_run.reason_codes.includes("planner_selected_open_scope_value_flow_total_from_data_need_graph") ||
pilot.dry_run.reason_codes.includes("planner_selected_monthly_open_scope_value_flow_total_from_data_need_graph")
);
}
function isDocumentLaneClarification(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
return (
isDocumentPilot(pilot) ||
@ -241,6 +259,17 @@ function dryRunMissingAxis(pilot: AssistantMcpDiscoveryPilotExecutionContract, a
function clarificationNeedRu(
pilot: AssistantMcpDiscoveryPilotExecutionContract
): { subject: string; verb: string } {
const organizationScopedOpenTotal =
pilot.reason_codes.includes("data_need_graph_open_scope_total_needs_organization") ||
pilot.dry_run.reason_codes.includes("data_need_graph_open_scope_total_needs_organization") ||
pilot.reason_codes.includes("planner_requires_organization_scope_from_data_need_graph") ||
pilot.dry_run.reason_codes.includes("planner_requires_organization_scope_from_data_need_graph");
if (organizationScopedOpenTotal) {
return {
subject: "\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\u0438\u044e",
verb: "\u043d\u0443\u0436\u043d\u043e"
};
}
const hasCounterparty = dryRunHasAxis(pilot, "counterparty");
const hasAccount = dryRunHasAxis(pilot, "account");
const needsPeriod = dryRunMissingAxis(pilot, "period");
@ -261,9 +290,17 @@ function clarificationNextStepLine(
pilot: AssistantMcpDiscoveryPilotExecutionContract,
laneLabel: string
): string {
const organizationScopedOpenTotal =
pilot.reason_codes.includes("data_need_graph_open_scope_total_needs_organization") ||
pilot.dry_run.reason_codes.includes("data_need_graph_open_scope_total_needs_organization") ||
pilot.reason_codes.includes("planner_requires_organization_scope_from_data_need_graph") ||
pilot.dry_run.reason_codes.includes("planner_requires_organization_scope_from_data_need_graph");
const needsPeriod = dryRunMissingAxis(pilot, "period");
const needsOrganization = dryRunMissingAxis(pilot, "organization");
const scopeSuffix = laneScopeSuffix(pilot);
if (organizationScopedOpenTotal && !needsPeriod) {
return `Уточните организацию, и я продолжу поиск по ${laneLabel}${scopeSuffix} в 1С.`;
}
if (needsPeriod && needsOrganization) {
return `Уточните период и организацию, и я продолжу поиск по ${laneLabel}${scopeSuffix} в 1С.`;
}
@ -378,6 +415,10 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
const need = clarificationNeedRu(pilot);
return `Могу посчитать ranking по денежному потоку между контрагентами, но для bounded поиска в 1С ${need.verb} ${need.subject}.`;
}
if (mode === "needs_clarification" && isOpenScopeValueFlowClarification(pilot)) {
const need = clarificationNeedRu(pilot);
return `Могу посчитать общий денежный поток в проверяемом окне, но для bounded поиска в 1С ${need.verb} ${need.subject}.`;
}
if (mode === "needs_clarification") {
return "Нужно уточнить контекст перед поиском в 1С.";
}
@ -422,6 +463,9 @@ function nextStepFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
if (mode === "needs_clarification" && isRankedValueFlowClarification(pilot)) {
return clarificationNextStepLine(pilot, "ranking-поиску между контрагентами");
}
if (mode === "needs_clarification" && isOpenScopeValueFlowClarification(pilot)) {
return clarificationNextStepLine(pilot, "денежному потоку");
}
if (mode === "needs_clarification") {
return "Уточните контрагента, период или организацию, и я смогу выполнить проверку по 1С.";
}
@ -646,6 +690,8 @@ function derivedValueFlowConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecutio
if (!flow) {
return null;
}
const organizationScope = explicitOrganizationScope(pilot);
const organization = organizationScope ? ` по организации ${organizationScope}` : "";
const counterparty = flow.counterparty ? ` по контрагенту ${flow.counterparty}` : "";
const period = flow.period_scope ? ` за период ${flow.period_scope}` : " в проверенном окне";
const movementLabel =

View File

@ -132,6 +132,40 @@ function comparisonNeedFor(action: string): string | null {
return null;
}
function hasOpenScopeOneSidedValueTotalHint(rawUtterance: string, action: string): boolean {
if (!rawUtterance) {
return false;
}
if (action === "turnover") {
return /(?:\bсколько\s+(?:мы\s+)?(?:получили|получено|входящих(?:\s+денег)?|поступлений|денег\s+пришло)\b|(?:сумма|объем)\s+(?:входящих|поступлений)|поступлений\s+за\b)/iu.test(
rawUtterance
);
}
if (action === "payout") {
return /(?:\bсколько\s+(?:мы\s+)?(?:заплатили|выплатили|потратили|исходящих(?:\s+денег)?|платежей|списаний)\b|(?:сумма|объем)\s+(?:исходящих|платежей|списаний)|(?:платежей|списаний)\s+за\b)/iu.test(
rawUtterance
);
}
return false;
}
function hasOpenScopeOneSidedValueTotalHintUtf8Safe(rawUtterance: string, action: string): boolean {
if (!rawUtterance) {
return false;
}
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
);
}
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 false;
}
function supportsOrganizationScopedOpenTotal(action: string): boolean {
return action === "turnover" || action === "payout";
}
@ -142,6 +176,7 @@ function allowsOpenScopeWithoutSubject(input: {
organizationScope: string | null;
comparisonNeed: string | null;
rankingNeed: string | null;
oneSidedOpenScopeTotalHint: boolean;
}): boolean {
if (input.family !== "value_flow") {
return false;
@ -149,7 +184,9 @@ function allowsOpenScopeWithoutSubject(input: {
if (input.rankingNeed || input.comparisonNeed === "incoming_vs_outgoing") {
return true;
}
return Boolean(input.organizationScope && supportsOrganizationScopedOpenTotal(input.action));
return Boolean(
supportsOrganizationScopedOpenTotal(input.action) && (input.organizationScope || input.oneSidedOpenScopeTotalHint)
);
}
function rankingNeedFromRawUtterance(value: string): string | null {
@ -303,6 +340,7 @@ export function buildAssistantMcpDiscoveryDataNeedGraph(
const aggregationNeed = aggregationNeedFor(aggregationAxis);
const comparisonNeed = comparisonNeedFor(action);
const rankingNeed = rankingNeedFromRawUtterance(rawUtterance);
const oneSidedOpenScopeTotalHint = hasOpenScopeOneSidedValueTotalHintUtf8Safe(rawUtterance, action);
const openScopeWithoutSubject =
subjectCandidates.length === 0 &&
allowsOpenScopeWithoutSubject({
@ -310,13 +348,24 @@ export function buildAssistantMcpDiscoveryDataNeedGraph(
action,
organizationScope: explicitOrganizationScope,
comparisonNeed,
rankingNeed
rankingNeed,
oneSidedOpenScopeTotalHint
});
const clarificationGaps: string[] = [];
if (unsupported === "metadata_lane_choice_clarification" || action === "resolve_next_lane") {
pushUnique(clarificationGaps, "lane_family_choice");
}
if (subjectCandidates.length === 0 && businessFactFamily !== "schema_surface" && !openScopeWithoutSubject) {
if (
subjectCandidates.length === 0 &&
businessFactFamily === "value_flow" &&
openScopeWithoutSubject &&
!rankingNeed &&
!comparisonNeed &&
oneSidedOpenScopeTotalHint &&
!explicitOrganizationScope
) {
pushUnique(clarificationGaps, "organization");
} else if (subjectCandidates.length === 0 && businessFactFamily !== "schema_surface" && !openScopeWithoutSubject) {
pushUnique(clarificationGaps, "subject");
}
const timeScopeNeed = timeScopeNeedFor({
@ -353,6 +402,9 @@ export function buildAssistantMcpDiscoveryDataNeedGraph(
if (openScopeWithoutSubject && !rankingNeed && !comparisonNeed) {
pushReason(reasonCodes, "data_need_graph_open_scope_total_without_subject");
}
if (clarificationGaps.includes("organization")) {
pushReason(reasonCodes, "data_need_graph_open_scope_total_needs_organization");
}
if (clarificationGaps.length > 0) {
pushReason(reasonCodes, "data_need_graph_has_clarification_gaps");
}

View File

@ -102,6 +102,13 @@ function hasSubjectCandidates(graph: AssistantMcpDiscoveryDataNeedGraphContract
return (graph?.subject_candidates.length ?? 0) > 0;
}
function hasReasonCode(
graph: AssistantMcpDiscoveryDataNeedGraphContract | null | undefined,
reasonCode: string
): boolean {
return (graph?.reason_codes ?? []).includes(reasonCode);
}
function aggregationAxis(meaning: AssistantMcpDiscoveryTurnMeaningRef | null | undefined): string | null {
return toNonEmptyString(meaning?.asked_aggregation_axis)?.toLowerCase() ?? null;
}
@ -154,6 +161,10 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
const graphAggregation = lower(dataNeedGraph?.aggregation_need);
const graphClarificationGaps = (dataNeedGraph?.clarification_gaps ?? []).map((item) => lower(item));
const organizationScope = toNonEmptyString(meaning?.explicit_organization_scope);
const openScopeTotalWithoutSubject =
graphFactFamily === "value_flow" &&
!hasSubjectCandidates(dataNeedGraph) &&
hasReasonCode(dataNeedGraph, "data_need_graph_open_scope_total_without_subject");
const combined = `${domain} ${action} ${unsupported}`.trim();
const axes: string[] = [];
const requestedAggregationAxis = aggregationAxis(meaning);
@ -205,7 +216,8 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
: "planner_selected_top_ranked_value_flow_from_data_need_graph"
};
}
if (!hasSubjectCandidates(dataNeedGraph) && organizationScope) {
if (openScopeTotalWithoutSubject) {
pushUnique(axes, "organization");
pushUnique(axes, "aggregate_axis");
pushUnique(axes, "amount");
pushUnique(axes, "coverage_target");
@ -451,7 +463,30 @@ export function planAssistantMcpDiscovery(
maxProbeCount: budgetOverride.maxProbeCount
});
const review = reviewAssistantMcpDiscoveryPlanAgainstCatalog(plan);
const plannerStatus = statusFrom(plan, review);
const organizationClarificationRequired =
(dataNeedGraph?.clarification_gaps ?? []).includes("organization") &&
!toNonEmptyString(input.turnMeaning?.explicit_organization_scope);
const adjustedReview =
organizationClarificationRequired && recipe.primitives.includes("query_movements")
? {
...review,
review_status: "needs_more_axes" as const,
missing_axes_by_primitive: {
...review.missing_axes_by_primitive,
query_movements: review.missing_axes_by_primitive.query_movements?.length
? review.missing_axes_by_primitive.query_movements
: [["organization"]]
},
reason_codes: review.reason_codes.includes("catalog_requires_organization_scope_from_data_need_graph")
? review.reason_codes
: [...review.reason_codes, "catalog_requires_organization_scope_from_data_need_graph"]
}
: review;
const plannerStatus = organizationClarificationRequired ? "needs_clarification" : statusFrom(plan, adjustedReview);
if (organizationClarificationRequired) {
pushReason(reasonCodes, "planner_requires_organization_scope_from_data_need_graph");
}
if (plannerStatus === "ready_for_execution") {
pushReason(reasonCodes, "planner_ready_for_guarded_mcp_execution");
@ -467,12 +502,12 @@ export function planAssistantMcpDiscovery(
planner_status: plannerStatus,
semantic_data_need: semanticDataNeed,
data_need_graph: dataNeedGraph,
selected_chain_id: recipe.chainId,
selected_chain_summary: recipe.chainSummary,
proposed_primitives: recipe.primitives,
required_axes: recipe.axes,
discovery_plan: plan,
catalog_review: review,
reason_codes: reasonCodes
};
selected_chain_id: recipe.chainId,
selected_chain_summary: recipe.chainSummary,
proposed_primitives: recipe.primitives,
required_axes: recipe.axes,
discovery_plan: plan,
catalog_review: adjustedReview,
reason_codes: reasonCodes
};
}

View File

@ -214,6 +214,27 @@ function readDiscoveryTurnMeaning(
return toRecordObject(turnInput?.turn_meaning_ref);
}
function readDiscoveryDataNeedGraph(
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
): Record<string, unknown> | null {
const turnInput = toRecordObject(entryPoint?.turn_input);
return toRecordObject(turnInput?.data_need_graph);
}
function isOpenScopeValueFlowWithoutSubject(
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
): boolean {
const graph = readDiscoveryDataNeedGraph(entryPoint);
const businessFactFamily = toNonEmptyString(graph?.business_fact_family);
const subjectCandidates = Array.isArray(graph?.subject_candidates) ? graph.subject_candidates : [];
const reasonCodes = Array.isArray(graph?.reason_codes) ? graph.reason_codes : [];
return (
businessFactFamily === "value_flow" &&
subjectCandidates.length === 0 &&
reasonCodes.some((reason) => toNonEmptyString(reason) === "data_need_graph_open_scope_total_without_subject")
);
}
function readTruthAnswerShape(input: ApplyAssistantMcpDiscoveryResponsePolicyInput): Record<string, unknown> | null {
const directShape = toRecordObject(input.addressRuntimeMeta?.answer_shape_contract);
if (directShape) {
@ -286,6 +307,9 @@ function hasAlignedFactualAddressReply(
if (!hasEffectivelyFactualAddressReply(input)) {
return false;
}
if (hasSemanticConflictWithDiscoveryTurnMeaning(input, entryPoint)) {
return false;
}
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
return isDetectedIntentAlignedWithTurnMeaning(detectedIntent, readDiscoveryTurnMeaning(entryPoint));
}
@ -311,6 +335,12 @@ function hasSemanticConflictWithDiscoveryTurnMeaning(
if (!detectedIntent || (!askedDomain && !askedAction && !unsupportedFamily)) {
return false;
}
if (
detectedIntent === "customer_revenue_and_payments" &&
isOpenScopeValueFlowWithoutSubject(entryPoint)
) {
return true;
}
return !isDetectedIntentAlignedWithTurnMeaning(detectedIntent, turnMeaning);
}

View File

@ -436,6 +436,20 @@ function hasOrganizationScopeSignalUtf8(text: string): boolean {
);
}
function extractOrganizationScopeFromRawText(value: unknown): string | null {
const text = toNonEmptyString(value);
if (!text) {
return null;
}
const match = text.match(
/(?:^|[\s,;:])(?:\u043f\u043e|for|in|within)?\s*((?:\u041e\u041e\u041e|\u0418\u041f|\u0410\u041e|\u041f\u0410\u041e|\u0417\u0410\u041e|LLC|Inc|LTD|Corp)\s+[^\n,.;:!?]+)/u
);
if (!match?.[1]) {
return null;
}
return toNonEmptyString(match[1]);
}
function hasMonthlyAggregationSignal(text: string): boolean {
return /(?:\u043f\u043e\s+\u043c\u0435\u0441\u044f\u0446\u0430\u043c|\u043f\u043e\u043c\u0435\u0441\u044f\u0447\u043d\u043e|\u0435\u0436\u0435\u043c\u0435\u0441\u044f\u0447\u043d\u043e|month\s+by\s+month|by\s+month|monthly)/iu.test(
text
@ -803,18 +817,35 @@ export function buildAssistantMcpDiscoveryTurnInput(
const explicitIntentCandidate = toNonEmptyString(assistantTurnMeaning?.explicit_intent_candidate);
const assistantTurnMeaningDateScope = toNonEmptyString(assistantTurnMeaning?.explicit_date_scope);
const assistantTurnMeaningOrganizationScope = toNonEmptyString(assistantTurnMeaning?.explicit_organization_scope);
const rawOrganizationMentionSignal = hasOrganizationScopeSignalUtf8(rawText);
const rawOrganizationScope = extractOrganizationScopeFromRawText(rawUserText ?? rawEffectiveText ?? rawSignalSourceText);
const explicitOrganizationScopeSignal = Boolean(
rawOrganizationMentionSignal &&
(rawOrganizationScope ?? predecomposeEntities.organization ?? assistantTurnMeaningOrganizationScope)
);
const organizationClarificationFollowupApplicable = Boolean(
followupSeed.domain === "counterparty_value" &&
!followupSeed.counterparty &&
rawOrganizationMentionSignal &&
(rawOrganizationScope || followupSeed.organization) &&
!rawLifecycleSignal &&
!rawValueFlowSignal &&
!rawMetadataSignal
);
const rawOpenScopeValueFlowOrganizationSignal = Boolean(
rawValueFlowSignal &&
!rawBidirectionalValueFlowSignal &&
hasOrganizationScopeSignalUtf8(rawText) &&
(predecomposeEntities.organization ?? assistantTurnMeaningOrganizationScope)
explicitOrganizationScopeSignal
);
const predecomposeOrganizationMirrorsCounterparty = sameScopedName(
predecomposeEntities.counterparty,
predecomposeEntities.organization
);
const organizationMirrorsPredecomposeCounterparty = Boolean(
(rawBidirectionalValueFlowSignal || hasValueRankingSignal(rawText) || rawOpenScopeValueFlowOrganizationSignal) &&
(rawBidirectionalValueFlowSignal ||
hasValueRankingSignal(rawText) ||
rawOpenScopeValueFlowOrganizationSignal ||
explicitOrganizationScopeSignal) &&
(sameScopedName(predecomposeEntities.counterparty, assistantTurnMeaningOrganizationScope) ||
predecomposeOrganizationMirrorsCounterparty)
);
@ -826,7 +857,11 @@ export function buildAssistantMcpDiscoveryTurnInput(
followupSeed.domain &&
!rawLifecycleSignal &&
!rawValueFlowSignal &&
(monthlyAggregationSignal || explicitDateScopeLiteralDetected || predecomposeDateScope)
(monthlyAggregationSignal ||
explicitDateScopeLiteralDetected ||
predecomposeDateScope ||
explicitOrganizationScopeSignal ||
organizationClarificationFollowupApplicable)
);
const metadataFollowupSeedApplicable = Boolean(
followupSeed.domain === "metadata" &&
@ -1114,6 +1149,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
bidirectionalValueFlowSignal ||
hasValueRankingSignal(rawText) ||
rawOpenScopeValueFlowOrganizationSignal ||
explicitOrganizationScopeSignal ||
followupSeed.organization
);
if (openScopeValueFlowWithoutCounterparty && !valueFlowOrganizationStaysScope) {
@ -1122,7 +1158,10 @@ export function buildAssistantMcpDiscoveryTurnInput(
}
const explicitOrganizationScope =
valueFlowOrganizationStaysScope || !openScopeValueFlowWithoutCounterparty
? predecomposeEntities.organization ?? assistantTurnMeaningOrganizationScope ?? followupSeed.organization
? rawOrganizationScope ??
predecomposeEntities.organization ??
assistantTurnMeaningOrganizationScope ??
followupSeed.organization
: null;
if (valueFlowOrganizationStaysScope && explicitOrganizationScope) {
for (let index = entityCandidates.length - 1; index >= 0; index -= 1) {
@ -1258,6 +1297,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
groundedValueFlowFollowupApplicable,
forceDiscoveryOverExplicitIntent:
Boolean(entityResolutionClarificationCandidate) ||
organizationClarificationFollowupApplicable ||
metadataAmbiguityLaneClarificationApplicable ||
metadataGroundedMovementLaneApplicable ||
metadataGroundedDocumentLaneApplicable ||
@ -1308,6 +1348,9 @@ export function buildAssistantMcpDiscoveryTurnInput(
if (entityResolutionClarificationCandidate) {
pushReason(reasonCodes, "mcp_discovery_entity_resolution_clarification_candidate_selected");
}
if (organizationClarificationFollowupApplicable) {
pushReason(reasonCodes, "mcp_discovery_organization_clarification_followup_from_followup_context");
}
if (payoutSignal) {
pushReason(reasonCodes, "mcp_discovery_payout_signal_detected");
}

View File

@ -137,4 +137,27 @@ describe("assistant MCP discovery data need graph", () => {
]);
expect(result.reason_codes).toContain("data_need_graph_open_scope_total_without_subject");
});
it("treats a generic incoming total as an understood open-scope ask that still needs organization", () => {
const result = buildAssistantMcpDiscoveryDataNeedGraph({
semanticDataNeed: "counterparty value-flow evidence",
rawUtterance: "сколько входящих денег за 2020 год?",
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_date_scope: "2020"
}
});
expect(result.business_fact_family).toBe("value_flow");
expect(result.subject_candidates).toEqual([]);
expect(result.clarification_gaps).toEqual(["organization"]);
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");
});
});

View File

@ -474,4 +474,42 @@ describe("assistant MCP discovery planner", () => {
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");
});
it("keeps generic one-sided open totals in organization clarification instead of forcing entity resolution", () => {
const result = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
business_fact_family: "value_flow",
action_family: "turnover",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: null,
ranking_need: null,
proof_expectation: "clarification_required",
clarification_gaps: ["organization"],
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_open_scope_total_needs_organization"
]
},
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_date_scope: "2020"
}
});
expect(result.planner_status).toBe("needs_clarification");
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(["period", "organization", "aggregate_axis", "amount", "coverage_target"]);
expect(result.catalog_review.review_status).toBe("needs_more_axes");
expect(result.catalog_review.missing_axes_by_primitive.query_movements).toContainEqual(["organization"]);
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");
});
});

View File

@ -161,6 +161,52 @@ describe("assistant MCP discovery response policy", () => {
expect(result.reason_codes).toContain("mcp_discovery_response_policy_keep_aligned_factual_address_reply");
});
it("does not treat an open-scope value-flow total as aligned with exact top-counterparty carryover", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: "\u0421\u0430\u043c\u044b\u0439 \u0434\u043e\u0445\u043e\u0434\u043d\u044b\u0439 \u043a\u043b\u0438\u0435\u043d\u0442: \u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a.",
currentReplySource: "address_query_runtime_v1",
currentReplyType: "factual",
addressRuntimeMeta: {
detected_intent: "customer_revenue_and_payments",
dialogContinuationContract: {
target_intent: "customer_revenue_and_payments"
},
truth_gate_contract_status: "full_confirmed",
assistant_truth_answer_policy_v1: {
truth_gate: {
coverage_status: "full",
grounding_status: "grounded",
source_truth_gate_status: "full_confirmed"
}
},
assistant_mcp_discovery_entry_point_v1: entryPoint({
turn_input: {
adapter_status: "ready",
should_run_discovery: true,
data_need_graph: {
business_fact_family: "value_flow",
subject_candidates: [],
reason_codes: ["data_need_graph_open_scope_total_without_subject"]
},
turn_meaning_ref: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_organization_scope: "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441",
explicit_date_scope: "2020"
}
}
})
}
});
expect(result.applied).toBe(true);
expect(result.decision).toBe("apply_candidate");
expect(result.reason_codes).toContain("mcp_discovery_response_policy_semantic_conflict_allows_candidate_override");
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_aligned_factual_address_reply");
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_factual_address_continuation_target");
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_full_confirmed_factual_address_reply");
});
it("keeps factual address follow-up replies when they already match the continuation target intent", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: "ИП Калинин Н.М. | сумма: 216600 | операций: 2",

View File

@ -316,4 +316,42 @@ describe("assistant MCP discovery runtime bridge", () => {
expect(result.answer_draft.confirmed_lines.join("\n")).toContain("5 000");
expect(result.answer_draft.confirmed_lines.join("\n")).not.toContain("контрагенту");
});
it("keeps generic one-sided open totals in organization clarification without asking for a counterparty", async () => {
const result = await runAssistantMcpDiscoveryRuntimeBridge({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
business_fact_family: "value_flow",
action_family: "turnover",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: null,
ranking_need: null,
proof_expectation: "clarification_required",
clarification_gaps: ["organization"],
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_open_scope_total_needs_organization"
]
},
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_date_scope: "2020"
},
deps: buildDeps([])
});
expect(result.bridge_status).toBe("needs_clarification");
expect(result.requires_user_clarification).toBe(true);
expect(result.planner.selected_chain_id).toBe("value_flow");
expect(result.answer_draft.next_step_line).toContain("\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\u0438\u044e");
expect(result.answer_draft.next_step_line).not.toContain(
"\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0430"
);
});
});

View File

@ -408,4 +408,104 @@ describe("assistant MCP discovery runtime entry point", () => {
expect(result.bridge?.pilot.pilot_scope).toBe("counterparty_supplier_payout_query_movements_v1");
expect(result.bridge?.answer_draft.confirmed_lines.join("\n")).toContain("исход");
});
it("keeps a generic incoming total in organization clarification instead of asking for counterparty", async () => {
const result = await runAssistantMcpDiscoveryRuntimeEntryPoint({
userMessage: "сколько входящих денег за 2020 год?",
deps: buildDeps([])
});
expect(result.entry_status).toBe("bridge_executed");
expect(result.discovery_attempted).toBe(true);
expect(result.turn_input.turn_meaning_ref).toMatchObject({
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_date_scope: "2020"
});
expect(result.turn_input.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.turn_input.data_need_graph?.clarification_gaps).toEqual(["organization"]);
expect(result.bridge?.planner.selected_chain_id).toBe("value_flow");
expect(result.bridge?.bridge_status).toBe("needs_clarification");
expect(result.bridge?.answer_draft.next_step_line).toContain("\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\u0438\u044e");
expect(result.bridge?.answer_draft.next_step_line).not.toContain(
"\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0430"
);
});
it("resumes a generic incoming total after organization clarification without reintroducing a counterparty", async () => {
const orgName = "ООО Альтернатива Плюс";
const result = await runAssistantMcpDiscoveryRuntimeEntryPoint({
userMessage: "по ООО Альтернатива Плюс",
predecomposeContract: {
entities: { organization: orgName }
},
followupContext: {
previous_discovery_pilot_scope: "counterparty_value_flow_query_movements_v1",
previous_filters: {
period_from: "2020-01-01",
period_to: "2020-12-31"
}
},
deps: buildDeps([
{ Period: "2020-01-15T00:00:00", Amount: 2500, Counterparty: "Клиент-А" },
{ Period: "2020-06-20T00:00:00", Amount: 1000, Counterparty: "Клиент-Б" }
])
});
expect(result.entry_status).toBe("bridge_executed");
expect(result.discovery_attempted).toBe(true);
expect(result.turn_input.source_signal).toBe("followup_context");
expect(result.turn_input.turn_meaning_ref).toMatchObject({
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_organization_scope: orgName,
explicit_date_scope: "2020"
});
expect(result.turn_input.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.bridge?.planner.selected_chain_id).toBe("value_flow");
expect(result.bridge?.pilot.pilot_scope).toBe("counterparty_value_flow_query_movements_v1");
expect(result.bridge?.pilot.derived_value_flow).toMatchObject({
counterparty: null,
period_scope: "2020",
total_amount: 3500
});
});
it("overrides a supported exact intent when organization-only follow-up resolves an open-scope total", async () => {
const orgName = "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
const result = await runAssistantMcpDiscoveryRuntimeEntryPoint({
userMessage: "\u043f\u043e \u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441",
assistantTurnMeaning: {
asked_domain_family: "counterparty",
asked_action_family: "turnover",
explicit_intent_candidate: "customer_revenue_and_payments"
},
followupContext: {
previous_discovery_pilot_scope: "counterparty_value_flow_query_movements_v1",
previous_filters: {
period_from: "2020-01-01",
period_to: "2020-12-31"
}
},
deps: buildDeps([
{ Period: "2020-01-15T00:00:00", Amount: 2500, Counterparty: "\u041a\u043b\u0438\u0435\u043d\u0442-\u0410" },
{ Period: "2020-06-20T00:00:00", Amount: 1000, Counterparty: "\u041a\u043b\u0438\u0435\u043d\u0442-\u0411" }
])
});
expect(result.entry_status).toBe("bridge_executed");
expect(result.discovery_attempted).toBe(true);
expect(result.turn_input.turn_meaning_ref).toMatchObject({
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_organization_scope: orgName,
explicit_date_scope: "2020"
});
expect(result.turn_input.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.bridge?.planner.selected_chain_id).toBe("value_flow");
expect(result.bridge?.pilot.pilot_scope).toBe("counterparty_value_flow_query_movements_v1");
expect(result.bridge?.pilot.derived_value_flow).toMatchObject({
counterparty: null,
period_scope: "2020",
total_amount: 3500
});
});
});

View File

@ -1422,4 +1422,93 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.data_need_graph?.subject_candidates).toEqual([]);
});
it("treats a generic incoming total as an open-scope value ask that needs organization rather than a counterparty", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "сколько входящих денег за 2020 год?"
});
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.semantic_data_need).toBe("counterparty value-flow evidence");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_date_scope: "2020",
unsupported_but_understood_family: "counterparty_value_or_turnover",
stale_replay_forbidden: true
});
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.data_need_graph?.subject_candidates).toEqual([]);
expect(result.data_need_graph?.clarification_gaps).toEqual(["organization"]);
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", () => {
const orgName = "ООО Альтернатива Плюс";
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "по ООО Альтернатива Плюс",
predecomposeContract: {
entities: { organization: orgName }
},
followupContext: {
previous_discovery_pilot_scope: "counterparty_value_flow_query_movements_v1",
previous_filters: {
period_from: "2020-01-01",
period_to: "2020-12-31"
}
}
});
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.source_signal).toBe("followup_context");
expect(result.semantic_data_need).toBe("counterparty value-flow evidence");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_organization_scope: orgName,
explicit_date_scope: "2020",
unsupported_but_understood_family: "counterparty_value_or_turnover",
stale_replay_forbidden: true
});
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.reason_codes).toContain("mcp_discovery_seeded_from_followup_context");
expect(result.data_need_graph?.clarification_gaps).toEqual([]);
});
it("forces discovery over a supported exact intent when organization-only follow-up resolves an open-scope total", () => {
const orgName = "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "\u043f\u043e \u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441",
assistantTurnMeaning: {
asked_domain_family: "counterparty",
asked_action_family: "turnover",
explicit_intent_candidate: "customer_revenue_and_payments"
},
followupContext: {
previous_discovery_pilot_scope: "counterparty_value_flow_query_movements_v1",
previous_filters: {
period_from: "2020-01-01",
period_to: "2020-12-31"
}
}
});
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.semantic_data_need).toBe("counterparty value-flow evidence");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_organization_scope: orgName,
explicit_date_scope: "2020",
unsupported_but_understood_family: "counterparty_value_or_turnover",
stale_replay_forbidden: true
});
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.reason_codes).toContain(
"mcp_discovery_organization_clarification_followup_from_followup_context"
);
expect(result.reason_codes).not.toContain("mcp_discovery_not_applicable_for_supported_exact_turn");
});
});