ARCH: добавить supplier payout pilot MCP discovery

This commit is contained in:
2026-04-20 18:42:58 +03:00
parent 49fd08652c
commit 52da709671
20 changed files with 688 additions and 48 deletions
@@ -162,6 +162,80 @@ describe("assistant address lane response runtime adapter", () => {
);
});
it("can replace a stale address exact answer with a guarded MCP discovery candidate", () => {
const finalizeAddressTurn = vi.fn((input) => ({
response: {
ok: true,
assistant_reply: input.assistantReply,
reply_type: input.replyType,
debug: input.debug
}
}));
const runtime = runAssistantAddressLaneResponseRuntime({
sessionId: "asst-mcp-address",
userMessage: "сколько мы заплатили СВК за 2020?",
effectiveAddressUserMessage: "сколько мы заплатили СВК за 2020?",
addressLane: {
handled: true,
reply_text: "stale documents answer",
reply_type: "factual",
debug: {}
},
llmPreDecomposeMeta: {
mcpDiscoveryRuntimeEntryPoint: {
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
entry_status: "bridge_executed",
hot_runtime_wired: false,
discovery_attempted: true,
turn_input: { adapter_status: "ready", should_run_discovery: true },
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: "Discovery payout answer",
confirmed_lines: ["1C supplier-payout rows were found for counterparty SVK"],
inference_lines: ["Counterparty supplier-payout total was calculated from confirmed 1C outgoing payment rows"],
unknown_lines: ["Full supplier-payout amount outside the checked period is not proven by this MCP discovery pilot"],
limitation_lines: [],
next_step_line: null
}
},
reason_codes: ["runtime_entry_point_bridge_executed"]
}
},
knownOrganizations: [],
activeOrganization: null,
sanitizeOutgoingAssistantText: (text) => String(text ?? ""),
buildAddressDebugPayload: () => ({}),
buildAddressFollowupOffer: () => null,
mergeKnownOrganizations: (items) => items,
toNonEmptyString: () => null,
appendItem: () => {},
getSession: () => ({ session_id: "asst-mcp-address", updated_at: "", items: [], investigation_state: null } as any),
persistSession: () => {},
cloneConversation: (items) => items,
logEvent: () => {},
messageIdFactory: () => "msg-mcp-address",
finalizeAddressTurn
});
expect(finalizeAddressTurn).toHaveBeenCalledWith(
expect.objectContaining({
assistantReply: expect.stringContaining("Discovery payout answer"),
replyType: "partial_coverage",
debug: expect.objectContaining({
mcp_discovery_response_applied: true
})
})
);
expect(String((runtime.response as any).assistant_reply)).toContain("исходящих платежей/списаний");
});
it("keeps debug bounded to shadow contracts when optional enrichment is absent", () => {
const runtime = runAssistantAddressLaneResponseRuntime({
sessionId: "asst-2",
@@ -110,6 +110,35 @@ describe("assistant MCP discovery answer adapter", () => {
expect(draft.limitation_lines.join("\n")).not.toContain("pilot_");
});
it("turns supplier payout evidence into a bounded outgoing payment answer draft", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "payout",
explicit_entity_candidates: ["SVK"],
explicit_date_scope: "2020",
unsupported_but_understood_family: "counterparty_payouts_or_outflow"
}
});
const pilot = await executeAssistantMcpDiscoveryPilot(
planner,
buildDeps([
{ Period: "2020-03-15T00:00:00", Amount: 4100, Counterparty: "SVK" },
{ Period: "2020-04-20T00:00:00", Amount: "900,25", 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("исходящих платежей/списаний");
expect(confirmedText).toContain("5 000,25 руб.");
expect(draft.inference_lines.join("\n")).toContain("supplier-payout total");
expect(draft.unknown_lines).toContain("Full supplier-payout amount outside the checked period is not proven by this MCP discovery pilot");
});
it("does not leak primitive names or query text into user-facing lines", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
@@ -106,6 +106,7 @@ describe("assistant MCP discovery pilot executor", () => {
rows_matched: 2,
rows_with_amount: 2,
total_amount: 3750.5,
coverage_limited_by_probe_limit: false,
first_movement_date: "2020-01-15",
latest_movement_date: "2020-02-20",
inference_basis: "sum_of_confirmed_1c_value_flow_rows"
@@ -119,6 +120,69 @@ describe("assistant MCP discovery pilot executor", () => {
expect(call?.limit).toBeGreaterThan(0);
});
it("executes supplier payout query_movements and derives a guarded outgoing payment sum", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "payout",
explicit_entity_candidates: ["SVK"],
explicit_date_scope: "2020",
unsupported_but_understood_family: "counterparty_payouts_or_outflow"
}
});
const deps = buildDeps([
{ Period: "2020-03-15T00:00:00", Amount: 4100, Counterparty: "SVK" },
{ Period: "2020-04-20T00:00:00", Amount: "900,25", Counterparty: "SVK" }
]);
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
expect(result.pilot_status).toBe("executed");
expect(result.pilot_scope).toBe("counterparty_supplier_payout_query_movements_v1");
expect(result.derived_value_flow).toMatchObject({
value_flow_direction: "outgoing_supplier_payout",
counterparty: "SVK",
period_scope: "2020",
rows_matched: 2,
rows_with_amount: 2,
total_amount: 5000.25,
coverage_limited_by_probe_limit: false,
first_movement_date: "2020-03-15",
latest_movement_date: "2020-04-20"
});
expect(result.evidence.confirmed_facts[0]).toContain("supplier-payout rows");
expect(result.evidence.inferred_facts[0]).toContain("supplier-payout total");
expect(result.evidence.unknown_facts).toContain("Full supplier-payout amount outside the checked period is not proven by this MCP discovery pilot");
expect(result.reason_codes).toContain("pilot_supplier_payout_recipe_selected");
const call = deps.executeAddressMcpQuery.mock.calls[0]?.[0];
expect(String(call?.query ?? "")).toContain("СписаниеСРасчетногоСчета");
});
it("marks value-flow coverage as limited when the probe row limit is reached", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "payout",
explicit_entity_candidates: ["SVK"],
explicit_date_scope: "2020",
unsupported_but_understood_family: "counterparty_payouts_or_outflow"
}
});
const rows = Array.from({ length: 100 }, (_, index) => ({
Period: `2020-01-${String((index % 28) + 1).padStart(2, "0")}T00:00:00`,
Amount: 10,
Counterparty: "SVK"
}));
const result = await executeAssistantMcpDiscoveryPilot(planner, buildDeps(rows));
expect(result.derived_value_flow?.coverage_limited_by_probe_limit).toBe(true);
expect(result.evidence.unknown_facts).toContain(
"Complete requested-period coverage is not proven because the MCP discovery probe row limit was reached"
);
});
it("keeps non-lifecycle ready plans unsupported until a dedicated pilot exists", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
@@ -76,6 +76,42 @@ describe("assistant MCP discovery response candidate", () => {
expect(candidate.reply_text).not.toContain("query_movements");
});
it("localizes supplier-payout 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 supplier-payout rows were found for counterparty SVK",
"По найденным строкам исходящих платежей/списаний в 1С по контрагенту SVK за период 2020 сумма исходящих платежей/списаний составляет 5 000 руб."
],
inference_lines: ["Counterparty supplier-payout total was calculated from confirmed 1C outgoing payment rows"],
unknown_lines: [
"Complete requested-period coverage is not proven because the MCP discovery probe row limit was reached",
"Full supplier-payout amount outside the checked period is not proven by this MCP discovery pilot"
],
limitation_lines: ["pilot_value_flow_uses_query_movements_and_derives_aggregate"],
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("5 000 руб.");
expect(candidate.reply_text).toContain("Полное покрытие запрошенного периода не подтверждено");
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",
@@ -109,6 +109,45 @@ describe("assistant MCP discovery response policy", () => {
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_not_discovery_ready_deep_candidate");
});
it("applies a guarded candidate for discovery-ready address lane answers", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: "stale exact route answer",
currentReplySource: "address_query_runtime_v1",
addressRuntimeMeta: {
assistant_mcp_discovery_entry_point_v1: entryPoint({
turn_input: {
adapter_status: "ready",
should_run_discovery: true
}
})
}
});
expect(result.applied).toBe(true);
expect(result.reply_source).toBe("mcp_discovery_response_candidate_guarded");
expect(result.reply_text).toContain("Confirmed fact");
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_not_discovery_ready_address_candidate");
});
it("keeps address lane answers when discovery was not requested for the current turn", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: "supported exact route answer",
currentReplySource: "address_query_runtime_v1",
addressRuntimeMeta: {
assistant_mcp_discovery_entry_point_v1: entryPoint({
turn_input: {
adapter_status: "not_applicable",
should_run_discovery: false
}
})
}
});
expect(result.applied).toBe(false);
expect(result.reply_text).toBe("supported exact route answer");
expect(result.reason_codes).toContain("mcp_discovery_response_policy_not_discovery_ready_address_candidate");
});
it("keeps the current reply when the candidate has no grounded text", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: "route is not wired",
@@ -86,6 +86,28 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.turn_meaning_ref?.explicit_organization_scope).toBeUndefined();
});
it("keeps payout wording as outgoing supplier-payout 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: "payout",
explicit_entity_candidates: ["Группа СВК"],
explicit_date_scope: "2020",
unsupported_but_understood_family: "counterparty_payouts_or_outflow",
stale_replay_forbidden: true
});
expect(result.reason_codes).toContain("mcp_discovery_payout_signal_detected");
});
it("does not activate discovery for supported exact current-turn intent", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
assistantTurnMeaning: {