NODEDC_1C/llm_normalizer/backend/dist/services/answerComposer.js

4207 lines
224 KiB
JavaScript
Raw Permalink 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.

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.sanitizeAssistantReplyForUserFacing = sanitizeAssistantReplyForUserFacing;
exports.composeAssistantAnswer = composeAssistantAnswer;
function fallbackFromSummary(routeSummary) {
if (!routeSummary || routeSummary.mode !== "deterministic_v2") {
return "none";
}
return routeSummary.fallback.type;
}
function uniqueStrings(values, limit = 6) {
return Array.from(new Set(values.map((item) => item.trim()).filter(Boolean))).slice(0, limit);
}
function withUniquePush(target, value) {
const normalized = String(value ?? "").trim();
if (!normalized) {
return;
}
if (!target.includes(normalized)) {
target.push(normalized);
}
}
function normalizeAnchorForMatch(value) {
return String(value ?? "")
.toLowerCase()
.replace(/[^\p{L}\p{N}.:/-]+/gu, " ")
.replace(/\s+/g, " ")
.trim();
}
function collectCompanyAnchorTokens(anchors) {
if (!anchors) {
return [];
}
const tokens = [];
for (const item of anchors.contract_numbers ?? [])
withUniquePush(tokens, item);
for (const item of anchors.document_numbers ?? [])
withUniquePush(tokens, item);
for (const item of anchors.dates ?? [])
withUniquePush(tokens, item);
for (const item of anchors.amounts ?? [])
withUniquePush(tokens, item);
for (const item of anchors.accounts ?? [])
withUniquePush(tokens, `\u0441\u0447\u0435\u0442 ${item}`);
for (const item of anchors.accounts ?? [])
withUniquePush(tokens, item);
for (const item of anchors.periods ?? [])
withUniquePush(tokens, item);
for (const item of anchors.document_types ?? [])
withUniquePush(tokens, item);
for (const item of anchors.all ?? [])
withUniquePush(tokens, item);
return uniqueStrings(tokens, 48);
}
function collectRetrievalCorpus(results) {
const chunks = [];
for (const result of results) {
chunks.push(JSON.stringify(result.summary ?? {}));
for (const item of result.items.slice(0, 10)) {
chunks.push(JSON.stringify(item));
}
for (const evidence of result.evidence.slice(0, 16)) {
chunks.push(JSON.stringify(evidence));
}
chunks.push(...result.why_included.slice(0, 16));
chunks.push(...result.selection_reason.slice(0, 16));
chunks.push(...result.business_interpretation.slice(0, 16));
}
return chunks.join(" ").toLowerCase();
}
function isAnchorMatchedInCorpus(anchor, corpus) {
const normalized = normalizeAnchorForMatch(anchor);
if (!normalized) {
return false;
}
if (normalized.length < 3) {
return false;
}
if (corpus.includes(normalized)) {
return true;
}
const withoutPrefix = normalized
.replace(/^(?:\u0434\u043e\u0433\u043e\u0432\u043e\u0440|document|account|period|doc_type)\s*[:№#]?\s*/iu, "")
.trim();
if (withoutPrefix.length >= 3 && corpus.includes(withoutPrefix)) {
return true;
}
if (/^\d+(?:[.,]\d{2})?$/.test(withoutPrefix)) {
const normalizedAmount = withoutPrefix.replace(",", ".");
return corpus.includes(withoutPrefix) || corpus.includes(normalizedAmount);
}
return false;
}
function evaluateCompanyAnchorUsage(anchors, retrievalResults) {
const present = collectCompanyAnchorTokens(anchors);
if (present.length === 0) {
return {
present: [],
used: [],
unused: []
};
}
const corpus = normalizeAnchorForMatch(collectRetrievalCorpus(retrievalResults));
const used = [];
const unused = [];
for (const anchor of present) {
if (isAnchorMatchedInCorpus(anchor, corpus)) {
withUniquePush(used, anchor);
}
else {
withUniquePush(unused, anchor);
}
}
return {
present: uniqueStrings(present, 24),
used: uniqueStrings(used, 12),
unused: uniqueStrings(unused, 12)
};
}
const UUID_PATTERN = /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/gi;
const LONG_HEX_PATTERN = /\b[0-9a-f]{24,}\b/gi;
const RAW_REF_BLOB_PATTERN = /\bevidence_source_ref_v1\|[^\s,;]+/gi;
const RAW_REF_TOKEN_PATTERN = /\b(?:source_ref|canonical_ref|entity_id|fragment_id|guid|uuid)\b/gi;
const SYNTHETIC_PLACEHOLDER_PATTERN = /\bunknown_entity(?::[^\s,;]+)?\b/gi;
const SYNTHETIC_FALLBACK_MARKER_PATTERN = /\b(?:unknown_source|unknown_record)\b/gi;
const SYNTHETIC_ROUTE_TOKEN_PATTERN = /\bbatch_refresh_then_store:[^\s,;]+/gi;
const CYRILLIC_MOJIBAKE_FRAGMENT_PATTERN = /(?:[\u0420\u0421][\u0080-\u04FF\u2000-\u20CF]){2,}/u;
const LATIN_MOJIBAKE_FRAGMENT_PATTERN = /(?:[\u00D0\u00D1][\u0080-\u00FF]){2,}/u;
const SHORT_CYRILLIC_MOJIBAKE_TOKEN_PATTERN = /^[\u0420\u0421][\u0080-\u04FF\u2000-\u20CF]{1,2}$/u;
const PREFIXED_SHORT_CYRILLIC_MOJIBAKE_TOKEN_PATTERN = /^[\p{L}\p{N}_-]+[\u0420\u0421][\u0080-\u04FF\u2000-\u20CF]{1,2}$/u;
const MOJIBAKE_SINGLE_MARKER_PATTERN = /^[\u0420\u0421\u00D0\u00D1]$/u;
const MOJIBAKE_MARKER_CHAR_PATTERN = /[\u0402\u0403\u040A\u040C\u040E\u040F\u0452\u0453\u0459\u045A\u045C\u045E\u045F\u201A\u201E\u2020\u2021\u2026\u2030\u20AC\u2122]/u;
const CYRILLIC_MOJIBAKE_FRAGMENT_GLOBAL_PATTERN = /(?:[\u0420\u0421][\u0080-\u04FF\u2000-\u20CF]){2,}/gu;
const LATIN_MOJIBAKE_FRAGMENT_GLOBAL_PATTERN = /(?:[\u00D0\u00D1][\u0080-\u00FF]){2,}/g;
const MOJIBAKE_MARKER_CHAR_GLOBAL_PATTERN = /[\u0402\u0403\u040A\u040C\u040E\u040F\u0452\u0453\u0459\u045A\u045C\u045E\u045F\u201A\u201E\u2020\u2021\u2026\u2030\u20AC\u2122]/gu;
const INTERNAL_DEBUG_LINE_PATTERNS = [
/\bgraph traversal mode\b/i,
/\bplanner mode\b/i,
/\bsemantic_only\b/i,
/\btyped_domain_path\b/i,
/\bmatched=\d+\s*\/\s*\d+\b/i,
/\bdomain\/document\/relation\b/i,
/\baccount_scope\b/i,
/\bdomain_scope\b/i,
/\bdocument_types\b/i,
/\brelation_patterns\b/i,
/\banomaly_patterns\b/i,
/\bsemantic retrieval profile\b/i,
/\bsemantic profile\b/i,
/\bgraph signal counts?\b/i,
/\bgraph ranking signals?\b/i,
/\broute_focus\b/i,
/\bcross_entity_breakage\b/i,
/\bnarrowing\s+\d+\s+\d+\b/i,
/\breference-mode\b/i,
/\bbasis:\s*(?:closure_risk|repeatability|financial_impact)\b/i,
/\bgraph_runtime_(?:enabled|signals?|summary)\b/i,
/\bgraph_(?:eligible|traversal|domain_scope|match_hits|traversal_score|signal_counts|ranking_shift_signals)\b/i,
/\bdomain\s+purity\s+guardrail\b/i,
/\bdebug_payload_json\b/i,
/\bdebug_payload\b/i,
/^\s*```json\s*$/i,
/^\s*```\s*$/i,
/\btechnical_breakdown_json\b/i,
/\b(?:coverage_report|retrieval_status|problem_unit_state)\b/i
];
const USER_FACING_LEAKAGE_PATTERNS = [
/\bgraph_[a-z0-9_]+\b/i,
/\bdomain_scope\b/i,
/\brelation_patterns\b/i,
/\baccount_scope\b/i,
/\bsemantic_profile\b/i,
/\bsemantic\s+retrieval\s+profile\b/i,
/\broute(?:_hint|_focus)?\b/i,
/\b(?:store_canonical|hybrid_store_plus_live|deterministic_v2|typed_domain_path|semantic_only)\b/i,
/\b(?:problem_unit|candidate_evidence|raw_entities|graph_binding|graph_node_id|relation_path)\b/i,
/\b(?:missing_transition|conflicting_transition|terminal_state_gap|wrong_closing_document_type)\b/i,
/\b(?:expected_transition_not_observed|stale_active_state|misclosed_state|cross_branch_state_conflict)\b/i,
/\b(?:document|catalog|accumulationregister|register)_[a-z0-9_:-]+\b/i,
/\b(?:domain|state|missing):[a-z0-9_><:-]+\b/i,
/\b(?:lifecycle_domain|lifecycle_defect_type|lifecycle_ranking_basis)\b/i,
/\bdomain\s+purity\s+guardrail\b/i,
/\bdebug_payload_json\b/i,
/\bdebug_payload\b/i,
/\btechnical_breakdown_json\b/i,
/\b(?:coverage_report|retrieval_status|problem_unit_state|requirements_(?:covered|uncovered|partially_covered))\b/i,
/\b(?:lifecycle_anomaly_node|unresolved_settlement_cluster|period_risk_cluster|cross_branch_inconsistency_cluster|broken_chain_segment)\b/i,
/\b(?:bank_settlement|customer_settlement|deferred_expense|vat_flow|period_close|fixed_asset)\b/i
];
const TECHNICAL_TOKEN_PATTERN = /^[a-z0-9_:-]+$/i;
const TECHNICAL_GRAPH_TOKEN_PATTERN = /(?:^|_)(?:graph|domain|relation|signal|pattern|profile|scope)(?:_|$)/i;
const TECHNICAL_INTERNAL_TOKEN_PATTERN = /(?:^|_)(?:risk|closure|lifecycle|anomaly|transition|link|mismatch|posting|narrowing|ranking)(?:_|$)/i;
const HUMAN_SIGNAL_MAP = {
lifecycle_anomaly_node_detected: "Выявлен признак незавершенного этапа в учетной цепочке.",
cross_branch_inconsistency_cluster_detected: "Выявлен конфликт состояния между связанными ветками цепочки.",
broken_chain_segment_detected: "Выявлен разрыв связанной цепочки операций.",
missing_transition: "Ожидаемый закрывающий переход не подтвержден.",
expected_transition_not_observed: "Ожидаемый переход к следующему этапу не подтвержден.",
conflicting_transition: "Есть конфликт состояния между связанными участками цепочки.",
terminal_state_gap: "Контур дошел почти до завершения, но финальный шаг не подтвержден.",
wrong_closing_document_type: "Закрытие похоже выполнено неподходящим типом документа.",
missing_link: "Часть обязательных связей между документами и проводками не подтверждена.",
broken_lifecycle: "Контур операций выглядит незавершенным.",
posting_mismatch: "Документ и проводка расходятся по состоянию.",
cross_domain_inconsistency: "Между связанными участками учета есть конфликт состояния.",
closure_risk: "Разрыв может мешать закрытию периода и сверке.",
repeated_anomaly: "Паттерн повторяется и похож на системную проблему.",
amount_independent_risk: "Проблема не выглядит случайной суммовой погрешностью.",
wrong_document_type: "Есть признак неверного типа закрывающего документа.",
fixed_asset_card_mismatch: "Есть несоответствие между карточкой ОС, документом движения и начислением.",
contradictory_asset_state: "Состояние объекта ОС выглядит противоречивым по текущей опоре.",
disposed: "Есть признак выбытия объекта ОС в цепочке состояния.",
invalid_document_or_posting_transition: "Переход состояния ОС не подтвержден документами и проводками.",
asset_card_to_depreciation: "Переход от карточки ОС к начислению амортизации подтвержден не полностью.",
supplier_tail_analysis: "Есть признаки незавершенного расчетного контура по поставщикам.",
cross_entity_breakage: "Есть разрыв между связанными объектами в одной цепочке.",
deferred_expense_to_writeoff: "Ожидаемая цепочка списания РБП выглядит незавершенной.",
overdue_writeoff: "объект завис в просроченном состоянии списания",
fully_written_off: "полное списание",
unknown_snapshot_window: "точный интервал зависания не определен в текущем snapshot",
broken_chain_segment: "Есть разрыв цепочки: ожидаемое закрытие операции не подтверждено.",
unresolved_settlement_cluster: "По расчетам остался незавершенный хвост.",
period_risk_cluster: "Есть признак сбоя в контуре закрытия месяца.",
lifecycle_anomaly_node: "Операция зависла в промежуточном состоянии.",
document_conflict: "Между документами и проводками есть расхождение.",
cross_branch_inconsistency_cluster: "Есть конфликт между связанными ветками учета.",
"failed_edge:payment_to_settlement": "Оплата отражена, но ожидаемое закрытие расчета не подтверждено.",
payment_to_settlement: "Переход от оплаты к закрытию расчета не подтвержден.",
missing_expected_transition: "Ожидаемый переход в учетной цепочке не подтвержден.",
stale_unlinked_payment: "Оплата зависла без подтвержденного закрытия расчета.",
settlement_closed: "расчет закрыт",
recognized: "признано в учете",
partially_written_off: "частично списано",
period_boundary_exceeded: "срок перехода по цепочке превышен"
};
function isInternalDebugLikeLine(value) {
const text = String(value ?? "").trim();
if (!text) {
return false;
}
return INTERNAL_DEBUG_LINE_PATTERNS.some((pattern) => pattern.test(text));
}
function hasUserFacingLeakage(value) {
const text = String(value ?? "").trim();
if (!text) {
return false;
}
if (isInternalDebugLikeLine(text)) {
return true;
}
return USER_FACING_LEAKAGE_PATTERNS.some((pattern) => pattern.test(text));
}
function normalizeTechnicalToken(value) {
return String(value ?? "")
.trim()
.toLowerCase()
.replace(/[^\p{L}\p{N}_:-]+/gu, "_")
.replace(/_+/g, "_")
.replace(/^_+|_+$/g, "");
}
function humanizeTechnicalToken(value) {
const normalized = normalizeTechnicalToken(value);
if (!normalized) {
return null;
}
if (Object.prototype.hasOwnProperty.call(HUMAN_SIGNAL_MAP, normalized)) {
return HUMAN_SIGNAL_MAP[normalized];
}
if (TECHNICAL_TOKEN_PATTERN.test(normalized) && (TECHNICAL_GRAPH_TOKEN_PATTERN.test(normalized) || TECHNICAL_INTERNAL_TOKEN_PATTERN.test(normalized))) {
return null;
}
if (/_/.test(normalized) && TECHNICAL_TOKEN_PATTERN.test(normalized)) {
return null;
}
return null;
}
function toStringList(value) {
if (!Array.isArray(value)) {
return [];
}
return value
.map((item) => String(item ?? "").trim())
.filter((item) => item.length > 0);
}
function collectGraphCausalInsight(results) {
let hasChainResult = false;
let hasMissingTransition = false;
let hasConflictingTransition = false;
let hasTerminalGap = false;
let hasWrongClosingType = false;
let hasNeighborBranchLifting = false;
let hasPeriodCloseImpact = false;
const domains = new Set();
for (const result of results) {
if (result.result_type !== "chain" || (result.status !== "ok" && result.status !== "partial")) {
continue;
}
hasChainResult = true;
const summaryGraphTraversal = summaryValue(result, "graph_traversal");
if (summaryGraphTraversal && typeof summaryGraphTraversal === "object") {
const traversal = summaryGraphTraversal;
for (const domain of toStringList(traversal.target_domains)) {
domains.add(domain);
if (domain === "period_close") {
hasPeriodCloseImpact = true;
}
}
const rankingSignals = toStringList(traversal.ranking_shift_signals);
if (rankingSignals.includes("neighbor_branch_lifting")) {
hasNeighborBranchLifting = true;
}
const signalCounts = traversal.signal_counts;
if (signalCounts && typeof signalCounts === "object") {
const counts = signalCounts;
const missingCount = Number(counts.missing_transition ?? 0);
const conflictCount = Number(counts.conflicting_transition ?? 0);
const terminalCount = Number(counts.terminal_state_gap ?? 0);
const wrongTypeCount = Number(counts.wrong_closing_document_type ?? 0);
if (missingCount > 0) {
hasMissingTransition = true;
}
if (conflictCount > 0) {
hasConflictingTransition = true;
}
if (terminalCount > 0) {
hasTerminalGap = true;
}
if (wrongTypeCount > 0) {
hasWrongClosingType = true;
}
}
}
for (const item of result.items.slice(0, 4)) {
for (const signal of toStringList(item.graph_runtime_signals)) {
if (signal === "missing_transition") {
hasMissingTransition = true;
}
if (signal === "conflicting_transition") {
hasConflictingTransition = true;
}
if (signal === "terminal_state_gap") {
hasTerminalGap = true;
}
if (signal === "wrong_closing_document_type") {
hasWrongClosingType = true;
}
}
for (const domain of toStringList(item.graph_domain_scope)) {
domains.add(domain);
if (domain === "period_close") {
hasPeriodCloseImpact = true;
}
}
for (const risk of toStringList(item.risk_factors)) {
if (risk === "closure_risk") {
hasPeriodCloseImpact = true;
}
}
}
}
const strongSignalHits = Number(hasMissingTransition) + Number(hasConflictingTransition) + Number(hasTerminalGap) + Number(hasWrongClosingType);
return {
active: hasChainResult && (hasMissingTransition || hasConflictingTransition || hasTerminalGap || hasWrongClosingType || hasNeighborBranchLifting),
has_missing_transition: hasMissingTransition,
has_conflicting_transition: hasConflictingTransition,
has_terminal_gap: hasTerminalGap,
has_wrong_closing_type: hasWrongClosingType,
has_neighbor_branch_lifting: hasNeighborBranchLifting,
has_period_close_impact: hasPeriodCloseImpact,
looks_systemic: strongSignalHits >= 2 || hasNeighborBranchLifting,
domains: Array.from(domains)
};
}
function buildGraphProblemFirstLead(insight, scopeLabel) {
if (!insight.active) {
return scopeLabel === "full"
? "Коротко: запрос обработан по доступной опоре, явные проблемные разрывы подтверждены выборочно."
: "Коротко: есть подтвержденные сигналы, но часть вывода остается ограниченной.";
}
const causes = [];
if (insight.has_missing_transition) {
causes.push("подтверждено движение по контуру, но закрывающий переход не подтвержден");
}
if (insight.has_conflicting_transition) {
causes.push("между связанными участками есть конфликт состояния");
}
if (insight.has_terminal_gap) {
causes.push("цепочка дошла почти до финала, но последний шаг не зафиксирован");
}
if (insight.has_wrong_closing_type) {
causes.push("закрытие похоже выполнено неверным типом документа");
}
const causeText = causes.length > 0 ? causes.join("; ") : "виден разрыв в связанной цепочке операций";
return `Коротко: есть признаки реальной проблемы в связанном контуре - ${causeText}.`;
}
function buildGraphCausalLines(insight) {
if (!insight.active) {
return [];
}
const lines = [];
if (insight.has_neighbor_branch_lifting) {
lines.push("Проблема проявляется не внутри одного документа, а между связанными ветками цепочки.");
}
if (insight.has_period_close_impact) {
lines.push("Такой разрыв может мешать закрытию периода и итоговой сверке.");
}
if (insight.looks_systemic) {
lines.push("Это больше похоже на реальную проблему, чем на случайный хвост.");
}
else {
lines.push("Сигнал пока слабый: часть признаков может быть шумом, нужна точечная проверка.");
}
return lines;
}
function buildGraphFirstChecks(insight) {
if (!insight.active) {
return [];
}
const checks = [];
const hasSettlementDomain = insight.domains.includes("bank_settlement") ||
insight.domains.includes("customer_settlement") ||
insight.domains.includes("settlements");
if (hasSettlementDomain) {
checks.push("Сначала проверьте связку: документ оплаты -> закрывающий документ расчета -> проводка по расчетам.");
}
if (insight.domains.includes("deferred_expense")) {
checks.push("Проверьте РБП-контур: документ признания -> документ списания -> проводка списания.");
}
if (insight.domains.includes("fixed_asset")) {
checks.push("Проверьте ОС-контур: карточка объекта -> документ движения -> начисление амортизации.");
}
if (insight.domains.includes("vat_flow")) {
checks.push("Проверьте НДС-контур: документ операции -> запись регистра -> проводка.");
}
if (insight.domains.includes("period_close")) {
checks.push("Проверьте регламентную операцию закрытия периода и документы, которые она должна завершить.");
}
if (insight.has_conflicting_transition) {
checks.push("Сверьте связанный контур в двух ветках: где одна ветка закрыта, а другая остается в промежуточном состоянии.");
}
return uniqueStrings(checks, 6);
}
function extractChainCausalFacts(result) {
const topSignals = uniqueStrings(result.items
.slice(0, 3)
.flatMap((item) => toStringList(item.graph_runtime_signals)), 6);
const topRiskFactors = uniqueStrings(result.items
.slice(0, 3)
.flatMap((item) => toStringList(item.risk_factors)), 6);
const lines = [];
if (topSignals.includes("missing_transition")) {
lines.push("Оплата/движение зафиксированы, но ожидаемый закрывающий шаг не подтвержден.");
}
if (topSignals.includes("conflicting_transition")) {
lines.push("Есть конфликт между связанными участками: состояния веток расходятся.");
}
if (topSignals.includes("terminal_state_gap")) {
lines.push("Контур почти завершен, но финальный переход не подтвержден.");
}
if (topSignals.includes("wrong_closing_document_type") || topRiskFactors.includes("wrong_document_type")) {
lines.push("Есть признак неверного типа закрывающего документа.");
}
if (topRiskFactors.includes("closure_risk")) {
lines.push("Разрыв потенциально влияет на закрытие периода.");
}
if (lines.length === 0) {
lines.push("Есть признаки незавершенной связки документов и проводок в связанном контуре.");
}
return lines;
}
function normalizeToken(value) {
return value.replace(/^[^\p{L}\p{N}_-]+|[^\p{L}\p{N}_-]+$/gu, "");
}
function isLikelyMojibakeToken(value) {
const token = normalizeToken(String(value ?? ""));
if (!token) {
return false;
}
if (/^[А-ЯЁ]{2,5}$/u.test(token)) {
return false;
}
if (MOJIBAKE_SINGLE_MARKER_PATTERN.test(token)) {
return true;
}
if (SHORT_CYRILLIC_MOJIBAKE_TOKEN_PATTERN.test(token)) {
return true;
}
if (token.length <= 8 && PREFIXED_SHORT_CYRILLIC_MOJIBAKE_TOKEN_PATTERN.test(token)) {
return true;
}
return CYRILLIC_MOJIBAKE_FRAGMENT_PATTERN.test(token) || LATIN_MOJIBAKE_FRAGMENT_PATTERN.test(token);
}
function countMojibakeTokens(value) {
return String(value ?? "")
.split(/[\s,.;:!?()[\]{}"']+/g)
.filter((token) => token.length > 0)
.filter((token) => isLikelyMojibakeToken(token)).length;
}
function countMojibakeSingleMarkers(value) {
return String(value ?? "")
.split(/[\s,.;:!?()[\]{}"']+/g)
.filter((token) => token.length > 0)
.map((token) => normalizeToken(token))
.filter((token) => MOJIBAKE_SINGLE_MARKER_PATTERN.test(token)).length;
}
function stripMojibakeFragments(value) {
const removedByToken = String(value ?? "")
.split(/(\s+)/g)
.map((part) => {
if (/^\s+$/u.test(part)) {
return part;
}
return isLikelyMojibakeToken(part) ? "" : part;
})
.join("");
return removedByToken
.replace(CYRILLIC_MOJIBAKE_FRAGMENT_GLOBAL_PATTERN, "")
.replace(LATIN_MOJIBAKE_FRAGMENT_GLOBAL_PATTERN, "")
.replace(MOJIBAKE_MARKER_CHAR_GLOBAL_PATTERN, "")
.replace(/\s+([,.;:!?])/g, "$1")
.replace(/\s{2,}/g, " ")
.trim();
}
function looksLikeMojibake(value) {
const text = String(value ?? "");
if (!text.trim()) {
return false;
}
const tokenHits = countMojibakeTokens(text);
const singleMarkers = countMojibakeSingleMarkers(text);
if (tokenHits >= 2 || (tokenHits >= 1 && singleMarkers >= 1) || singleMarkers >= 3) {
return true;
}
if (MOJIBAKE_MARKER_CHAR_PATTERN.test(text)) {
return true;
}
if (CYRILLIC_MOJIBAKE_FRAGMENT_PATTERN.test(text) || LATIN_MOJIBAKE_FRAGMENT_PATTERN.test(text)) {
return true;
}
if (/\uFFFD/u.test(text)) {
return true;
}
return false;
}
function looksLikeTechnicalIdentifier(value) {
const text = String(value ?? "").trim();
if (!text) {
return false;
}
if (UUID_PATTERN.test(text)) {
UUID_PATTERN.lastIndex = 0;
return true;
}
UUID_PATTERN.lastIndex = 0;
if (LONG_HEX_PATTERN.test(text)) {
LONG_HEX_PATTERN.lastIndex = 0;
return true;
}
LONG_HEX_PATTERN.lastIndex = 0;
return /(?:evidence_source_ref_v1\||cmp%3a|batch_refresh_then_store:|^cmp:)/i.test(text);
}
function scrubRawTechnicalRefs(value) {
const raw = String(value ?? "").trim();
if (!raw) {
return "";
}
return raw
.replace(RAW_REF_BLOB_PATTERN, "linked source")
.replace(UUID_PATTERN, "[id]")
.replace(LONG_HEX_PATTERN, "[id]")
.replace(RAW_REF_TOKEN_PATTERN, "reference")
.replace(/\(\s*\[id\]\s*\)/g, "")
.replace(/\[\s*id\s*\](?:\s*,\s*\[\s*id\s*\])+/g, "[id]")
.replace(/[ \t]{2,}/g, " ")
.trim();
}
function stripSyntheticPlaceholders(value) {
return String(value ?? "")
.replace(SYNTHETIC_PLACEHOLDER_PATTERN, "")
.replace(SYNTHETIC_FALLBACK_MARKER_PATTERN, "")
.replace(SYNTHETIC_ROUTE_TOKEN_PATTERN, "")
.replace(/[;,:]\s*[;,:]+/g, "; ")
.replace(/\s{2,}/g, " ")
.trim();
}
function sanitizeUserFacingReply(value) {
const raw = String(value ?? "");
const hardCutMatch = raw.match(/(?:^|\n)\s*#{0,6}\s*(?:debug_payload_json|technical_breakdown_json|technical_debug_payload_json)\b/i);
const preCut = hardCutMatch ? raw.slice(0, hardCutMatch.index) : raw;
const withoutDebugBlocks = preCut
.replace(/###\s*debug_payload_json[\s\S]*?(?:```[\s\S]*?```|$)/gi, "")
.replace(/###\s*technical_debug_payload_json[\s\S]*?(?:```[\s\S]*?```|$)/gi, "")
.replace(/###\s*technical_breakdown_json[\s\S]*?(?:```[\s\S]*?```|$)/gi, "")
.replace(/(?:^|\n)\s*#{0,6}\s*(?:debug_payload_json|technical_breakdown_json|technical_debug_payload_json)\b[\s\S]*$/gi, "")
.replace(/```json[\s\S]*?```/gi, "");
const normalized = scrubRawTechnicalRefs(withoutDebugBlocks).replace(/[ \t]+\n/g, "\n");
const preparedLines = normalized
.split(/\r?\n/g)
.map((line) => stripSyntheticPlaceholders(line))
.map((line) => stripMojibakeFragments(line))
.map((line) => line.trim());
const cleanedLines = [];
let previousWasBlank = false;
for (const line of preparedLines) {
if (line.length === 0) {
if (!previousWasBlank && cleanedLines.length > 0) {
cleanedLines.push("");
}
previousWasBlank = true;
continue;
}
if (/^(?:-\s*)?(?:action|clarify|open|limit|note):\s*$/i.test(line)) {
continue;
}
if (hasUserFacingLeakage(line)) {
continue;
}
if (looksLikeMojibake(line)) {
continue;
}
cleanedLines.push(line);
previousWasBlank = false;
}
while (cleanedLines.length > 0 && cleanedLines[0] === "") {
cleanedLines.shift();
}
while (cleanedLines.length > 0 && cleanedLines[cleanedLines.length - 1] === "") {
cleanedLines.pop();
}
const cleaned = cleanedLines.join("\n").replace(/\n{3,}/g, "\n\n").trim();
return cleaned || "Available data requires clarification for a reliable user-facing answer.";
}
function sanitizeUserText(value) {
const normalized = stripMojibakeFragments(stripSyntheticPlaceholders(scrubRawTechnicalRefs(String(value ?? "").replace(/\s+/g, " ").trim())));
if (!normalized) {
return null;
}
if (hasUserFacingLeakage(normalized)) {
return null;
}
if (looksLikeMojibake(normalized)) {
return null;
}
return normalized;
}
function sanitizeUserLines(values, limit = 6) {
const cleaned = values
.map((item) => sanitizeUserText(item))
.filter((item) => Boolean(item))
.filter((item) => !isInternalDebugLikeLine(item));
return uniqueStrings(cleaned, limit);
}
function formatList(items) {
if (items.length === 0) {
return "";
}
return items.map((item) => `- ${item}`).join("\n");
}
function renderStage4ContractReply(input) {
const shortLine = ensureSentence(input.shortLine);
const checkedLines = dedupeNarrativeLines(input.checkedLines, 6);
const foundLines = dedupeNarrativeLines(input.foundLines, 6);
const unresolvedLines = dedupeNarrativeLines(input.unresolvedLines, 6);
const nextStepLines = dedupeNarrativeLines(input.nextStepLines, 5);
const contextLead = ensureSentence(String(input.contextLead ?? ""));
return sanitizeUserFacingReply([
`Коротко: ${shortLine}`,
contextLead || "",
`Что именно проверено:\n${formatList(checkedLines.length > 0
? checkedLines
: ["Подтвержденная опора собрана частично; для полного вывода нужен дополнительный проход."])}`,
`Что найдено:\n${formatList(foundLines.length > 0 ? foundLines : ["Явные отклонения по текущей опоре не подтверждены."])}`,
`Что пока не доказано:\n${formatList(unresolvedLines.length > 0
? unresolvedLines
: ["Существенных ограничений в текущем срезе не выявлено."])}`,
`${input.nextStepTitle}:\n${formatList(nextStepLines.length > 0
? nextStepLines
: ["Уточните период, объект или контрагента, чтобы продолжить проверку по 1С."])}`
]
.filter(Boolean)
.join("\n\n"));
}
function formatSafeItemLine(entity, sourceId, riskScore) {
const entityLabel = sanitizeUserText(String(entity ?? "")) ?? "Record";
const idRaw = String(sourceId ?? "").trim();
const exposeId = idRaw.length > 0 && !looksLikeTechnicalIdentifier(idRaw);
const subject = exposeId ? `${entityLabel} (${idRaw})` : entityLabel;
if (riskScore !== undefined) {
return `${subject} - risk ${String(riskScore)}.`;
}
return `${subject}.`;
}
function extractTopFacts(results) {
const lines = [];
for (const result of results.filter((item) => item.status === "ok").slice(0, 3)) {
if (result.result_type === "chain") {
const top = extractChainCausalFacts(result);
lines.push(...top);
continue;
}
if (result.result_type === "ranking") {
const top = result.items
.slice(0, 5)
.map((item) => `${item.rank ?? "*"}. ${String(item.entity ?? "Entity")} - ${String(item.records_count ?? 0)}.`);
lines.push(...top);
continue;
}
if (result.result_type === "list") {
const top = result.items.slice(0, 5).map((item) => {
if (item.risk_score !== undefined) {
return formatSafeItemLine(item.source_entity ?? "Record", item.source_id ?? "", item.risk_score);
}
return formatSafeItemLine(item.source_entity ?? "Record", item.source_id ?? "");
});
lines.push(...top);
continue;
}
const top = result.items
.slice(0, 3)
.map((item) => formatSafeItemLine(item.source_entity ?? "Record", item.source_id ?? ""));
lines.push(...top);
}
return sanitizeUserLines(lines, 8);
}
function sanitizeSupportLine(value) {
const raw = String(value ?? "").trim();
if (!raw) {
return null;
}
const mechanismMatch = raw.match(/^mechanism candidate:\s*(.+?)\.?$/i);
if (mechanismMatch?.[1]) {
const mechanismToken = mechanismMatch[1].trim();
const mechanismHumanized = humanizeTechnicalToken(mechanismToken);
if (mechanismHumanized) {
return mechanismHumanized;
}
}
const rawCore = raw.replace(/[.,;:!?]+$/g, "");
const directHumanized = humanizeTechnicalToken(rawCore);
if (directHumanized) {
return directHumanized;
}
const cleaned = sanitizeUserText(raw);
if (!cleaned) {
return null;
}
const cleanedCore = cleaned.replace(/[.,;:!?]+$/g, "");
const mapped = humanizeTechnicalToken(cleanedCore);
if (mapped) {
return mapped;
}
if (/^(?:lifecycle|domain|relation)\s+[a-z0-9_:-]+$/i.test(cleanedCore)) {
return null;
}
if (TECHNICAL_TOKEN_PATTERN.test(cleanedCore) && /_/.test(cleanedCore)) {
return null;
}
return cleaned;
}
function extractWhyIncluded(results) {
return uniqueStrings(results
.flatMap((item) => item.why_included)
.map((line) => sanitizeSupportLine(line))
.filter((line) => Boolean(line)), 8);
}
function extractSelectionReasons(results) {
return uniqueStrings(results
.flatMap((item) => item.selection_reason)
.map((line) => sanitizeSupportLine(line))
.filter((line) => Boolean(line)), 8);
}
function extractRiskFactors(results) {
const mapped = results.flatMap((item) => item.risk_factors.flatMap((factor) => {
const humanized = humanizeTechnicalToken(factor);
if (humanized) {
return [humanized];
}
const cleaned = sanitizeUserText(String(factor ?? ""));
if (!cleaned) {
return [];
}
if (TECHNICAL_TOKEN_PATTERN.test(cleaned) && /_/.test(cleaned)) {
return [];
}
return [cleaned];
}));
return sanitizeUserLines(mapped);
}
function extractBusinessInterpretation(results) {
return sanitizeUserLines(results.flatMap((item) => item.business_interpretation));
}
function extractLimitations(results) {
return sanitizeUserLines(results.flatMap((item) => item.limitations), 10);
}
function summaryValue(result, key) {
const summary = result.summary ?? {};
return Object.prototype.hasOwnProperty.call(summary, key) ? summary[key] : undefined;
}
function summaryBoolean(result, key) {
return summaryValue(result, key) === true;
}
function summaryString(result, key) {
const value = summaryValue(result, key);
return typeof value === "string" ? value : null;
}
function summaryNumber(result, key) {
const value = summaryValue(result, key);
return typeof value === "number" && Number.isFinite(value) ? value : null;
}
function summaryStringArray(result, key) {
const value = summaryValue(result, key);
if (!Array.isArray(value)) {
return [];
}
return sanitizeUserLines(value.map((item) => String(item)), 6);
}
function buildFallbackWhyIncluded(results) {
const lines = [];
for (const result of results.slice(0, 2)) {
const routeFocus = summaryString(result, "route_focus");
const sourceRecords = summaryNumber(result, "source_records");
const filteredRecords = summaryNumber(result, "filtered_records_after_narrowing");
const checkedRecords = summaryNumber(result, "checked_records");
if (routeFocus && !isInternalDebugLikeLine(routeFocus)) {
lines.push(`Проверка выполнена по предметному фокусу: ${routeFocus}.`);
}
if (sourceRecords !== null && filteredRecords !== null && filteredRecords < sourceRecords) {
lines.push(`Выборка сужена до ${filteredRecords} из ${sourceRecords} записей по условиям запроса.`);
}
if (checkedRecords !== null) {
lines.push(`Проверено записей в текущем проходе: ${checkedRecords}.`);
}
}
return sanitizeUserLines(lines, 4);
}
function buildFallbackSelectionReasons(results) {
const lines = [];
for (const result of results.slice(0, 2)) {
if (summaryBoolean(result, "semantic_narrowing_applied")) {
lines.push("Отбор выполнен по предметному сужению запроса.");
}
const rankingBasis = summaryStringArray(result, "ranking_basis");
if (rankingBasis.length > 0) {
lines.push("Ранжирование учитывает устойчивость разрыва, повторяемость и учетное влияние.");
}
if (summaryBoolean(result, "broad_guard_applied")) {
lines.push("Для широкого запроса включен защитный режим против ложной точности.");
}
}
if (lines.length === 0) {
lines.push("Отбор выполнен по совпадению предметных сигналов с доступной опорой.");
}
return sanitizeUserLines(lines, 4);
}
function suggestNextStep(requirements, coverage) {
const next = [];
if (coverage.clarification_needed_for.length > 0) {
next.push("Уточните период, счет, документ или контрагента для требований: " + coverage.clarification_needed_for.join(", ") + ".");
}
if (coverage.requirements_uncovered.length > 0) {
next.push("Проверьте непокрытые требования: " + coverage.requirements_uncovered.join(", ") + ".");
}
if (coverage.out_of_scope_requirements.length > 0) {
next.push("Часть запроса вне текущего учетного контура: " + coverage.out_of_scope_requirements.join(", ") + ".");
}
if (next.length === 0 && requirements.length > 0) {
next.push("Следующим шагом откройте точечную проверку по связанным документам и проводкам.");
}
return next;
}
const PROBLEM_HEAVY_TYPES = new Set([
"document_conflict",
"broken_chain_segment",
"lifecycle_anomaly_node",
"unresolved_settlement_cluster",
"period_risk_cluster",
"cross_branch_inconsistency_cluster"
]);
function flattenEvidence(results) {
return results.flatMap((item) => item.evidence);
}
function flattenProblemUnits(results) {
const units = [];
for (const result of results) {
if (!Array.isArray(result.problem_units)) {
continue;
}
units.push(...result.problem_units);
}
const byId = new Map();
for (const unit of units) {
byId.set(unit.problem_unit_id, unit);
}
return Array.from(byId.values());
}
function selectProblemUnitSummary(results) {
let selected = null;
for (const result of results) {
if (!result.problem_unit_summary) {
continue;
}
if (!selected || result.problem_unit_summary.units_total > selected.units_total) {
selected = result.problem_unit_summary;
}
}
return selected;
}
function isLikelyAccountToken(value) {
const text = String(value ?? "").trim();
if (!text) {
return false;
}
if (/^\d{2}(?:\.\d{1,2})?$/.test(text)) {
return true;
}
return /(?:^|[^a-zа-яё])(account|счет)(?:[^a-zа-яё]|$)/iu.test(text);
}
function isOpaqueEntityRef(value) {
const text = String(value ?? "").trim();
if (!text) {
return false;
}
if (/^[a-zа-яё0-9_]+:\[id\]$/iu.test(text)) {
return true;
}
if (/^(?:document|accumulationregister|catalog|register)_/iu.test(text)) {
return true;
}
return false;
}
function humanizeLifecycleStateToken(value) {
const raw = String(value ?? "").trim();
if (!raw) {
return raw;
}
const mapped = humanizeTechnicalToken(raw);
if (mapped) {
return mapped;
}
const normalized = normalizeTechnicalToken(raw);
if (normalized.includes("->")) {
return normalized
.split("->")
.map((part) => humanizeLifecycleStateToken(part))
.join(" -> ");
}
return normalized.replace(/_/g, " ");
}
function humanizeLifecycleTransitionToken(value) {
const raw = String(value ?? "").trim();
if (!raw) {
return raw;
}
const mapped = humanizeTechnicalToken(raw);
if (mapped) {
return mapped;
}
if (raw.includes("->")) {
return raw
.split("->")
.map((part) => humanizeLifecycleStateToken(part))
.join(" -> ");
}
return humanizeLifecycleStateToken(raw);
}
function formatAffectedScope(unit) {
const accountScope = sanitizeUserLines(unit.affected_accounts, 3).filter((item) => isLikelyAccountToken(item));
const counterpartyScope = sanitizeUserLines(unit.affected_counterparties, 2).filter((item) => !isOpaqueEntityRef(item));
const documentScopeRaw = sanitizeUserLines(unit.affected_documents, 2);
const documentScope = documentScopeRaw.filter((item) => !isOpaqueEntityRef(item));
const entityScopeRaw = sanitizeUserLines(unit.affected_entities, 2);
const entityScope = entityScopeRaw.filter((item) => !isOpaqueEntityRef(item));
const scopeParts = [];
if (accountScope.length > 0) {
scopeParts.push(`счета: ${accountScope.join(", ")}`);
}
if (counterpartyScope.length > 0) {
scopeParts.push(`контрагенты: ${counterpartyScope.join(", ")}`);
}
if (documentScopeRaw.length > 0) {
if (documentScope.length > 0) {
scopeParts.push(`документы: ${documentScope.join(", ")}`);
}
else {
scopeParts.push("документы: связанные документы контура");
}
}
if (scopeParts.length === 0 && entityScope.length > 0) {
scopeParts.push(`связанные объекты: ${entityScope.join(", ")}`);
}
if (scopeParts.length === 0) {
return "контур проверки: требуется уточнение объекта";
}
return scopeParts.join("; ");
}
function formatLifecycleScope(unit) {
if (!unit.lifecycle_domain) {
return null;
}
const domainLabel = unit.lifecycle_domain === "bank_settlement"
? "расчеты с оплатами"
: unit.lifecycle_domain === "customer_settlement"
? "расчеты с покупателями"
: unit.lifecycle_domain === "deferred_expense"
? "расходы будущих периодов"
: unit.lifecycle_domain === "fixed_asset"
? "основные средства"
: unit.lifecycle_domain === "vat_flow"
? "НДС"
: unit.lifecycle_domain === "period_close"
? "закрытие периода"
: "жизненный цикл операции";
const parts = [`контур: ${domainLabel}`];
if (unit.current_lifecycle_state && unit.expected_lifecycle_state) {
const currentState = humanizeLifecycleStateToken(unit.current_lifecycle_state);
const expectedState = humanizeLifecycleStateToken(unit.expected_lifecycle_state);
parts.push(`фактическое состояние "${currentState}", ожидаемое "${expectedState}"`);
}
else if (unit.expected_lifecycle_state) {
const expectedState = humanizeLifecycleStateToken(unit.expected_lifecycle_state);
parts.push(`ожидаемое состояние "${expectedState}" не подтверждено`);
}
if (unit.missing_transition) {
const missingTransition = humanizeLifecycleTransitionToken(unit.missing_transition);
parts.push(`не подтвержден ожидаемый переход "${missingTransition}"`);
}
if (unit.invalid_transition) {
const invalidTransition = humanizeLifecycleTransitionToken(unit.invalid_transition);
parts.push(`обнаружен конфликтный переход "${invalidTransition}"`);
}
if (unit.lifecycle_defect_type === "misclosed_state") {
parts.push("закрытие похоже формально завершено, но путь закрытия некорректный");
}
if (unit.lifecycle_defect_type === "cross_branch_state_conflict") {
parts.push("между связанными ветками есть конфликт состояния");
}
if (unit.stale_duration) {
const staleDuration = humanizeLifecycleStateToken(unit.stale_duration);
parts.push(`зависание по времени: ${staleDuration}`);
}
return parts.join(", ");
}
function formatGraphScope(unit) {
const binding = unit.graph_binding;
if (!binding) {
return null;
}
const parts = [];
if (binding.missing_links.length > 0) {
const missingLinks = binding.missing_links.map((item) => humanizeLifecycleTransitionToken(item));
parts.push(`в связанной цепочке не подтверждены переходы: ${missingLinks.join(", ")}`);
}
if (binding.conflicting_links.length > 0) {
const conflictingLinks = binding.conflicting_links.map((item) => humanizeLifecycleTransitionToken(item));
parts.push(`между связанными участками есть конфликт: ${conflictingLinks.join(", ")}`);
}
if (binding.relation_path.length > 2) {
parts.push("разрыв обнаружен между связанными объектами, а не только внутри одного документа");
}
if (binding.graph_confidence === "high") {
parts.push("сигнал по связям устойчивый");
}
else if (binding.graph_confidence === "medium") {
parts.push("сигнал по связям умеренной силы");
}
else {
parts.push("сигнал по связям слабый, нужна проверка");
}
return parts.join(", ");
}
function isSettlementAccountToken(value) {
return /^(?:60|62)(?:\.|$)/.test(String(value ?? "").trim());
}
function isVatAccountToken(value) {
return /^(?:19|68)(?:\.|$)/.test(String(value ?? "").trim());
}
function isCloseCostsAccountToken(value) {
const account = String(value ?? "").trim();
if (!account) {
return false;
}
if (/^97(?:\.|$)/.test(account)) {
return true;
}
const match = account.match(/^(\d{2})/);
if (!match) {
return false;
}
const prefix = Number(match[1]);
return prefix >= 20 && prefix <= 44;
}
function problemUnitSignalCorpus(unit) {
return [
unit.problem_unit_type,
unit.lifecycle_domain ?? "",
unit.business_defect_class ?? "",
unit.failed_expected_edge ?? "",
unit.missing_transition ?? "",
unit.invalid_transition ?? "",
unit.mechanism_summary ?? "",
unit.business_lifecycle_interpretation ?? ""
]
.join(" ")
.toLowerCase();
}
function hasControlledCrossDomainHandoff(unit) {
const corpus = problemUnitSignalCorpus(unit);
return /controlled_cross_domain_handoff|secondary_hypothesis|related_area_to_check|cross_domain_handoff/i.test(corpus);
}
function isProblemUnitAlignedWithNarrativeDomain(unit, domain) {
if (!domain) {
return true;
}
const accounts = unit.affected_accounts ?? [];
const corpus = problemUnitSignalCorpus(unit);
if (domain === "settlements_60_62") {
const foreignSettlementDomain = ["deferred_expense", "vat_flow", "period_close", "fixed_asset"].includes(String(unit.lifecycle_domain ?? ""));
if (foreignSettlementDomain && !hasControlledCrossDomainHandoff(unit)) {
return false;
}
if (unit.lifecycle_domain === "bank_settlement" || unit.lifecycle_domain === "customer_settlement") {
return true;
}
if (accounts.some((item) => isSettlementAccountToken(item))) {
return true;
}
if (unit.problem_unit_type === "broken_chain_segment" || unit.problem_unit_type === "unresolved_settlement_cluster") {
return true;
}
return /(payment_to_settlement|settlement_closed|settlement|аванс|зачет|зачёт|расчет|расч[её]т|оплат)/i.test(corpus);
}
if (domain === "vat_document_register_book") {
const foreignVatDomain = ["period_close", "deferred_expense", "fixed_asset", "bank_settlement", "customer_settlement"].includes(String(unit.lifecycle_domain ?? ""));
if (foreignVatDomain && !hasControlledCrossDomainHandoff(unit)) {
return false;
}
if (unit.lifecycle_domain === "vat_flow") {
return true;
}
if (accounts.some((item) => isVatAccountToken(item))) {
return true;
}
return /(vat|ндс|invoice|book_entry|register|книг|сч[её]т(?:а|у|ом|е)?[\s-]?фактур(?:а|ы|е|у|ой)?|вычет|налогов(?:ый|ого)?\s+эффект)/i.test(corpus);
}
if (domain === "month_close_costs_20_44") {
const foreignMonthCloseDomain = ["vat_flow", "bank_settlement", "customer_settlement", "fixed_asset"].includes(String(unit.lifecycle_domain ?? ""));
if (foreignMonthCloseDomain && !hasControlledCrossDomainHandoff(unit)) {
return false;
}
if (unit.lifecycle_domain === "period_close" ||
unit.lifecycle_domain === "deferred_expense" ||
unit.lifecycle_domain === "fixed_asset") {
return true;
}
if (accounts.some((item) => isCloseCostsAccountToken(item))) {
return true;
}
if (unit.problem_unit_type === "period_risk_cluster" || unit.problem_unit_type === "lifecycle_anomaly_node") {
return true;
}
return /(period_close|allocation|writeoff|cost|затрат|закрыти|списан|распредел)/i.test(corpus);
}
return true;
}
function rankProblemUnitsForAnswer(units, lifecycleAnswerEnabled, domainLock) {
const sorted = !lifecycleAnswerEnabled
? units.slice().sort((left, right) => {
const severityDiff = right.severity.score - left.severity.score;
if (severityDiff !== 0)
return severityDiff;
return right.confidence.score - left.confidence.score;
})
: units.slice().sort((left, right) => {
const lifecycleRankDiff = (right.lifecycle_ranking_score ?? 0) - (left.lifecycle_ranking_score ?? 0);
if (lifecycleRankDiff !== 0)
return lifecycleRankDiff;
const lifecycleConfidenceDiff = (right.lifecycle_confidence?.score ?? 0) - (left.lifecycle_confidence?.score ?? 0);
if (lifecycleConfidenceDiff !== 0)
return lifecycleConfidenceDiff;
const severityDiff = right.severity.score - left.severity.score;
if (severityDiff !== 0)
return severityDiff;
return right.confidence.score - left.confidence.score;
});
if (!domainLock) {
return sorted;
}
const aligned = sorted.filter((unit) => isProblemUnitAlignedWithNarrativeDomain(unit, domainLock));
if (aligned.length === 0) {
return sorted;
}
const alignedIds = new Set(aligned.map((unit) => unit.problem_unit_id));
return [...aligned, ...sorted.filter((unit) => !alignedIds.has(unit.problem_unit_id))];
}
function hasLifecycleResolution(units) {
return units.some((unit) => Boolean(unit.lifecycle_domain) &&
Boolean(unit.current_lifecycle_state) &&
Boolean(unit.expected_lifecycle_state) &&
Boolean(unit.lifecycle_defect_type));
}
function hasGraphResolution(units) {
return units.some((unit) => Boolean(unit.graph_binding?.graph_node_id));
}
function buildProblemCentricActions(input) {
const actions = [];
const unitTypes = new Set(input.units.map((item) => item.problem_unit_type));
if (input.focusDomain === "settlements_60_62") {
actions.push("Проверьте договор и объект расчетов по платежу/зачету.");
actions.push("Сверьте регистр расчетов и привязку платежа к закрывающему документу по 60/62.");
actions.push("Проверьте зачет аванса или взаимозачет и связку платежа с закрытием расчета.");
}
if (unitTypes.has("broken_chain_segment")) {
actions.push("Проверьте связку выписка -> документ -> проводка по проблемным участкам цепочки.");
}
if (unitTypes.has("unresolved_settlement_cluster")) {
actions.push("Сверьте хвосты по расчетам: закрылся ли документ оплаты корректным закрывающим документом.");
}
if (unitTypes.has("period_risk_cluster")) {
actions.push("Оцените влияние дефекта на закрытие периода и корректность регламентных операций.");
}
if (unitTypes.has("cross_branch_inconsistency_cluster")) {
actions.push("Сверьте противоречия между документами, проводками и регистрами по НДС/межконтурным связям.");
}
if (unitTypes.has("lifecycle_anomaly_node")) {
actions.push("Проверьте lifecycle объекта: ожидаемый этап не должен оставаться в partially_linked состоянии.");
}
for (const unit of input.units) {
if (unit.lifecycle_defect_type === "stale_active_state") {
actions.push("Проверьте, почему объект завис: ожидаемый переход не должен оставаться в активной стадии.");
}
if (unit.lifecycle_defect_type === "misclosed_state") {
actions.push("Проверьте закрывающий документ и проводки: закрытие может быть формальным, но некорректным по пути.");
}
if (unit.lifecycle_defect_type === "cross_branch_state_conflict") {
actions.push("Сверьте бухгалтерскую и смежную ветки (например, НДС/расчеты): обнаружен межконтурный конфликт состояния.");
}
}
if (input.missingAnchors.period && input.mode !== "clarification_required") {
actions.push("Уточните период проверки (например, июль 2020), чтобы подтвердить незавершенное списание без лишнего шума.");
}
if (input.mode === "clarification_required") {
if (input.missingAnchors.period) {
actions.push("Уточните период проверки, чтобы зафиксировать границы проблемного контура.");
}
if (input.missingAnchors.account) {
actions.push("Уточните счет или группу счетов для предметной локализации дефекта.");
}
if (input.missingAnchors.documentOrObject) {
actions.push("Укажите конкретный документ или объект трассировки для проверки механизма отклонения.");
}
if (input.missingAnchors.counterparty) {
actions.push("Укажите контрагента/договор, чтобы проверить хвосты и разрывы на конкретной связке.");
}
}
if (input.coverageReport.requirements_uncovered.length > 0) {
actions.push(`Закройте непокрытые требования: ${input.coverageReport.requirements_uncovered.join(", ")}.`);
}
return uniqueStrings(actions, 6);
}
function buildProblemCentricClarifications(input) {
if (input.mode !== "clarification_required") {
return [];
}
const questions = [];
const unitTypes = new Set(input.units.map((item) => item.problem_unit_type));
if (input.missingAnchors.period) {
questions.push("Уточните период (например, июль 2020), в котором нужно проверить проблемный кластер.");
}
if (input.missingAnchors.account) {
questions.push("Уточните счет или связку счетов (например, 51/60), где вы ожидаете дефект.");
}
if (input.missingAnchors.documentOrObject) {
questions.push("Укажите документ/объект, от которого нужно строить проверку цепочки.");
}
if (input.missingAnchors.counterparty) {
questions.push("Укажите контрагента или договор, по которому проверить незакрытую экспозицию.");
}
if (unitTypes.has("broken_chain_segment")) {
questions.push("Уточните участок цепочки: выписка, платежный документ или проводка.");
}
if (unitTypes.has("period_risk_cluster")) {
questions.push("Уточните, какой этап закрытия периода критичен: начисление, закрытие счетов или НДС-блок.");
}
if (unitTypes.has("unresolved_settlement_cluster")) {
questions.push("Уточните, интересуют хвосты поставщиков, покупателей или оба направления.");
}
if (input.coverageReport.clarification_needed_for.length > 0) {
questions.push(`Закройте уточнения для требований: ${input.coverageReport.clarification_needed_for.join(", ")}.`);
}
return uniqueStrings(questions, 6);
}
function buildClaimEvidenceLinks(results) {
const byClaim = new Map();
for (const evidence of flattenEvidence(results)) {
const claimRef = String(evidence.claim_ref ?? "").trim();
const evidenceId = String(evidence.evidence_id ?? "").trim();
if (!claimRef || !evidenceId) {
continue;
}
const current = byClaim.get(claimRef) ?? [];
current.push(evidenceId);
byClaim.set(claimRef, current);
}
return Array.from(byClaim.entries())
.slice(0, 10)
.map(([claim_ref, evidenceIds]) => ({
claim_ref,
evidence_ids: uniqueStrings(evidenceIds, 10)
}));
}
function aggregatePolicySignals(results) {
const broad_query_detected = results.some((item) => summaryBoolean(item, "broad_query_detected"));
const broad_result_flag = results.some((item) => summaryBoolean(item, "broad_result_flag"));
const minimum_evidence_failed = results.some((item) => summaryBoolean(item, "minimum_evidence_failed"));
let degraded_to = null;
for (const result of results) {
const degraded = summaryString(result, "degraded_to");
if (degraded === "clarification") {
degraded_to = "clarification";
break;
}
if (degraded === "partial") {
degraded_to = "partial";
}
}
const narrowingOrder = {
weak: 0,
medium: 1,
strong: 2
};
let narrowing_strength = null;
for (const result of results) {
const value = summaryString(result, "narrowing_strength");
if (value !== "weak" && value !== "medium" && value !== "strong") {
continue;
}
if (!narrowing_strength || narrowingOrder[value] < narrowingOrder[narrowing_strength]) {
narrowing_strength = value;
}
}
return {
broad_query_detected,
broad_result_flag,
minimum_evidence_failed,
degraded_to,
narrowing_strength
};
}
function confidenceToScore(value) {
if (value === "high")
return 3;
if (value === "medium")
return 2;
return 1;
}
function aggregateConfidence(results, evidenceItems) {
const scores = [];
for (const evidence of evidenceItems) {
scores.push(confidenceToScore(evidence.confidence));
}
for (const result of results) {
if (result.status === "error") {
continue;
}
scores.push(confidenceToScore(result.confidence));
}
if (scores.length === 0) {
return "low";
}
const average = scores.reduce((acc, item) => acc + item, 0) / scores.length;
if (average >= 2.6)
return "high";
if (average >= 1.8)
return "medium";
return "low";
}
function collectLimitationReasonCodes(evidenceItems) {
const codes = evidenceItems
.map((item) => item.limitation?.reason_code ?? null)
.filter((item) => Boolean(item));
return uniqueStrings(codes, 8);
}
function limitationReasonToText(code) {
if (code === "snapshot_only")
return "Evidence is snapshot-only and may lag source-of-record.";
if (code === "heuristic_inference")
return "Part of the conclusion relies on heuristic inference.";
if (code === "missing_mechanism")
return "Mechanism is unresolved for part of the evidence.";
if (code === "weak_source_mapping")
return "Source mapping is weak for part of the evidence.";
if (code === "insufficient_detail")
return "Evidence lacks detail for a strong factual claim.";
return "Some evidence limitations remain unresolved.";
}
function asRecordObject(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return null;
}
return value;
}
const EXPLICIT_PERIOD_ANCHOR_PATTERN = /(?:\b20\d{2}(?:[-./](?:0?[1-9]|1[0-2]))?(?:[-./](?:0?[1-9]|[12]\d|3[01]))?\b|\b(?:0?[1-9]|[12]\d|3[01])[./-](?:0?[1-9]|1[0-2])[./-](?:\d{2}|\d{4})\b|\b(?:январ[ьяе]|феврал[ьяе]|март[ае]?|апрел[ьяе]|ма[йея]|июн[ьяе]|июл[ьяе]|август[ае]?|сентябр[ьяе]|октябр[ьяе]|ноябр[ьяе]|декабр[ьяе]|january|february|march|april|may|june|july|august|september|october|november|december)\b)/i;
function hasPeriodAnchorInCompanyAnchors(anchors) {
if (!anchors) {
return false;
}
const dates = Array.isArray(anchors.dates) ? anchors.dates : [];
const periods = Array.isArray(anchors.periods) ? anchors.periods : [];
return dates.some((item) => String(item ?? "").trim().length > 0) || periods.some((item) => String(item ?? "").trim().length > 0);
}
function hasPeriodAnchorInRetrieval(results) {
for (const result of results) {
const summary = asRecordObject(result.summary);
if (!summary) {
continue;
}
const semanticProfile = asRecordObject(summary.semantic_profile);
const periodScope = semanticProfile ? asRecordObject(semanticProfile.period_scope) : null;
if (periodScope) {
const from = String(periodScope.from ?? "").trim();
const to = String(periodScope.to ?? "").trim();
const granularity = String(periodScope.granularity ?? "").trim().toLowerCase();
if (from.length > 0 || to.length > 0 || (granularity.length > 0 && granularity !== "unknown")) {
return true;
}
}
const queryPeriod = String(summary.query_period ?? "").trim();
if (queryPeriod.length > 0) {
return true;
}
}
return false;
}
function hasAccountAnchorInRetrieval(results) {
for (const result of results) {
const summary = asRecordObject(result.summary);
if (!summary) {
continue;
}
const semanticProfile = asRecordObject(summary.semantic_profile);
if (!semanticProfile) {
continue;
}
const accountScope = Array.isArray(semanticProfile.account_scope) ? semanticProfile.account_scope : [];
if (accountScope.length > 0) {
return true;
}
}
return false;
}
function detectMissingAnchors(userMessage, retrievalResults = [], options) {
const lower = String(userMessage ?? "").toLowerCase();
const hasPeriod = EXPLICIT_PERIOD_ANCHOR_PATTERN.test(lower) ||
hasPeriodAnchorInRetrieval(retrievalResults) ||
Boolean(options?.normalizationPeriodExplicit) ||
hasPeriodAnchorInCompanyAnchors(options?.companyAnchors);
const hasAccount = /(?:\bсчет\b|\baccount\b|\bschet\b|\b(?:0[1-9]|[1-9]\d)(?:\.\d{2})?\b|\b(?:60|62)\.\d{2}\s*\/\s*(?:60|62)\.\d{2}\b)/i.test(lower) || hasAccountAnchorInRetrieval(retrievalResults);
const hasDocumentOrObject = /(?:документ|invoice|guid|object|obj|#\d+|\b№\s*[a-zа-я0-9-]+\b|\bid\b|\bref\b|dokument|doc)/i.test(lower);
const hasCounterparty = /(?:контрагент|supplier|buyer|customer|kontragent|postavsh|pokupatel|договор|contract)/i.test(lower);
const hasAnomalyType = /(?:аномал|risk|отклон|разрыв|mismatch|duplicate|tail|цепочк|anomali|hvost)/i.test(lower);
return {
period: !hasPeriod,
account: !hasAccount,
documentOrObject: !hasDocumentOrObject,
counterparty: !hasCounterparty,
anomalyType: !hasAnomalyType
};
}
function buildClarificationQuestions(input) {
const questions = [];
const shouldAsk = input.mode === "clarification_required" || input.coverageReport.clarification_needed_for.length > 0;
if (!shouldAsk) {
return questions;
}
if (input.missingAnchors.period) {
questions.push("Уточните период проверки (например, июль 2020).");
}
if (input.missingAnchors.account) {
questions.push("Уточните счет или группу счетов (например, 19, 60, 62).");
}
if (input.missingAnchors.documentOrObject) {
questions.push("Укажите документ/GUID/конкретный объект для трассировки.");
}
if (input.missingAnchors.counterparty) {
questions.push("Укажите контрагента или группу контрагентов.");
}
if (input.policySignals.broad_query_detected && input.missingAnchors.anomalyType) {
questions.push("Уточните тип отклонения: разрыв цепочки, неверный документ или аномальный риск.");
}
if (input.coverageReport.clarification_needed_for.length > 0) {
questions.push(`Закройте уточнения для требований: ${input.coverageReport.clarification_needed_for.join(", ")}.`);
}
return uniqueStrings(questions, 6);
}
function buildRecommendedActions(input) {
const actions = [];
if (input.mode === "focused_grounded") {
actions.push("Проверьте 1-2 ключевые записи в учетной базе и зафиксируйте итог в рабочем файле проверки.");
}
if (input.mode === "broad_partial") {
actions.push("Сузьте запрос до периода + счета или периода + документа и повторите проверку.");
}
if (input.mode === "clarification_required") {
actions.push("Дайте недостающие якоря (период/счет/объект), иначе сильный factual вывод невозможен.");
}
if (input.coverageReport.requirements_uncovered.length > 0) {
actions.push(`Закройте непокрытые требования: ${input.coverageReport.requirements_uncovered.join(", ")}.`);
}
if (input.coverageReport.requirements_partially_covered.length > 0) {
actions.push(`Доуточните частично покрытые требования: ${input.coverageReport.requirements_partially_covered.join(", ")}.`);
}
if (input.policySignals.broad_query_detected && input.policySignals.narrowing_strength !== "strong") {
actions.push("Добавьте более узкий контекст: тип отклонения, группу документов и бизнес-участок.");
}
if (input.limitationReasonCodes.includes("snapshot_only")) {
actions.push("Сверьте критичные выводы с live source-of-record в 1C.");
}
if (input.limitationReasonCodes.includes("weak_source_mapping")) {
actions.push("Проверьте source mapping для связей document/register по указанным ref.");
}
if (input.sourceRefs.length > 0) {
actions.push(`Начните проверку с ${input.sourceRefs.length} подтвержденных записей и сверьте их с первичными документами.`);
}
return uniqueStrings(actions, 6);
}
function firstMeaningfulFact(results) {
const facts = extractTopFacts(results);
return facts.length > 0 ? facts[0] : null;
}
function buildPolicyDecision(input) {
const hasCoverageGaps = input.coverageReport.requirements_uncovered.length > 0 ||
input.coverageReport.requirements_partially_covered.length > 0 ||
input.coverageReport.clarification_needed_for.length > 0 ||
input.coverageReport.out_of_scope_requirements.length > 0;
if (input.fallbackType === "out_of_scope" && input.coverageReport.requirements_covered === 0) {
return {
mode: "out_of_scope",
fallback_type: "out_of_scope",
reply_type: "out_of_scope"
};
}
if (input.groundingCheck.status === "route_mismatch_blocked") {
return {
mode: "route_mismatch",
fallback_type: "partial",
reply_type: "route_mismatch_blocked"
};
}
if ((input.policySignals.degraded_to === "clarification" && input.policySignals.minimum_evidence_failed) ||
(input.fallbackType === "clarification" && !input.hasSupport) ||
(input.groundingCheck.status === "no_grounded_answer" && !input.hasSupport)) {
return {
mode: "clarification_required",
fallback_type: "clarification",
reply_type: "clarification_required"
};
}
if (input.errorResults.length > 0 && input.okResults.length === 0 && input.partialResults.length === 0) {
return {
mode: "backend_error",
fallback_type: input.fallbackType,
reply_type: "backend_error"
};
}
if (input.okResults.length === 0 && input.partialResults.length === 0 && input.emptyResults.length > 0) {
return {
mode: "empty",
fallback_type: input.fallbackType,
reply_type: "empty_but_valid"
};
}
if (input.groundingCheck.status === "no_grounded_answer" && input.okResults.length === 0 && input.partialResults.length === 0) {
return {
mode: "no_grounded",
fallback_type: input.fallbackType,
reply_type: "no_grounded_answer"
};
}
if (input.focusedStrong &&
!input.policySignals.broad_query_detected &&
!input.policySignals.minimum_evidence_failed &&
!hasCoverageGaps) {
return {
mode: "focused_grounded",
fallback_type: "none",
reply_type: "factual_with_explanation"
};
}
if (input.okResults.length > 0 ||
input.partialResults.length > 0 ||
hasCoverageGaps ||
input.policySignals.minimum_evidence_failed ||
input.policySignals.broad_result_flag ||
input.groundingCheck.status === "partial") {
return {
mode: "broad_partial",
fallback_type: "partial",
reply_type: "partial_coverage"
};
}
return {
mode: "backend_error",
fallback_type: "unknown",
reply_type: "backend_error"
};
}
function buildAnswerSummary(mode) {
if (mode === "focused_grounded")
return "Сформирован прямой ответ на основе подтвержденной опоры.";
if (mode === "broad_partial")
return "Вывод ограничен: есть частичная опора, но покрытие неполное.";
if (mode === "clarification_required")
return "Нужны уточнения: без сужения фокуса надежный вывод невозможен.";
if (mode === "out_of_scope")
return "Запрос вне доступного учетного контура.";
if (mode === "route_mismatch")
return "Предмет результата не совпал с предметом вопроса.";
if (mode === "empty")
return "В текущем срезе данных релевантные записи не обнаружены.";
if (mode === "no_grounded")
return "Недостаточно опоры для обоснованного ответа.";
return "Не удалось собрать обоснованный ответ по текущему запросу.";
}
const BOUNDARY_CAPABILITY_SUGGESTIONS = [
{
key: "settlements_60_62",
label: "Взаиморасчеты 60/62",
helpText: "найти хвосты, незакрытые оплаты и рисковые связки по контрагентам.",
signals: /(контраг|долг|сальдо|взаиморасчет|оплат|аванс|покупат|поставщ|банк|выписк|\b60\b|\b62\b|\b76\b)/iu
},
{
key: "vat_document_register_book",
label: "НДС 19/68",
helpText: "проверить цепочку документ -> счет-фактура -> регистр -> книга.",
signals: /(ндс|сч[её]т[-\s]?фактур|регистр|книга\s+покуп|книга\s+продаж|декларац|\b19\b|\b68\b)/iu
},
{
key: "month_close_costs_20_44",
label: "Закрытие месяца 20/44",
helpText: "проверить распределение затрат и остатки после регламентных операций.",
signals: /(закрыти[ея]|месяц|затрат|распределени|рбп|аморт|основн|ос\b|\b20\b|\b25\b|\b26\b|\b44\b)/iu
}
];
function formatNarrativeDomainLabel(domain) {
if (domain === "settlements_60_62") {
return "взаиморасчетов 60/62";
}
if (domain === "vat_document_register_book") {
return "НДС-контура 19/68";
}
if (domain === "month_close_costs_20_44") {
return "закрытия месяца (20/44)";
}
return "доступного учетного контура";
}
function pickDeterministicBoundaryVariant(seed, variants) {
if (variants.length === 0) {
return "";
}
let score = 0;
for (const char of String(seed ?? "")) {
score = (score + char.charCodeAt(0)) % 104_729;
}
return variants[score % variants.length];
}
function pickBoundaryCapabilityLines(userMessage, limit = 3) {
const text = String(userMessage ?? "").toLowerCase();
const scored = BOUNDARY_CAPABILITY_SUGGESTIONS.map((item, index) => ({
item,
score: (text.match(item.signals) ?? []).length,
order: index
}));
const ranked = scored
.slice()
.sort((left, right) => right.score - left.score || left.order - right.order)
.map((entry) => entry.item);
const selected = ranked.slice(0, Math.max(2, limit));
return uniqueStrings(selected.map((item) => `${item.label}: ${item.helpText}`), limit);
}
function buildBoundaryQuickActionLine(capabilities) {
const actions = capabilities
.slice(0, 2)
.map((item) => item.replace(/:\s*/u, " — ").trim())
.filter((item) => item.length > 0);
if (actions.length === 0) {
return null;
}
return `Что могу сделать сейчас: ${actions.join("; ")}.`;
}
function buildBoundaryQuickActionItems(capabilities) {
const actions = capabilities
.slice(0, 3)
.map((item) => item.replace(/:\s*/u, " — ").trim())
.filter((item) => item.length > 0);
return uniqueStrings(actions, 3);
}
function buildNaturalClarificationHints(input) {
const hints = [];
if (input.missingAnchors.period) {
hints.push("Укажи период проверки (например, июль 2020).");
}
if (input.missingAnchors.account) {
hints.push("Укажи счет или связку счетов (например, 60/62, 19/68 или 20/44).");
}
if (input.missingAnchors.counterparty) {
hints.push("Добавь контрагента или договор, чтобы зафиксировать контур проверки.");
}
if (input.missingAnchors.documentOrObject) {
hints.push("Укажи документ или объект, от которого строить проверку цепочки.");
}
if (input.missingAnchors.anomalyType) {
hints.push("Уточни тип отклонения: разрыв цепочки, неверное закрытие или аномальный хвост.");
}
if (input.coverageReport.clarification_needed_for.length > 0) {
hints.push(`Закрой уточнения для требований: ${input.coverageReport.clarification_needed_for.join(", ")}.`);
}
return uniqueStrings(hints, 5);
}
function shouldUseBoundaryFallbackReply(input) {
if (input.mode === "out_of_scope") {
return true;
}
if (input.mode !== "clarification_required" && input.mode !== "no_grounded" && input.mode !== "broad_partial") {
return false;
}
const hasNoEvidenceRoutes = input.okResultsCount === 0 && input.partialResultsCount === 0;
const hasNoConfirmedCoverage = input.coverageReport.requirements_covered === 0 &&
input.coverageReport.requirements_partially_covered.length === 0;
const groundingBlocked = input.groundingCheck.status === "no_grounded_answer" ||
input.groundingCheck.status === "partial" ||
input.groundingCheck.status === "route_mismatch_blocked";
if (hasNoEvidenceRoutes && hasNoConfirmedCoverage && groundingBlocked) {
return true;
}
const domainNotCovered = input.focusDomain === null || input.focusDomainGroundingBlocked;
const weakEvidenceEnvelope = input.okResultsCount === 0 &&
(input.partialResultsCount === 0 ||
input.aggregateEvidenceConfidence === "low" ||
input.hasCriticalEvidenceLimitation);
if (domainNotCovered && hasNoConfirmedCoverage && groundingBlocked && weakEvidenceEnvelope) {
return true;
}
return false;
}
function buildBoundaryFallbackReply(input) {
const nearbyCapabilities = pickBoundaryCapabilityLines(input.userMessage, 3);
const quickActionLine = buildBoundaryQuickActionLine(nearbyCapabilities);
const quickActionItems = buildBoundaryQuickActionItems(nearbyCapabilities);
if (input.focusDomain === null) {
const heading = pickDeterministicBoundaryVariant(input.userMessage, [
"По этому запросу у меня нет надежного доменного покрытия, поэтому даю мягкий отказ вместо технического шаблона.",
"Сейчас у меня нет надежного доменного маршрута по этому запросу, поэтому даю мягкий отказ вместо шаблонной технички."
]);
return renderStage4ContractReply({
shortLine: heading,
checkedLines: [
"Проверен доступный контур 1С и возможность маршрутизации запроса.",
"Надежный доменный путь для текущей формулировки не подтвержден."
],
foundLines: nearbyCapabilities.length > 0
? nearbyCapabilities
: ["Близких поддерживаемых сценариев по текущей формулировке не найдено."],
unresolvedLines: ["Запрос не подтвержден в поддерживаемом контуре как надежный бухгалтерский сценарий."],
nextStepLines: quickActionItems.length > 0
? quickActionItems
: [quickActionLine ?? "Переформулируйте вопрос через период, счет или объект, и я сразу продолжу проверку."],
nextStepTitle: "Что могу сделать сейчас"
});
}
const clarificationHints = buildNaturalClarificationHints({
missingAnchors: input.missingAnchors,
coverageReport: input.coverageReport
});
const domainHeading = pickDeterministicBoundaryVariant(`${input.userMessage}|${input.focusDomain}`, [
`Сейчас не могу надежно ответить по сценарию ${formatNarrativeDomainLabel(input.focusDomain)}: не хватает опоры.`,
`По сценарию ${formatNarrativeDomainLabel(input.focusDomain)} пока не хватает подтвержденной опоры для надежного вывода.`
]);
return renderStage4ContractReply({
shortLine: domainHeading,
checkedLines: [
`Проверен сценарий ${formatNarrativeDomainLabel(input.focusDomain)} в текущем контуре 1С.`,
"Оценена достаточность подтвержденной опоры для прямого вывода."
],
foundLines: nearbyCapabilities.length > 0
? nearbyCapabilities.slice(0, 2)
: ["Подтвержденных близких сценариев в текущем проходе не найдено."],
unresolvedLines: clarificationHints.length > 0
? clarificationHints
: ["Без дополнительного ориентира (период, объект или контрагент) вывод останется частичным."],
nextStepLines: quickActionItems.length > 0
? quickActionItems
: [quickActionLine ?? "Добавьте один уточняющий ориентир, и я продолжу проверку без смены контура."],
nextStepTitle: "Что могу сделать сейчас"
});
}
function ensureSentence(value) {
const sanitized = sanitizeUserText(value) ?? String(value ?? "").trim();
const normalized = sanitized.replace(/\s+/g, " ").trim();
if (!normalized) {
return "";
}
return /[.!?]$/u.test(normalized) ? normalized : `${normalized}.`;
}
function normalizeNarrativeKey(value) {
return normalizeTechnicalToken(value).replace(/[^\p{L}\p{N}]+/gu, "_");
}
function dedupeNarrativeLines(values, limit = 4) {
const byKey = new Map();
for (const value of values) {
const sentence = ensureSentence(value);
if (!sentence) {
continue;
}
const key = normalizeNarrativeKey(sentence);
if (!key) {
continue;
}
if (!byKey.has(key)) {
byKey.set(key, sentence);
}
}
return Array.from(byKey.values()).slice(0, limit);
}
function mapProblemUnitTypeToNarrative(value) {
if (value === "broken_chain_segment") {
return "Оплата отражена, но ожидаемое закрытие расчета по цепочке не подтверждено.";
}
if (value === "unresolved_settlement_cluster") {
return "По расчетам остался незавершенный хвост: закрывающий документ или зачет не подтвержден.";
}
if (value === "period_risk_cluster") {
return "Контур закрытия месяца не завершен: часть затрат не дошла до ожидаемого закрытия.";
}
if (value === "lifecycle_anomaly_node") {
return "Операция зависла в промежуточном состоянии и не дошла до ожидаемого этапа.";
}
if (value === "document_conflict") {
return "Документ и проводка расходятся по состоянию, поэтому закрытие не подтверждено.";
}
if (value === "cross_branch_inconsistency_cluster") {
return "Между связанными ветками учета есть конфликт состояния.";
}
return null;
}
function mapDefectTokenToNarrative(value) {
const normalized = normalizeTechnicalToken(value);
if (!normalized) {
return null;
}
if (/expected transition is missing/i.test(value)) {
return "Ожидаемый переход в учетной цепочке не подтвержден.";
}
if (normalized.includes("payment_to_settlement") || normalized.includes("failed_edge:payment_to_settlement")) {
return "Оплата отражена, но ожидаемое закрытие расчета не подтверждено.";
}
if (normalized.includes("invoice") || normalized.includes("vat_flow") || normalized.includes("book_entry")) {
return "В цепочке НДС не подтвержден ожидаемый переход от документа к регистру и книге.";
}
if (normalized.includes("allocation") ||
normalized.includes("period_close") ||
normalized.includes("writeoff") ||
normalized.includes("costs_accumulated")) {
return "Цепочка затрат и закрытия месяца подтверждена только частично.";
}
if (normalized.includes("missing_transition") || normalized.includes("expected_transition_not_observed")) {
return "Ожидаемый переход в учетной цепочке не подтвержден.";
}
if (normalized.includes("conflict") || normalized.includes("mismatch")) {
return "Между связанными участками учета есть конфликт состояния.";
}
if (normalized.includes("broken_chain") || normalized.includes("failed_edge")) {
return "Связанная цепочка документов и проводок выглядит разорванной.";
}
return null;
}
const KNOWN_ACCOUNT_PREFIXES = new Set([
"01",
"02",
"07",
"08",
"10",
"13",
"19",
"20",
"21",
"23",
"25",
"26",
"28",
"29",
"41",
"43",
"44",
"45",
"50",
"51",
"52",
"55",
"57",
"58",
"60",
"62",
"66",
"67",
"68",
"69",
"70",
"71",
"73",
"76",
"90",
"91",
"94",
"96",
"97"
]);
function collectDateLikeSpansForNarrative(text) {
const spans = [];
const patterns = [
/\b20\d{2}[./-](?:0[1-9]|1[0-2])(?:[./-](?:0[1-9]|[12]\d|3[01]))?\b/g,
/\b(?:0?[1-9]|[12]\d|3[01])[./-](?:0?[1-9]|1[0-2])[./-](?:\d{2}|\d{4})\b/g,
/\b(?:0?[1-9]|[12]\d|3[01])\s+(?:января|февраля|марта|апреля|мая|июня|июля|августа|сентября|октября|ноября|декабря)\b/giu
];
for (const pattern of patterns) {
let match = null;
while ((match = pattern.exec(text)) !== null) {
spans.push({
start: match.index,
end: match.index + match[0].length
});
}
}
return spans;
}
function collectAmountLikeSpansForNarrative(text) {
const spans = [];
const pattern = /\b\d{1,3}(?:[ \u00A0]\d{3})+(?:[.,]\d{2})?\b/g;
let match = null;
while ((match = pattern.exec(text)) !== null) {
spans.push({
start: match.index,
end: match.index + match[0].length
});
}
return spans;
}
function intersectsNarrativeSpan(start, end, spans) {
return spans.some((span) => start < span.end && end > span.start);
}
function hasAccountContextMarker(text, start, end) {
const left = text.slice(Math.max(0, start - 24), start);
const right = text.slice(end, Math.min(text.length, end + 24));
return /(?:счет|сч\.?|account|schet|по\s+60|по\s+62|по\s+19|по\s+68|по\s+20|по\s+25|по\s+26|по\s+44|расчет|ндс|закрыти|рбп|амортиз|settlement|vat|close)/iu.test(`${left} ${right}`);
}
function toKnownAccountToken(value) {
const token = String(value ?? "").trim();
const prefix = token.match(/^(\d{2})/)?.[1];
if (!prefix || !KNOWN_ACCOUNT_PREFIXES.has(prefix)) {
return null;
}
return token;
}
function extractAccountNumbers(values) {
const tokens = [];
for (const value of values) {
const raw = String(value ?? "");
const matches = raw.match(/\b\d{2}(?:\.\d{1,2})?\b/g) ?? [];
for (const match of matches) {
const account = toKnownAccountToken(match);
if (account) {
tokens.push(account);
}
}
}
return uniqueStrings(tokens, 16);
}
function extractAccountNumbersFromNarrativeText(value) {
const text = String(value ?? "").toLowerCase();
if (!text.trim()) {
return [];
}
const result = [];
const dateSpans = collectDateLikeSpansForNarrative(text);
const amountSpans = collectAmountLikeSpansForNarrative(text);
const blockedSpans = [...dateSpans, ...amountSpans];
const contextualPattern = /(?:\b(?:счет(?:а|у|ом|ов)?|сч\.?|account(?:s)?|schet(?:a|u|om|ov)?)\b)\s*(?:№|#|:)?\s*([0-9./,\sиand]{2,96})/giu;
let contextualMatch = null;
while ((contextualMatch = contextualPattern.exec(text)) !== null) {
const chunk = String(contextualMatch[1] ?? "");
const chunkTokens = chunk.match(/\b\d{2}(?:\.\d{1,2})?\b/g) ?? [];
for (const token of chunkTokens) {
const account = toKnownAccountToken(token);
if (account) {
result.push(account);
}
}
}
const accountPairPattern = /\b(\d{2}(?:\.\d{1,2})?)\s*\/\s*(\d{2}(?:\.\d{1,2})?)\b/g;
let pairMatch = null;
while ((pairMatch = accountPairPattern.exec(text)) !== null) {
const left = toKnownAccountToken(String(pairMatch[1] ?? ""));
const right = toKnownAccountToken(String(pairMatch[2] ?? ""));
if (left) {
result.push(left);
}
if (right) {
result.push(right);
}
}
const explicitPattern = /\b\d{2}(?:\.\d{1,2})?\b/g;
let explicitMatch = null;
while ((explicitMatch = explicitPattern.exec(text)) !== null) {
const token = String(explicitMatch[0] ?? "");
const account = toKnownAccountToken(token);
if (!account) {
continue;
}
const start = explicitMatch.index;
const end = start + token.length;
if (intersectsNarrativeSpan(start, end, blockedSpans)) {
continue;
}
if (!hasAccountContextMarker(text, start, end)) {
continue;
}
result.push(account);
}
return uniqueStrings(result, 16);
}
function inferP0NarrativeDomain(units) {
const allAccounts = extractAccountNumbers(units.flatMap((unit) => unit.affected_accounts ?? []));
const hasSettlementAccount = allAccounts.some((account) => account === "60" || account === "62");
const hasVatAccount = allAccounts.some((account) => account === "19" || account === "68");
const hasCloseAccount = allAccounts.some((account) => /^(2\d|3\d|4[0-4])/.test(account));
if (hasSettlementAccount ||
units.some((unit) => unit.lifecycle_domain === "bank_settlement" || unit.lifecycle_domain === "customer_settlement") ||
units.some((unit) => unit.problem_unit_type === "broken_chain_segment" || unit.problem_unit_type === "unresolved_settlement_cluster")) {
return "settlements_60_62";
}
if (hasVatAccount ||
units.some((unit) => unit.lifecycle_domain === "vat_flow") ||
units.some((unit) => unit.problem_unit_type === "cross_branch_inconsistency_cluster")) {
return "vat_document_register_book";
}
if (hasCloseAccount ||
units.some((unit) => ["period_close", "deferred_expense"].includes(String(unit.lifecycle_domain ?? ""))) ||
units.some((unit) => unit.problem_unit_type === "period_risk_cluster")) {
return "month_close_costs_20_44";
}
return null;
}
function p0NarrativeDomainFromDomainCardId(value) {
const normalized = String(value ?? "").trim().toLowerCase();
if (!normalized) {
return null;
}
if (normalized === "settlements_60_62") {
return "settlements_60_62";
}
if (normalized === "vat_document_register_book") {
return "vat_document_register_book";
}
if (normalized === "month_close_costs_20_44") {
return "month_close_costs_20_44";
}
return null;
}
function p0NarrativeDomainFromHint(value) {
const normalized = String(value ?? "").trim().toLowerCase();
if (!normalized) {
return null;
}
const byCardId = p0NarrativeDomainFromDomainCardId(normalized);
if (byCardId) {
return byCardId;
}
if (normalized.includes("settlements_60_62") ||
normalized.includes("bank_settlement") ||
normalized.includes("customer_settlement") ||
normalized === "settlements") {
return "settlements_60_62";
}
if (normalized.includes("vat_document_register_book") ||
normalized.includes("vat_flow") ||
normalized.includes("vat")) {
return "vat_document_register_book";
}
if (normalized.includes("month_close_costs_20_44") ||
normalized.includes("period_close") ||
normalized.includes("deferred_expense")) {
return "month_close_costs_20_44";
}
return null;
}
function inferP0NarrativeDomainFromDomainGuards(results) {
const weightedCounts = new Map();
for (const result of results) {
const guard = summaryValue(result, "domain_purity_guard");
if (!guard || typeof guard !== "object") {
continue;
}
const domainCardId = p0NarrativeDomainFromDomainCardId(String(guard.domain_card_id ?? ""));
if (!domainCardId) {
continue;
}
const weight = result.status === "ok" ? 2 : result.status === "partial" ? 1 : 0;
if (weight <= 0) {
continue;
}
weightedCounts.set(domainCardId, (weightedCounts.get(domainCardId) ?? 0) + weight);
}
if (weightedCounts.size === 0) {
return null;
}
return Array.from(weightedCounts.entries()).sort((left, right) => right[1] - left[1])[0]?.[0] ?? null;
}
function collectSemanticProfileScopes(results) {
const accounts = [];
const domains = [];
for (const result of results) {
const semanticProfile = summaryValue(result, "semantic_profile");
if (semanticProfile && typeof semanticProfile === "object") {
const profile = semanticProfile;
accounts.push(...toStringList(profile.account_scope));
domains.push(...toStringList(profile.domain_scope));
}
for (const item of result.items.slice(0, 6)) {
const source = item;
accounts.push(...toStringList(source.account_context));
domains.push(...toStringList(source.graph_domain_scope));
}
}
return {
accounts: uniqueStrings(accounts, 16),
domains: uniqueStrings(domains, 16).map((item) => item.toLowerCase())
};
}
function hasControlledCrossDomainHandoffInResult(result) {
const corpus = JSON.stringify({
summary: result.summary,
selection_reason: result.selection_reason,
why_included: result.why_included,
limitations: result.limitations
}).toLowerCase();
return /controlled_cross_domain_handoff|secondary_hypothesis|related_area_to_check|cross_domain_handoff/.test(corpus);
}
function isSettlementDomainToken(value) {
return /(?:bank_settlement|customer_settlement|settlements?|supplier_payments|suppliers?|customers?)/i.test(String(value ?? ""));
}
function isVatDomainToken(value) {
return /(?:vat_flow|vat|nds|taxes?|purchase_book|sales_book|invoice|book_entry|register)/i.test(String(value ?? ""));
}
function isMonthCloseDomainToken(value) {
return /(?:period_close|month_close|close_operation|cost_close|cost_allocation|deferred_expense)/i.test(String(value ?? ""));
}
function isForeignToSettlementDomainToken(value) {
return /(?:vat_flow|vat|deferred_expense|period_close|fixed_asset|fixed_assets|taxes?)/i.test(String(value ?? ""));
}
function isForeignToVatDomainToken(value) {
return /(?:bank_settlement|customer_settlement|settlements?|period_close|deferred_expense|fixed_asset|fixed_assets|month_close)/i.test(String(value ?? ""));
}
function isForeignToMonthCloseDomainToken(value) {
return /(?:bank_settlement|customer_settlement|settlements?|vat_flow|vat|fixed_asset|fixed_assets)/i.test(String(value ?? ""));
}
function collectResultAccounts(result) {
const accounts = [];
const semanticProfile = summaryValue(result, "semantic_profile");
if (semanticProfile && typeof semanticProfile === "object") {
accounts.push(...toStringList(semanticProfile.account_scope));
}
for (const item of result.items.slice(0, 8)) {
const source = item;
accounts.push(...toStringList(source.account_context));
}
return uniqueStrings(accounts, 24);
}
function collectResultDomains(result) {
const domains = [];
const semanticProfile = summaryValue(result, "semantic_profile");
if (semanticProfile && typeof semanticProfile === "object") {
domains.push(...toStringList(semanticProfile.domain_scope));
}
for (const item of result.items.slice(0, 8)) {
const source = item;
domains.push(...toStringList(source.graph_domain_scope));
}
return uniqueStrings(domains.map((item) => item.toLowerCase()), 24);
}
function collectResultRelations(result) {
const relations = [];
const semanticProfile = summaryValue(result, "semantic_profile");
if (semanticProfile && typeof semanticProfile === "object") {
relations.push(...toStringList(semanticProfile.relation_patterns));
}
for (const item of result.items.slice(0, 8)) {
const source = item;
relations.push(...toStringList(source.relation_pattern_hits));
}
return uniqueStrings(relations.map((item) => item.toLowerCase()), 24);
}
function isSubstantiveResult(result) {
if (result.status !== "ok" && result.status !== "partial") {
return false;
}
return result.items.length > 0 || result.evidence.length > 0;
}
function evaluateP0DomainEvidenceGrounding(results, focusDomain) {
if (!focusDomain) {
return {
has_primary: false,
has_foreign_primary: false,
foreign_primary_domains: [],
blocked: false
};
}
const substantive = results.filter((item) => isSubstantiveResult(item));
if (substantive.length === 0) {
return {
has_primary: false,
has_foreign_primary: false,
foreign_primary_domains: [],
blocked: false
};
}
const classify = (result) => {
const accounts = collectResultAccounts(result);
const domains = collectResultDomains(result);
const relations = collectResultRelations(result);
let inDomain = false;
let foreignDomains = [];
if (focusDomain === "settlements_60_62") {
inDomain =
accounts.some((item) => isSettlementAccountToken(item) || /^(?:51|76)(?:\.|$)/.test(item)) ||
domains.some((item) => isSettlementDomainToken(item)) ||
relations.some((item) => /payment_to_settlement|statement_to_document|contract_to_documents|linked_to_settlement|settlement_closed/.test(item));
foreignDomains = domains.filter((item) => isForeignToSettlementDomainToken(item));
}
else if (focusDomain === "vat_document_register_book") {
inDomain =
accounts.some((item) => isVatAccountToken(item)) ||
domains.some((item) => isVatDomainToken(item)) ||
relations.some((item) => /invoice_to_vat|source_doc_present|invoice_linked|book_entry_generated|deduction_posted|register_to_book|vat_/i.test(item));
foreignDomains = domains.filter((item) => isForeignToVatDomainToken(item));
}
else if (focusDomain === "month_close_costs_20_44") {
inDomain =
accounts.some((item) => isCloseCostsAccountToken(item)) ||
domains.some((item) => isMonthCloseDomainToken(item)) ||
relations.some((item) => /costs_accumulated|allocation_rules_resolved|close_operation_runs|residuals_zero|close_operation|period_close|allocation|writeoff/i.test(item));
foreignDomains = domains.filter((item) => isForeignToMonthCloseDomainToken(item));
}
return {
inDomain,
foreignDomains: uniqueStrings(foreignDomains, 8)
};
};
const top = substantive[0];
const topClass = classify(top);
const hasAnyPrimary = substantive.some((item) => classify(item).inDomain);
const hasForeignPrimary = topClass.foreignDomains.length > 0 && !topClass.inDomain;
const topAccounts = collectResultAccounts(top);
const topDomains = collectResultDomains(top);
const topRelations = collectResultRelations(top);
const vatPrimarySignals = topAccounts.filter((item) => isVatAccountToken(item)).length +
topDomains.filter((item) => isVatDomainToken(item)).length +
topRelations.filter((item) => /invoice_to_vat|source_doc_present|invoice_linked|register_to_book|book_entry_generated|deduction_posted|vat_/i.test(item)).length;
const vatForeignSignals = topAccounts.filter((item) => isSettlementAccountToken(item) || isCloseCostsAccountToken(item)).length +
topDomains.filter((item) => isForeignToVatDomainToken(item)).length +
topRelations.filter((item) => /payment_to_settlement|statement_to_document|deferred_expense_to_writeoff|close_operation|allocation|period_close|fixed_asset/i.test(item)).length;
const vatContaminatedPrimary = focusDomain === "vat_document_register_book" &&
topClass.inDomain &&
topClass.foreignDomains.length > 0 &&
vatForeignSignals > Math.max(1, vatPrimarySignals) &&
!hasControlledCrossDomainHandoffInResult(top);
const blocked = (hasForeignPrimary && !hasAnyPrimary && !hasControlledCrossDomainHandoffInResult(top)) || vatContaminatedPrimary;
return {
has_primary: hasAnyPrimary,
has_foreign_primary: hasForeignPrimary,
foreign_primary_domains: topClass.foreignDomains,
blocked
};
}
function hasStrongNarrativeDomainSignalInText(userMessage, domain) {
if (!domain) {
return false;
}
const text = String(userMessage ?? "").toLowerCase();
const accountTokens = extractAccountNumbersFromNarrativeText(text);
if (domain === "settlements_60_62") {
return (accountTokens.some((item) => isSettlementAccountToken(item)) ||
/(60\.0[12]|62\.0[12]|долг|аванс|зач[её]т|взаимозач|расч[её]т|оплат|плат[её]ж|деньг[аи])/i.test(text));
}
if (domain === "vat_document_register_book") {
return (accountTokens.some((item) => isVatAccountToken(item)) ||
/(ндс|vat|сч[её]т(?:а|у|ом|е)?[-\s]?фактур(?:а|ы|е|у|ой)?|книг[аи]|регистр|вычет|налогов(?:ый|ого)?\s+эффект)/i.test(text));
}
if (domain === "month_close_costs_20_44") {
return (accountTokens.some((item) => isCloseCostsAccountToken(item)) ||
/(закрыти[ея]\s+месяц|закрытие\s+счетов|регламентн|косвенн|затрат|распределени|рбп|финансовых\s+результат|month\s*close|period\s*close|close\s+operation)/i.test(text));
}
return false;
}
function hasFixedAssetAmortizationSignalInText(userMessage) {
const text = String(userMessage ?? "").toLowerCase();
const explicitFixedAssetAccountMention = /(?:сч(?:е|ё)т(?:а|у|ом|ов)?\s*(?:№|#|:)?\s*0[12](?:\.\d{1,2})?|\b0[12]\s*\/\s*0[12]\b)/iu.test(text);
return (explicitFixedAssetAccountMention ||
/(основн(ые|ых|ым)?\s+средств|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|амортиз|depreciat|fixed\s*asset)/i.test(text));
}
function hasExplicitMonthCloseSignalInText(userMessage) {
const text = String(userMessage ?? "").toLowerCase();
return /(закрыти[ея]\s+месяц|закрытие\s+счетов|регламентн|косвенн|затрат|распределени|рбп|финансовых\s+результат|month\s*close|period\s*close|close\s+operation)/i.test(text);
}
function inferP0FocusNarrativeDomain(userMessage, results, units, focusDomainHint) {
const fromHint = p0NarrativeDomainFromHint(focusDomainHint);
const fromMessage = inferNarrativeDomainFromText(userMessage);
const strongFromMessage = Boolean(fromMessage && hasStrongNarrativeDomainSignalInText(userMessage, fromMessage));
const fromDomainGuard = inferP0NarrativeDomainFromDomainGuards(results);
const fixedAssetOnlySignal = hasFixedAssetAmortizationSignalInText(userMessage) && !hasExplicitMonthCloseSignalInText(userMessage);
if (fromHint && fromMessage && fromHint !== fromMessage) {
return strongFromMessage ? fromMessage : fromHint;
}
if (fromHint) {
return fromHint;
}
if (fromDomainGuard === "month_close_costs_20_44" && fixedAssetOnlySignal) {
return null;
}
if (fromDomainGuard && fromMessage && fromDomainGuard !== fromMessage) {
return strongFromMessage ? fromMessage : fromDomainGuard;
}
if (fromDomainGuard) {
return fromDomainGuard;
}
if (strongFromMessage) {
return fromMessage;
}
if (fromMessage) {
return fromMessage;
}
const semanticScopes = collectSemanticProfileScopes(results);
const messageAccounts = extractAccountNumbersFromNarrativeText(userMessage);
const hasExplicitP0AccountSignal = [...messageAccounts, ...semanticScopes.accounts].some((item) => isSettlementAccountToken(item) || isVatAccountToken(item) || isCloseCostsAccountToken(item));
// Domain lock is only applied when we have an explicit P0 signal from the query/profile.
if (!hasExplicitP0AccountSignal) {
return null;
}
const fromUnits = inferP0NarrativeDomain(units);
if (fromUnits) {
return fromUnits;
}
if (semanticScopes.accounts.some((item) => isSettlementAccountToken(item))) {
return "settlements_60_62";
}
if (semanticScopes.accounts.some((item) => isVatAccountToken(item))) {
return "vat_document_register_book";
}
if (semanticScopes.accounts.some((item) => isCloseCostsAccountToken(item))) {
return "month_close_costs_20_44";
}
if (semanticScopes.domains.some((item) => item.includes("bank_settlement") || item.includes("customer_settlement") || item === "settlements")) {
return "settlements_60_62";
}
if (semanticScopes.domains.some((item) => item.includes("vat_flow"))) {
return "vat_document_register_book";
}
if (semanticScopes.domains.some((item) => item.includes("period_close") || item.includes("deferred_expense") || item.includes("fixed_asset"))) {
return "month_close_costs_20_44";
}
return null;
}
function domainNarrativeAnchor(domain) {
if (domain === "settlements_60_62") {
return "Оплата отражена, но ожидаемое закрытие расчета не подтверждено.";
}
if (domain === "vat_document_register_book") {
return "По НДС переход от документа к регистру и книге подтвержден не полностью.";
}
if (domain === "month_close_costs_20_44") {
return "Затраты накоплены, но распределение и закрытие месяца подтверждены частично.";
}
return null;
}
function buildMechanismNarrativeFromUnit(unit) {
const candidates = [
unit.mechanism_summary,
unit.business_lifecycle_interpretation,
unit.business_defect_class,
unit.failed_expected_edge,
unit.missing_transition,
unit.invalid_transition,
unit.lifecycle_defect_type
].filter((value) => typeof value === "string" && value.trim().length > 0);
for (const candidate of candidates) {
const stripped = candidate.replace(/^mechanism candidate:\s*/i, "").trim();
const defectMapped = mapDefectTokenToNarrative(stripped);
if (defectMapped) {
return ensureSentence(defectMapped);
}
const humanizedToken = humanizeTechnicalToken(stripped);
if (humanizedToken && !hasUserFacingLeakage(humanizedToken)) {
return ensureSentence(humanizedToken);
}
const cleaned = sanitizeUserText(stripped);
if (!cleaned) {
continue;
}
const core = cleaned.replace(/[.,;:!?]+$/g, "");
if (TECHNICAL_TOKEN_PATTERN.test(core) && /_/.test(core)) {
continue;
}
if (hasUserFacingLeakage(core)) {
continue;
}
return ensureSentence(cleaned);
}
const unitTypeNarrative = mapProblemUnitTypeToNarrative(unit.problem_unit_type);
return unitTypeNarrative ? ensureSentence(unitTypeNarrative) : null;
}
function buildProblemCentricMechanismNarrative(input) {
if (input.mode === "out_of_scope") {
return "Запрос вне доступного учетного контура.";
}
if (input.mode === "route_mismatch") {
return "Текущий результат не совпал с предметом вопроса, нужен более точный фокус.";
}
if (input.mode === "empty") {
return "По заданному условию проблемный механизм в текущем срезе не подтвержден.";
}
if (input.mode === "no_grounded") {
return "Подтвержденной опоры для механизма проблемы пока недостаточно.";
}
const narrativeDomain = input.forcedDomain ?? inferP0NarrativeDomain(input.units);
const domainAnchor = domainNarrativeAnchor(narrativeDomain);
const unitNarratives = dedupeNarrativeLines(input.units
.map((unit) => buildMechanismNarrativeFromUnit(unit))
.filter((value) => Boolean(value)), 4);
const primary = (input.domainLockMiss ? null : unitNarratives[0]) ?? (domainAnchor ? ensureSentence(domainAnchor) : "");
if (!primary) {
if (input.mode === "clarification_required") {
return "Есть признаки проблемы, но без уточнения периода и объекта вывод будет ненадежным и ограниченным.";
}
if (input.weakUnits) {
return "Есть признаки проблемы, но подтверждение механизма пока частичное, поэтому вывод ограничен.";
}
return "Есть признаки проблемы в связанном контуре, требуется первичная проверка.";
}
const withoutDot = primary.replace(/[.!?]+$/u, "");
if (input.domainLockMiss && domainAnchor) {
return `${withoutDot}; по текущей опоре подтверждение частичное, нужна отдельная проверка расчетной связки.`;
}
if (input.mode === "clarification_required") {
return `${withoutDot}, но для уверенного вывода нужны уточнения по периоду и объекту проверки; текущий вывод ограничен.`;
}
if (input.weakUnits) {
return `${withoutDot}; подтверждение пока частичное, вывод ограничен.`;
}
return primary;
}
function humanizeFactForDirectAnswer(value) {
if (!value) {
return null;
}
const cleaned = sanitizeSupportLine(value);
if (!cleaned) {
return null;
}
const mapped = mapDefectTokenToNarrative(cleaned) ?? humanizeTechnicalToken(cleaned);
if (mapped && !hasUserFacingLeakage(mapped)) {
return ensureSentence(mapped);
}
const plain = sanitizeUserText(cleaned);
if (!plain) {
return null;
}
if (/^(?:record|entity|document)\b/i.test(plain)) {
return null;
}
if (/^[\p{L}]+\s*\([^)]+\)\.?$/u.test(plain)) {
return null;
}
if (/^\d+\.\s+/u.test(plain)) {
return null;
}
const core = plain.replace(/[.,;:!?]+$/g, "");
if (TECHNICAL_TOKEN_PATTERN.test(core) && /_/.test(core)) {
return null;
}
return ensureSentence(plain);
}
function buildDirectAnswer(input) {
const topFact = humanizeFactForDirectAnswer(firstMeaningfulFact(input.retrievalResults));
const domainAnchor = domainNarrativeAnchor(input.focusDomain);
const topFactDomain = topFact ? inferNarrativeDomainFromText(topFact) : null;
const topFactAligned = Boolean(topFact) && (!input.focusDomain || topFactDomain === input.focusDomain);
const preferredFact = topFactAligned ? topFact : null;
if (input.mode === "focused_grounded") {
return preferredFact ?? domainAnchor ?? "Проблема подтверждена на текущей опоре и готова к точечной проверке.";
}
if (input.mode === "broad_partial") {
if (preferredFact) {
return `${preferredFact.replace(/[.!?]+$/u, "")}; подтверждение пока частичное.`;
}
if (domainAnchor) {
return `${domainAnchor.replace(/[.!?]+$/u, "")}; подтверждение пока частичное.`;
}
return "Есть признаки проблемы, но опора частичная и вывод ограничен.";
}
if (input.mode === "clarification_required") {
return "Есть признаки проблемы, но без уточнений по периоду и объекту вывод ненадежен.";
}
if (input.mode === "out_of_scope") {
return "Могу отвечать только в пределах данных доступного учетного контура.";
}
if (input.mode === "route_mismatch") {
return "Предмет результата не совпал с предметом вопроса; нужен более точный фокус.";
}
if (input.mode === "empty") {
return "В текущем срезе данных проблемный механизм по условию не подтвержден.";
}
if (input.mode === "no_grounded") {
return "Недостаточно подтвержденной опоры для уверенного вывода.";
}
if (input.policySignals.minimum_evidence_failed) {
return "Минимальная опора не набрана, поэтому вывод ограничен.";
}
return "Не удалось сформировать обоснованный ответ; нужно уточнение запроса.";
}
function buildProblemCentricAnswerSummary(input) {
if (input.graphEnriched && input.summary?.graph_summary && input.summary.graph_summary.bound_units > 0) {
if (input.mode === "clarification_required") {
return "Выявлены связанные проблемные контуры, но для надежного вывода нужны предметные уточнения.";
}
return `Выявлены связанные проблемные контуры: подтверждены разрывы и конфликты между участками цепочки (${input.summary.graph_summary.bound_units} из ${input.summary.graph_summary.total_units} узлов).`;
}
if (input.lifecycleEnriched && input.summary?.lifecycle_enriched_units && input.summary.lifecycle_enriched_units > 0) {
if (input.mode === "clarification_required") {
return "Выявлены lifecycle-дефекты, но для надежного вывода нужны предметные уточнения.";
}
return `Выделены lifecycle-узлы с подтвержденными нарушениями переходов: ${input.summary.lifecycle_enriched_units}.`;
}
if (input.mode === "clarification_required") {
return "Выявлены проблемные кластеры, но для надежного вывода требуется уточнение фокуса.";
}
if (input.weakUnits) {
return "Сформирован problem-first срез с ограниченной опорой; вывод предварительный и требует проверки.";
}
if (input.summary?.units_total && input.summary.units_total > 1) {
return `Сформирован problem-first срез: выделено ${input.summary.units_total} приоритетных проблемных кластеров.`;
}
return "Сформирован problem-first срез: выделен ключевой проблемный кластер и затронутый контур.";
}
function buildProblemCentricDirectAnswer(input) {
const _lifecycleFlag = input.lifecycleAnswerEnabled;
void _lifecycleFlag;
return buildProblemCentricMechanismNarrative({
mode: input.mode,
units: input.units,
weakUnits: input.weakUnits,
forcedDomain: input.focusDomain,
domainLockMiss: input.domainLockMiss
});
}
function buildProblemCentricAnswerStructure(input) {
const weakUnits = input.selectedUnits.every((item) => item.confidence.grade === "low");
const lifecycleEnriched = input.lifecycleAnswerEnabled && hasLifecycleResolution(input.selectedUnits);
const graphEnriched = hasGraphResolution(input.selectedUnits);
const unitMechanismNotes = uniqueStrings(input.selectedUnits
.map((item) => item.mechanism_summary)
.filter((item) => typeof item === "string" && item.trim().length > 0), 6);
const sourceRefs = uniqueStrings(input.evidenceItems
.map((item) => item.source_ref?.canonical_ref)
.filter((item) => typeof item === "string" && item.trim().length > 0), 6);
const evidenceIds = uniqueStrings(input.evidenceItems.map((item) => item.evidence_id), 10);
const aggregateEvidenceConfidence = aggregateConfidence(input.retrievalResults, input.evidenceItems);
const hasCriticalEvidenceLimitation = input.limitationReasonCodes.includes("weak_source_mapping") ||
input.limitationReasonCodes.includes("insufficient_detail");
const confidenceLimited = input.mode !== "focused_grounded" ||
weakUnits ||
input.domainLockMiss ||
input.limitationReasonCodes.includes("missing_mechanism") ||
input.limitationReasonCodes.includes("heuristic_inference") ||
hasCriticalEvidenceLimitation ||
aggregateEvidenceConfidence === "low";
const mechanismStatus = unitMechanismNotes.length === 0
? "unresolved"
: confidenceLimited
? "limited"
: "grounded";
const problemSpecificLimitations = [];
if (input.domainLockMiss && input.focusDomain) {
problemSpecificLimitations.push("Текущая выборка не подтвердила целевой механизм домена в явном виде; вывод ограничен.");
}
if (weakUnits) {
problemSpecificLimitations.push("Problem units remain weak-confidence; conclusions are intentionally limited.");
}
if (input.problemSummary?.duplicate_collapses && input.problemSummary.duplicate_collapses > 0) {
problemSpecificLimitations.push("Part of the problem signal was merged due to duplicate collapse.");
}
const limitations = uniqueStrings([
...problemSpecificLimitations,
...(input.missingAnchors.period ? ["Период в запросе не указан; вывод ограничен и требует проверки в конкретном периоде."] : []),
...input.limitationReasonCodes.map((code) => limitationReasonToText(code)),
...extractLimitations(input.retrievalResults),
...input.groundingCheck.reasons
], 10);
const openUncertainties = uniqueStrings([
...input.groundingCheck.missing_requirements,
...(input.domainLockMiss ? ["primary_domain_evidence_not_confirmed"] : []),
...(input.missingAnchors.period ? ["missing_anchor:period"] : []),
...(input.mode === "clarification_required" && input.missingAnchors.account ? ["missing_anchor:account"] : []),
...(input.mode === "clarification_required" && input.missingAnchors.documentOrObject
? ["missing_anchor:document_or_object"]
: []),
...(input.mode === "clarification_required" && input.missingAnchors.counterparty ? ["missing_anchor:counterparty"] : [])
], 8);
return {
schema_version: "answer_structure_v1_1",
answer_summary: buildProblemCentricAnswerSummary({
mode: input.mode,
weakUnits,
summary: input.problemSummary,
lifecycleEnriched,
graphEnriched
}),
direct_answer: buildProblemCentricDirectAnswer({
mode: input.mode,
units: input.selectedUnits,
weakUnits,
lifecycleAnswerEnabled: input.lifecycleAnswerEnabled,
focusDomain: input.focusDomain,
domainLockMiss: input.domainLockMiss
}),
mechanism_block: {
status: mechanismStatus,
mechanism_notes: unitMechanismNotes,
limitation_reason_codes: input.limitationReasonCodes
},
evidence_block: {
evidence_ids: evidenceIds,
source_refs: sourceRefs,
mechanism_notes: unitMechanismNotes,
coverage_note: input.coverageReport.requirements_total > 0 &&
input.coverageReport.requirements_total === input.coverageReport.requirements_covered &&
input.coverageReport.requirements_uncovered.length === 0 &&
input.coverageReport.requirements_partially_covered.length === 0
? "coverage_full_or_near_full"
: "coverage_partial_or_limited",
...(input.claimEvidenceLinks.length > 0
? {
claim_evidence_links: input.claimEvidenceLinks
}
: {})
},
uncertainty_block: {
open_uncertainties: openUncertainties,
limitations
},
next_step_block: {
recommended_actions: buildProblemCentricActions({
units: input.selectedUnits,
mode: input.mode,
missingAnchors: input.missingAnchors,
coverageReport: input.coverageReport,
focusDomain: input.focusDomain
}),
clarification_questions: buildProblemCentricClarifications({
units: input.selectedUnits,
missingAnchors: input.missingAnchors,
coverageReport: input.coverageReport,
mode: input.mode
})
}
};
}
function limitationReasonToUserText(code) {
if (code === "snapshot_only")
return "Оценка построена на snapshot-срезе и может не включать самые свежие изменения.";
if (code === "heuristic_inference")
return "Часть вывода построена эвристически и требует проверки в базе.";
if (code === "missing_mechanism")
return "Механизм проблемы подтвержден не полностью.";
if (code === "weak_source_mapping")
return "Связка между источниками подтверждена частично.";
if (code === "insufficient_detail")
return "В данных не хватает деталей для сильного вывода.";
return "Есть ограничения по качеству опоры.";
}
function inferNarrativeDomainFromText(value) {
const text = String(value ?? "").toLowerCase();
const accountTokens = extractAccountNumbersFromNarrativeText(text);
const fixedAssetSignal = hasFixedAssetAmortizationSignalInText(text);
const explicitMonthCloseSignal = hasExplicitMonthCloseSignalInText(text);
let settlementScore = 0;
let vatScore = 0;
let monthCloseScore = 0;
if (accountTokens.some((token) => isSettlementAccountToken(token))) {
settlementScore += 3;
}
if (accountTokens.some((token) => isVatAccountToken(token))) {
vatScore += 3;
}
if (accountTokens.some((token) => isCloseCostsAccountToken(token))) {
monthCloseScore += 3;
}
if (/(долг|аванс|взаимозач|зачет|зачёт|62\.01|62\.02|60\.01|60\.02|не\s+сход)/i.test(text)) {
settlementScore += 2;
}
if (/(расч[её]т|оплат|плат[её]ж|деньг[аи]|закрыти[ея]\s+расч)/i.test(text)) {
settlementScore += 2;
}
if (/(ндс|vat|сч[её]т(?:а|у|ом|е)?[-\s]?фактур(?:а|ы|е|у|ой)?|книг[аи]|регистр|вычет|налогов(?:ый|ого)?\s+эффект)/i.test(text)) {
vatScore += 3;
}
if (explicitMonthCloseSignal) {
monthCloseScore += 3;
}
if (fixedAssetSignal && !explicitMonthCloseSignal && settlementScore === 0 && vatScore === 0) {
return null;
}
const maxScore = Math.max(settlementScore, vatScore, monthCloseScore);
if (maxScore <= 0) {
return null;
}
// Tie-break prioritizes explicit VAT and month-close lexical markers over broad settlement wording.
if (vatScore === maxScore) {
return "vat_document_register_book";
}
if (monthCloseScore === maxScore) {
return "month_close_costs_20_44";
}
if (settlementScore === maxScore) {
return "settlements_60_62";
}
return null;
}
function isIncompleteEvidence(structure) {
return (structure.mechanism_block.status !== "grounded" ||
structure.evidence_block.coverage_note !== "coverage_full_or_near_full" ||
structure.uncertainty_block.limitations.length > 0 ||
structure.uncertainty_block.open_uncertainties.length > 0);
}
function buildShortSectionLine(structure) {
const broken = sanitizeUserText(structure.direct_answer) ?? "";
const domain = inferNarrativeDomainFromText(broken);
const incomplete = isIncompleteEvidence(structure);
const shortSeed = `${domain}|${incomplete ? "partial" : "grounded"}|${broken}`;
if (/вне доступного учетного контура/i.test(broken)) {
return "Запрос вне доступного учетного контура.";
}
if (/не совпал с предметом вопроса|более точный фокус/i.test(broken)) {
return pickDeterministicBoundaryVariant(shortSeed, [
"Требуется уточнить фокус, чтобы ответить по нужному участку учета.",
"Сейчас не хватает точного фокуса вопроса для надежного вывода.",
"Нужна чуть более точная формулировка фокуса запроса."
]);
}
if (/ненадежен|уточнен/i.test(broken)) {
return pickDeterministicBoundaryVariant(shortSeed, [
"Сигналы есть, но для уверенного вывода нужны уточнения.",
"Картина пока частичная: подтверждения есть, но не хватает уточнений.",
"Промежуточный вывод собран, однако без уточнений он остается ограниченным."
]);
}
if (domain === "settlements_60_62") {
return incomplete
? pickDeterministicBoundaryVariant(shortSeed, [
"По взаиморасчетам видны признаки неполного закрытия, но картина пока частичная.",
"Есть подтвержденные сигналы разрыва закрытия расчетов, часть вывода остается ограниченной.",
"Риск незакрытых взаиморасчетов подтвержден частично и требует уточнения."
])
: pickDeterministicBoundaryVariant(shortSeed, [
"Проблема с закрытием расчетов подтверждена на текущей опоре.",
"Разрыв в закрытии взаиморасчетов подтвержден.",
"По текущей опоре несходимость в закрытии расчетов подтверждена."
]);
}
if (domain === "vat_document_register_book") {
return incomplete
? pickDeterministicBoundaryVariant(shortSeed, [
"В цепочке НДС есть подтвержденные отклонения, но пока не по всем звеньям.",
"Сигналы по НДС подтверждены частично; для полного вывода нужна дополнительная проверка.",
"Есть частичное подтверждение разрыва в НДС-контуре."
])
: pickDeterministicBoundaryVariant(shortSeed, [
"Проблема в цепочке НДС подтверждена.",
"Разрыв в НДС-контуре подтвержден на текущей опоре.",
"По НДС выявлен и подтвержден проблемный переход в цепочке."
]);
}
if (domain === "month_close_costs_20_44") {
return incomplete
? pickDeterministicBoundaryVariant(shortSeed, [
"По закрытию месяца есть проблемные сигналы, но они подтверждены частично.",
"Контур 20/44 показывает частично подтвержденные отклонения закрытия.",
"Есть признаки сбоя в закрытии месяца, однако опора пока неполная."
])
: pickDeterministicBoundaryVariant(shortSeed, [
"Проблема в контуре закрытия месяца подтверждена.",
"Сбой в закрытии месяца подтвержден на текущей выборке.",
"Разрыв в контуре 20/44 подтвержден."
]);
}
return incomplete
? pickDeterministicBoundaryVariant(shortSeed, [
"Есть подтвержденные сигналы, но вывод пока частичный.",
"Промежуточная проверка выявила проблему, однако опора еще неполная.",
"Часть признаков подтверждена, для полного вывода нужна допроверка."
])
: pickDeterministicBoundaryVariant(shortSeed, [
"Проблема подтверждена на текущей опоре.",
"Текущая опора подтверждает наличие проблемы.",
"Подтверждение проблемы по текущей выборке получено."
]);
}
function humanizeCompositeDirectAnswer(value) {
const raw = String(value ?? "").trim();
if (!raw) {
return null;
}
const tokenPattern = /\b[a-z][a-z0-9_:-]{2,}\b/gi;
const tokenMappings = uniqueStrings(Array.from(raw.matchAll(tokenPattern))
.map((match) => humanizeTechnicalToken(String(match?.[0] ?? "")))
.filter((item) => Boolean(item))
.map((item) => ensureSentence(item)), 4);
const residualRaw = raw
.replace(tokenPattern, " ")
.replace(/[()]/g, " ")
.replace(/\s*[;:]\s*/g, " ")
.replace(/\s{2,}/g, " ")
.trim();
const residualText = sanitizeUserText(residualRaw);
const lines = [...tokenMappings];
if (residualText && !hasUserFacingLeakage(residualText)) {
lines.push(ensureSentence(residualText));
}
const compact = dedupeNarrativeLines(lines, 3);
if (compact.length === 0) {
return null;
}
return compact.join(" ");
}
function buildBrokenSectionLines(structure) {
const direct = sanitizeUserText(structure.direct_answer);
if (direct) {
if (/\b[a-z]+_[a-z0-9_:-]+\b/i.test(direct)) {
const compositeHumanized = humanizeCompositeDirectAnswer(direct);
if (compositeHumanized) {
return [compositeHumanized];
}
}
const mapped = mapDefectTokenToNarrative(direct) ?? humanizeTechnicalToken(direct);
if (mapped) {
return [ensureSentence(mapped)];
}
if (/[a-z]/i.test(direct) && !/[а-яё]/iu.test(direct)) {
return ["Ожидаемый переход в учетной цепочке не подтвержден."];
}
return [ensureSentence(direct)];
}
return ["Есть признаки нарушения в связанной цепочке документов и проводок."];
}
function buildWhySectionLines(structure, context) {
const noteLines = dedupeNarrativeLines(structure.mechanism_block.mechanism_notes
.map((item) => sanitizeSupportLine(item))
.filter((item) => Boolean(item))
.map((item) => mapDefectTokenToNarrative(item) ?? humanizeTechnicalToken(item) ?? item), 4);
const domain = context?.focusDomain ?? inferNarrativeDomainFromText(sanitizeUserText(structure.direct_answer) ?? "");
const mechanismCorpus = `${structure.direct_answer} ${structure.mechanism_block.mechanism_notes.join(" ")} ${structure.evidence_block.mechanism_notes.join(" ")}`;
const fixedAssetContextSignal = hasFixedAssetContextSignal(context);
const fixedAssetSignal = fixedAssetContextSignal ||
((context?.focusDomain ?? null) !== "settlements_60_62" && hasFixedAssetSignalInStructure(structure, context));
const rbpSignal = hasRbpContextSignal(context) || hasRbpSignalInText(mechanismCorpus);
const lines = [...noteLines];
if (structure.mechanism_block.status === "grounded") {
lines.push("Признак проблемы повторяется в связанных документах и проводках.");
}
else if (structure.mechanism_block.status === "limited") {
if (domain === "vat_document_register_book") {
lines.push("Часть НДС-цепочки подтверждена, но один или несколько переходов документ -> счет-фактура -> регистр -> книга не подтверждены.");
}
else if (fixedAssetSignal) {
lines.push("По ОС часть переходов к начислению амортизации подтверждена не полностью, поэтому есть риск пропуска отдельных объектов.");
}
else if (rbpSignal) {
lines.push("По РБП часть списаний к концу периода подтверждена не полностью, поэтому остаток может сохраняться дольше ожидаемого.");
}
else if (domain === "month_close_costs_20_44") {
lines.push("Часть шагов закрытия периода подтверждена, но ключевой переход распределения/закрытия не подтвержден.");
}
else {
lines.push("Часть ожидаемой цепочки подтверждена, но ключевой переход не подтвержден.");
}
}
else {
lines.push("Сигнал проблемы есть, но механизм подтвержден не полностью.");
}
return dedupeNarrativeLines(lines, 3);
}
function extractRequirementIdsFromText(value) {
const matches = String(value ?? "").match(/\bR\d+\b/gi);
return uniqueStrings((matches ?? []).map((item) => item.toUpperCase()), 8);
}
function buildCoverageSplitLines(structure, questionType = "unknown") {
const confirmed = uniqueStrings((structure.evidence_block.claim_evidence_links ?? [])
.flatMap((item) => extractRequirementIdsFromText(item.claim_ref))
.map((item) => item.toUpperCase()), 8);
const unresolved = uniqueStrings(structure.uncertainty_block.open_uncertainties.flatMap((item) => extractRequirementIdsFromText(item)), 8);
const lines = [];
if (questionType === "which_chains_are_complete_vs_incomplete") {
if (confirmed.length > 0) {
lines.push(`Цепочки подтверждены: ${confirmed.join(", ")}.`);
}
if (unresolved.length > 0) {
lines.push(`Цепочки подтверждены частично или не подтверждены: ${unresolved.join(", ")}.`);
}
else if (structure.evidence_block.coverage_note === "coverage_partial_or_limited") {
lines.push("Часть цепочек подтверждена частично; для остальных не хватает опоры.");
}
if (lines.length === 0) {
lines.push("Цепочки пока не удалось уверенно разделить на полные и неполные.");
}
return dedupeNarrativeLines(lines, 3);
}
if (confirmed.length > 0) {
lines.push(`Подтверждено по требованиям: ${confirmed.join(", ")}.`);
}
if (unresolved.length > 0) {
lines.push(`Отдельно не подтверждено или покрыто частично: ${unresolved.join(", ")}.`);
}
else if (structure.evidence_block.coverage_note === "coverage_partial_or_limited") {
lines.push("Подтверждена только часть запроса; оставшаяся часть вынесена в ограничения.");
}
return dedupeNarrativeLines(lines, 3);
}
function buildEvidenceSectionLines(structure, questionType = "unknown", context) {
const evidenceCount = Array.isArray(structure.evidence_block.evidence_ids) ? structure.evidence_block.evidence_ids.length : 0;
const sourceCount = Array.isArray(structure.evidence_block.source_refs) ? structure.evidence_block.source_refs.length : 0;
const claimLinks = Array.isArray(structure.evidence_block.claim_evidence_links)
? structure.evidence_block.claim_evidence_links.length
: 0;
const reliabilityLimited = structure.mechanism_block.status !== "grounded" ||
structure.uncertainty_block.limitations.length > 0 ||
structure.uncertainty_block.open_uncertainties.length > 0 ||
structure.evidence_block.coverage_note === "coverage_partial_or_limited";
const lines = [];
const coverageSplitLines = buildCoverageSplitLines(structure, questionType);
const domain = context?.focusDomain ?? inferNarrativeDomainFromText(sanitizeUserText(structure.direct_answer) ?? "");
const evidenceCorpus = `${structure.direct_answer} ${structure.mechanism_block.mechanism_notes.join(" ")} ${structure.evidence_block.mechanism_notes.join(" ")}`;
const fixedAssetContextSignal = hasFixedAssetContextSignal(context);
const fixedAssetSignal = fixedAssetContextSignal ||
((context?.focusDomain ?? null) !== "settlements_60_62" && hasFixedAssetSignalInStructure(structure, context));
const rbpSignal = hasRbpContextSignal(context) || hasRbpSignalInText(evidenceCorpus);
if (questionType === "what_is_it_grounded_on") {
if (domain === "vat_document_register_book") {
lines.push("Основание собрано по НДС-цепочке: документ, счет-фактура, регистр НДС и запись книги.");
}
else if (fixedAssetSignal) {
lines.push("Основание собрано по ОС: карточка объекта, параметры амортизации, начисление и движения по 01/02.");
}
else if (rbpSignal) {
lines.push("Основание собрано по РБП: объект списания, документ списания и остаток на конец периода.");
}
else {
lines.push("Основание вывода перечислено по подтвержденным документам, регистрам и проводкам.");
}
}
else if (questionType === "prove_or_guess") {
lines.push("Основание разделено на подтвержденную часть и зону гипотез.");
}
else if (questionType === "which_chains_are_complete_vs_incomplete") {
if (domain === "vat_document_register_book") {
lines.push("Опора собрана по звеньям НДС-цепочки, чтобы разделить полные и неполные переходы.");
}
else if (rbpSignal) {
lines.push("Опора собрана по РБП-цепочке, чтобы разделить подтвержденное и неподтвержденное списание.");
}
else if (fixedAssetSignal) {
lines.push("Опора собрана по ОС-цепочке, чтобы разделить подтвержденные и неподтвержденные начисления амортизации.");
}
else {
lines.push("Опора собрана так, чтобы разделить цепочки на полные и неполные.");
}
}
if (evidenceCount > 0) {
lines.push(`Вывод опирается на ${evidenceCount} подтвержденных наблюдений в текущем срезе.`);
}
if (sourceCount > 0) {
lines.push(`Проверены связанные документы и проводки по ${sourceCount} источникам.`);
}
if (claimLinks > 0) {
lines.push("Есть связка между основным выводом и подтверждающими записями.");
}
if (structure.evidence_block.coverage_note === "coverage_partial_or_limited" || reliabilityLimited) {
if (domain === "vat_document_register_book") {
lines.push("Опора частичная: по НДС-цепочке не подтверждены одно или несколько звеньев.");
}
else if (fixedAssetSignal) {
lines.push("Опора частичная: не по всем объектам ОС подтверждено попадание в начисление амортизации.");
}
else if (rbpSignal) {
lines.push("Опора частичная: не по всем объектам РБП подтверждено списание к концу периода.");
}
else if (structure.evidence_block.coverage_note === "coverage_partial_or_limited") {
lines.push("Опора частичная: часть требований покрыта не полностью.");
}
else if (evidenceCount > 0) {
lines.push("Опора есть, но достаточна только для предварительного вывода.");
}
}
else if (evidenceCount > 0) {
lines.push("Опора достаточна для первичного вывода.");
}
if (lines.length === 0) {
lines.push("Использована доступная выборка документов и проводок в текущем snapshot.");
}
return dedupeNarrativeLines([...lines, ...coverageSplitLines], 6);
}
function buildDefaultChecksByDomain(domain) {
if (domain === "settlements_60_62") {
return [
"Сверьте договор и объект расчетов, затем подтвердите запись в регистре расчетов и документ зачета.",
"Проверьте связку платеж -> расчетный документ -> проводки по 60/62/76 и факт закрытия хвоста."
];
}
if (domain === "vat_document_register_book") {
return [
"Проверьте связку: исходный документ -> запись регистра НДС -> запись в книге.",
"Сверьте счет-фактуру и момент отражения вычета в нужном периоде."
];
}
if (domain === "month_close_costs_20_44") {
return [
"Проверьте накопление и распределение затрат по счетам 20-44 перед закрытием месяца.",
"Сверьте регламентную операцию закрытия и остатки, которые должны быть нулевыми или объясненными."
];
}
return ["Проверьте связку документов и проводок по проблемному участку в указанном периоде."];
}
function hasFixedAssetAnchorContext(context) {
if (!context) {
return false;
}
const corpus = [...context.anchors.present, ...context.anchors.used].join(" ").toLowerCase();
return /(?:doc_type:amortization|account:0[12]|амортиз|основн|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|fixed\s*asset|depreciat)/i.test(corpus);
}
function hasFixedAssetContextSignal(context) {
if (!context) {
return false;
}
const corpus = [...context.anchors.present, ...context.anchors.used, context.userMessage ?? ""].join(" ").toLowerCase();
return (hasFixedAssetAnchorContext(context) ||
hasFixedAssetAmortizationSignalInText(corpus) ||
/(?:\bос\b|основн(?:ые|ых)?\s+средств|амортиз|сч(?:е|ё)т\s*0[12])/i.test(corpus));
}
function hasRbpAnchorContext(context) {
if (!context) {
return false;
}
const corpus = [...context.anchors.present, ...context.anchors.used].join(" ").toLowerCase();
return /(?:\brbp(?:[_\s-]?writeoff)?\b|рбп|deferred[_\s-]?expense(?:[_\s-]?to[_\s-]?writeoff)?|doc_type:(?:deferred|rbp_writeoff)|счет\s*97|account:97)/i.test(corpus);
}
function hasRbpContextSignal(context) {
if (!context) {
return false;
}
const corpus = [...context.anchors.present, ...context.anchors.used, context.userMessage ?? ""].join(" ");
return hasRbpAnchorContext(context) || hasRbpSignalInText(corpus);
}
function hasRbpSignalInText(value) {
const text = String(value ?? "").toLowerCase();
return /(?:\brbp(?:[_\s-]?writeoff)?\b|рбп|deferred[_\s-]?expense(?:[_\s-]?to[_\s-]?writeoff)?|счет\s*97|списани[ея]\s+рбп|остат(ок|ки)\s+рбп)/i.test(text);
}
function hasFixedAssetSignalInStructure(structure, context) {
const corpus = [
structure.direct_answer,
...structure.mechanism_block.mechanism_notes,
...structure.evidence_block.mechanism_notes,
...(structure.evidence_block.source_refs ?? []),
...(structure.evidence_block.evidence_ids ?? []),
...(context?.anchors.present ?? []),
...(context?.anchors.used ?? [])
]
.filter(Boolean)
.join(" ");
if (hasFixedAssetAnchorContext(context) || hasFixedAssetAmortizationSignalInText(corpus)) {
return true;
}
return /(?:asset_card_to_depreciation|fixed_asset|fixed_assets|амортиз|основн(?:ые|ых)?\s+средств|сч(?:е|ё)т\s*0[12]|\b0[12](?:\.\d{2})?\b)/i.test(corpus);
}
function buildFixedAssetChecksByQuestionType(questionType) {
if (questionType === "what_to_check_first") {
return [
"Проверьте по каждому объекту ОС карточку и параметр амортизации (способ, срок, дата начала начисления).",
"Сверьте ввод в эксплуатацию и попадание объекта в набор начисления амортизации за нужный период.",
"Подтвердите начисление по объектам проводками и регистром амортизации."
];
}
if (questionType === "prove_or_guess") {
return [
"Разделите доказанные и предположительные участки по цепочке ОС: принятие -> ввод -> начисление амортизации.",
"Проверьте, какие объекты отсутствуют в наборе начисления или имеют некорректные параметры амортизации."
];
}
if (questionType === "where_break_is") {
return [
"Локализуйте разрыв в цепочке ОС: карточка объекта -> ввод в эксплуатацию -> начисление амортизации.",
"Сверьте, на каком шаге пропадает подтверждение по конкретным объектам."
];
}
if (questionType === "what_is_it_grounded_on") {
return [
"Перечислите основание: карточка ОС, документ ввода в эксплуатацию, запись регистра амортизации, проводки по начислению."
];
}
return [
"Проверьте ОС-контур: объект ОС -> ввод в эксплуатацию -> начисление амортизации по счетам 01/02.",
"Сверьте параметр амортизации и наличие начисления по каждому объекту ОС в периоде."
];
}
function buildRbpChecksByQuestionType(questionType) {
if (questionType === "what_to_check_first") {
return [
"Проверьте список объектов РБП, которые должны были списаться к концу периода.",
"Сверьте документ списания РБП и движение по счету 97 по каждому объекту.",
"Проверьте остаток РБП после списания и причину, если часть суммы остается активной."
];
}
if (questionType === "prove_or_guess") {
return [
"Разделите по РБП доказанное и гипотезу: где списание подтверждено, а где есть только косвенные признаки.",
"Проверьте, для каких объектов РБП нет подтверждения списания на конец периода."
];
}
if (questionType === "where_break_is") {
return [
"Локализуйте разрыв в РБП-цепочке: объект РБП -> документ списания -> движение по счету 97.",
"Проверьте, на каком шаге исчезает подтверждение списания."
];
}
if (questionType === "what_is_it_grounded_on") {
return [
"Перечислите основание по РБП: объект, документ списания, движение по счету 97, остаток на конец периода."
];
}
if (questionType === "which_chains_are_complete_vs_incomplete") {
return [
"Разделите РБП-цепочки на: списание подтверждено, подтверждено частично, не подтверждено.",
"Проверьте, где к концу периода остается РБП без подтвержденного списания."
];
}
return [
"Проверьте РБП-контур: объект РБП -> документ списания -> движение по счету 97.",
"Сверьте остаток РБП на конец периода и причину, если часть суммы не списана."
];
}
function buildQuestionTypeDomainChecks(questionType, domain) {
if (questionType === "what_to_check_first") {
if (domain === "settlements_60_62") {
return [
"Сверьте договор и объект расчетов по спорной операции.",
"Проверьте регистр расчетов и зачет аванса/взаимозачет.",
"Подтвердите проводки по 60/62/76 и факт закрытия хвоста."
];
}
if (domain === "vat_document_register_book") {
return [
"Сверьте исходный документ и счет-фактуру.",
"Проверьте запись в регистре НДС и попадание в книгу.",
"Подтвердите налоговые проводки по 19/68 в нужном периоде."
];
}
if (domain === "month_close_costs_20_44") {
return [
"Проверьте регламентную операцию закрытия за нужный период.",
"Сверьте базу распределения затрат и проводки по 20/25/26/44.",
"Убедитесь, что остатки объяснены или закрыты после операции."
];
}
return ["Начните с первого подтверждаемого документа и пройдите цепочку без пропусков."];
}
if (questionType === "where_break_is") {
if (domain === "settlements_60_62") {
return [
"Локализуйте разрыв в узле: договор -> объект расчетов -> регистр расчетов -> закрывающий документ.",
"Сверьте, где прерывается переход платеж -> зачет/закрытие -> проводки 60/62/76."
];
}
if (domain === "vat_document_register_book") {
return [
"Локализуйте разрыв в узле: документ -> счет-фактура -> регистр НДС -> книга.",
"Сверьте, где прерывается переход от исходного документа к налоговой записи."
];
}
if (domain === "month_close_costs_20_44") {
return [
"Локализуйте разрыв в узле: накопление затрат -> правило распределения -> операция закрытия.",
"Сверьте, на каком шаге исчезает подтверждение перехода к закрытию остатков."
];
}
}
if (questionType === "prove_or_guess") {
if (domain === "settlements_60_62") {
return [
"Отдельно отметьте, что доказано документами и проводками, а что остается гипотезой.",
"Для доказательства проверьте связку платеж -> расчетный документ -> регистр расчетов -> 60/62/76."
];
}
if (domain === "vat_document_register_book") {
return [
"Разделите доказанное и предположительное по цепочке: документ -> счет-фактура -> регистр -> книга.",
"Подтвердите налоговую запись по 19/68 в нужном периоде."
];
}
if (domain === "month_close_costs_20_44") {
return [
"Разделите доказанные и предположительные участки в цепочке закрытия месяца.",
"Проверьте подтверждение: операция закрытия -> распределение -> остатки по 20/25/26/44."
];
}
}
if (questionType === "what_is_it_grounded_on") {
if (domain === "settlements_60_62") {
return [
"Перечислите основание: платежный документ, расчетный документ, запись регистра расчетов, проводки 60/62/76."
];
}
if (domain === "vat_document_register_book") {
return [
"Перечислите основание: исходный документ, счет-фактура, запись регистра НДС, запись книги, проводки 19/68."
];
}
if (domain === "month_close_costs_20_44") {
return [
"Перечислите основание: операция закрытия, база распределения, проводки по затратам, остатки после закрытия."
];
}
}
if (questionType === "which_chains_are_complete_vs_incomplete") {
if (domain === "settlements_60_62") {
return [
"Разделите цепочки на: подтверждена, подтверждена частично, не подтверждена по переходу платеж -> закрытие расчета.",
"Проверьте разницу между закрытыми и незакрытыми связками по 60/62/76."
];
}
if (domain === "vat_document_register_book") {
return [
"Разделите цепочки на: полная, частичная, неполная по связке документ -> счет-фактура -> регистр -> книга.",
"Проверьте, где отсутствует подтверждение налоговой записи."
];
}
if (domain === "month_close_costs_20_44") {
return [
"Разделите цепочки закрытия на полные и неполные по шагам распределения и регламентной операции.",
"Проверьте, какие остатки после закрытия подтверждены, а какие нет."
];
}
}
return buildDefaultChecksByDomain(domain);
}
function buildChecksSectionLines(structure, context) {
const actionLines = dedupeNarrativeLines([
...structure.next_step_block.recommended_actions,
...structure.next_step_block.clarification_questions
]
.map((item) => sanitizeSupportLine(item))
.filter((item) => Boolean(item))
.filter((item) => !hasUserFacingLeakage(item))
.filter((item) => /[а-яё]/iu.test(item))
.filter((item) => item.length >= 18)
.filter((item) => !/\b(?:factual|source-of-record|reference)\b/i.test(item)), 4);
const broken = sanitizeUserText(structure.direct_answer) ?? "";
const domain = context?.focusDomain ?? inferNarrativeDomainFromText(broken);
const questionType = context?.questionType ?? "unknown";
const effectiveQuestionType = questionType === "unknown" ? "what_to_check_first" : questionType;
const fixedAssetMechanismSignal = hasFixedAssetAmortizationSignalInText(`${structure.direct_answer} ${structure.mechanism_block.mechanism_notes.join(" ")} ${structure.evidence_block.mechanism_notes.join(" ")}`);
const domainAndEvidenceCorpus = `${broken} ${structure.mechanism_block.mechanism_notes.join(" ")} ${structure.evidence_block.mechanism_notes.join(" ")}`;
const fixedAssetContextSignal = hasFixedAssetContextSignal(context);
const fixedAssetCase = fixedAssetContextSignal ||
(domain !== "settlements_60_62" && (hasFixedAssetSignalInStructure(structure, context) || fixedAssetMechanismSignal));
const rbpCase = hasRbpContextSignal(context) || hasRbpSignalInText(domainAndEvidenceCorpus);
const domainFallback = fixedAssetCase
? buildFixedAssetChecksByQuestionType(effectiveQuestionType)
: rbpCase
? buildRbpChecksByQuestionType(effectiveQuestionType)
: buildQuestionTypeDomainChecks(questionType, domain);
const hasMissingPeriod = structure.uncertainty_block.open_uncertainties.some((item) => /missing_anchor:period/i.test(String(item ?? "")));
const lines = [];
if (questionType === "what_to_check_first") {
lines.push(...domainFallback.slice(0, 3));
if (lines.length < 3) {
lines.push(...actionLines.slice(0, 3 - lines.length));
}
}
else if (questionType === "what_is_it_grounded_on") {
lines.push(...domainFallback.slice(0, 2));
lines.push(...actionLines.slice(0, 1));
}
else if (questionType === "prove_or_guess" || questionType === "where_break_is") {
lines.push(...domainFallback.slice(0, 2));
lines.push(...actionLines.slice(0, 2));
}
else {
if (domain === "settlements_60_62") {
lines.push(...domainFallback.slice(0, 2));
lines.push(...actionLines.slice(0, 2));
}
else {
lines.push(...domainFallback.slice(0, 1));
lines.push(...actionLines.slice(0, 2));
if (lines.length < 2) {
lines.push(...domainFallback.slice(1, 2));
}
}
}
const filteredLines = fixedAssetCase || rbpCase
? lines.filter((item) => !/проверьте связку документов и проводок по проблемному участку/i.test(item))
: lines;
if (hasMissingPeriod) {
if (questionType === "what_to_check_first") {
filteredLines.push("Уточните период, если он не зафиксирован в исходной формулировке вопроса.");
}
else if (domain === "settlements_60_62" && filteredLines.length > 0) {
filteredLines.push("Уточните период проверки, чтобы подтвердить проблему без лишнего шума.");
}
else {
filteredLines.unshift("Уточните период проверки, чтобы подтвердить проблему без лишнего шума.");
}
}
return dedupeNarrativeLines(filteredLines, questionType === "what_to_check_first" ? 3 : 5);
}
function humanizeLimitationToken(value) {
const raw = String(value ?? "").trim();
if (!raw) {
return null;
}
if (hasUserFacingLeakage(raw)) {
return null;
}
const normalized = normalizeTechnicalToken(raw);
if (normalized === "missing_anchor:period")
return "Период проверки не указан.";
if (normalized === "missing_anchor:account")
return "Счет или группа счетов не указаны.";
if (normalized === "missing_anchor:document_or_object")
return "Не указан документ или объект для трассировки.";
if (normalized === "missing_anchor:counterparty")
return "Не указан контрагент или договор.";
if (normalized === "primary_domain_evidence_not_confirmed")
return "Целевой механизм активного домена подтвержден частично; вывод ограничен.";
if (normalized === "settlement_primary_evidence_not_confirmed")
return "Опора по расчетному контуру не подтверждена: в приоритете были сигналы из смежных доменов.";
if (normalized.includes("snapshot"))
return "Оценка сделана на snapshot-срезе и может не включать часть цепочки.";
if (normalized.includes("heuristic"))
return "Часть вывода основана на эвристике.";
if (normalized.includes("weak_source_mapping"))
return "Связка между источниками подтверждена частично.";
if (normalized.includes("missing_mechanism"))
return "Механизм проблемы подтвержден не полностью.";
if (normalized.includes("insufficient_detail"))
return "Недостаточно деталей для сильного вывода.";
if (normalized.includes("duplicate_collapse"))
return "Часть дублирующихся сигналов объединена в одну проблему.";
if (/problem units remain weak-confidence/i.test(raw))
return "Надежность problem-сигнала низкая, поэтому вывод ограничен.";
if (/source mapping is weak/i.test(raw))
return "Связка между источниками подтверждена частично.";
if (/mechanism is unresolved/i.test(raw))
return "Механизм проблемы подтвержден не полностью.";
if (/minimum evidence gate failed/i.test(raw))
return "Минимальная опора не набрана для уверенного вывода.";
if (/coverage is partial/i.test(raw))
return "Покрытие требований частичное.";
if (/broad query support is limited/i.test(raw))
return "Запрос широкий, поэтому вывод ограничен частичной опорой.";
if (/broad ranking output was tightened/i.test(raw))
return "Часть ранжирования ограничена, чтобы избежать ложной точности.";
if (/weak mechanism evidence/i.test(raw))
return "Доказательность механизма слабая, нужен ручной контроль.";
if (/evidence is snapshot-only/i.test(raw))
return "Оценка сделана на snapshot-срезе и может не включать самые свежие изменения.";
if (/source-of-record/i.test(raw))
return "Часть цепочки нужно подтвердить в исходной учетной базе.";
if (/[a-z]/i.test(raw) && !/[а-яё]/iu.test(raw))
return null;
const cleaned = sanitizeUserText(raw);
if (!cleaned) {
return null;
}
if (hasUserFacingLeakage(cleaned)) {
return null;
}
const core = cleaned.replace(/[.,;:!?]+$/g, "");
if (TECHNICAL_TOKEN_PATTERN.test(core) && /_/.test(core)) {
return null;
}
return ensureSentence(cleaned);
}
function buildLimitationsSectionLines(structure) {
const limitationLines = dedupeNarrativeLines([
...structure.mechanism_block.limitation_reason_codes.map((code) => limitationReasonToUserText(code)),
...structure.uncertainty_block.limitations.map((item) => humanizeLimitationToken(item)),
...structure.uncertainty_block.open_uncertainties.map((item) => humanizeLimitationToken(item))
].filter((item) => Boolean(item)), 5);
if (limitationLines.length > 0) {
return limitationLines;
}
if (isIncompleteEvidence(structure)) {
return ["Вывод ограничен: часть цепочки пока не подтверждена в текущем срезе."];
}
return ["Существенных ограничений в текущем срезе не выявлено."];
}
function domainNameForQuestionType(domain) {
if (domain === "settlements_60_62")
return "\u0440\u0430\u0441\u0447\u0435\u0442\u043d\u043e\u0433\u043e \u043a\u043e\u043d\u0442\u0443\u0440\u0430";
if (domain === "vat_document_register_book")
return "\u0446\u0435\u043f\u043e\u0447\u043a\u0438 \u041d\u0414\u0421";
if (domain === "month_close_costs_20_44")
return "\u043a\u043e\u043d\u0442\u0443\u0440\u0430 \u0437\u0430\u043a\u0440\u044b\u0442\u0438\u044f \u043c\u0435\u0441\u044f\u0446\u0430";
return "\u0432\u044b\u0431\u0440\u0430\u043d\u043d\u043e\u0433\u043e \u0443\u0447\u0430\u0441\u0442\u043a\u0430";
}
function buildQuestionTypeShortLine(context) {
const domainName = domainNameForQuestionType(context.focusDomain);
if (context.questionType === "where_break_is") {
return `\u041b\u043e\u043a\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d \u043d\u0430\u0438\u0431\u043e\u043b\u0435\u0435 \u0432\u0435\u0440\u043e\u044f\u0442\u043d\u044b\u0439 \u0443\u0437\u0435\u043b \u0440\u0430\u0437\u0440\u044b\u0432\u0430 \u0432\u043d\u0443\u0442\u0440\u0438 ${domainName}.`;
}
if (context.questionType === "prove_or_guess") {
return "\u0412\u044b\u0432\u043e\u0434 \u0440\u0430\u0437\u0434\u0435\u043b\u0435\u043d \u043d\u0430 \u0434\u043e\u043a\u0430\u0437\u0430\u043d\u043d\u0443\u044e \u0447\u0430\u0441\u0442\u044c \u0438 \u0433\u0438\u043f\u043e\u0442\u0435\u0437\u0443.";
}
if (context.questionType === "what_is_it_grounded_on") {
if (hasRbpContextSignal(context)) {
return "Ниже перечислены основания вывода по РБП: списание, остаток и подтверждение на конец периода.";
}
if (hasFixedAssetAnchorContext(context)) {
return "Ниже перечислены основания вывода по ОС/амортизации по данным учета.";
}
if (context.focusDomain === "vat_document_register_book") {
return "Ниже перечислены основания вывода по НДС-цепочке по данным учета.";
}
return "\u041d\u0438\u0436\u0435 \u043f\u0435\u0440\u0435\u0447\u0438\u0441\u043b\u0435\u043d\u044b \u043e\u0441\u043d\u043e\u0432\u0430\u043d\u0438\u044f \u0432\u044b\u0432\u043e\u0434\u0430 \u043f\u043e \u0434\u0430\u043d\u043d\u044b\u043c \u0443\u0447\u0435\u0442\u0430.";
}
if (context.questionType === "which_chains_are_complete_vs_incomplete") {
return "\u0426\u0435\u043f\u043e\u0447\u043a\u0438 \u0440\u0430\u0437\u0434\u0435\u043b\u0435\u043d\u044b \u043d\u0430 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u043d\u044b\u0435, \u0447\u0430\u0441\u0442\u0438\u0447\u043d\u043e \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u043d\u044b\u0435 \u0438 \u043d\u0435\u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u043d\u044b\u0435.";
}
if (context.questionType === "what_to_check_first") {
return `\u041a\u043e\u0440\u043e\u0442\u043a\u0438\u0439 \u043c\u0430\u0440\u0448\u0440\u0443\u0442 \u043f\u0435\u0440\u0432\u044b\u0445 \u043f\u0440\u043e\u0432\u0435\u0440\u043e\u043a \u0432\u043d\u0443\u0442\u0440\u0438 ${domainName}.`;
}
if (context.questionType === "why_breaks") {
if (context.focusDomain === "settlements_60_62") {
return "Наиболее вероятная причина: переход от оплаты к закрытию расчета подтвержден не полностью.";
}
if (context.focusDomain === "vat_document_register_book") {
return "Наиболее вероятная причина: переход документа НДС к регистру и книге подтвержден частично.";
}
if (context.focusDomain === "month_close_costs_20_44") {
return "Наиболее вероятная причина: цепочка распределения затрат и закрытия месяца подтверждена не полностью.";
}
if (hasFixedAssetAnchorContext(context)) {
return "Наиболее вероятная причина: по ОС часть переходов от параметров амортизации к начислению подтверждена не полностью.";
}
return "Наиболее вероятный механизм проблемы подтвержден частично и требует первичной проверки.";
}
if (context.questionType === "unknown" && hasFixedAssetAnchorContext(context)) {
return "Риск неполного начисления амортизации подтвержден частично и требует проверки по объектам ОС.";
}
return null;
}
function buildQuestionTypeBrokenLine(context) {
if (context.questionType !== "where_break_is") {
return null;
}
if (context.focusDomain === "settlements_60_62") {
return "\u0412\u0435\u0440\u043e\u044f\u0442\u043d\u044b\u0439 \u0443\u0437\u0435\u043b \u0440\u0430\u0437\u0440\u044b\u0432\u0430: \u043f\u0440\u0438\u0432\u044f\u0437\u043a\u0430 \u043e\u043f\u043b\u0430\u0442\u044b \u043a \u043e\u0431\u044a\u0435\u043a\u0442\u0443 \u0440\u0430\u0441\u0447\u0435\u0442\u043e\u0432 \u0438 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0443 \u0437\u0430\u043a\u0440\u044b\u0442\u0438\u044f.";
}
if (context.focusDomain === "vat_document_register_book") {
return "\u0412\u0435\u0440\u043e\u044f\u0442\u043d\u044b\u0439 \u0443\u0437\u0435\u043b \u0440\u0430\u0437\u0440\u044b\u0432\u0430: \u0441\u0432\u044f\u0437\u043a\u0430 \u0438\u0441\u0445\u043e\u0434\u043d\u043e\u0433\u043e \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430, \u0441\u0447\u0435\u0442\u0430-\u0444\u0430\u043a\u0442\u0443\u0440\u044b \u0438 \u0437\u0430\u043f\u0438\u0441\u0438 \u043a\u043d\u0438\u0433\u0438.";
}
if (context.focusDomain === "month_close_costs_20_44") {
return "\u0412\u0435\u0440\u043e\u044f\u0442\u043d\u044b\u0439 \u0443\u0437\u0435\u043b \u0440\u0430\u0437\u0440\u044b\u0432\u0430: \u043f\u0435\u0440\u0435\u0445\u043e\u0434 \u043e\u0442 \u043d\u0430\u043a\u043e\u043f\u043b\u0435\u043d\u0438\u044f \u0437\u0430\u0442\u0440\u0430\u0442 \u043a \u0440\u0430\u0441\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u044e/\u0437\u0430\u043a\u0440\u044b\u0442\u0438\u044e.";
}
return "\u0412\u0435\u0440\u043e\u044f\u0442\u043d\u044b\u0439 \u0443\u0437\u0435\u043b \u0440\u0430\u0437\u0440\u044b\u0432\u0430 \u043b\u043e\u043a\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d \u0447\u0430\u0441\u0442\u0438\u0447\u043d\u043e; \u043d\u0443\u0436\u043d\u0430 \u0442\u043e\u0447\u0435\u0447\u043d\u0430\u044f \u0441\u0432\u0435\u0440\u043a\u0430.";
}
function buildQuestionTypeWhyLine(context) {
if (context.questionType === "where_break_is") {
return "\u0424\u043e\u043a\u0443\u0441 \u043e\u0442\u0432\u0435\u0442\u0430: \u043d\u0435 \u043e\u0431\u0449\u0438\u0439 \u043c\u0435\u0445\u0430\u043d\u0438\u0437\u043c, \u0430 \u0442\u043e\u0447\u043a\u0430 \u0440\u0430\u0437\u0440\u044b\u0432\u0430 \u0432 \u0446\u0435\u043f\u043e\u0447\u043a\u0435.";
}
if (context.questionType === "prove_or_guess") {
return "\u041e\u0442\u0434\u0435\u043b\u044c\u043d\u043e \u043f\u043e\u043a\u0430\u0437\u0430\u043d\u043e, \u0447\u0442\u043e \u0443\u0436\u0435 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u043e, \u0430 \u0447\u0442\u043e \u043f\u043e\u043a\u0430 \u043e\u0441\u0442\u0430\u0435\u0442\u0441\u044f \u0433\u0438\u043f\u043e\u0442\u0435\u0437\u043e\u0439.";
}
if (context.questionType === "which_chains_are_complete_vs_incomplete") {
return "\u0426\u0435\u043f\u043e\u0447\u043a\u0438 \u0440\u0430\u0437\u0434\u0435\u043b\u0435\u043d\u044b \u043d\u0430 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043d\u044b\u0435 \u0438 \u043d\u0435\u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043d\u044b\u0435 \u043f\u043e \u0442\u0435\u043a\u0443\u0449\u0435\u0439 \u043e\u043f\u043e\u0440\u0435.";
}
if (context.questionType === "what_is_it_grounded_on") {
if (hasRbpContextSignal(context)) {
return "Фокус ответа по РБП: подтверждение списания и остатка на конец периода, а не общий close-narrative.";
}
if (hasFixedAssetAnchorContext(context)) {
return "Фокус ответа по ОС: подтверждение попадания объектов в начисление амортизации.";
}
if (context.focusDomain === "vat_document_register_book") {
return "Фокус ответа по НДС: подтверждение переходов между документом, счетом-фактурой, регистром и книгой.";
}
return "\u0424\u043e\u043a\u0443\u0441 \u043e\u0442\u0432\u0435\u0442\u0430 \u0441\u043c\u0435\u0449\u0435\u043d \u0432 \u0434\u043e\u043a\u0430\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0438, \u0430 \u043d\u0435 \u0432 \u043e\u0431\u0449\u0438\u0439 narrative.";
}
return null;
}
function buildQuestionTypeEvidenceLine(context) {
if (context.questionType === "what_is_it_grounded_on") {
if (hasRbpContextSignal(context)) {
return "Опора перечислена по РБП-объектам, документам списания и остаткам на конец периода.";
}
if (hasFixedAssetAnchorContext(context)) {
return "Опора перечислена по ОС-объектам, параметрам амортизации и движениям начисления.";
}
if (context.focusDomain === "vat_document_register_book") {
return "Опора перечислена по НДС-звеньям: документ, счет-фактура, регистр и книга.";
}
return "\u0412 \u044d\u0442\u043e\u043c \u043e\u0442\u0432\u0435\u0442\u0435 \u0432 \u043f\u0440\u0438\u043e\u0440\u0438\u0442\u0435\u0442\u0435 \u043f\u043e\u043a\u0430\u0437\u0430\u043d\u044b \u0438\u043c\u0435\u043d\u043d\u043e \u043e\u0441\u043d\u043e\u0432\u0430\u043d\u0438\u044f \u0432\u044b\u0432\u043e\u0434\u0430.";
}
if (context.questionType === "prove_or_guess") {
return "\u0421\u0438\u043b\u0430 \u0432\u044b\u0432\u043e\u0434\u0430 \u043e\u0446\u0435\u043d\u0435\u043d\u0430 \u043f\u043e \u043f\u0440\u044f\u043c\u043e\u0439 \u043e\u043f\u043e\u0440\u0435, \u0430 \u043d\u0435 \u043f\u043e \u0434\u043e\u0433\u0430\u0434\u043a\u0430\u043c.";
}
if (context.questionType === "which_chains_are_complete_vs_incomplete") {
if (context.focusDomain === "vat_document_register_book") {
return "Опора собрана по НДС-звеньям, чтобы разделить полные и неполные переходы.";
}
if (hasRbpContextSignal(context)) {
return "Опора собрана по РБП-цепочке, чтобы разделить подтвержденное и неподтвержденное списание.";
}
if (hasFixedAssetAnchorContext(context)) {
return "Опора собрана по ОС-цепочке, чтобы разделить подтвержденные и неподтвержденные начисления амортизации.";
}
return "\u041e\u043f\u043e\u0440\u0430 \u0441\u043e\u0431\u0440\u0430\u043d\u0430 \u0442\u0430\u043a, \u0447\u0442\u043e\u0431\u044b \u0447\u0435\u0441\u0442\u043d\u043e \u0440\u0430\u0437\u0434\u0435\u043b\u0438\u0442\u044c \u043f\u043e\u043b\u043d\u044b\u0435 \u0438 \u043d\u0435\u043f\u043e\u043b\u043d\u044b\u0435 \u0446\u0435\u043f\u043e\u0447\u043a\u0438.";
}
return null;
}
function formatAnchorList(anchors, prefix) {
if (anchors.length === 0) {
return null;
}
return `${prefix}: ${anchors.join(", ")}.`;
}
function buildQuestionTypeCheckLine(context) {
if (context.questionType === "what_to_check_first") {
return "\u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043f\u0443\u043d\u043a\u0442\u044b \u043f\u043e \u043f\u043e\u0440\u044f\u0434\u043a\u0443: \u0448\u0430\u0433 1 -> \u0448\u0430\u0433 2 -> \u0448\u0430\u0433 3.";
}
if (context.questionType === "where_break_is") {
return "\u041b\u043e\u043a\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044e \u0440\u0430\u0437\u0440\u044b\u0432\u0430 \u043d\u0430\u0447\u043d\u0438\u0442\u0435 \u0441 \u0443\u0437\u043b\u0430, \u0433\u0434\u0435 \u043f\u0435\u0440\u0435\u0445\u043e\u0434 \u043f\u0435\u0440\u0435\u0441\u0442\u0430\u043b \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0430\u0442\u044c\u0441\u044f.";
}
if (context.questionType === "prove_or_guess") {
return "\u041f\u0435\u0440\u0432\u044b\u043c \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435\u043c \u043e\u0442\u0434\u0435\u043b\u0438\u0442\u0435 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u043b\u044c\u043d\u043e \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u043d\u044b\u0435 \u0444\u0430\u043a\u0442\u044b \u043e\u0442 \u0433\u0438\u043f\u043e\u0442\u0435\u0437.";
}
if (context.questionType === "what_is_it_grounded_on") {
if (hasRbpContextSignal(context)) {
return "Сначала перечислите по РБП: объект, документ списания и остаток после списания на конец периода.";
}
if (hasFixedAssetAnchorContext(context)) {
return "Сначала перечислите по ОС: объект, параметры амортизации и подтверждение начисления за период.";
}
if (context.focusDomain === "vat_document_register_book") {
return "Сначала перечислите по НДС: документ, счет-фактуру, запись регистра и запись книги.";
}
return "\u0421\u043d\u0430\u0447\u0430\u043b\u0430 \u043f\u0435\u0440\u0435\u0447\u0438\u0441\u043b\u0438\u0442\u0435 \u043e\u043f\u043e\u0440\u043d\u044b\u0435 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u044b \u0438 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u044b, \u0437\u0430\u0442\u0435\u043c \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0430\u044e\u0449\u0438\u0435 \u043f\u0440\u043e\u0432\u043e\u0434\u043a\u0438.";
}
if (context.questionType === "which_chains_are_complete_vs_incomplete") {
if (context.focusDomain === "vat_document_register_book") {
return "Сначала разложите НДС-цепочку по шагам: документ -> счет-фактура -> регистр -> книга.";
}
if (hasRbpContextSignal(context)) {
return "Сначала разложите РБП-цепочку на подтвержденное списание, частичное и неподтвержденное.";
}
if (hasFixedAssetAnchorContext(context)) {
return "Сначала разложите ОС-цепочку на подтвержденное начисление, частичное и неподтвержденное.";
}
return "\u0421\u043d\u0430\u0447\u0430\u043b\u0430 \u0440\u0430\u0437\u043b\u043e\u0436\u0438\u0442\u0435 \u0446\u0435\u043f\u043e\u0447\u043a\u0438 \u043d\u0430 \u043f\u043e\u043b\u043d\u044b\u0435, \u0447\u0430\u0441\u0442\u0438\u0447\u043d\u043e \u043f\u043e\u043b\u043d\u044b\u0435 \u0438 \u043d\u0435\u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u043d\u044b\u0435.";
}
return null;
}
function buildQuestionTypeLimitationLine(context) {
if (context.questionType === "prove_or_guess") {
return "\u0414\u043b\u044f \u0444\u043e\u0440\u043c\u0430\u0442\u0430 \u00ab\u0434\u043e\u043a\u0430\u0437\u0430\u043d\u043e \u0438\u043b\u0438 \u0433\u0438\u043f\u043e\u0442\u0435\u0437\u0430\u00bb \u0432\u0441\u0435 \u043d\u0435\u0434\u043e\u043a\u0430\u0437\u0430\u043d\u043d\u044b\u0435 \u0447\u0430\u0441\u0442\u0438 \u043e\u0442\u0434\u0435\u043b\u0435\u043d\u044b \u0432 \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u044f.";
}
if (context.questionType === "which_chains_are_complete_vs_incomplete") {
return "\u0414\u0435\u043b\u0435\u043d\u0438\u0435 \u043d\u0430 \u00abcomplete/incomplete\u00bb \u0437\u0430\u0432\u0438\u0441\u0438\u0442 \u043e\u0442 \u043f\u043e\u043b\u043d\u043e\u0442\u044b \u0446\u0435\u043f\u043e\u0447\u043a\u0438 \u0432 \u0442\u0435\u043a\u0443\u0449\u0435\u043c \u0441\u0440\u0435\u0437\u0435.";
}
if (context.questionType === "where_break_is") {
return "\u0422\u043e\u0447\u043d\u0430\u044f \u043b\u043e\u043a\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f \u043c\u043e\u0436\u0435\u0442 \u0441\u043c\u0435\u0449\u0430\u0442\u044c\u0441\u044f, \u0435\u0441\u043b\u0438 \u0447\u0430\u0441\u0442\u044c \u043f\u0435\u0440\u0435\u0445\u043e\u0434\u043e\u0432 \u0432 \u0446\u0435\u043f\u043e\u0447\u043a\u0435 \u043d\u0435 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0430.";
}
if (context.questionType === "what_is_it_grounded_on") {
return "\u0412 \u043e\u0441\u043d\u043e\u0432\u0430\u043d\u0438\u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u044b \u0442\u043e\u043b\u044c\u043a\u043e \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u043d\u044b\u0435 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0438; \u043d\u0435\u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u043d\u044b\u0435 \u0432\u044b\u043d\u0435\u0441\u0435\u043d\u044b \u0432 \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u044f.";
}
if (context.questionType === "what_to_check_first") {
return "\u041c\u0430\u0440\u0448\u0440\u0443\u0442 \u043f\u0435\u0440\u0432\u0438\u0447\u043d\u044b\u0439 \u0438 \u043c\u043e\u0436\u0435\u0442 \u0443\u0442\u043e\u0447\u043d\u044f\u0442\u044c\u0441\u044f \u043f\u043e\u0441\u043b\u0435 \u043f\u0435\u0440\u0432\u043e\u0433\u043e \u0448\u0430\u0433\u0430.";
}
return null;
}
function applyQuestionTypeAndAnchorPolicy(input) {
const nextShort = buildQuestionTypeShortLine(input.context) ?? input.shortLine;
const nextBroken = dedupeNarrativeLines([buildQuestionTypeBrokenLine(input.context), ...input.brokenLines].filter((item) => Boolean(item)), 4);
const nextWhy = dedupeNarrativeLines([buildQuestionTypeWhyLine(input.context), ...input.whyLines].filter((item) => Boolean(item)), 4);
const anchorUsedLine = formatAnchorList(input.context.anchors.used, "\u0412 \u043e\u043f\u043e\u0440\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u044b \u044f\u043a\u043e\u0440\u044f \u0432\u043e\u043f\u0440\u043e\u0441\u0430");
const anchorUnusedLine = formatAnchorList(input.context.anchors.unused, "\u042f\u043a\u043e\u0440\u044f \u0438\u0437 \u0432\u043e\u043f\u0440\u043e\u0441\u0430 \u0431\u0435\u0437 \u043f\u0440\u044f\u043c\u043e\u0433\u043e \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u0438\u044f");
const nextEvidence = dedupeNarrativeLines([buildQuestionTypeEvidenceLine(input.context), ...input.evidenceLines, anchorUsedLine].filter((item) => Boolean(item)), input.context.questionType === "what_to_check_first" ? 4 : 7);
const nextChecks = dedupeNarrativeLines([buildQuestionTypeCheckLine(input.context), ...input.checkLines].filter((item) => Boolean(item)), input.context.questionType === "what_to_check_first" ? 3 : 5);
const nextLimitations = dedupeNarrativeLines([buildQuestionTypeLimitationLine(input.context), anchorUnusedLine, ...input.limitationLines].filter((item) => Boolean(item)), 6);
return {
shortLine: ensureSentence(nextShort),
brokenLines: nextBroken,
whyLines: nextWhy,
evidenceLines: nextEvidence,
checkLines: nextChecks,
limitationLines: nextLimitations
};
}
const RBP_WORDING_PATTERN = /(?:\bрбп\b|deferred[_\s-]?expense|сч(?:е|ё)т\s*97|объект\w*\s+рбп|списани[ея]\s+рбп|остат(?:ок|ки)\s+рбп|документ\s+списани[яе])/iu;
const FA_WORDING_PATTERN = /(?:\bос\b|основн(?:ые|ых)?\s+средств|амортиз|сч(?:е|ё)т\s*0[12]|01\/02|карточк\w*\s+ос|объект\w*\s+ос|ввод\w*\s+в\s+эксплуатац|fixed\s*asset|depreciat)/iu;
function hasRbpWordingPhrase(value) {
return RBP_WORDING_PATTERN.test(String(value ?? ""));
}
function hasFaWordingPhrase(value) {
return FA_WORDING_PATTERN.test(String(value ?? ""));
}
function resolveDomainWordingMode(structure, context) {
if (!context) {
return "neutral";
}
const userMessage = String(context.userMessage ?? "");
const explicitRbpFromMessage = hasRbpSignalInText(userMessage);
const explicitFaFromMessage = hasFixedAssetAmortizationSignalInText(userMessage);
if (explicitRbpFromMessage && !explicitFaFromMessage) {
return "rbp";
}
if (explicitFaFromMessage && !explicitRbpFromMessage) {
return "fa_amortization";
}
const anchorRbp = hasRbpAnchorContext(context);
const anchorFa = hasFixedAssetAnchorContext(context);
if (anchorRbp && !anchorFa) {
return "rbp";
}
if (anchorFa && !anchorRbp) {
return "fa_amortization";
}
const structureCorpus = [
structure.direct_answer,
...structure.mechanism_block.mechanism_notes,
...structure.evidence_block.mechanism_notes,
...(structure.evidence_block.source_refs ?? []),
...(context.anchors.present ?? []),
...(context.anchors.used ?? [])
]
.filter(Boolean)
.join(" ");
const structureRbp = hasRbpSignalInText(structureCorpus);
const structureFa = hasFixedAssetAmortizationSignalInText(structureCorpus);
const rbpScore = [explicitRbpFromMessage, anchorRbp, structureRbp].filter(Boolean).length;
const faScore = [explicitFaFromMessage, anchorFa, structureFa].filter(Boolean).length;
if (rbpScore > faScore) {
return "rbp";
}
if (faScore > rbpScore) {
return "fa_amortization";
}
return "neutral";
}
function enforceDomainWordingIsolation(payload, structure, context) {
const mode = resolveDomainWordingMode(structure, context);
if (mode === "neutral" || !context) {
return payload;
}
const effectiveQuestionType = context.questionType === "unknown" ? "what_to_check_first" : context.questionType;
const isForbidden = mode === "rbp" ? hasFaWordingPhrase : hasRbpWordingPhrase;
const filterLines = (lines) => lines.filter((line) => !isForbidden(line));
const shortFallback = mode === "rbp"
? "Признаки по РБП подтверждены частично и требуют проверки списания к концу периода."
: "Риск неполного начисления амортизации по объектам ОС подтвержден частично.";
const whyFallback = mode === "rbp"
? ["По РБП часть списаний к концу периода подтверждена не полностью, поэтому остаток может сохраняться дольше ожидаемого."]
: ["По ОС часть переходов к начислению амортизации подтверждена не полностью, поэтому есть риск пропуска отдельных объектов."];
const evidenceFallback = mode === "rbp"
? ["Основание собрано по РБП: объект списания, документ списания и остаток на конец периода."]
: ["Основание собрано по ОС: карточка объекта, параметры амортизации, начисление и движения по 01/02."];
const checkFallback = mode === "rbp"
? buildRbpChecksByQuestionType(effectiveQuestionType).slice(0, 2)
: buildFixedAssetChecksByQuestionType(effectiveQuestionType).slice(0, 2);
const filteredShort = isForbidden(payload.shortLine) ? shortFallback : payload.shortLine;
const filteredBroken = dedupeNarrativeLines(filterLines(payload.brokenLines), 4);
const filteredWhy = dedupeNarrativeLines([...filterLines(payload.whyLines), ...(filterLines(payload.whyLines).length === 0 ? whyFallback : [])], 4);
const filteredEvidence = dedupeNarrativeLines([...filterLines(payload.evidenceLines), ...(filterLines(payload.evidenceLines).length === 0 ? evidenceFallback : [])], 7);
const filteredChecks = dedupeNarrativeLines([...filterLines(payload.checkLines), ...(filterLines(payload.checkLines).length === 0 ? checkFallback : [])], effectiveQuestionType === "what_to_check_first" ? 3 : 5);
const filteredLimitations = dedupeNarrativeLines(filterLines(payload.limitationLines), 6);
return {
shortLine: ensureSentence(filteredShort),
brokenLines: filteredBroken.length > 0 ? filteredBroken : payload.brokenLines,
whyLines: filteredWhy.length > 0 ? filteredWhy : whyFallback,
evidenceLines: filteredEvidence.length > 0 ? filteredEvidence : evidenceFallback,
checkLines: filteredChecks.length > 0 ? filteredChecks : checkFallback,
limitationLines: filteredLimitations.length > 0 ? filteredLimitations : payload.limitationLines
};
}
function renderPolicyReply(structure, context) {
const questionType = context?.questionType ?? "unknown";
const shortLine = ensureSentence(buildShortSectionLine(structure));
const brokenLines = buildBrokenSectionLines(structure);
const whyLines = buildWhySectionLines(structure, context);
const evidenceLines = buildEvidenceSectionLines(structure, questionType, context);
const checkLines = buildChecksSectionLines(structure, context);
const limitationLines = buildLimitationsSectionLines(structure);
const enrichedBase = context
? applyQuestionTypeAndAnchorPolicy({
shortLine,
brokenLines,
whyLines,
evidenceLines,
checkLines,
limitationLines,
context
})
: {
shortLine,
brokenLines,
whyLines,
evidenceLines,
checkLines,
limitationLines
};
const enriched = enforceDomainWordingIsolation(enrichedBase, structure, context);
return renderStage4ContractReply({
shortLine: enriched.shortLine,
checkedLines: enriched.evidenceLines,
foundLines: [...enriched.brokenLines, ...enriched.whyLines],
unresolvedLines: enriched.limitationLines,
nextStepLines: enriched.checkLines,
nextStepTitle: "Что проверить первым"
});
}
function shouldUseSoftPolicyReply(input) {
if (input.mode === "focused_grounded" || input.mode === "route_mismatch" || input.mode === "backend_error" || input.mode === "out_of_scope") {
return false;
}
if (input.mode === "clarification_required" || input.mode === "no_grounded" || input.mode === "empty") {
return true;
}
if (input.mode !== "broad_partial") {
return false;
}
const hasCoverageGaps = input.coverageReport.requirements_uncovered.length > 0 ||
input.coverageReport.requirements_partially_covered.length > 0 ||
input.coverageReport.clarification_needed_for.length > 0 ||
input.coverageReport.out_of_scope_requirements.length > 0;
const weakEvidenceSignals = input.policySignals.broad_query_detected ||
input.policySignals.broad_result_flag ||
input.policySignals.minimum_evidence_failed ||
input.aggregateEvidenceConfidence === "low" ||
input.hasCriticalEvidenceLimitation ||
input.limitationReasonCodes.includes("weak_source_mapping") ||
input.limitationReasonCodes.includes("insufficient_detail") ||
input.limitationReasonCodes.includes("missing_mechanism");
return hasCoverageGaps || weakEvidenceSignals;
}
function renderSoftPolicyReply(input) {
const questionType = input.context?.questionType ?? "unknown";
const shortLine = ensureSentence(buildShortSectionLine(input.structure));
const foundLines = dedupeNarrativeLines([...buildBrokenSectionLines(input.structure), ...buildWhySectionLines(input.structure, input.context)], 3);
const evidenceLines = dedupeNarrativeLines(buildEvidenceSectionLines(input.structure, questionType, input.context), 3);
const limitationLines = dedupeNarrativeLines(buildLimitationsSectionLines(input.structure), 3);
const checkLines = dedupeNarrativeLines(buildChecksSectionLines(input.structure, input.context), 3);
const clarificationLines = dedupeNarrativeLines(input.structure.next_step_block.clarification_questions ?? [], 2);
const actionLines = dedupeNarrativeLines([...checkLines, ...(input.structure.next_step_block.recommended_actions ?? []), ...clarificationLines], 3);
const modeLine = input.mode === "clarification_required"
? "Чтобы дать точный ответ, нужно уточнить несколько ориентиров."
: input.mode === "no_grounded" || input.mode === "empty"
? "Сейчас подтвержденной опоры недостаточно для прямого вывода."
: "Есть рабочие сигналы, но часть вывода пока ограничена.";
return renderStage4ContractReply({
shortLine,
checkedLines: evidenceLines.length > 0
? evidenceLines
: ["Подтвержденная опора собрана частично; для полного вывода нужна дополнительная проверка."],
foundLines: foundLines.length > 0 ? foundLines : ["Пока подтверждена только часть сигнала, без финальной фиксации причины."],
unresolvedLines: limitationLines.length > 0 ? [modeLine, ...limitationLines] : [modeLine, "Для полного вывода не хватает деталей по части требований."],
nextStepLines: actionLines.length > 0
? actionLines
: ["Уточните период, объект или контрагента, чтобы завершить проверку в следующем ходе."],
nextStepTitle: "Что могу сделать сейчас"
});
}
function composeAssistantAnswerV11(input) {
const fallbackType = fallbackFromSummary(input.routeSummary);
const questionType = input.questionTypeHint ?? "unknown";
const anchorUsage = evaluateCompanyAnchorUsage(input.companyAnchors, input.retrievalResults);
const okResults = input.retrievalResults.filter((item) => item.status === "ok");
const partialResults = input.retrievalResults.filter((item) => item.status === "partial");
const emptyResults = input.retrievalResults.filter((item) => item.status === "empty");
const errorResults = input.retrievalResults.filter((item) => item.status === "error");
const evidenceItems = flattenEvidence(input.retrievalResults);
const policySignals = aggregatePolicySignals(input.retrievalResults);
const limitationReasonCodes = collectLimitationReasonCodes(evidenceItems);
const sourceRefs = uniqueStrings(evidenceItems
.map((item) => item.source_ref?.canonical_ref)
.filter((item) => typeof item === "string" && item.trim().length > 0), 8);
const mechanismNotes = uniqueStrings(evidenceItems
.map((item) => item.mechanism_note)
.filter((item) => typeof item === "string" && item.trim().length > 0), 6);
const lifecycleAnswerEnabled = Boolean(input.enableLifecycleAnswerV1);
const problemUnits = flattenProblemUnits(input.retrievalResults);
const problemUnitSummary = selectProblemUnitSummary(input.retrievalResults);
const problemHeavyUnits = problemUnits.filter((item) => PROBLEM_HEAVY_TYPES.has(item.problem_unit_type));
const focusNarrativeDomain = inferP0FocusNarrativeDomain(input.userMessage, input.retrievalResults, problemHeavyUnits, input.focusDomainHint);
const focusDomainGrounding = evaluateP0DomainEvidenceGrounding(input.retrievalResults, focusNarrativeDomain);
const focusDomainGroundingBlocked = Boolean(focusNarrativeDomain && focusDomainGrounding.blocked);
const rankedProblemUnits = rankProblemUnitsForAnswer(problemHeavyUnits, lifecycleAnswerEnabled, focusNarrativeDomain);
const domainAlignedProblemUnits = focusNarrativeDomain === null
? rankedProblemUnits
: rankedProblemUnits.filter((unit) => isProblemUnitAlignedWithNarrativeDomain(unit, focusNarrativeDomain));
const domainLockMissBase = Boolean(focusNarrativeDomain &&
rankedProblemUnits.length > 0 &&
domainAlignedProblemUnits.length === 0);
const domainLockMiss = domainLockMissBase || focusDomainGroundingBlocked;
const selectedProblemUnits = (focusNarrativeDomain === null ? rankedProblemUnits : domainAlignedProblemUnits).slice(0, 4);
const claimEvidenceLinks = buildClaimEvidenceLinks(input.retrievalResults);
const aggregateEvidenceConfidence = aggregateConfidence(input.retrievalResults, evidenceItems);
const lowConfidenceSignals = evidenceItems.filter((item) => item.confidence === "low").length;
const lowConfidenceShare = evidenceItems.length > 0 ? lowConfidenceSignals / evidenceItems.length : 0;
const lowConfidenceConcentration = lowConfidenceShare >= 0.6;
const hasSupport = okResults.length > 0 ||
partialResults.length > 0 ||
evidenceItems.length > 0 ||
input.retrievalResults.some((item) => item.items.length > 0);
const hasCoverageGaps = input.coverageReport.requirements_uncovered.length > 0 ||
input.coverageReport.requirements_partially_covered.length > 0 ||
input.coverageReport.clarification_needed_for.length > 0 ||
input.coverageReport.out_of_scope_requirements.length > 0;
const hasCriticalEvidenceLimitation = limitationReasonCodes.includes("weak_source_mapping") ||
limitationReasonCodes.includes("insufficient_detail");
const hasNonLowRouteConfidence = input.retrievalResults.some((item) => item.status === "ok" && item.confidence !== "low");
const focusedStrong = okResults.length > 0 &&
input.groundingCheck.status === "grounded" &&
!hasCoverageGaps &&
!policySignals.broad_query_detected &&
!policySignals.broad_result_flag &&
!policySignals.minimum_evidence_failed &&
!hasCriticalEvidenceLimitation &&
(aggregateEvidenceConfidence !== "low" || hasNonLowRouteConfidence);
const decision = buildPolicyDecision({
fallbackType,
coverageReport: input.coverageReport,
groundingCheck: input.groundingCheck,
okResults,
partialResults,
emptyResults,
errorResults,
hasSupport,
focusedStrong,
policySignals
});
const guardedDecision = focusDomainGroundingBlocked &&
decision.mode !== "out_of_scope" &&
decision.mode !== "route_mismatch" &&
decision.mode !== "backend_error"
? {
mode: "clarification_required",
fallback_type: "clarification",
reply_type: "clarification_required"
}
: decision;
const missingAnchors = detectMissingAnchors(input.userMessage, input.retrievalResults, {
normalizationPeriodExplicit: Boolean(input.normalizationPeriodExplicit),
companyAnchors: input.companyAnchors ?? null
});
const useBoundaryFallbackReply = shouldUseBoundaryFallbackReply({
mode: guardedDecision.mode,
groundingCheck: input.groundingCheck,
coverageReport: input.coverageReport,
okResultsCount: okResults.length,
partialResultsCount: partialResults.length,
focusDomain: focusNarrativeDomain,
focusDomainGroundingBlocked,
aggregateEvidenceConfidence,
hasCriticalEvidenceLimitation
});
const hasProblemWeakSignal = policySignals.narrowing_strength !== "strong" ||
policySignals.minimum_evidence_failed ||
limitationReasonCodes.includes("missing_mechanism") ||
limitationReasonCodes.includes("weak_source_mapping") ||
limitationReasonCodes.includes("insufficient_detail") ||
aggregateEvidenceConfidence === "low" ||
domainLockMiss ||
lowConfidenceConcentration;
const hardBlockedMode = guardedDecision.mode === "out_of_scope" ||
guardedDecision.mode === "route_mismatch" ||
guardedDecision.mode === "backend_error";
const problemCentricModeEligible = guardedDecision.mode === "broad_partial" ||
guardedDecision.mode === "clarification_required" ||
(guardedDecision.mode === "focused_grounded" && hasProblemWeakSignal);
const shouldUseProblemCentricAnswer = Boolean(input.enableProblemCentricAnswerV1) &&
!useBoundaryFallbackReply &&
!hardBlockedMode &&
problemCentricModeEligible &&
(!focusedStrong || hasProblemWeakSignal) &&
(selectedProblemUnits.length > 0 || domainLockMiss);
if (shouldUseProblemCentricAnswer) {
const problemCentricStructure = buildProblemCentricAnswerStructure({
mode: guardedDecision.mode,
selectedUnits: selectedProblemUnits,
problemSummary: problemUnitSummary,
evidenceItems,
claimEvidenceLinks,
limitationReasonCodes,
groundingCheck: input.groundingCheck,
retrievalResults: input.retrievalResults,
missingAnchors,
coverageReport: input.coverageReport,
lifecycleAnswerEnabled,
focusDomain: focusNarrativeDomain,
domainLockMiss
});
const lifecycleModeActive = lifecycleAnswerEnabled && selectedProblemUnits.length > 0 && hasLifecycleResolution(selectedProblemUnits);
return {
assistant_reply: renderPolicyReply(problemCentricStructure, {
questionType,
focusDomain: focusNarrativeDomain,
anchors: anchorUsage,
userMessage: input.userMessage
}),
fallback_type: guardedDecision.fallback_type,
reply_type: guardedDecision.reply_type,
answer_structure_v11: problemCentricStructure,
problem_centric_answer_applied: true,
problem_units_used_count: selectedProblemUnits.length,
problem_answer_mode: lifecycleModeActive ? "stage3_lifecycle_aware_v1" : "stage2_problem_centric_v1",
problem_unit_ids_used: selectedProblemUnits.map((item) => item.problem_unit_id)
};
}
const clarificationQuestions = buildClarificationQuestions({
mode: guardedDecision.mode,
missingAnchors,
coverageReport: input.coverageReport,
policySignals
});
const recommendedActions = buildRecommendedActions({
mode: guardedDecision.mode,
coverageReport: input.coverageReport,
policySignals,
limitationReasonCodes,
sourceRefs
});
const limitations = uniqueStrings([
...limitationReasonCodes.map((code) => limitationReasonToText(code)),
...extractLimitations(input.retrievalResults),
...input.groundingCheck.reasons,
...(focusDomainGroundingBlocked
? ["Целевой механизм активного домена подтвержден частично; часть первичной опоры пришла из смежного контура."]
: []),
...(anchorUsage.unused.length > 0
? [
`Часть якорей запроса пока не подтверждена в опоре: ${anchorUsage.unused.slice(0, 5).join(", ")}.`
]
: []),
...(policySignals.minimum_evidence_failed ? ["Minimum evidence gate failed for current scope."] : []),
...(policySignals.broad_query_detected && policySignals.narrowing_strength === "weak"
? ["Broad query remains weakly narrowed; precision is intentionally limited."]
: [])
], 10);
const openUncertainties = uniqueStrings([
...input.groundingCheck.missing_requirements,
...(guardedDecision.mode === "clarification_required" && missingAnchors.period ? ["missing_anchor:period"] : []),
...(guardedDecision.mode === "clarification_required" && missingAnchors.account ? ["missing_anchor:account"] : []),
...(guardedDecision.mode === "clarification_required" && missingAnchors.documentOrObject ? ["missing_anchor:document_or_object"] : []),
...(guardedDecision.mode === "clarification_required" && missingAnchors.counterparty ? ["missing_anchor:counterparty"] : []),
...(focusDomainGroundingBlocked ? ["primary_domain_evidence_not_confirmed"] : [])
], 8);
const confidenceLimited = guardedDecision.mode !== "focused_grounded" ||
limitationReasonCodes.includes("missing_mechanism") ||
limitationReasonCodes.includes("heuristic_inference") ||
limitationReasonCodes.includes("weak_source_mapping") ||
limitationReasonCodes.includes("insufficient_detail") ||
aggregateEvidenceConfidence === "low" ||
focusDomainGroundingBlocked;
const mechanismStatus = mechanismNotes.length === 0
? "unresolved"
: confidenceLimited
? "limited"
: "grounded";
const answerStructure = {
schema_version: "answer_structure_v1_1",
answer_summary: buildAnswerSummary(guardedDecision.mode),
direct_answer: buildDirectAnswer({
mode: guardedDecision.mode,
retrievalResults: input.retrievalResults,
policySignals,
focusDomain: focusNarrativeDomain
}),
mechanism_block: {
status: mechanismStatus,
mechanism_notes: mechanismNotes,
limitation_reason_codes: limitationReasonCodes
},
evidence_block: {
evidence_ids: uniqueStrings(evidenceItems.map((item) => item.evidence_id), 10),
source_refs: sourceRefs,
mechanism_notes: mechanismNotes,
coverage_note: input.coverageReport.requirements_total > 0 &&
input.coverageReport.requirements_total === input.coverageReport.requirements_covered &&
input.coverageReport.requirements_uncovered.length === 0 &&
input.coverageReport.requirements_partially_covered.length === 0
? "coverage_full_or_near_full"
: "coverage_partial_or_limited",
...(claimEvidenceLinks.length > 0
? {
claim_evidence_links: claimEvidenceLinks
}
: {})
},
uncertainty_block: {
open_uncertainties: openUncertainties,
limitations
},
next_step_block: {
recommended_actions: recommendedActions,
clarification_questions: clarificationQuestions
}
};
const finalAssistantReply = useBoundaryFallbackReply
? buildBoundaryFallbackReply({
userMessage: input.userMessage,
focusDomain: focusNarrativeDomain,
missingAnchors,
coverageReport: input.coverageReport
})
: shouldUseSoftPolicyReply({
mode: guardedDecision.mode,
policySignals,
limitationReasonCodes,
aggregateEvidenceConfidence,
coverageReport: input.coverageReport,
hasCriticalEvidenceLimitation
})
? renderSoftPolicyReply({
structure: answerStructure,
context: {
questionType,
focusDomain: focusNarrativeDomain,
anchors: anchorUsage,
userMessage: input.userMessage
},
mode: guardedDecision.mode
})
: renderPolicyReply(answerStructure, {
questionType,
focusDomain: focusNarrativeDomain,
anchors: anchorUsage,
userMessage: input.userMessage
});
return {
assistant_reply: finalAssistantReply,
fallback_type: guardedDecision.fallback_type,
reply_type: guardedDecision.reply_type,
answer_structure_v11: answerStructure,
problem_centric_answer_applied: false,
problem_units_used_count: 0,
problem_answer_mode: "stage1_policy_v11"
};
}
function composeExplainableAnswer(input, scopeLabel) {
const graphInsight = collectGraphCausalInsight(input.retrievalResults);
const facts = extractTopFacts(input.retrievalResults);
const whyIncludedRaw = extractWhyIncluded(input.retrievalResults);
const selectionReasonsRaw = extractSelectionReasons(input.retrievalResults);
const whyIncluded = whyIncludedRaw.length > 0 ? whyIncludedRaw : buildFallbackWhyIncluded(input.retrievalResults);
const selectionReasons = selectionReasonsRaw.length > 0 ? selectionReasonsRaw : buildFallbackSelectionReasons(input.retrievalResults);
const riskFactors = extractRiskFactors(input.retrievalResults);
const interpretation = extractBusinessInterpretation(input.retrievalResults);
const limitations = uniqueStrings([...extractLimitations(input.retrievalResults), ...input.groundingCheck.reasons]);
const graphFirstChecks = buildGraphFirstChecks(graphInsight);
const nextSteps = uniqueStrings([...graphFirstChecks, ...suggestNextStep(input.requirements, input.coverageReport)], 8);
const graphCausalLines = buildGraphCausalLines(graphInsight);
const lead = buildGraphProblemFirstLead(graphInsight, scopeLabel);
const uncertaintyLines = uniqueStrings([
...limitations,
...(scopeLabel === "partial" ? ["Покрытие частичное: часть вывода требует дополнительной проверки."] : []),
graphInsight.active
? graphInsight.looks_systemic
? "Сигнал устойчивый: разрыв повторяется в связанном контуре."
: "Сигнал ограниченный: часть признаков может быть шумом."
: "Сигнал ограниченный: сильных graph/lifecycle-индикаторов в верхнем слое мало."
], 8);
return sanitizeUserFacingReply([
lead,
facts.length > 0 ? "Подтвержденные наблюдения:\n" + formatList(facts) : "",
graphCausalLines.length > 0 ? "Почему это похоже на проблему:\n" + formatList(graphCausalLines) : "",
whyIncluded.length > 0 ? "Что поддерживает вывод:\n" + formatList(whyIncluded) : "",
selectionReasons.length > 0 ? "Как отбирались сигналы:\n" + formatList(selectionReasons) : "",
riskFactors.length > 0 ? "Ключевые признаки проблемы:\n" + formatList(riskFactors) : "",
interpretation.length > 0 ? "Почему это важно для учета:\n" + formatList(interpretation) : "",
uncertaintyLines.length > 0 ? "Ограничения и уверенность:\n" + formatList(uncertaintyLines) : "",
nextSteps.length > 0 ? "Что проверить первым делом:\n" + formatList(nextSteps) : ""
]
.filter(Boolean)
.join("\n\n"));
}
function sanitizeAssistantReplyForUserFacing(value) {
return sanitizeUserFacingReply(value);
}
function composeAssistantAnswer(input) {
if (input.enableAnswerPolicyV11) {
return composeAssistantAnswerV11(input);
}
const fallbackType = fallbackFromSummary(input.routeSummary);
const okResults = input.retrievalResults.filter((item) => item.status === "ok");
const partialResults = input.retrievalResults.filter((item) => item.status === "partial");
const emptyResults = input.retrievalResults.filter((item) => item.status === "empty");
const errorResults = input.retrievalResults.filter((item) => item.status === "error");
const legacyEvidenceItems = flattenEvidence(input.retrievalResults);
const legacyLimitationReasonCodes = collectLimitationReasonCodes(legacyEvidenceItems);
const hasBroadMinimumEvidenceSignal = input.retrievalResults.some((item) => summaryBoolean(item, "broad_guard_applied") && summaryBoolean(item, "minimum_evidence_failed"));
const hasBroadClarificationSignal = input.retrievalResults.some((item) => summaryBoolean(item, "broad_guard_applied") &&
summaryBoolean(item, "minimum_evidence_failed") &&
summaryString(item, "degraded_to") === "clarification");
if (fallbackType === "out_of_scope" && input.coverageReport.requirements_covered === 0) {
return {
assistant_reply: "Я могу отвечать только по данным вашей учетной базы. Этот запрос выходит за рамки доступного контура.",
fallback_type: "out_of_scope",
reply_type: "out_of_scope"
};
}
if (input.groundingCheck.status === "route_mismatch_blocked") {
return {
assistant_reply: [
"Не отправляю финальный ответ, потому что предмет результата не совпал с предметом вопроса.",
"Уточните формулировку (например, нужный счет или участок учета), и я выполню повторный проход."
].join("\n\n"),
fallback_type: "partial",
reply_type: "route_mismatch_blocked"
};
}
if (input.groundingCheck.status === "no_grounded_answer" && okResults.length === 0 && !hasBroadMinimumEvidenceSignal) {
return {
assistant_reply: "Пока не удалось собрать предметно подтвержденный ответ по вашему вопросу. Нужны дополнительные уточнения по периоду или объекту проверки.",
fallback_type: fallbackType,
reply_type: "no_grounded_answer"
};
}
if (hasBroadClarificationSignal && okResults.length === 0 && partialResults.length === 0) {
return {
assistant_reply: "Запрос слишком широкий для надежного вывода по текущей опоре. Уточните период, участок учета или объект проверки, после чего я дам предметный результат.",
fallback_type: "clarification",
reply_type: "clarification_required"
};
}
if (fallbackType === "clarification" && okResults.length === 0 && partialResults.length === 0) {
return {
assistant_reply: "Уточните, пожалуйста, период, счет, документ или контрагента, чтобы корректно закрыть все части вопроса.",
fallback_type: "clarification",
reply_type: "clarification_required"
};
}
if (errorResults.length > 0 && okResults.length === 0 && partialResults.length === 0) {
return {
assistant_reply: "Не удалось получить данные из контура. Попробуйте повторить запрос или уточнить формулировку.",
fallback_type: fallbackType,
reply_type: "backend_error"
};
}
if (partialResults.length > 0 && okResults.length === 0) {
return {
assistant_reply: composeExplainableAnswer(input, "partial"),
fallback_type: "partial",
reply_type: "partial_coverage"
};
}
if (okResults.length === 0 && partialResults.length === 0 && emptyResults.length > 0) {
return {
assistant_reply: "По заданному условию в текущем срезе данных явных проблемных записей не найдено.",
fallback_type: fallbackType,
reply_type: "empty_but_valid"
};
}
const hasPartialCoverage = input.coverageReport.requirements_uncovered.length > 0 ||
input.coverageReport.requirements_partially_covered.length > 0 ||
input.coverageReport.clarification_needed_for.length > 0 ||
input.coverageReport.out_of_scope_requirements.length > 0 ||
input.groundingCheck.status === "partial" ||
errorResults.length > 0 ||
legacyLimitationReasonCodes.includes("weak_source_mapping") ||
legacyLimitationReasonCodes.includes("missing_mechanism");
if (okResults.length > 0 && hasPartialCoverage) {
return {
assistant_reply: composeExplainableAnswer(input, "partial"),
fallback_type: "partial",
reply_type: "partial_coverage"
};
}
if (okResults.length > 0) {
return {
assistant_reply: composeExplainableAnswer(input, "full"),
fallback_type: "none",
reply_type: "factual_with_explanation"
};
}
return {
assistant_reply: "По текущему запросу не удалось построить обоснованный ответ. Уточните формулировку и попробуйте снова.",
fallback_type: "unknown",
reply_type: "backend_error"
};
}