ARCH: добавить open-scope totals по организации в MCP discovery

This commit is contained in:
dctouch 2026-04-22 21:00:15 +03:00
parent f2bd2dfdb1
commit 94e537210c
12 changed files with 380 additions and 7 deletions

View File

@ -0,0 +1,56 @@
{
"schema_version": "domain_truth_harness_spec_v1",
"scenario_id": "address_truth_harness_phase34_open_scope_value_flow_totals",
"domain": "address_phase34_open_scope_value_flow_totals",
"title": "Phase 34 open-scope value-flow totals replay",
"description": "Targeted AGENT replay for Big Block D where organization-scoped incoming/outgoing money totals must be understood as bounded open-scope value-flow questions rather than as missing-counterparty fact asks.",
"bindings": {},
"steps": [
{
"step_id": "step_01_incoming_total_for_org",
"title": "Raw organization-scoped incoming wording produces a bounded incoming total without inventing a counterparty",
"question": "Сколько входящих денег за 2020 год по ООО Альтернатива Плюс?",
"allowed_reply_types": ["factual_with_explanation", "partial_coverage"],
"required_answer_patterns_all": [
"(?i)2020",
"(?i)входящ|получ|поступ",
"(?i)руб"
],
"required_answer_patterns_any": [
"(?i)альтернатива",
"(?i)проверенн|найденн"
],
"forbidden_answer_patterns": [
"(?i)уточните контрагента",
"(?i)не найден контрагент",
"(?i)по какому контрагенту",
"(?i)не найдено контрагента"
],
"criticality": "critical",
"semantic_tags": ["value_flow_total", "incoming", "open_scope", "organization_scoped", "bounded_autonomy"]
},
{
"step_id": "step_02_outgoing_total_for_org",
"title": "Raw organization-scoped outgoing wording produces a bounded payout total without inventing a counterparty",
"question": "Сколько исходящих денег за 2020 год по ООО Альтернатива Плюс?",
"allowed_reply_types": ["factual_with_explanation", "partial_coverage"],
"required_answer_patterns_all": [
"(?i)2020",
"(?i)исходящ|заплат|списан|платеж",
"(?i)руб"
],
"required_answer_patterns_any": [
"(?i)альтернатива",
"(?i)проверенн|найденн"
],
"forbidden_answer_patterns": [
"(?i)уточните контрагента",
"(?i)не найден контрагент",
"(?i)по какому контрагенту",
"(?i)не найдено контрагента"
],
"criticality": "critical",
"semantic_tags": ["value_flow_total", "outgoing", "open_scope", "organization_scoped", "bounded_autonomy"]
}
]
}

View File

@ -85,11 +85,17 @@ function comparisonNeedFor(action) {
}
return null;
}
function supportsOrganizationScopedOpenTotal(action) {
return action === "turnover" || action === "payout";
}
function allowsOpenScopeWithoutSubject(input) {
if (input.family !== "value_flow") {
return false;
}
return Boolean(input.rankingNeed || input.comparisonNeed === "incoming_vs_outgoing");
if (input.rankingNeed || input.comparisonNeed === "incoming_vs_outgoing") {
return true;
}
return Boolean(input.organizationScope && supportsOrganizationScopedOpenTotal(input.action));
}
function rankingNeedFromRawUtterance(value) {
const text = lower(value);
@ -147,6 +153,12 @@ function decompositionCandidatesFor(input) {
pushUnique(result, "probe_coverage");
return result;
}
if (input.openScopeWithoutSubject) {
pushUnique(result, "collect_scoped_movements");
pushUnique(result, input.aggregationNeed === "by_month" ? "aggregate_by_month" : "aggregate_checked_amounts");
pushUnique(result, "probe_coverage");
return result;
}
pushUnique(result, "resolve_entity_reference");
if (input.action === "net_value_flow") {
pushUnique(result, "collect_incoming_movements");
@ -204,6 +216,7 @@ function buildAssistantMcpDiscoveryDataNeedGraph(input) {
const rawUtterance = lower(input.rawUtterance);
const aggregationAxis = lower(turnMeaning?.asked_aggregation_axis);
const explicitDateScope = toNonEmptyString(turnMeaning?.explicit_date_scope);
const explicitOrganizationScope = toNonEmptyString(turnMeaning?.explicit_organization_scope);
const subjectCandidates = (turnMeaning?.explicit_entity_candidates ?? [])
.map((item) => toNonEmptyString(item))
.filter((item) => Boolean(item));
@ -219,6 +232,8 @@ function buildAssistantMcpDiscoveryDataNeedGraph(input) {
const openScopeWithoutSubject = subjectCandidates.length === 0 &&
allowsOpenScopeWithoutSubject({
family: businessFactFamily,
action,
organizationScope: explicitOrganizationScope,
comparisonNeed,
rankingNeed
});
@ -261,6 +276,9 @@ function buildAssistantMcpDiscoveryDataNeedGraph(input) {
if (comparisonNeed) {
pushReason(reasonCodes, `data_need_graph_comparison_${comparisonNeed}`);
}
if (openScopeWithoutSubject && !rankingNeed && !comparisonNeed) {
pushReason(reasonCodes, "data_need_graph_open_scope_total_without_subject");
}
if (clarificationGaps.length > 0) {
pushReason(reasonCodes, "data_need_graph_has_clarification_gaps");
}

View File

@ -86,6 +86,7 @@ function recipeFor(input) {
const graphAction = lower(dataNeedGraph?.action_family);
const graphAggregation = lower(dataNeedGraph?.aggregation_need);
const graphClarificationGaps = (dataNeedGraph?.clarification_gaps ?? []).map((item) => lower(item));
const organizationScope = toNonEmptyString(meaning?.explicit_organization_scope);
const combined = `${domain} ${action} ${unsupported}`.trim();
const axes = [];
const requestedAggregationAxis = aggregationAxis(meaning);
@ -132,6 +133,24 @@ function recipeFor(input) {
: "planner_selected_top_ranked_value_flow_from_data_need_graph"
};
}
if (!hasSubjectCandidates(dataNeedGraph) && organizationScope) {
pushUnique(axes, "aggregate_axis");
pushUnique(axes, "amount");
pushUnique(axes, "coverage_target");
if (requestedAggregationAxis === "month" || graphAggregation === "by_month") {
pushUnique(axes, "calendar_month");
}
return {
semanticDataNeed: "organization-scoped value-flow evidence",
chainId: "value_flow",
chainSummary: "Query scoped movements for the checked period and organization without a preselected counterparty, aggregate checked amounts, then probe coverage before answering a bounded total.",
primitives: ["query_movements", "aggregate_by_axis", "probe_coverage"],
axes,
reason: requestedAggregationAxis === "month" || graphAggregation === "by_month"
? "planner_selected_monthly_open_scope_value_flow_total_from_data_need_graph"
: "planner_selected_open_scope_value_flow_total_from_data_need_graph"
};
}
pushUnique(axes, "aggregate_axis");
pushUnique(axes, "amount");
pushUnique(axes, "coverage_target");

View File

@ -305,6 +305,14 @@ function hasBidirectionalValueFlowSignal(text) {
function hasValueRankingSignal(text) {
return /(?:кто\s+больше\s+всего.*ден[её]г|больше\s+всего.*ден[её]г|прин[её]с.*ден[её]г|сам(?:ый|ая|ое|ые).*(?:доходн|прибыльн)|most.*money|highest\s+(?:revenue|payment))/iu.test(text);
}
function hasOrganizationScopeSignal(text) {
return /(?:\bооо\b|\bип\b|\bао\b|\bпао\b|\bзао\b|\bllc\b|\binc\b|\bcorp\b|\bcompany\b|\borganization\b|\borganisation\b|организац|компан)/iu.test(text);
}
function hasOrganizationScopeSignalUtf8(text) {
return (/(?<!\p{L})(?:\u043e\u043e\u043e|\u0438\u043f|\u0430\u043e|\u043f\u0430\u043e|\u0437\u0430\u043e)(?!\p{L})/iu.test(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 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);
}
@ -582,8 +590,12 @@ 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 rawOpenScopeValueFlowOrganizationSignal = Boolean(rawValueFlowSignal &&
!rawBidirectionalValueFlowSignal &&
hasOrganizationScopeSignalUtf8(rawText) &&
(predecomposeEntities.organization ?? assistantTurnMeaningOrganizationScope));
const predecomposeOrganizationMirrorsCounterparty = sameScopedName(predecomposeEntities.counterparty, predecomposeEntities.organization);
const organizationMirrorsPredecomposeCounterparty = Boolean((rawBidirectionalValueFlowSignal || hasValueRankingSignal(rawText)) &&
const organizationMirrorsPredecomposeCounterparty = Boolean((rawBidirectionalValueFlowSignal || hasValueRankingSignal(rawText) || rawOpenScopeValueFlowOrganizationSignal) &&
(sameScopedName(predecomposeEntities.counterparty, assistantTurnMeaningOrganizationScope) ||
predecomposeOrganizationMirrorsCounterparty));
const normalizedPredecomposeCounterparty = organizationMirrorsPredecomposeCounterparty
@ -829,7 +841,10 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
}
const openScopeValueFlowWithoutCounterparty = valueFlowSignal && !normalizedPredecomposeCounterparty && !followupSeed.counterparty;
const valueFlowOrganizationStaysScope = openScopeValueFlowWithoutCounterparty &&
(bidirectionalValueFlowSignal || hasValueRankingSignal(rawText));
Boolean(bidirectionalValueFlowSignal ||
hasValueRankingSignal(rawText) ||
rawOpenScopeValueFlowOrganizationSignal ||
followupSeed.organization);
if (openScopeValueFlowWithoutCounterparty && !valueFlowOrganizationStaysScope) {
pushUnique(entityCandidates, predecomposeEntities.organization);
pushUnique(entityCandidates, followupSeed.organization);

View File

@ -132,15 +132,24 @@ function comparisonNeedFor(action: string): string | null {
return null;
}
function supportsOrganizationScopedOpenTotal(action: string): boolean {
return action === "turnover" || action === "payout";
}
function allowsOpenScopeWithoutSubject(input: {
family: string | null;
action: string;
organizationScope: string | null;
comparisonNeed: string | null;
rankingNeed: string | null;
}): boolean {
if (input.family !== "value_flow") {
return false;
}
return Boolean(input.rankingNeed || input.comparisonNeed === "incoming_vs_outgoing");
if (input.rankingNeed || input.comparisonNeed === "incoming_vs_outgoing") {
return true;
}
return Boolean(input.organizationScope && supportsOrganizationScopedOpenTotal(input.action));
}
function rankingNeedFromRawUtterance(value: string): string | null {
@ -215,6 +224,12 @@ function decompositionCandidatesFor(input: {
pushUnique(result, "probe_coverage");
return result;
}
if (input.openScopeWithoutSubject) {
pushUnique(result, "collect_scoped_movements");
pushUnique(result, input.aggregationNeed === "by_month" ? "aggregate_by_month" : "aggregate_checked_amounts");
pushUnique(result, "probe_coverage");
return result;
}
pushUnique(result, "resolve_entity_reference");
if (input.action === "net_value_flow") {
pushUnique(result, "collect_incoming_movements");
@ -275,6 +290,7 @@ export function buildAssistantMcpDiscoveryDataNeedGraph(
const rawUtterance = lower(input.rawUtterance);
const aggregationAxis = lower(turnMeaning?.asked_aggregation_axis);
const explicitDateScope = toNonEmptyString(turnMeaning?.explicit_date_scope);
const explicitOrganizationScope = toNonEmptyString(turnMeaning?.explicit_organization_scope);
const subjectCandidates = (turnMeaning?.explicit_entity_candidates ?? [])
.map((item) => toNonEmptyString(item))
.filter((item): item is string => Boolean(item));
@ -291,6 +307,8 @@ export function buildAssistantMcpDiscoveryDataNeedGraph(
subjectCandidates.length === 0 &&
allowsOpenScopeWithoutSubject({
family: businessFactFamily,
action,
organizationScope: explicitOrganizationScope,
comparisonNeed,
rankingNeed
});
@ -332,6 +350,9 @@ export function buildAssistantMcpDiscoveryDataNeedGraph(
if (comparisonNeed) {
pushReason(reasonCodes, `data_need_graph_comparison_${comparisonNeed}`);
}
if (openScopeWithoutSubject && !rankingNeed && !comparisonNeed) {
pushReason(reasonCodes, "data_need_graph_open_scope_total_without_subject");
}
if (clarificationGaps.length > 0) {
pushReason(reasonCodes, "data_need_graph_has_clarification_gaps");
}

View File

@ -153,6 +153,7 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
const graphAction = lower(dataNeedGraph?.action_family);
const graphAggregation = lower(dataNeedGraph?.aggregation_need);
const graphClarificationGaps = (dataNeedGraph?.clarification_gaps ?? []).map((item) => lower(item));
const organizationScope = toNonEmptyString(meaning?.explicit_organization_scope);
const combined = `${domain} ${action} ${unsupported}`.trim();
const axes: string[] = [];
const requestedAggregationAxis = aggregationAxis(meaning);
@ -204,6 +205,26 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
: "planner_selected_top_ranked_value_flow_from_data_need_graph"
};
}
if (!hasSubjectCandidates(dataNeedGraph) && organizationScope) {
pushUnique(axes, "aggregate_axis");
pushUnique(axes, "amount");
pushUnique(axes, "coverage_target");
if (requestedAggregationAxis === "month" || graphAggregation === "by_month") {
pushUnique(axes, "calendar_month");
}
return {
semanticDataNeed: "organization-scoped value-flow evidence",
chainId: "value_flow",
chainSummary:
"Query scoped movements for the checked period and organization without a preselected counterparty, aggregate checked amounts, then probe coverage before answering a bounded total.",
primitives: ["query_movements", "aggregate_by_axis", "probe_coverage"],
axes,
reason:
requestedAggregationAxis === "month" || graphAggregation === "by_month"
? "planner_selected_monthly_open_scope_value_flow_total_from_data_need_graph"
: "planner_selected_open_scope_value_flow_total_from_data_need_graph"
};
}
pushUnique(axes, "aggregate_axis");
pushUnique(axes, "amount");
pushUnique(axes, "coverage_target");

View File

@ -422,6 +422,20 @@ function hasValueRankingSignal(text: string): boolean {
);
}
function hasOrganizationScopeSignal(text: string): boolean {
return /(?:\bРѕРѕРѕ\b|\bРёРї\b|\bао\b|\bпао\b|\bзао\b|\bllc\b|\binc\b|\bcorp\b|\bcompany\b|\borganization\b|\borganisation\b|организаС|компан)/iu.test(
text
);
}
function hasOrganizationScopeSignalUtf8(text: string): boolean {
return (
/(?<!\p{L})(?:\u043e\u043e\u043e|\u0438\u043f|\u0430\u043e|\u043f\u0430\u043e|\u0437\u0430\u043e)(?!\p{L})/iu.test(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 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
@ -789,12 +803,18 @@ 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 rawOpenScopeValueFlowOrganizationSignal = Boolean(
rawValueFlowSignal &&
!rawBidirectionalValueFlowSignal &&
hasOrganizationScopeSignalUtf8(rawText) &&
(predecomposeEntities.organization ?? assistantTurnMeaningOrganizationScope)
);
const predecomposeOrganizationMirrorsCounterparty = sameScopedName(
predecomposeEntities.counterparty,
predecomposeEntities.organization
);
const organizationMirrorsPredecomposeCounterparty = Boolean(
(rawBidirectionalValueFlowSignal || hasValueRankingSignal(rawText)) &&
(rawBidirectionalValueFlowSignal || hasValueRankingSignal(rawText) || rawOpenScopeValueFlowOrganizationSignal) &&
(sameScopedName(predecomposeEntities.counterparty, assistantTurnMeaningOrganizationScope) ||
predecomposeOrganizationMirrorsCounterparty)
);
@ -1090,7 +1110,12 @@ export function buildAssistantMcpDiscoveryTurnInput(
valueFlowSignal && !normalizedPredecomposeCounterparty && !followupSeed.counterparty;
const valueFlowOrganizationStaysScope =
openScopeValueFlowWithoutCounterparty &&
(bidirectionalValueFlowSignal || hasValueRankingSignal(rawText));
Boolean(
bidirectionalValueFlowSignal ||
hasValueRankingSignal(rawText) ||
rawOpenScopeValueFlowOrganizationSignal ||
followupSeed.organization
);
if (openScopeValueFlowWithoutCounterparty && !valueFlowOrganizationStaysScope) {
pushUnique(entityCandidates, predecomposeEntities.organization);
pushUnique(entityCandidates, followupSeed.organization);

View File

@ -114,4 +114,27 @@ describe("assistant MCP discovery data need graph", () => {
]);
expect(result.reason_codes).toContain("data_need_graph_comparison_incoming_vs_outgoing");
});
it("treats organization-scoped incoming totals as an open-scope value need rather than a missing-subject fact ask", () => {
const result = buildAssistantMcpDiscoveryDataNeedGraph({
semanticDataNeed: "counterparty value-flow evidence",
rawUtterance: "сколько входящих денег за 2020 год по ООО Альтернатива Плюс?",
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_date_scope: "2020",
explicit_organization_scope: "ООО Альтернатива Плюс"
}
});
expect(result.business_fact_family).toBe("value_flow");
expect(result.comparison_need).toBeNull();
expect(result.ranking_need).toBeNull();
expect(result.clarification_gaps).toEqual([]);
expect(result.decomposition_candidates).toEqual([
"collect_scoped_movements",
"aggregate_checked_amounts",
"probe_coverage"
]);
expect(result.reason_codes).toContain("data_need_graph_open_scope_total_without_subject");
});
});

View File

@ -440,4 +440,38 @@ describe("assistant MCP discovery planner", () => {
expect(result.selected_chain_summary).toContain("resolve the most relevant 1C reference");
expect(result.proposed_primitives).toEqual(["search_business_entity", "resolve_entity_reference", "probe_coverage"]);
});
it("keeps organization-scoped one-sided value-flow totals executable without forcing a counterparty", () => {
const result = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
business_fact_family: "value_flow",
action_family: "turnover",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: null,
ranking_need: null,
proof_expectation: "coverage_checked_fact",
clarification_gaps: [],
decomposition_candidates: ["collect_scoped_movements", "aggregate_checked_amounts", "probe_coverage"],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
reason_codes: ["data_need_graph_built", "data_need_graph_open_scope_total_without_subject"]
},
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_date_scope: "2020",
explicit_organization_scope: "ООО Альтернатива Плюс"
}
});
expect(result.planner_status).toBe("ready_for_execution");
expect(result.semantic_data_need).toBe("organization-scoped value-flow evidence");
expect(result.selected_chain_id).toBe("value_flow");
expect(result.proposed_primitives).toEqual(["query_movements", "aggregate_by_axis", "probe_coverage"]);
expect(result.required_axes).toEqual(["organization", "period", "aggregate_axis", "amount", "coverage_target"]);
expect(result.catalog_review.review_status).toBe("catalog_compatible");
expect(result.reason_codes).toContain("planner_selected_open_scope_value_flow_total_from_data_need_graph");
});
});

View File

@ -275,4 +275,45 @@ describe("assistant MCP discovery runtime bridge", () => {
expect(userFacing).not.toContain("runtime_bridge");
expect(userFacing).not.toContain("primitive");
});
it("produces a bounded one-sided value-flow answer for an organization-scoped total without inventing a counterparty", async () => {
const result = await runAssistantMcpDiscoveryRuntimeBridge({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
business_fact_family: "value_flow",
action_family: "turnover",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: null,
ranking_need: null,
proof_expectation: "coverage_checked_fact",
clarification_gaps: [],
decomposition_candidates: ["collect_scoped_movements", "aggregate_checked_amounts", "probe_coverage"],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
reason_codes: ["data_need_graph_built", "data_need_graph_open_scope_total_without_subject"]
},
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_date_scope: "2020",
explicit_organization_scope: "ООО Альтернатива Плюс"
},
deps: buildDeps([
{ Period: "2020-01-10T00:00:00", Amount: 3200, Counterparty: "Клиент-А" },
{ Period: "2020-05-22T00:00:00", Amount: 1800, Counterparty: "Клиент-Б" }
])
});
expect(result.bridge_status).toBe("answer_draft_ready");
expect(result.business_fact_answer_allowed).toBe(true);
expect(result.planner.selected_chain_id).toBe("value_flow");
expect(result.pilot.derived_value_flow).toMatchObject({
counterparty: null,
period_scope: "2020",
total_amount: 5000
});
expect(result.answer_draft.confirmed_lines.join("\n")).toContain("5 000");
expect(result.answer_draft.confirmed_lines.join("\n")).not.toContain("контрагенту");
});
});

View File

@ -353,4 +353,59 @@ describe("assistant MCP discovery runtime entry point", () => {
expect(result.turn_input.data_need_graph?.subject_candidates).toEqual([]);
expect(result.bridge?.planner.selected_chain_id).toBe("value_flow_comparison");
});
it("runs raw organization-scoped incoming totals as an open value-flow chain without inventing a counterparty", async () => {
const result = await runAssistantMcpDiscoveryRuntimeEntryPoint({
userMessage: "сколько входящих денег за 2020 год по ООО Альтернатива Плюс?",
predecomposeContract: {
entities: { organization: "ООО Альтернатива Плюс" },
period: { period_from: "2020-01-01", period_to: "2020-12-31" }
},
deps: buildDeps([
{ Period: "2020-01-15T00:00:00", Amount: 2500, Counterparty: "Клиент-А" },
{ Period: "2020-06-20T00:00:00", Amount: 1000, Counterparty: "Клиент-Б" }
])
});
expect(result.entry_status).toBe("bridge_executed");
expect(result.discovery_attempted).toBe(true);
expect(result.turn_input.turn_meaning_ref).toMatchObject({
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020"
});
expect(result.turn_input.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.turn_input.data_need_graph?.subject_candidates).toEqual([]);
expect(result.bridge?.planner.selected_chain_id).toBe("value_flow");
expect(result.bridge?.pilot.pilot_scope).toBe("counterparty_value_flow_query_movements_v1");
expect(result.bridge?.answer_draft.confirmed_lines.join("\n")).toContain("входящ");
});
it("runs raw organization-scoped outgoing totals as an open payout chain without inventing a counterparty", async () => {
const orgName = "ООО Альтернатива Плюс";
const result = await runAssistantMcpDiscoveryRuntimeEntryPoint({
userMessage: "сколько исходящих денег за 2020 год по ООО Альтернатива Плюс?",
predecomposeContract: {
entities: { counterparty: orgName, organization: orgName },
period: { period_from: "2020-01-01", period_to: "2020-12-31" }
},
deps: buildDeps([
{ Period: "2020-02-18T00:00:00", Amount: 900, Counterparty: "Поставщик-А" },
{ Period: "2020-08-07T00:00:00", Amount: 300, Counterparty: "Поставщик-Б" }
])
});
expect(result.entry_status).toBe("bridge_executed");
expect(result.discovery_attempted).toBe(true);
expect(result.turn_input.turn_meaning_ref).toMatchObject({
asked_domain_family: "counterparty_value",
asked_action_family: "payout",
explicit_organization_scope: orgName,
explicit_date_scope: "2020"
});
expect(result.turn_input.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.bridge?.planner.selected_chain_id).toBe("value_flow");
expect(result.bridge?.pilot.pilot_scope).toBe("counterparty_supplier_payout_query_movements_v1");
expect(result.bridge?.answer_draft.confirmed_lines.join("\n")).toContain("исход");
});
});

View File

@ -1377,4 +1377,49 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.data_need_graph?.comparison_need).toBe("incoming_vs_outgoing");
expect(result.reason_codes).not.toContain("mcp_discovery_counterparty_from_predecompose");
});
it("keeps organization-scoped incoming totals in an open value-flow lane without inventing a counterparty", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "сколько входящих денег за 2020 год по ООО Альтернатива Плюс?",
predecomposeContract: {
entities: { organization: "ООО Альтернатива Плюс" },
period: { period_from: "2020-01-01", period_to: "2020-12-31" }
}
});
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.semantic_data_need).toBe("counterparty value-flow evidence");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020",
unsupported_but_understood_family: "counterparty_value_or_turnover",
stale_replay_forbidden: true
});
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.data_need_graph?.subject_candidates).toEqual([]);
expect(result.data_need_graph?.reason_codes).toContain("data_need_graph_open_scope_total_without_subject");
});
it("does not treat mirrored organization/counterparty predecompose as a real subject for organization-scoped payouts", () => {
const orgName = "ООО Альтернатива Плюс";
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "сколько исходящих денег за 2020 год по ООО Альтернатива Плюс?",
predecomposeContract: {
entities: { counterparty: orgName, organization: orgName },
period: { period_from: "2020-01-01", period_to: "2020-12-31" }
}
});
expect(result.adapter_status).toBe("ready");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "counterparty_value",
asked_action_family: "payout",
explicit_organization_scope: orgName,
explicit_date_scope: "2020"
});
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.data_need_graph?.subject_candidates).toEqual([]);
});
});