4129 lines
220 KiB
JavaScript
4129 lines
220 KiB
JavaScript
"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(/\s{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)\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_breakdown_json[\s\S]*?(?:```[\s\S]*?```|$)/gi, "")
|
||
.replace(/(?:^|\n)\s*#{0,6}\s*(?:debug_payload_json|technical_breakdown_json)\b[\s\S]*$/gi, "")
|
||
.replace(/```json[\s\S]*?```/gi, "");
|
||
const normalized = scrubRawTechnicalRefs(withoutDebugBlocks).replace(/[ \t]+\n/g, "\n");
|
||
const cleanedLines = normalized
|
||
.split(/\r?\n/g)
|
||
.map((line) => stripSyntheticPlaceholders(line))
|
||
.map((line) => stripMojibakeFragments(line))
|
||
.map((line) => line.trim())
|
||
.filter((line) => line.length > 0)
|
||
.filter((line) => !/^(?:-\s*)?(?:action|clarify|open|limit|note):\s*$/i.test(line))
|
||
.filter((line) => !hasUserFacingLeakage(line))
|
||
.filter((line) => !looksLikeMojibake(line));
|
||
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 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 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);
|
||
if (input.focusDomain === null) {
|
||
const heading = pickDeterministicBoundaryVariant(input.userMessage, [
|
||
"По этому запросу у меня нет надежного доменного покрытия, поэтому даю мягкий отказ вместо технического шаблона.",
|
||
"Сейчас у меня нет надежного доменного маршрута по этому запросу, поэтому даю мягкий отказ вместо шаблонной технички."
|
||
]);
|
||
return sanitizeUserFacingReply([
|
||
heading,
|
||
nearbyCapabilities.length > 0 ? `Что могу сделать рядом по смыслу:\n${formatList(nearbyCapabilities)}` : "",
|
||
quickActionLine ?? "",
|
||
"Переформулируй вопрос через один из вариантов выше, и я сразу перейду к проверке по данным 1С."
|
||
]
|
||
.filter(Boolean)
|
||
.join("\n\n"));
|
||
}
|
||
const clarificationHints = buildNaturalClarificationHints({
|
||
missingAnchors: input.missingAnchors,
|
||
coverageReport: input.coverageReport
|
||
});
|
||
const domainHeading = pickDeterministicBoundaryVariant(`${input.userMessage}|${input.focusDomain}`, [
|
||
`Сейчас не могу надежно ответить по сценарию ${formatNarrativeDomainLabel(input.focusDomain)}: не хватает опоры.`,
|
||
`По сценарию ${formatNarrativeDomainLabel(input.focusDomain)} пока не хватает подтвержденной опоры для надежного вывода.`
|
||
]);
|
||
return sanitizeUserFacingReply([
|
||
domainHeading,
|
||
clarificationHints.length > 0 ? `Чтобы сразу перейти к проверке, уточни:\n${formatList(clarificationHints)}` : "",
|
||
nearbyCapabilities.length > 0 ? `Если удобнее, могу начать с близкого сценария:\n${formatList(nearbyCapabilities.slice(0, 2))}` : "",
|
||
quickActionLine ?? ""
|
||
]
|
||
.filter(Boolean)
|
||
.join("\n\n"));
|
||
}
|
||
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 sanitizeUserFacingReply([
|
||
`Коротко: ${enriched.shortLine}`,
|
||
`Что сломано:\n${formatList(enriched.brokenLines)}`,
|
||
`Почему это похоже на проблему:\n${formatList(enriched.whyLines)}`,
|
||
`На чем это основано:\n${formatList(enriched.evidenceLines)}`,
|
||
`Что проверить первым:\n${formatList(enriched.checkLines)}`,
|
||
`Ограничения:\n${formatList(enriched.limitationLines)}`
|
||
]
|
||
.filter(Boolean)
|
||
.join("\n\n"));
|
||
}
|
||
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 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 sanitizeUserFacingReply([
|
||
`Коротко: ${shortLine}`,
|
||
modeLine,
|
||
evidenceLines.length > 0 ? `Что уже проверено: ${evidenceLines.join("; ")}` : "",
|
||
limitationLines.length > 0 ? `Что пока не доказано: ${limitationLines.join("; ")}` : "",
|
||
actionLines.length > 0 ? `Что могу сделать сейчас: ${actionLines.join("; ")}` : ""
|
||
]
|
||
.filter(Boolean)
|
||
.join("\n\n"));
|
||
}
|
||
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"
|
||
};
|
||
}
|