import fs from "fs"; import os from "os"; import path from "path"; import { afterEach, describe, expect, it } from "vitest"; import { AssistantDataLayer } from "../src/services/assistantDataLayer"; import { composeAssistantAnswer } from "../src/services/answerComposer"; import { resolveQuestionType } from "../src/services/questionTypeResolver"; import type { AnswerGroundingCheck, RequirementCoverageReport, UnifiedRetrievalResult } from "../src/types/assistant"; import type { ProblemUnit } from "../src/types/stage2ProblemUnits"; const TEMP_DIRS: string[] = []; function cleanupTempDirs(): void { for (const dir of TEMP_DIRS.splice(0)) { fs.rmSync(dir, { recursive: true, force: true }); } } function createSnapshotRoot(records: Array>): string { const root = fs.mkdtempSync(path.join(os.tmpdir(), "assistant-wave16-")); TEMP_DIRS.push(root); const payload = JSON.stringify({ records }, null, 2); const write = (name: string, content = payload): void => { fs.writeFileSync(path.resolve(root, name), content, "utf-8"); }; write("09_samples_key_fields_Recorder_Ref_Supplier_Buyer_Responsible.json"); write("03_snapshot_fragment_problem_cases.json"); write("07_samples_DocumentJournals.json"); write("08_samples_NDS_registers.json"); write("04_samples_SpisanieSRaschetnogoScheta.json"); write("05_samples_RealizaciyaTovarovUslug.json"); write("06_samples_PostuplenieTovarovUslug.json"); return root; } function buildMixedDomainRecord(id: string, description: string, account?: string): Record { return { source_entity: "Document", source_id: id, display_name: description, unknown_link_count: 0, attributes: { Description: description, Account: account ?? "", Period: "2020-07-15T00:00:00", Amount: 276873.6 }, links: [ { relation: "document_has_counterparty", target_entity: "Counterparty", target_id: `CP-${id}`, source_field: "Counterparty" } ] }; } 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(input?: Partial): RequirementCoverageReport { return { requirements_total: 1, requirements_covered: 1, requirements_uncovered: [], requirements_partially_covered: [], clarification_needed_for: [], out_of_scope_requirements: [], ...input }; } function buildGrounding(input?: Partial): AnswerGroundingCheck { return { status: "partial", route_subject_match: true, missing_requirements: [], reasons: [], why_included_summary: ["wave16-test"], selection_reason_summary: ["wave16-test"], ...input }; } function buildProblemUnit(input: { id: string; type: ProblemUnit["problem_unit_type"]; account: string; defect: string; lifecycleDomain: ProblemUnit["lifecycle_domain"]; }): ProblemUnit { return { schema_version: "problem_unit_v0_1", problem_unit_id: input.id, problem_unit_type: input.type, title: "Wave16 problem unit", mechanism_summary: `Mechanism candidate: ${input.defect}.`, business_defect_class: input.defect, severity: { score: 0.73, grade: "high" }, confidence: { score: 0.62, grade: "medium" }, lifecycle_domain: input.lifecycleDomain, affected_entities: ["Document:DOC-1"], affected_documents: ["Document:DOC-1"], affected_postings: ["Posting:POST-1"], affected_accounts: [input.account], affected_counterparties: ["Counterparty:CP-1"], affected_contracts: ["Contract:CTR-1"], failed_expected_edge: input.defect, period_impact: { is_period_sensitive: true, impact_class: "close_risk" }, evidence_pack: ["cand-1"], entity_backlinks: [{ entity: "Document", id: "DOC-1" }], snapshot_limitations: [] }; } function buildRetrieval(input: { status: UnifiedRetrievalResult["status"]; accountScope?: string[]; domainScope?: string[]; relationPatterns?: string[]; units?: ProblemUnit[]; }): UnifiedRetrievalResult { const accountScope = input.accountScope ?? []; const domainScope = input.domainScope ?? ["settlements"]; const relationPatterns = input.relationPatterns ?? ["payment_to_settlement"]; const units = input.units ?? []; return { fragment_id: "F1", requirement_ids: ["R1"], route: "hybrid_store_plus_live", status: input.status, result_type: "chain", items: input.status === "empty" ? [] : [ { source_entity: "Document", source_id: "DOC-1", display_name: "Счет №4 от 07.07.20", account_context: accountScope, graph_domain_scope: domainScope, relation_pattern_hits: relationPatterns, period: "2020-07", amount: "276 873,60" } ], summary: { semantic_profile: { account_scope: accountScope, domain_scope: domainScope, relation_patterns: relationPatterns, period_scope: { from: "2020-07-01", to: "2020-07-31", granularity: "month" } }, domain_purity_guard: { domain_card_id: domainScope.includes("vat") ? "vat_document_register_book" : "settlements_60_62" }, broad_query_detected: false, broad_result_flag: false, minimum_evidence_failed: false, narrowing_strength: "strong", degraded_to: null }, evidence: input.status === "empty" ? [] : [ { evidence_id: "ev-R1", claim_ref: "requirement:R1", source_type: "retrieval_item", source_ref: { schema_version: "evidence_source_ref_v1", namespace: "snapshot_2020_07", entity: "document", id: "DOC-1", period: "2020-07", canonical_ref: "evidence_source_ref_v1|snapshot_2020_07|document|doc-1|2020-07" }, pointer: { fragment_id: "F1", route: "hybrid_store_plus_live", source: { namespace: "snapshot_2020_07", entity: "document", id: "DOC-1", period: "2020-07" }, locator: { field_path: "risk_score", item_index: 0 } }, evidence_kind: "mechanism_link", mechanism_note: relationPatterns[0], confidence: "medium", limitation: null, payload: { contract: "договор № 01/19-ПТ" } } ], candidate_evidence: [], problem_units: units, problem_unit_summary: units.length > 0 ? { schema_version: "problem_unit_summary_v0_1", units_total: units.length, duplicate_collapses: 0, unit_types: units.map((unit) => unit.problem_unit_type), type_distribution: { [units[0]?.problem_unit_type ?? "broken_chain_segment"]: units.length }, severity_distribution: { low: 0, medium: 0, high: units.length }, confidence_distribution: { low: 0, medium: units.length, high: 0 }, primary_unit_type: units[0]?.problem_unit_type ?? null } : null, why_included: ["wave16-test"], selection_reason: ["wave16-test"], risk_factors: ["wave16"], business_interpretation: ["wave16"], confidence: input.status === "ok" ? "medium" : "low", limitations: [], errors: [] }; } afterEach(() => { cleanupTempDirs(); }); describe("wave16 residual fail cleanup", () => { it("q10-style VAT query must not derive account scope from percent/amount fragments", () => { const dataLayer = new AssistantDataLayer( createSnapshotRoot([ buildMixedDomainRecord("SET-1", "Оплата покупателя по договору", "62.01"), buildMixedDomainRecord("VAT-1", "Счет-фактура и НДС в книге продаж", "68.02") ]) ); const result = dataLayer.executeRoute( "hybrid_store_plus_live", "По оплате от 13 июля на 276 873,60 указан НДС 20% = 46 145,60. Докажи отражение НДС." ); expect(result.status).toBe("ok"); const summary = result.summary as Record; const semanticProfile = (summary.semantic_profile ?? {}) as Record; const accountScope = Array.isArray(semanticProfile.account_scope) ? semanticProfile.account_scope : []; expect(accountScope).not.toContain("20"); expect(accountScope).not.toContain("60"); const domainGuard = (summary.domain_purity_guard ?? {}) as Record; expect(domainGuard.domain_card_id).toBe("vat_document_register_book"); }); it("q13-style broad VAT query on batch route must stay VAT-domain", () => { const dataLayer = new AssistantDataLayer( createSnapshotRoot([ buildMixedDomainRecord("SET-2", "Платеж и расчеты с покупателем", "62.02"), buildMixedDomainRecord("VAT-2", "Полученный счет-фактура и налоговый эффект", "19.03") ]) ); const result = dataLayer.executeRoute( "batch_refresh_then_store", "Есть ли в июльском срезе покупки, где есть товар/услуга, но не видно счета-фактуры или налогового эффекта?" ); const summary = result.summary as Record; const domainGuard = (summary.domain_purity_guard ?? {}) as Record; expect(domainGuard.domain_card_id).toBe("vat_document_register_book"); }); it("q06 first-check should be settlement-specific for prove_or_guess without explicit account tokens", () => { const settlementUnit = buildProblemUnit({ id: "pu-settlement-wave16", type: "broken_chain_segment", account: "62.02", defect: "failed_edge:payment_to_settlement", lifecycleDomain: "customer_settlement" }); const output = composeAssistantAnswer({ userMessage: "Есть ли в июльском срезе ситуация, где деньги уже пришли, но закрытие расчётов не подтверждено тем документом?", routeSummary: buildRouteSummary(), retrievalResults: [buildRetrieval({ status: "ok", units: [settlementUnit], domainScope: ["settlements"] })], requirements: [ { requirement_id: "R1", source_fragment_id: "F1", requirement_text: "Wave16 requirement", subject_tokens: [], status: "covered", route: "hybrid_store_plus_live" } ], coverageReport: buildCoverage(), groundingCheck: buildGrounding(), focusDomainHint: null, questionTypeHint: "prove_or_guess", enableAnswerPolicyV11: true, enableProblemCentricAnswerV1: true, enableLifecycleAnswerV1: true }); expect(output.assistant_reply).toMatch(/регистр расчет|60\/62\/76|договор|объект расчет/i); }); it("q12-like question should resolve prove_or_guess instead of chain classification", () => { const resolved = resolveQuestionType( "Связан ли полученный 31 июля счёт-фактура с услугой так, чтобы вычет был корректен, и это доказано или только предположение?" ); expect(resolved).toBe("prove_or_guess"); }); it("why_breaks short line should avoid generic collapse and keep domain-specific mechanism", () => { const settlementUnit = buildProblemUnit({ id: "pu-settlement-wave16-2", type: "broken_chain_segment", account: "62.01", defect: "failed_edge:payment_to_settlement", lifecycleDomain: "customer_settlement" }); const output = composeAssistantAnswer({ userMessage: "Почему по расчетам долг остался после оплаты?", routeSummary: buildRouteSummary(), retrievalResults: [buildRetrieval({ status: "ok", units: [settlementUnit], domainScope: ["settlements"] })], requirements: [ { requirement_id: "R1", source_fragment_id: "F1", requirement_text: "Wave16 requirement", subject_tokens: [], status: "covered", route: "hybrid_store_plus_live" } ], coverageReport: buildCoverage({ requirements_covered: 0, requirements_partially_covered: ["R1"] }), groundingCheck: buildGrounding({ status: "partial", reasons: ["Mechanism is unresolved for part of the evidence."] }), focusDomainHint: "settlements_60_62", questionTypeHint: "why_breaks", enableAnswerPolicyV11: true, enableProblemCentricAnswerV1: true, enableLifecycleAnswerV1: true }); expect(output.assistant_reply).toMatch(/наиболее вероятн/i); expect(output.assistant_reply).not.toContain("Коротко: Проблема с закрытием расчета подтверждается частично."); }); it("VAT why_breaks first-check must include VAT-specific checks", () => { const vatUnit = buildProblemUnit({ id: "pu-vat-wave16", type: "cross_branch_inconsistency_cluster", account: "68.02", defect: "invoice_to_vat", lifecycleDomain: "vat_flow" }); const output = composeAssistantAnswer({ userMessage: "Почему по НДС не видно ожидаемого налогового эффекта?", routeSummary: buildRouteSummary(), retrievalResults: [buildRetrieval({ status: "ok", units: [vatUnit], domainScope: ["vat", "taxes"] })], requirements: [ { requirement_id: "R1", source_fragment_id: "F1", requirement_text: "Wave16 requirement", subject_tokens: [], status: "covered", route: "hybrid_store_plus_live" } ], coverageReport: buildCoverage(), groundingCheck: buildGrounding(), focusDomainHint: "vat_document_register_book", questionTypeHint: "why_breaks", enableAnswerPolicyV11: true, enableProblemCentricAnswerV1: true, enableLifecycleAnswerV1: true }); expect(output.assistant_reply).toMatch(/счет-?фактур|регистр НДС|19\/68|книг/i); }); });