ARCH: добавить двусторонний MCP discovery для нетто-потока
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
||||
resolveAddressDebugCarryoverFilters,
|
||||
resolveAddressDebugContextFacts,
|
||||
resolveAddressDebugAnchorContext,
|
||||
resolveOrganizationClarificationContinuation,
|
||||
resolveDisplayedEntityFollowupRetarget,
|
||||
resolveFollowupTargetIntent,
|
||||
resolveInventoryFollowupPivotFlags,
|
||||
@@ -146,6 +147,27 @@ describe("assistantContinuityPolicy organization authority", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves organization clarification continuation through one shared helper", () => {
|
||||
const continuation = resolveOrganizationClarificationContinuation({
|
||||
rawMessages: ["Альтернатива Плюс", null, " "],
|
||||
organizationClarificationCandidates: ["ООО Альтернатива Плюс", "РАЙМ"],
|
||||
organizationClarificationSelectionFromScope: "РАЙМ",
|
||||
lastOrganizationClarificationDebug: {
|
||||
organization_candidates: ["ООО Альтернатива Плюс", "РАЙМ"]
|
||||
},
|
||||
resolveOrganizationSelectionFromMessage: (message, candidates) =>
|
||||
/альтернатива/i.test(String(message))
|
||||
? String(candidates[0] ?? "")
|
||||
: null
|
||||
});
|
||||
|
||||
expect(continuation).toEqual({
|
||||
explicitSelection: "ООО Альтернатива Плюс",
|
||||
selection: "ООО Альтернатива Плюс",
|
||||
hasContinuation: true
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves carryover temporal scope and inferred anchor from debug filters when explicit anchor fields are absent", () => {
|
||||
const debug = {
|
||||
extracted_filters: {
|
||||
|
||||
@@ -15,6 +15,22 @@ function buildDeps(rows: Array<Record<string, unknown>>, error: string | null =
|
||||
};
|
||||
}
|
||||
|
||||
function buildSequentialDeps(results: Array<{ rows: Array<Record<string, unknown>>; error?: string | null }>) {
|
||||
const executeAddressMcpQuery = vi.fn(async () => {
|
||||
const next = results.shift() ?? { rows: [] };
|
||||
const rows = next.rows;
|
||||
const error = next.error ?? null;
|
||||
return {
|
||||
fetched_rows: rows.length,
|
||||
matched_rows: error ? 0 : rows.length,
|
||||
raw_rows: rows,
|
||||
rows: error ? [] : rows,
|
||||
error
|
||||
};
|
||||
});
|
||||
return { executeAddressMcpQuery };
|
||||
}
|
||||
|
||||
describe("assistant MCP discovery answer adapter", () => {
|
||||
it("turns confirmed lifecycle evidence into a human-safe bounded answer draft", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
@@ -139,6 +155,42 @@ describe("assistant MCP discovery answer adapter", () => {
|
||||
expect(draft.unknown_lines).toContain("Full supplier-payout amount outside the checked period is not proven by this MCP discovery pilot");
|
||||
});
|
||||
|
||||
it("turns bidirectional value-flow evidence into a bounded net cash answer draft", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "net_value_flow",
|
||||
explicit_entity_candidates: ["SVK"],
|
||||
explicit_date_scope: "2020",
|
||||
unsupported_but_understood_family: "counterparty_bidirectional_value_flow_or_netting"
|
||||
}
|
||||
});
|
||||
const pilot = await executeAssistantMcpDiscoveryPilot(
|
||||
planner,
|
||||
buildSequentialDeps([
|
||||
{
|
||||
rows: [
|
||||
{ Period: "2020-01-15T00:00:00", Amount: 10000, Counterparty: "SVK" },
|
||||
{ Period: "2020-02-20T00:00:00", Amount: "2 500,50", Counterparty: "SVK" }
|
||||
]
|
||||
},
|
||||
{ rows: [{ Period: "2020-03-10T00:00:00", Amount: 4000, Counterparty: "SVK" }] }
|
||||
])
|
||||
);
|
||||
|
||||
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
|
||||
const confirmedText = draft.confirmed_lines.join("\n");
|
||||
|
||||
expect(draft.answer_mode).toBe("confirmed_with_bounded_inference");
|
||||
expect(draft.headline).toContain("входящих и исходящих денежных движений");
|
||||
expect(confirmedText).toContain("получили 12 500,50 руб.");
|
||||
expect(confirmedText).toContain("заплатили 4 000 руб.");
|
||||
expect(confirmedText).toContain("нетто в нашу сторону: 8 500,50 руб.");
|
||||
expect(draft.inference_lines.join("\n")).toContain("net value-flow");
|
||||
expect(draft.unknown_lines).toContain("Full bidirectional value-flow outside the checked period is not proven by this MCP discovery pilot");
|
||||
expect(draft.must_not_claim).toContain("Do not present a derived sum as a legal/accounting final total outside the checked 1C rows.");
|
||||
});
|
||||
|
||||
it("does not leak primitive names or query text into user-facing lines", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
|
||||
@@ -14,6 +14,22 @@ function buildDeps(rows: Array<Record<string, unknown>>, error: string | null =
|
||||
};
|
||||
}
|
||||
|
||||
function buildSequentialDeps(results: Array<{ rows: Array<Record<string, unknown>>; error?: string | null }>) {
|
||||
const executeAddressMcpQuery = vi.fn(async () => {
|
||||
const next = results.shift() ?? { rows: [] };
|
||||
const rows = next.rows;
|
||||
const error = next.error ?? null;
|
||||
return {
|
||||
fetched_rows: rows.length,
|
||||
matched_rows: error ? 0 : rows.length,
|
||||
raw_rows: rows,
|
||||
rows: error ? [] : rows,
|
||||
error
|
||||
};
|
||||
});
|
||||
return { executeAddressMcpQuery };
|
||||
}
|
||||
|
||||
describe("assistant MCP discovery pilot executor", () => {
|
||||
it("executes only the lifecycle query_documents primitive through injected MCP deps", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
@@ -183,6 +199,67 @@ describe("assistant MCP discovery pilot executor", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("executes bidirectional value-flow queries and derives guarded net cash flow", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "net_value_flow",
|
||||
explicit_entity_candidates: ["SVK"],
|
||||
explicit_date_scope: "2020",
|
||||
unsupported_but_understood_family: "counterparty_bidirectional_value_flow_or_netting"
|
||||
}
|
||||
});
|
||||
const deps = buildSequentialDeps([
|
||||
{
|
||||
rows: [
|
||||
{ Period: "2020-01-15T00:00:00", Amount: 10000, Counterparty: "SVK" },
|
||||
{ Period: "2020-02-20T00:00:00", Amount: "2 500,50", Counterparty: "SVK" }
|
||||
]
|
||||
},
|
||||
{
|
||||
rows: [{ Period: "2020-03-10T00:00:00", Amount: 4000, Counterparty: "SVK" }]
|
||||
}
|
||||
]);
|
||||
|
||||
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
|
||||
|
||||
expect(result.pilot_status).toBe("executed");
|
||||
expect(result.pilot_scope).toBe("counterparty_bidirectional_value_flow_query_movements_v1");
|
||||
expect(result.mcp_execution_performed).toBe(true);
|
||||
expect(result.executed_primitives).toEqual(["query_movements"]);
|
||||
expect(result.derived_value_flow).toBeNull();
|
||||
expect(result.derived_bidirectional_value_flow).toMatchObject({
|
||||
counterparty: "SVK",
|
||||
period_scope: "2020",
|
||||
net_amount: 8500.5,
|
||||
net_amount_human_ru: "8 500,50 руб.",
|
||||
net_direction: "net_incoming",
|
||||
coverage_limited_by_probe_limit: false,
|
||||
incoming_customer_revenue: {
|
||||
rows_matched: 2,
|
||||
rows_with_amount: 2,
|
||||
total_amount: 12500.5,
|
||||
first_movement_date: "2020-01-15",
|
||||
latest_movement_date: "2020-02-20"
|
||||
},
|
||||
outgoing_supplier_payout: {
|
||||
rows_matched: 1,
|
||||
rows_with_amount: 1,
|
||||
total_amount: 4000,
|
||||
first_movement_date: "2020-03-10",
|
||||
latest_movement_date: "2020-03-10"
|
||||
}
|
||||
});
|
||||
expect(result.evidence.confirmed_facts[0]).toContain("bidirectional value-flow rows");
|
||||
expect(result.evidence.inferred_facts[0]).toContain("net value-flow");
|
||||
expect(result.evidence.unknown_facts).toContain(
|
||||
"Full bidirectional value-flow outside the checked period is not proven by this MCP discovery pilot"
|
||||
);
|
||||
expect(result.reason_codes).toContain("pilot_bidirectional_value_flow_recipes_selected");
|
||||
expect(result.reason_codes).toContain("pilot_derived_bidirectional_value_flow_from_confirmed_rows");
|
||||
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("keeps non-lifecycle ready plans unsupported until a dedicated pilot exists", async () => {
|
||||
const planner = planAssistantMcpDiscovery({
|
||||
turnMeaning: {
|
||||
|
||||
@@ -112,6 +112,45 @@ describe("assistant MCP discovery response candidate", () => {
|
||||
expect(candidate.reply_text).not.toContain("query_movements");
|
||||
});
|
||||
|
||||
it("localizes bidirectional net value-flow evidence without leaking pilot mechanics", () => {
|
||||
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
|
||||
entryPoint({
|
||||
bridge: {
|
||||
bridge_status: "answer_draft_ready",
|
||||
user_facing_response_allowed: true,
|
||||
business_fact_answer_allowed: true,
|
||||
requires_user_clarification: false,
|
||||
answer_draft: {
|
||||
answer_mode: "confirmed_with_bounded_inference",
|
||||
headline: "По данным 1С найдены строки входящих и исходящих денежных движений; нетто можно называть только как расчет по найденным строкам и проверенному периоду.",
|
||||
confirmed_lines: [
|
||||
"1C bidirectional value-flow rows were checked for counterparty SVK: incoming=found, outgoing=found",
|
||||
"По найденным строкам 1С по контрагенту SVK за период 2020: получили 12 500,50 руб. по входящим движениям, заплатили 4 000 руб. по исходящим платежам/списаниям. Расчетное нетто в нашу сторону: 8 500,50 руб."
|
||||
],
|
||||
inference_lines: [
|
||||
"Counterparty net value-flow was calculated as incoming confirmed 1C rows minus outgoing confirmed 1C rows"
|
||||
],
|
||||
unknown_lines: [
|
||||
"Complete requested-period coverage for bidirectional value-flow is not proven because at least one MCP discovery probe row limit was reached",
|
||||
"Full bidirectional value-flow outside the checked period is not proven by this MCP discovery pilot"
|
||||
],
|
||||
limitation_lines: ["pilot_bidirectional_value_flow_uses_two_query_movements_and_derives_net"],
|
||||
next_step_line: null
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
expect(candidate.candidate_status).toBe("ready_for_guarded_use");
|
||||
expect(candidate.reply_text).toContain("В 1С проверены входящие и исходящие денежные строки по контрагенту SVK");
|
||||
expect(candidate.reply_text).toContain("получили 12 500,50 руб.");
|
||||
expect(candidate.reply_text).toContain("заплатили 4 000 руб.");
|
||||
expect(candidate.reply_text).toContain("нетто в нашу сторону: 8 500,50 руб.");
|
||||
expect(candidate.reply_text).toContain("Полное покрытие запрошенного периода по двустороннему денежному потоку не подтверждено");
|
||||
expect(candidate.reply_text).not.toContain("pilot_");
|
||||
expect(candidate.reply_text).not.toContain("query_movements");
|
||||
});
|
||||
|
||||
it("returns not applicable when discovery was skipped for an exact supported route", () => {
|
||||
const candidate = buildAssistantMcpDiscoveryResponseCandidate({
|
||||
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
|
||||
|
||||
@@ -108,6 +108,29 @@ describe("assistant MCP discovery turn input adapter", () => {
|
||||
expect(result.reason_codes).toContain("mcp_discovery_payout_signal_detected");
|
||||
});
|
||||
|
||||
it("keeps net cash wording as bidirectional value-flow discovery", () => {
|
||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||
userMessage: "какое нетто по деньгам с Группа СВК за 2020: сколько получили и сколько заплатили?",
|
||||
predecomposeContract: {
|
||||
entities: { counterparty: "Группа СВК" },
|
||||
period: { period_from: "2020-01-01", period_to: "2020-12-31" }
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.adapter_status).toBe("ready");
|
||||
expect(result.semantic_data_need).toBe("counterparty value-flow evidence");
|
||||
expect(result.turn_meaning_ref).toMatchObject({
|
||||
asked_domain_family: "counterparty_value",
|
||||
asked_action_family: "net_value_flow",
|
||||
explicit_entity_candidates: ["Группа СВК"],
|
||||
explicit_date_scope: "2020",
|
||||
unsupported_but_understood_family: "counterparty_bidirectional_value_flow_or_netting",
|
||||
stale_replay_forbidden: true
|
||||
});
|
||||
expect(result.reason_codes).toContain("mcp_discovery_bidirectional_value_flow_signal_detected");
|
||||
expect(result.reason_codes).not.toContain("mcp_discovery_payout_signal_detected");
|
||||
});
|
||||
|
||||
it("does not activate discovery for supported exact current-turn intent", () => {
|
||||
const result = buildAssistantMcpDiscoveryTurnInput({
|
||||
assistantTurnMeaning: {
|
||||
|
||||
Reference in New Issue
Block a user