ARCH: замкнуть metadata-scoped clarification recovery loops

This commit is contained in:
2026-04-23 13:42:14 +03:00
parent c96c9bab86
commit cddd6667fb
20 changed files with 993 additions and 43 deletions
@@ -229,4 +229,26 @@ describe("assistant MCP discovery data need graph", () => {
expect(result.reason_codes).toContain("data_need_graph_open_scope_total_without_subject");
expect(result.reason_codes).toContain("data_need_graph_all_time_scope_hint");
});
it("treats metadata-scoped movement evidence as subjectless and asks only for organization plus period", () => {
const result = buildAssistantMcpDiscoveryDataNeedGraph({
semanticDataNeed: "movement evidence",
rawUtterance: "по движениям",
turnMeaning: {
asked_domain_family: "movements",
asked_action_family: "list_movements",
metadata_scope_hint: "НДС",
subject_resolution_optional: true,
unsupported_but_understood_family: "movement_evidence"
}
});
expect(result.business_fact_family).toBe("movement_evidence");
expect(result.subject_candidates).toEqual([]);
expect(result.metadata_scope_hint).toBe("НДС");
expect(result.subject_resolution_optional).toBe(true);
expect(result.clarification_gaps).toEqual(["organization", "period"]);
expect(result.decomposition_candidates).toEqual(["fetch_scoped_movements", "probe_coverage"]);
expect(result.reason_codes).toContain("data_need_graph_metadata_scoped_open_lane_without_subject");
});
});
@@ -823,4 +823,86 @@ 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 metadata-scoped movement evidence in clarification instead of forcing entity resolution", () => {
const result = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
metadata_scope_hint: "НДС",
subject_resolution_optional: true,
business_fact_family: "movement_evidence",
action_family: "list_movements",
aggregation_need: null,
time_scope_need: "period_required",
comparison_need: null,
ranking_need: null,
proof_expectation: "clarification_required",
clarification_gaps: ["organization", "period"],
decomposition_candidates: ["fetch_scoped_movements", "probe_coverage"],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
reason_codes: [
"data_need_graph_built",
"data_need_graph_metadata_scoped_open_lane_without_subject"
]
},
turnMeaning: {
asked_domain_family: "movements",
asked_action_family: "list_movements",
metadata_scope_hint: "НДС",
subject_resolution_optional: true,
unsupported_but_understood_family: "movement_evidence"
}
});
expect(result.planner_status).toBe("needs_clarification");
expect(result.selected_chain_id).toBe("movement_evidence");
expect(result.proposed_primitives).toEqual(["query_movements", "probe_coverage"]);
expect(result.required_axes).toEqual(["metadata_scope", "organization", "coverage_target"]);
expect(result.reason_codes).toContain("planner_selected_metadata_scoped_movement_from_data_need_graph");
expect(result.selected_chain_id).not.toBe("entity_resolution");
});
it("keeps metadata-scoped movement evidence execution-ready once organization and period are known", () => {
const result = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
metadata_scope_hint: "НДС",
subject_resolution_optional: true,
business_fact_family: "movement_evidence",
action_family: "list_movements",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: null,
ranking_need: null,
proof_expectation: "coverage_checked_fact",
clarification_gaps: [],
decomposition_candidates: ["fetch_scoped_movements", "probe_coverage"],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
reason_codes: [
"data_need_graph_built",
"data_need_graph_metadata_scoped_open_lane_without_subject"
]
},
turnMeaning: {
asked_domain_family: "movements",
asked_action_family: "list_movements",
metadata_scope_hint: "НДС",
subject_resolution_optional: true,
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020",
unsupported_but_understood_family: "movement_evidence"
}
});
expect(result.planner_status).toBe("ready_for_execution");
expect(result.selected_chain_id).toBe("movement_evidence");
expect(result.proposed_primitives).toEqual(["query_movements", "probe_coverage"]);
expect(result.required_axes).toEqual(["organization", "period", "metadata_scope", "coverage_target"]);
expect(result.catalog_review.review_status).toBe("catalog_compatible");
expect(result.reason_codes).toContain("planner_selected_metadata_scoped_movement_from_data_need_graph");
});
});
@@ -429,4 +429,44 @@ describe("assistant MCP discovery runtime bridge", () => {
"\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0430"
);
});
it("persists metadata scope and subject-optional flags in the resumable loop state", async () => {
const result = await runAssistantMcpDiscoveryRuntimeBridge({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
metadata_scope_hint: "\u041d\u0414\u0421",
subject_resolution_optional: true,
business_fact_family: "movement_evidence",
action_family: "list_movements",
aggregation_need: null,
time_scope_need: "period_required",
comparison_need: null,
ranking_need: null,
proof_expectation: "clarification_required",
clarification_gaps: ["organization", "period"],
decomposition_candidates: ["fetch_scoped_movements", "probe_coverage"],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
reason_codes: ["data_need_graph_built", "data_need_graph_metadata_scoped_open_lane_without_subject"]
},
turnMeaning: {
asked_domain_family: "movements",
asked_action_family: "list_movements",
metadata_scope_hint: "\u041d\u0414\u0421",
subject_resolution_optional: true,
unsupported_but_understood_family: "movement_evidence"
},
deps: buildDeps([])
});
expect(result.bridge_status).toBe("needs_clarification");
expect(result.loop_state).toMatchObject({
loop_status: "awaiting_clarification",
selected_chain_id: "movement_evidence",
metadata_scope_hint: "\u041d\u0414\u0421",
subject_resolution_optional: true
});
expect(result.loop_state.pending_axes).toEqual(["organization", "period"]);
expect(result.loop_state.explicit_entity_candidates).toEqual([]);
});
});
@@ -1877,4 +1877,144 @@ describe("assistant MCP discovery turn input adapter", () => {
);
expect(result.reason_codes).not.toContain("mcp_discovery_not_applicable_for_supported_exact_turn");
});
it.skip("keeps metadata-born movement lane subjectless and asks for organization plus period", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "по движениям",
followupContext: {
previous_discovery_loop_status: "awaiting_clarification",
previous_discovery_loop_selected_chain_id: "metadata_lane_clarification",
previous_discovery_loop_pending_axes: ["lane_family_choice"],
previous_discovery_loop_asked_domain_family: "metadata",
previous_discovery_loop_asked_action_family: "resolve_next_lane",
previous_discovery_loop_unsupported_family: "metadata_lane_choice_clarification",
previous_discovery_entity_candidates: ["НДС"],
previous_discovery_metadata_ambiguity_detected: true,
previous_discovery_metadata_ambiguity_entity_sets: ["Документ", "РегистрНакопления"]
}
});
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.semantic_data_need).toBe("movement evidence");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "movements",
asked_action_family: "list_movements",
metadata_scope_hint: "НДС",
subject_resolution_optional: true,
unsupported_but_understood_family: "movement_evidence",
stale_replay_forbidden: true
});
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.data_need_graph?.clarification_gaps).toEqual(["organization", "period"]);
expect(result.reason_codes).toContain("mcp_discovery_metadata_ambiguity_resolved_to_movement_lane");
expect(result.reason_codes).toContain("mcp_discovery_metadata_scoped_lane_without_subject");
});
it.skip("keeps metadata scope through organization-only clarification and leaves only period pending", () => {
const orgName = "ООО Альтернатива Плюс";
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "по ООО Альтернатива Плюс",
predecomposeContract: {
entities: { organization: orgName }
},
followupContext: {
previous_discovery_loop_status: "awaiting_clarification",
previous_discovery_loop_selected_chain_id: "movement_evidence",
previous_discovery_loop_pending_axes: ["organization", "period"],
previous_discovery_loop_asked_domain_family: "movements",
previous_discovery_loop_asked_action_family: "list_movements",
previous_discovery_loop_unsupported_family: "movement_evidence",
previous_discovery_entity_candidates: ["НДС"]
}
});
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.semantic_data_need).toBe("movement evidence");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "movements",
asked_action_family: "list_movements",
metadata_scope_hint: "НДС",
subject_resolution_optional: true,
explicit_organization_scope: orgName,
unsupported_but_understood_family: "movement_evidence",
stale_replay_forbidden: true
});
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.data_need_graph?.clarification_gaps).toEqual(["period"]);
});
it("keeps metadata-born movement lane subjectless and asks for organization plus period (utf8-safe)", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "\u043f\u043e \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u044f\u043c",
followupContext: {
previous_discovery_loop_status: "awaiting_clarification",
previous_discovery_loop_selected_chain_id: "metadata_lane_clarification",
previous_discovery_loop_pending_axes: ["lane_family_choice"],
previous_discovery_loop_asked_domain_family: "metadata",
previous_discovery_loop_asked_action_family: "resolve_next_lane",
previous_discovery_loop_unsupported_family: "metadata_lane_choice_clarification",
previous_discovery_entity_candidates: ["\u041d\u0414\u0421"],
previous_discovery_metadata_ambiguity_detected: true,
previous_discovery_metadata_ambiguity_entity_sets: [
"\u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442",
"\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u041d\u0430\u043a\u043e\u043f\u043b\u0435\u043d\u0438\u044f"
]
}
});
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.semantic_data_need).toBe("movement evidence");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "movements",
asked_action_family: "list_movements",
metadata_scope_hint: "\u041d\u0414\u0421",
subject_resolution_optional: true,
unsupported_but_understood_family: "movement_evidence",
stale_replay_forbidden: true
});
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.data_need_graph?.clarification_gaps).toEqual(["organization", "period"]);
expect(result.reason_codes).toContain("mcp_discovery_metadata_ambiguity_resolved_to_movement_lane");
expect(result.reason_codes).toContain("mcp_discovery_metadata_scoped_lane_without_subject");
});
it("keeps metadata scope through organization-only clarification and leaves only period pending (utf8-safe)", () => {
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",
predecomposeContract: {
entities: { organization: orgName }
},
followupContext: {
previous_discovery_loop_status: "awaiting_clarification",
previous_discovery_loop_selected_chain_id: "movement_evidence",
previous_discovery_loop_pending_axes: ["organization", "period"],
previous_discovery_loop_asked_domain_family: "movements",
previous_discovery_loop_asked_action_family: "list_movements",
previous_discovery_loop_unsupported_family: "movement_evidence",
previous_discovery_loop_metadata_scope_hint: "\u041d\u0414\u0421",
previous_discovery_loop_subject_resolution_optional: true,
previous_discovery_entity_candidates: ["\u041d\u0414\u0421"]
}
});
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.semantic_data_need).toBe("movement evidence");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "movements",
asked_action_family: "list_movements",
metadata_scope_hint: "\u041d\u0414\u0421",
subject_resolution_optional: true,
explicit_organization_scope: orgName,
unsupported_but_understood_family: "movement_evidence",
stale_replay_forbidden: true
});
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.data_need_graph?.clarification_gaps).toEqual(["period"]);
});
});
@@ -1647,4 +1647,156 @@ describe("assistantTransitionPolicy", () => {
period_to: "2017-05-31"
});
});
it("carries metadata-scoped subjectless loop state through follow-up context", () => {
const policy = buildPolicy({
findLastAddressAssistantItem: () => ({
text: "\u043d\u0443\u0436\u043d\u044b \u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\u0438\u044f \u0438 \u043f\u0435\u0440\u0438\u043e\u0434",
debug: {
execution_lane: "living_chat",
mcp_discovery_response_applied: true,
assistant_mcp_discovery_entry_point_v1: {
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
entry_status: "bridge_executed",
turn_input: {
turn_meaning_ref: {
asked_domain_family: "movements",
asked_action_family: "list_movements",
metadata_scope_hint: "\u041d\u0414\u0421",
subject_resolution_optional: true,
unsupported_but_understood_family: "movement_evidence",
stale_replay_forbidden: true
}
},
bridge: {
bridge_status: "needs_clarification",
business_fact_answer_allowed: false,
pilot: {
pilot_scope: "counterparty_movement_evidence_query_movements_v1"
},
loop_state: {
schema_version: "assistant_mcp_discovery_loop_state_v1",
policy_owner: "assistantMcpDiscoveryRuntimeBridge",
loop_status: "awaiting_clarification",
selected_chain_id: "movement_evidence",
pilot_scope: "counterparty_movement_evidence_query_movements_v1",
asked_domain_family: "movements",
asked_action_family: "list_movements",
unsupported_but_understood_family: "movement_evidence",
ranking_need: null,
pending_axes: ["organization", "period"],
provided_axes: [],
explicit_entity_candidates: [],
metadata_scope_hint: "\u041d\u0414\u0421",
subject_resolution_optional: true,
explicit_organization_scope: null,
explicit_date_scope: null
},
answer_draft: {
answer_mode: "needs_clarification"
}
}
}
}
}),
hasAddressFollowupContextSignal: () => true,
hasReferentialPointer: () => false,
resolveAddressIntent: () => ({ intent: "unknown" }),
resolveAddressIntentFamily: () => null,
resolveAssistantTurnMeaning: () => null
});
const carryover = policy.resolveAddressFollowupCarryoverContext(
"\u043f\u043e \u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441",
[{ kind: "assistant", text: "\u043d\u0443\u0436\u043d\u044b \u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\u0438\u044f \u0438 \u043f\u0435\u0440\u0438\u043e\u0434" }],
"\u043f\u043e \u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441",
{ predecomposeContract: { intent: "unknown" } },
null
);
expect(carryover?.followupContext?.previous_discovery_loop_selected_chain_id).toBe("movement_evidence");
expect(carryover?.followupContext?.previous_discovery_loop_pending_axes).toEqual([
"organization",
"period"
]);
expect(carryover?.followupContext?.previous_discovery_loop_metadata_scope_hint).toBe(
"\u041d\u0414\u0421"
);
expect(carryover?.followupContext?.previous_discovery_loop_subject_resolution_optional).toBe(true);
});
it("does not backfill metadata scope into counterparty carryover during lane choice follow-up", () => {
const policy = buildPolicy({
findLastAddressAssistantItem: () => ({
text: "\u0443\u0442\u043e\u0447\u043d\u0438\u0442\u0435: \u043f\u043e \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u043c \u0438\u043b\u0438 \u043f\u043e \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u044f\u043c?",
debug: {
execution_lane: "living_chat",
mcp_discovery_response_applied: true,
assistant_mcp_discovery_entry_point_v1: {
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
entry_status: "bridge_executed",
turn_input: {
turn_meaning_ref: {
asked_domain_family: "metadata",
asked_action_family: "resolve_next_lane",
explicit_entity_candidates: ["\u041d\u0414\u0421"],
metadata_scope_hint: "\u041d\u0414\u0421",
metadata_ambiguity_entity_sets: [
"\u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442",
"\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u041d\u0430\u043a\u043e\u043f\u043b\u0435\u043d\u0438\u044f"
],
unsupported_but_understood_family: "metadata_lane_choice_clarification",
stale_replay_forbidden: true
}
},
bridge: {
bridge_status: "needs_clarification",
business_fact_answer_allowed: false,
pilot: {
pilot_scope: "metadata_inspection_v1"
},
loop_state: {
schema_version: "assistant_mcp_discovery_loop_state_v1",
policy_owner: "assistantMcpDiscoveryRuntimeBridge",
loop_status: "awaiting_clarification",
selected_chain_id: "metadata_lane_clarification",
pilot_scope: "metadata_inspection_v1",
asked_domain_family: "metadata",
asked_action_family: "resolve_next_lane",
unsupported_but_understood_family: "metadata_lane_choice_clarification",
ranking_need: null,
pending_axes: ["lane_family_choice"],
provided_axes: [],
explicit_entity_candidates: ["\u041d\u0414\u0421"],
metadata_scope_hint: "\u041d\u0414\u0421",
subject_resolution_optional: false,
explicit_organization_scope: null,
explicit_date_scope: null
},
answer_draft: {
answer_mode: "needs_clarification"
}
}
}
}
}),
hasAddressFollowupContextSignal: () => true,
hasReferentialPointer: () => false,
resolveAddressIntent: () => ({ intent: "unknown" }),
resolveAddressIntentFamily: () => null,
resolveAssistantTurnMeaning: () => null
});
const carryover = policy.resolveAddressFollowupCarryoverContext(
"\u043f\u043e \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u044f\u043c",
[{ kind: "assistant", text: "\u0443\u0442\u043e\u0447\u043d\u0438\u0442\u0435: \u043f\u043e \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u043c \u0438\u043b\u0438 \u043f\u043e \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u044f\u043c?" }],
"\u043f\u043e \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u044f\u043c",
{ predecomposeContract: { intent: "unknown" } },
null
);
expect(carryover?.followupContext?.previous_filters?.counterparty).toBeUndefined();
expect(carryover?.followupContext?.previous_anchor_type).toBeUndefined();
expect(carryover?.followupContext?.previous_anchor_value).toBeNull();
expect(carryover?.followupContext?.previous_discovery_entity_candidates).toEqual(["\u041d\u0414\u0421"]);
expect(carryover?.followupContext?.previous_discovery_pilot_scope).toBe("metadata_inspection_v1");
});
});