NODEDC_1C/llm_normalizer/backend/tests/assistantWave16ResidualClea...

432 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<Record<string, unknown>>): 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<string, unknown> {
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>): 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>): 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<string, unknown>;
const semanticProfile = (summary.semantic_profile ?? {}) as Record<string, unknown>;
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<string, unknown>;
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<string, unknown>;
const domainGuard = (summary.domain_purity_guard ?? {}) as Record<string, unknown>;
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);
});
});