import { describe, expect, it } from "vitest"; import { composeAssistantAnswer } from "../src/services/answerComposer"; import type { AnswerGroundingCheck, RequirementCoverageReport, UnifiedRetrievalResult } from "../src/types/assistant"; import type { ProblemUnit, ProblemUnitSummary } from "../src/types/stage2ProblemUnits"; function buildRouteSummary() { return { mode: "deterministic_v2" as const, message_in_scope: true, scope_confidence: "high" as const, planner: { total_fragments: 1, in_scope_fragments: 1, out_of_scope_fragments: 0, discarded_fragments: 0, contains_multiple_tasks: false }, decisions: [], fallback: { type: "none" as const, message: null } }; } function buildCoverage(partial = false): RequirementCoverageReport { return { requirements_total: 1, requirements_covered: partial ? 0 : 1, requirements_uncovered: partial ? ["R1"] : [], requirements_partially_covered: partial ? ["R1"] : [], clarification_needed_for: [], out_of_scope_requirements: [] }; } function buildGrounding(status: AnswerGroundingCheck["status"]): AnswerGroundingCheck { return { status, route_subject_match: true, missing_requirements: status === "partial" ? ["R1"] : [], reasons: status === "partial" ? ["Coverage is partial for lifecycle-focused analysis."] : [], why_included_summary: ["synthetic-test"], selection_reason_summary: ["synthetic-test"] }; } function buildLifecycleProblemUnit(): ProblemUnit { return { schema_version: "problem_unit_v0_1", problem_unit_id: "pu-lc-1", problem_unit_type: "lifecycle_anomaly_node", title: "Lifecycle anomaly node detected", mechanism_summary: "Mechanism candidate: expected transition is missing.", business_defect_class: "missing_expected_transition", severity: { score: 0.84, grade: "high" }, confidence: { score: 0.68, grade: "medium" }, affected_entities: ["Document:DOC-1"], affected_documents: ["Document:DOC-1"], affected_postings: [], affected_accounts: ["51", "60"], affected_counterparties: ["Counterparty:CP-1"], affected_contracts: ["Contract:CTR-1"], expected_state: "settlement_closed", actual_state: "stale_unlinked_payment", failed_expected_edge: "payment_to_settlement", period_impact: { is_period_sensitive: true, impact_class: "close_risk" }, evidence_pack: ["cand-1"], entity_backlinks: [{ entity: "Document", id: "DOC-1" }], snapshot_limitations: [], lifecycle_domain: "bank_settlement", lifecycle_object_id: "lcobj-pu-lc-1", current_lifecycle_state: "stale_unlinked_payment", expected_lifecycle_state: "settlement_closed", missing_transition: "payment_to_settlement", lifecycle_defect_type: "stale_active_state", stale_duration: "period_boundary_exceeded", lifecycle_confidence: { score: 0.79, grade: "high" }, business_lifecycle_interpretation: "Текущая стадия: stale_unlinked_payment; ожидаемая стадия: settlement_closed. Объект завис РІРѕ времени Рё РЅРµ дошел РґРѕ ожидаемого перехода.", lifecycle_ranking_score: 1.41, lifecycle_ranking_basis: ["base_problem_severity", "stale_duration_weight", "period_close_impact"] }; } function buildSummary(units: ProblemUnit[]): ProblemUnitSummary { const unitTypes = Array.from(new Set(units.map((item) => item.problem_unit_type))); return { schema_version: "problem_unit_summary_v0_1", units_total: units.length, duplicate_collapses: 0, unit_types: unitTypes, type_distribution: { lifecycle_anomaly_node: units.length }, severity_distribution: { low: 0, medium: 0, high: units.length }, confidence_distribution: { low: 0, medium: units.length, high: 0 }, primary_unit_type: unitTypes[0] ?? null, lifecycle_enriched_units: units.length, lifecycle_domain_distribution: { bank_settlement: units.length }, lifecycle_defect_distribution: { stale_active_state: units.length } }; } function buildRetrievalResult(problemUnits: ProblemUnit[]): UnifiedRetrievalResult { return { fragment_id: "F1", requirement_ids: ["R1"], route: "hybrid_store_plus_live", status: "ok", result_type: "chain", items: [ { counterparty_id: "CP-1", operations_count: 5, document_refs_count: 3 } ], raw_entities: [], candidate_evidence: [], problem_units: problemUnits, problem_unit_summary: buildSummary(problemUnits), summary: { broad_query_detected: true, broad_result_flag: true, minimum_evidence_failed: false, degraded_to: "partial", narrowing_strength: "weak" }, evidence: [ { evidence_id: "ev-1", claim_ref: "requirement:R1", source_type: "retrieval_item", source_ref: { schema_version: "evidence_source_ref_v1", namespace: "snapshot_2020", entity: "Document", id: "DOC-1", period: "2020-06", canonical_ref: "evidence_source_ref_v1|snapshot_2020|document|doc-1|2020-06" }, pointer: { fragment_id: "F1", route: "hybrid_store_plus_live", source: { namespace: "snapshot_2020", entity: "Document", id: "DOC-1", period: "2020-06" }, locator: { field_path: "risk_score", item_index: 0 } }, evidence_kind: "mechanism_link", mechanism_note: "failed_edge=payment_to_settlement", confidence: "medium", limitation: null, payload: { risk_score: 5 } } ], why_included: ["synthetic-test"], selection_reason: ["synthetic-test"], risk_factors: ["broken_chain"], business_interpretation: ["synthetic-test"], confidence: "medium", limitations: [], errors: [] }; } describe("assistant lifecycle-aware answer mode v1", () => { it("promotes stage3 lifecycle mode when lifecycle answer flag is enabled", () => { const units = [buildLifecycleProblemUnit()]; const output = composeAssistantAnswer({ userMessage: "Проверь, РіРґРµ зависли платежи РїРѕ 51/60 Рё какой переход РЅРµ завершился.", routeSummary: buildRouteSummary(), retrievalResults: [buildRetrievalResult(units)], requirements: [ { requirement_id: "R1", source_fragment_id: "F1", requirement_text: "Проверить lifecycle-переход", subject_tokens: ["chain", "account_51", "account_60"], status: "covered", route: "hybrid_store_plus_live" } ], coverageReport: buildCoverage(true), groundingCheck: buildGrounding("partial"), enableAnswerPolicyV11: true, enableProblemCentricAnswerV1: true, enableLifecycleAnswerV1: true }); expect(output.problem_centric_answer_applied).toBe(true); expect(output.problem_answer_mode).toBe("stage3_lifecycle_aware_v1"); expect(output.answer_structure_v11?.answer_summary).toMatch(/lifecycle|Lifecycle|жизненн/i); expect(String(output.answer_structure_v11?.direct_answer)).toMatch(/не подтвержден|ожидаем|зависл/i); expect(output.answer_structure_v11?.direct_answer).not.toMatch(/current=|expected=|defect=/i); }); });