"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 (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" }; }