import { describe, expect, it } from "vitest"; import { buildAssistantMcpDiscoveryDataNeedGraph } from "../src/services/assistantMcpDiscoveryDataNeedGraph"; describe("assistant MCP discovery data need graph", () => { it("builds a monthly bidirectional value-flow graph from grounded turn meaning", () => { const result = buildAssistantMcpDiscoveryDataNeedGraph({ semanticDataNeed: "counterparty value-flow evidence", rawUtterance: "какое нетто по деньгам с SVK за 2020 год по месяцам", turnMeaning: { asked_domain_family: "counterparty_value", asked_action_family: "net_value_flow", asked_aggregation_axis: "month", explicit_entity_candidates: ["SVK"], explicit_date_scope: "2020" } }); expect(result.business_fact_family).toBe("value_flow"); expect(result.action_family).toBe("net_value_flow"); expect(result.aggregation_need).toBe("by_month"); expect(result.time_scope_need).toBe("explicit_period"); expect(result.comparison_need).toBe("incoming_vs_outgoing"); expect(result.proof_expectation).toBe("coverage_checked_fact"); expect(result.clarification_gaps).toEqual([]); expect(result.decomposition_candidates).toEqual([ "resolve_entity_reference", "collect_incoming_movements", "collect_outgoing_movements", "aggregate_by_month", "probe_coverage" ]); expect(result.forbidden_overclaim_flags).toContain("no_unchecked_fact_totals"); }); it("marks metadata lane choice as a clarification-required graph", () => { const result = buildAssistantMcpDiscoveryDataNeedGraph({ semanticDataNeed: "metadata lane clarification", rawUtterance: "давай дальше", turnMeaning: { asked_domain_family: "metadata", asked_action_family: "resolve_next_lane", explicit_entity_candidates: ["SVK"], unsupported_but_understood_family: "metadata_lane_choice_clarification" } }); expect(result.business_fact_family).toBe("schema_surface"); expect(result.clarification_gaps).toEqual(["lane_family_choice"]); expect(result.proof_expectation).toBe("clarification_required"); }); it("keeps entity search as an entity-grounding graph", () => { const result = buildAssistantMcpDiscoveryDataNeedGraph({ semanticDataNeed: "entity discovery evidence", rawUtterance: "найди в 1С контрагента Группа СВК", turnMeaning: { asked_domain_family: "entity_resolution", asked_action_family: "search_business_entity", explicit_entity_candidates: ["Группа СВК"] } }); expect(result.business_fact_family).toBe("entity_grounding"); expect(result.subject_candidates).toEqual(["Группа СВК"]); expect(result.proof_expectation).toBe("entity_grounding"); expect(result.decomposition_candidates).toEqual([ "search_business_entity", "resolve_entity_reference", "probe_coverage" ]); expect(result.forbidden_overclaim_flags).toContain("no_unresolved_entity_claim"); }); it("builds a subjectless inventory stock snapshot graph with an as-of date gate", () => { const result = buildAssistantMcpDiscoveryDataNeedGraph({ semanticDataNeed: "inventory stock snapshot evidence", rawUtterance: "show stock snapshot for warehouse main as of 2021-09-30", turnMeaning: { asked_domain_family: "inventory_stock", asked_action_family: "stock_snapshot", explicit_organization_scope: "ООО Альтернатива Плюс", explicit_date_scope: "2021-09-30" } }); expect(result.business_fact_family).toBe("inventory_stock_snapshot"); expect(result.subject_candidates).toEqual([]); expect(result.clarification_gaps).toEqual([]); expect(result.time_scope_need).toBe("explicit_period"); expect(result.decomposition_candidates).toEqual([ "fetch_scoped_movements", "aggregate_checked_amounts", "probe_coverage", "explain_evidence_basis" ]); expect(result.forbidden_overclaim_flags).toContain("no_unchecked_stock_snapshot"); }); it("keeps inventory stock snapshot blocked on an explicit as-of date when no date exists", () => { const result = buildAssistantMcpDiscoveryDataNeedGraph({ semanticDataNeed: "inventory stock snapshot evidence", rawUtterance: "show current stock snapshot", turnMeaning: { asked_domain_family: "inventory_stock", asked_action_family: "stock_snapshot", explicit_organization_scope: "ООО Альтернатива Плюс" } }); expect(result.business_fact_family).toBe("inventory_stock_snapshot"); expect(result.clarification_gaps).toEqual(["as_of_date"]); expect(result.proof_expectation).toBe("clarification_required"); }); it("builds inventory provenance and sale-trace graphs as checked document drilldowns", () => { const provenance = buildAssistantMcpDiscoveryDataNeedGraph({ semanticDataNeed: "inventory purchase provenance evidence", rawUtterance: "who supplied the selected stock item", turnMeaning: { asked_domain_family: "inventory_stock", asked_action_family: "purchase_provenance", explicit_entity_candidates: ["Столешница 600"] } }); const saleTrace = buildAssistantMcpDiscoveryDataNeedGraph({ semanticDataNeed: "inventory sale trace evidence", rawUtterance: "who bought the selected stock item", turnMeaning: { asked_domain_family: "inventory_stock", asked_action_family: "sale_trace", explicit_entity_candidates: ["Столешница 600"] } }); expect(provenance.business_fact_family).toBe("inventory_purchase_provenance"); expect(provenance.decomposition_candidates).toEqual([ "resolve_entity_reference", "fetch_scoped_documents", "drilldown_related_objects", "probe_coverage", "explain_evidence_basis" ]); expect(provenance.forbidden_overclaim_flags).toContain("no_unproven_supplier_attribution"); expect(saleTrace.business_fact_family).toBe("inventory_sale_trace"); expect(saleTrace.decomposition_candidates).toEqual([ "resolve_entity_reference", "fetch_scoped_documents", "drilldown_related_objects", "probe_coverage", "explain_evidence_basis" ]); expect(saleTrace.forbidden_overclaim_flags).toContain("no_unproven_buyer_or_sale_trace"); }); it("treats top-value wording as a ranking ask 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" } }); expect(result.business_fact_family).toBe("value_flow"); expect(result.ranking_need).toBe("top_desc"); expect(result.clarification_gaps).toEqual(["organization"]); expect(result.proof_expectation).toBe("clarification_required"); expect(result.decomposition_candidates).toEqual([ "collect_scoped_movements", "aggregate_ranked_axis_values", "probe_coverage" ]); expect(result.reason_codes).toContain("data_need_graph_ranking_top_desc"); expect(result.reason_codes).toContain("data_need_graph_open_scope_total_needs_organization"); }); it("keeps organization-scoped ranking executable when the ranking axis comes from follow-up context", () => { const result = buildAssistantMcpDiscoveryDataNeedGraph({ semanticDataNeed: "counterparty value-flow evidence", rawUtterance: "по ООО Альтернатива Плюс", turnMeaning: { asked_domain_family: "counterparty_value", asked_action_family: "turnover", explicit_organization_scope: "ООО Альтернатива Плюс", explicit_date_scope: "2020", seeded_ranking_need: "top_desc" } }); expect(result.business_fact_family).toBe("value_flow"); expect(result.ranking_need).toBe("top_desc"); expect(result.clarification_gaps).toEqual([]); expect(result.proof_expectation).toBe("coverage_checked_fact"); }); it("treats incoming-vs-outgoing comparison 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: "net_value_flow", explicit_date_scope: "2020" } }); expect(result.business_fact_family).toBe("value_flow"); expect(result.comparison_need).toBe("incoming_vs_outgoing"); expect(result.clarification_gaps).toEqual(["organization"]); expect(result.decomposition_candidates).toEqual([ "collect_incoming_movements", "collect_outgoing_movements", "probe_coverage" ]); 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"); }); it("treats a generic incoming total as an understood open-scope ask that still needs organization", () => { const result = buildAssistantMcpDiscoveryDataNeedGraph({ semanticDataNeed: "counterparty value-flow evidence", rawUtterance: "сколько входящих денег за 2020 год?", turnMeaning: { asked_domain_family: "counterparty_value", asked_action_family: "turnover", explicit_date_scope: "2020" } }); expect(result.business_fact_family).toBe("value_flow"); expect(result.subject_candidates).toEqual([]); expect(result.clarification_gaps).toEqual(["organization"]); expect(result.proof_expectation).toBe("clarification_required"); 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"); expect(result.reason_codes).toContain("data_need_graph_open_scope_total_needs_organization"); }); it("treats colloquial open incoming total wording with filler words as an open-scope ask rather than a missing subject", () => { const result = buildAssistantMcpDiscoveryDataNeedGraph({ semanticDataNeed: "counterparty value-flow evidence", rawUtterance: "сколько вообще входящих денег было?", turnMeaning: { asked_domain_family: "counterparty_value", asked_action_family: "turnover" } }); expect(result.business_fact_family).toBe("value_flow"); expect(result.subject_candidates).toEqual([]); expect(result.clarification_gaps).toEqual(["organization", "period"]); expect(result.proof_expectation).toBe("clarification_required"); 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"); expect(result.reason_codes).toContain("data_need_graph_open_scope_total_needs_organization"); }); it("treats all-time open-scope totals as an open-ended period rather than a missing period", () => { const result = buildAssistantMcpDiscoveryDataNeedGraph({ semanticDataNeed: "counterparty value-flow evidence", rawUtterance: "сколько вообще денег мы заработали за все время?", turnMeaning: { asked_domain_family: "counterparty_value", asked_action_family: "turnover", explicit_organization_scope: "ООО Альтернатива Плюс" } }); expect(result.business_fact_family).toBe("value_flow"); expect(result.subject_candidates).toEqual([]); expect(result.time_scope_need).toBe("all_time_scope"); expect(result.clarification_gaps).toEqual([]); expect(result.proof_expectation).toBe("coverage_checked_fact"); 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"); 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"); }); });