ARCH: связать metadata grounding с movement lane и reusable follow-up

This commit is contained in:
2026-04-22 09:26:58 +03:00
parent 7ef788fa50
commit eac3709f2b
17 changed files with 762 additions and 54 deletions
@@ -116,6 +116,34 @@ describe("assistant MCP discovery answer adapter", () => {
expect(draft.must_not_claim).toContain("Do not claim full document history outside the checked period.");
});
it("turns generic movement evidence into a bounded movement answer draft", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "movements",
asked_action_family: "list_movements",
explicit_entity_candidates: ["SVK"],
explicit_date_scope: "2020",
unsupported_but_understood_family: "movement_evidence"
}
});
const pilot = await executeAssistantMcpDiscoveryPilot(
planner,
buildDeps([{ Period: "2020-01-15T00:00:00", Counterparty: "SVK", Registrar: "Move1" }])
);
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
expect(draft.answer_mode).toBe("confirmed_with_bounded_inference");
expect(draft.headline).toContain("движений");
expect(draft.confirmed_lines).toContain("1C movement rows were found for counterparty SVK");
expect(draft.inference_lines).toContain(
"Counterparty movement evidence is limited to confirmed 1C movement rows in the checked scope"
);
expect(draft.unknown_lines).toContain("Full movement history outside the checked period is not proven by this MCP discovery pilot");
expect(draft.must_not_claim).toContain("Do not claim full movement history outside the checked period.");
expect(draft.must_not_claim).toContain("Do not present the confirmed movement rows as a complete movement universe.");
});
it("asks for clarification when discovery did not execute due to missing scope", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
@@ -133,6 +133,44 @@ describe("assistant MCP discovery pilot executor", () => {
expect(result.source_rows_summary).toBe("2 MCP document rows fetched, 2 matched document scope");
});
it("executes generic movement evidence through query_movements without deriving turnover totals", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "movements",
asked_action_family: "list_movements",
explicit_entity_candidates: ["SVK"],
explicit_date_scope: "2020",
unsupported_but_understood_family: "movement_evidence"
}
});
const deps = buildDeps([
{ Period: "2020-01-15T00:00:00", Amount: 1250, Counterparty: "SVK", Registrar: "Move1" },
{ Period: "2020-03-20T00:00:00", Amount: "900,25", Counterparty: "SVK", Registrar: "Move2" }
]);
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
expect(result.pilot_status).toBe("executed");
expect(result.pilot_scope).toBe("counterparty_movement_evidence_query_movements_v1");
expect(result.executed_primitives).toEqual(["query_movements"]);
expect(result.derived_value_flow).toBeNull();
expect(result.derived_bidirectional_value_flow).toBeNull();
expect(result.evidence.confirmed_facts).toContain("1C movement rows were found for counterparty SVK");
expect(result.evidence.inferred_facts).toContain(
"Counterparty movement evidence is limited to confirmed 1C movement rows in the checked scope"
);
expect(result.evidence.unknown_facts).toContain(
"Full movement history outside the checked period is not proven by this MCP discovery pilot"
);
expect(result.source_rows_summary).toBe("2 MCP movement rows fetched, 2 matched movement scope");
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(1);
const call = deps.executeAddressMcpQuery.mock.calls[0]?.[0];
expect(String(call?.query ?? "")).toContain("Документ.СписаниеСРасчетногоСчета");
expect(String(call?.query ?? "")).toContain("Документ.ПоступлениеНаРасчетныйСчет");
expect(call?.limit).toBeGreaterThan(0);
});
it("executes inspect_1c_metadata and derives a confirmed metadata surface", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
@@ -87,6 +87,25 @@ describe("assistant MCP discovery planner", () => {
expect(result.required_axes).toEqual(["counterparty", "coverage_target"]);
});
it("builds a movement discovery plan without aggregating value-flow totals", () => {
const result = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "movements",
asked_action_family: "list_movements",
explicit_entity_candidates: ["SVK"],
explicit_date_scope: "2020",
unsupported_but_understood_family: "movement_evidence"
}
});
expect(result.planner_status).toBe("ready_for_execution");
expect(result.semantic_data_need).toBe("movement evidence");
expect(result.proposed_primitives).toEqual(["resolve_entity_reference", "query_movements", "probe_coverage"]);
expect(result.proposed_primitives).not.toContain("aggregate_by_axis");
expect(result.required_axes).toEqual(["counterparty", "period", "coverage_target"]);
expect(result.reason_codes).toContain("planner_selected_movement_recipe");
});
it("builds an inference-safe lifecycle plan with evidence explanation", () => {
const result = planAssistantMcpDiscovery({
turnMeaning: {
@@ -236,6 +236,33 @@ describe("assistant MCP discovery response candidate", () => {
expect(candidate.reply_text).not.toContain("1C document rows were found");
});
it("localizes movement evidence without leaking raw English facts", () => {
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 movement rows were found for counterparty SVK"],
inference_lines: ["Counterparty movement evidence is limited to confirmed 1C movement rows in the checked scope"],
unknown_lines: ["Full movement history outside the checked period is not proven by this MCP discovery pilot"],
limitation_lines: [],
next_step_line: null
}
}
})
);
expect(candidate.reply_text).toContain("В 1С найдены строки движений по контрагенту SVK.");
expect(candidate.reply_text).toContain("Срез движений ограничен только подтвержденными строками движений");
expect(candidate.reply_text).toContain("Полный исторический срез движений вне проверенного периода этим поиском не подтвержден.");
expect(candidate.reply_text).not.toContain("1C movement rows were found");
});
it("localizes metadata evidence without leaking raw MCP wording", () => {
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
entryPoint({
@@ -264,6 +264,103 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.reason_codes).toContain("mcp_discovery_counterparty_from_followup_context");
});
it("pivots grounded metadata follow-up into movement evidence when the next lane is explicit", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "then movements",
followupContext: {
previous_discovery_pilot_scope: "metadata_inspection_v1",
previous_discovery_metadata_route_family: "movement_evidence",
previous_discovery_metadata_selected_entity_set: "РегистрНакопления",
previous_filters: {
counterparty: "SVK",
period_from: "2020-01-01",
period_to: "2020-12-31"
},
previous_anchor_type: "counterparty",
previous_anchor_value: "SVK"
}
});
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("movement evidence");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "movements",
asked_action_family: "list_movements",
explicit_entity_candidates: ["SVK"],
explicit_date_scope: "2020",
unsupported_but_understood_family: "movement_evidence",
stale_replay_forbidden: true
});
expect(result.reason_codes).toContain("mcp_discovery_metadata_grounded_movement_followup");
expect(result.reason_codes).toContain("mcp_discovery_counterparty_from_followup_context");
});
it("continues from grounded metadata into document evidence on a generic downstream follow-up", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "давай дальше",
followupContext: {
previous_discovery_pilot_scope: "metadata_inspection_v1",
previous_discovery_metadata_route_family: "document_evidence",
previous_discovery_metadata_selected_entity_set: "Документ",
previous_filters: {
counterparty: "SVK",
period_from: "2020-01-01",
period_to: "2020-12-31"
},
previous_anchor_type: "counterparty",
previous_anchor_value: "SVK"
}
});
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("document evidence");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "documents",
asked_action_family: "list_documents",
explicit_entity_candidates: ["SVK"],
explicit_date_scope: "2020",
unsupported_but_understood_family: "document_evidence",
stale_replay_forbidden: true
});
expect(result.reason_codes).toContain("mcp_discovery_metadata_grounded_lane_continuation");
});
it("continues from grounded metadata into movement evidence on a generic downstream follow-up", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "continue with data",
followupContext: {
previous_discovery_pilot_scope: "metadata_inspection_v1",
previous_discovery_metadata_route_family: "movement_evidence",
previous_discovery_metadata_selected_entity_set: "РегистрНакопления",
previous_filters: {
counterparty: "SVK",
period_from: "2020-01-01",
period_to: "2020-12-31"
},
previous_anchor_type: "counterparty",
previous_anchor_value: "SVK"
}
});
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("movement evidence");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "movements",
asked_action_family: "list_movements",
explicit_entity_candidates: ["SVK"],
explicit_date_scope: "2020",
unsupported_but_understood_family: "movement_evidence",
stale_replay_forbidden: true
});
expect(result.reason_codes).toContain("mcp_discovery_metadata_grounded_lane_continuation");
});
it("switches the checked year on a short payout follow-up while keeping prior discovery counterparty", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "а теперь за 2021?",