"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ASSISTANT_MCP_DISCOVERY_RESPONSE_CANDIDATE_SCHEMA_VERSION = void 0; exports.buildAssistantMcpDiscoveryResponseCandidate = buildAssistantMcpDiscoveryResponseCandidate; const counterpartyRoleHeuristics_1 = require("./counterpartyRoleHeuristics"); exports.ASSISTANT_MCP_DISCOVERY_RESPONSE_CANDIDATE_SCHEMA_VERSION = "assistant_mcp_discovery_response_candidate_v1"; function toRecordObject(value) { if (!value || typeof value !== "object" || Array.isArray(value)) { return null; } return value; } function toNonEmptyString(value) { if (value === null || value === undefined) { return null; } const text = String(value).trim(); return text.length > 0 ? text : null; } function normalizeQuestionText(value) { return String(value ?? "") .toLowerCase() .replace(/ё/g, "е") .replace(/\s+/g, " ") .trim(); } function requestsFinancialCounterpartyBoundary(turnMeaning, graph) { const text = normalizeQuestionText([ turnMeaning?.raw_message, turnMeaning?.effective_message, graph?.source_message, graph?.question ].join(" ")); return (/(?:банк|сбербанк|финанс|кредит|депозит)/iu.test(text) && /(?:клиент|поставщик|выручк|топ|обычн|роль|поток)/iu.test(text)); } function toStringList(value) { if (!Array.isArray(value)) { return []; } return value.map((item) => toNonEmptyString(item)).filter((item) => Boolean(item)); } function normalizeReasonCode(value) { const normalized = value .trim() .replace(/[^\p{L}\p{N}_.:-]+/gu, "_") .replace(/^_+|_+$/g, "") .toLowerCase(); return normalized.length > 0 ? normalized.slice(0, 120) : null; } function pushReason(target, value) { const normalized = normalizeReasonCode(value); if (normalized && !target.includes(normalized)) { target.push(normalized); } } function uniqueStrings(values) { const result = []; for (const value of values) { const text = String(value ?? "").trim(); if (text && !result.includes(text)) { result.push(text); } } return result; } function hasInternalMechanics(value) { const text = value.toLowerCase(); return (text.includes("mcp fetch failed") || text.includes("this operation was aborted") || text.includes("entity-resolution") || text.includes("could not continue") || text.includes("checked catalog search step") || text.includes("query_documents") || text.includes("query_movements") || text.includes("primitive") || text.includes("pilot_") || text.includes("runtime_") || text.includes("planner_") || text.includes("catalog_") || text.includes("select ") || text.includes("needs more scope before execution") || text.includes("mcp_execution_performed")); } function userFacingLines(values) { return uniqueStrings(values).filter((line) => !hasInternalMechanics(line)); } function sanitizeUserFacingMechanics(value) { let text = String(value ?? "").replace(/MCP-срез(?:ом|у|е|а)?/giu, (match) => { const normalized = match.toLowerCase(); if (normalized.endsWith("ом")) { return "срезом 1С"; } if (normalized.endsWith("у")) { return "срезу 1С"; } if (normalized.endsWith("е")) { return "срезе 1С"; } if (normalized.endsWith("а")) { return "среза 1С"; } return "срез 1С"; }); const replacements = [ [/\bprocurement-concentration route\b/giu, "проверка концентрации закупок/исходящих платежей"], [/\breviewed vendor-risk route\b/giu, "отдельная проверка поставщицкого риска"], [/\bvendor-risk route\b/giu, "проверка поставщицкого риска"], [/\bdue-date route\b/giu, "проверка просрочки по срокам оплаты"], [/\bdebt-quality proxy\b/giu, "ограниченный долговой сигнал"], [/\bstaleness-risk proxy\b/giu, "косвенный признак залежалости"], [/\bstaleness risk proxy\b/giu, "косвенный признак залежалости"], [/\boperating-flow proxy\b/giu, "денежный операционный показатель"], [/\btrading-margin proxy\b/giu, "товарная маржинальность по проверенным документам"], [/\bprocurement concentration proxy\b/giu, "сигнал концентрации закупок/исходящих платежей"], [/\boutgoing cash concentration proxy\b/giu, "сигнал концентрации исходящих денег"], [/\bproxy-сигналы\b/giu, "косвенные признаки"], [/\bproxy\b/giu, "косвенный показатель"], [/\bsales-to-stock\b/giu, "отношение продаж к остатку"], [/\boverdue\/due-date aging\b/giu, "просрочку по договорным срокам"], [/\bP&L\b/gu, "полный отчет о прибылях и убытках"] ]; for (const [pattern, replacement] of replacements) { text = text.replace(pattern, replacement); } return text; } function localizeLine(value) { const sanitizedValue = sanitizeUserFacingMechanics(value); if (/^1C activity rows were found for the requested counterparty scope$/i.test(value)) { return "В 1С найдены строки активности в запрошенном срезе."; } if (/^1C value-flow rows were found for the requested counterparty scope$/i.test(value)) { return "В 1С найдены строки входящих денежных поступлений в запрошенном срезе."; } if (/^1C supplier-payout rows were found for the requested counterparty scope$/i.test(value)) { return "В 1С найдены строки исходящих платежей и списаний в запрошенном срезе."; } const openScopeBidirectionalMatch = value.match(/^1C bidirectional value-flow rows were checked for the requested counterparty scope: incoming=(found|not_found), outgoing=(found|not_found)$/i); if (openScopeBidirectionalMatch) { const incoming = openScopeBidirectionalMatch[1] === "found" ? "входящие строки найдены" : "входящие строки не найдены"; const outgoing = openScopeBidirectionalMatch[2] === "found" ? "исходящие строки найдены" : "исходящие строки не найдены"; return `В 1С проверены входящие и исходящие денежные строки в запрошенном срезе: ${incoming}, ${outgoing}.`; } if (/^Requested period hit the MCP row limit, but the approved monthly recovery probe budget is smaller than the required subperiod count$/i.test(value)) { return "Запрошенный период достиг лимита строк; доступного бюджета помесячных дозапросов не хватило, чтобы покрыть все подпериоды."; } const counterpartyMatch = value.match(/^1C activity rows were found for counterparty\s+(.+)$/i); if (counterpartyMatch) { return `В 1С найдены строки активности по контрагенту ${counterpartyMatch[1]}.`; } if (/^1C activity rows were found for the requested counterparty scope$/i.test(value)) { return "В 1С найдены строки активности по запрошенному контрагентскому контуру."; } const valueFlowMatch = value.match(/^1C value-flow rows were found for counterparty\s+(.+)$/i); if (valueFlowMatch) { return `В 1С найдены строки входящих денежных поступлений по контрагенту ${valueFlowMatch[1]}.`; } if (/^1C value-flow rows were found for the requested counterparty scope$/i.test(value)) { return "В 1С найдены строки входящих денежных поступлений по запрошенному контрагентскому контуру."; } const documentRowsMatch = value.match(/^1C document rows were found for counterparty\s+(.+)$/i); if (documentRowsMatch) { return `В 1С найдены строки документов по контрагенту ${documentRowsMatch[1]}.`; } if (/^1C document rows were found for the requested scope$/i.test(value)) { return "В 1С найдены строки документов по запрошенному контуру."; } const movementRowsMatch = value.match(/^1C movement rows were found for counterparty\s+(.+)$/i); if (movementRowsMatch) { return `В 1С найдены строки движений по контрагенту ${movementRowsMatch[1]}.`; } if (/^1C movement rows were found for the requested scope$/i.test(value)) { return "В 1С найдены строки движений по запрошенному контуру."; } const supplierPayoutMatch = value.match(/^1C supplier-payout rows were found for counterparty\s+(.+)$/i); if (supplierPayoutMatch) { return `В 1С найдены строки исходящих платежей/списаний по контрагенту ${supplierPayoutMatch[1]}.`; } if (/^1C supplier-payout rows were found for the requested counterparty scope$/i.test(value)) { return "В 1С найдены строки исходящих платежей/списаний по запрошенному контрагентскому контуру."; } const bidirectionalMatch = value.match(/^1C bidirectional value-flow rows were checked for counterparty\s+(.+): incoming=(found|not_found), outgoing=(found|not_found)$/i); if (bidirectionalMatch) { const incoming = bidirectionalMatch[2] === "found" ? "входящие строки найдены" : "входящие строки не найдены"; const outgoing = bidirectionalMatch[3] === "found" ? "исходящие строки найдены" : "исходящие строки не найдены"; return `В 1С проверены входящие и исходящие денежные строки по контрагенту ${bidirectionalMatch[1]}: ${incoming}, ${outgoing}.`; } const bidirectionalScopeMatch = value.match(/^1C bidirectional value-flow rows were checked for the requested counterparty scope: incoming=(found|not_found), outgoing=(found|not_found)$/i); if (bidirectionalScopeMatch) { const incoming = bidirectionalScopeMatch[1] === "found" ? "входящие строки найдены" : "входящие строки не найдены"; const outgoing = bidirectionalScopeMatch[2] === "found" ? "исходящие строки найдены" : "исходящие строки не найдены"; return `В 1С проверены входящие и исходящие денежные строки по запрошенному контрагентскому контуру: ${incoming}, ${outgoing}.`; } if (/^Business activity duration may be inferred from first and latest confirmed 1C activity rows$/i.test(value)) { return "Длительность деловой активности можно оценивать только как вывод по первой и последней подтвержденной строке активности в 1С."; } if (/^Counterparty document evidence is limited to confirmed 1C document rows in the checked scope$/i.test(value)) { return "Срез документов ограничен только подтвержденными строками документов в проверенном окне."; } if (/^Counterparty movement evidence is limited to confirmed 1C movement rows in the checked scope$/i.test(value)) { return "Срез движений ограничен только подтвержденными строками движений в проверенном окне."; } if (/^Counterparty value-flow total was calculated from confirmed 1C movement rows$/i.test(value)) { return "Сумма входящих поступлений рассчитана только по подтвержденным строкам поступлений в 1С."; } if (/^Counterparty monthly value-flow breakdown was grouped by month over confirmed 1C movement rows$/i.test(value)) { return "Помесячная раскладка входящих поступлений построена только по подтвержденным строкам поступлений в 1С."; } if (/^Counterparty supplier-payout total was calculated from confirmed 1C outgoing payment rows$/i.test(value)) { return "Сумма исходящих платежей рассчитана только по подтвержденным строкам списаний в 1С."; } if (/^Counterparty net value-flow was calculated as incoming confirmed 1C rows minus outgoing confirmed 1C rows$/i.test(value)) { return "Нетто денежного потока рассчитано только как входящие подтвержденные строки 1С минус исходящие подтвержденные строки 1С."; } if (/^Counterparty monthly net value-flow breakdown was grouped by month over confirmed incoming and outgoing 1C rows$/i.test(value)) { return "Помесячная нетто-раскладка сгруппирована только по подтвержденным входящим и исходящим строкам 1С."; } const metadataSurfaceMatch = value.match(/^Confirmed 1C metadata surface(?: for scope "([^"]+)")?: (\d+) rows and (\d+) matching objects$/i); if (metadataSurfaceMatch) { const scopePart = metadataSurfaceMatch[1] ? ` по области "${metadataSurfaceMatch[1]}"` : ""; return `В 1С подтверждена metadata-поверхность${scopePart}: ${metadataSurfaceMatch[2]} строк metadata-ответа и ${metadataSurfaceMatch[3]} совпавших объекта(ов).`; } const metadataObjectSetsMatch = value.match(/^Available metadata object sets: (.+)$/i); if (metadataObjectSetsMatch) { return `Доступные типы metadata-объектов: ${metadataObjectSetsMatch[1]}.`; } const selectedMetadataEntitySetMatch = value.match(/^Selected metadata entity set: (.+)$/i); if (selectedMetadataEntitySetMatch) { return `Выбранное семейство metadata-объектов: ${selectedMetadataEntitySetMatch[1]}.`; } const selectedMetadataObjectsMatch = value.match(/^Selected metadata objects: (.+)$/i); if (selectedMetadataObjectsMatch) { return `Выбранные metadata-объекты для следующего шага: ${selectedMetadataObjectsMatch[1]}.`; } const metadataFieldsMatch = value.match(/^Available metadata fields\/sections: (.+)$/i); if (metadataFieldsMatch) { return `Доступные metadata-поля/секции: ${metadataFieldsMatch[1]}.`; } const metadataLaneInferenceMatch = value.match(/^A likely next checked lane may be inferred as (document_evidence|movement_evidence|catalog_drilldown) from the confirmed metadata surface$/i); if (metadataLaneInferenceMatch) { const routeLabel = metadataLaneInferenceMatch[1] === "document_evidence" ? "контур документов" : metadataLaneInferenceMatch[1] === "movement_evidence" ? "контур движений/регистров" : "контур справочников и связанных объектов"; return `Следующий проверяемый контур по этой metadata-поверхности можно ограниченно оценить как ${routeLabel}.`; } if (/^Detailed metadata fields were not returned by this MCP metadata probe$/i.test(value)) { return "Эта MCP-проверка metadata не вернула детальный список полей."; } const metadataAmbiguityMatch = value.match(/^Exact downstream metadata surface remains ambiguous across: (.+)$/i); if (metadataAmbiguityMatch) { return `Точная downstream metadata-поверхность пока неоднозначна между family: ${metadataAmbiguityMatch[1]}.`; } const noMatchingMetadataScopeMatch = value.match(/^No matching 1C metadata objects were confirmed for scope "([^"]+)"$/i); if (noMatchingMetadataScopeMatch) { return `В 1С не подтверждены metadata-объекты по области "${noMatchingMetadataScopeMatch[1]}".`; } if (/^No matching 1C metadata objects were confirmed by this MCP metadata probe$/i.test(value)) { return "В 1С эта MCP-проверка не подтвердила подходящих metadata-объектов."; } if (/^Legal registration date is not proven by this MCP discovery pilot$/i.test(value)) { return "Юридическая дата регистрации этим поиском не подтверждена."; } if (/^Complete requested-period coverage is not proven because the MCP discovery probe row limit was reached$/i.test(value)) { return "Полное покрытие запрошенного периода не подтверждено: проверка достигла лимита найденных строк."; } if (/^Complete requested-period coverage for bidirectional value-flow is not proven because at least one MCP discovery probe row limit was reached$/i.test(value)) { return "Полное покрытие запрошенного периода по двустороннему денежному потоку не подтверждено: хотя бы одна сторона проверки достигла лимита найденных строк."; } if (/^Full turnover outside the checked period is not proven by this MCP discovery pilot$/i.test(value)) { return "Полный объем входящих поступлений вне проверенного периода этим поиском не подтвержден."; } if (/^Full all-time turnover is not proven without an explicit checked period$/i.test(value)) { return "Полный объем входящих поступлений за все время без явно проверенного периода не подтвержден."; } if (/^Full document history outside the checked period is not proven by this MCP discovery pilot$/i.test(value)) { return "Полный исторический срез документов вне проверенного периода этим поиском не подтвержден."; } if (/^Full document history is not proven without an explicit checked period$/i.test(value)) { return "Полный срез документов без явно проверенного периода не подтвержден."; } if (/^Full movement history outside the checked period is not proven by this MCP discovery pilot$/i.test(value)) { return "Полный исторический срез движений вне проверенного периода этим поиском не подтвержден."; } if (/^Full movement history is not proven without an explicit checked period$/i.test(value)) { return "Полный срез движений без явно проверенного периода не подтвержден."; } if (/^Full supplier-payout amount outside the checked period is not proven by this MCP discovery pilot$/i.test(value)) { return "Полный объем исходящих платежей вне проверенного периода этим поиском не подтвержден."; } if (/^Full all-time supplier-payout amount is not proven without an explicit checked period$/i.test(value)) { return "Полный объем исходящих платежей за все время без явно проверенного периода не подтвержден."; } if (/^Full bidirectional value-flow outside the checked period is not proven by this MCP discovery pilot$/i.test(value)) { return "Полный двусторонний денежный поток вне проверенного периода этим поиском не подтвержден."; } if (/^Full all-time bidirectional value-flow is not proven without an explicit checked period$/i.test(value)) { return "Полный двусторонний денежный поток за все время без явно проверенного периода не подтвержден."; } if (/^Requested period coverage was recovered through monthly 1C value-flow probes after the broad probe hit the row limit$/i.test(value)) { return "Покрытие запрошенного периода восстановлено помесячными проверками 1С после того, как общая выборка достигла лимита строк."; } if (/^Requested period coverage for bidirectional value-flow was recovered through monthly 1C side probes after a broad probe hit the row limit$/i.test(value)) { return "Покрытие запрошенного периода по двустороннему денежному потоку восстановлено помесячными проверками 1С после того, как общая выборка достигла лимита строк хотя бы по одной стороне."; } if (/^Requested period coverage was recovered through monthly 1C value-flow probes$/i.test(value)) { return "Покрытие запрошенного периода восстановлено помесячными проверками 1С."; } if (/^Requested period coverage for counterparty ranking was recovered through monthly 1C probes$/i.test(value)) { return "Покрытие запрошенного периода для рейтинга контрагентов восстановлено помесячными проверками 1С."; } if (/^Requested period coverage for bidirectional value-flow was recovered through monthly 1C side probes$/i.test(value)) { return "Покрытие запрошенного периода по двустороннему денежному потоку восстановлено помесячными проверками 1С."; } if (/^Complete requested-period coverage is not proven by the available checked rows$/i.test(value)) { return "Полное покрытие запрошенного периода не подтверждено доступными проверенными строками."; } if (/^Complete requested-period ranking coverage is not proven by the available checked rows$/i.test(value)) { return "Полное покрытие рейтинга за запрошенный период не подтверждено доступными проверенными строками."; } if (/^Complete requested-period coverage for bidirectional value-flow is not proven by the available checked rows$/i.test(value)) { return "Полное покрытие запрошенного периода по двустороннему денежному потоку не подтверждено доступными проверенными строками."; } return sanitizedValue; } function section(title, lines) { const clean = userFacingLines(lines.map(localizeLine)); if (clean.length === 0) { return null; } return `${title}\n${clean.map((line) => `- ${line}`).join("\n")}`; } function readStringArray(value) { return Array.isArray(value) ? value.map((item) => toNonEmptyString(item)).filter((item) => Boolean(item)) : []; } function moneyText(value) { const text = toNonEmptyString(value); if (!text) { return null; } return text.replace(/\s*руб\.$/u, " руб.").replace(/\s+/gu, " "); } function sentenceAmount(value) { return value ? value.replace(/[.]+$/u, "") : null; } function businessOverviewPeriodText(overview) { const period = toNonEmptyString(overview.period_scope); return period ? `за ${period}` : "за все доступное проверенное окно"; } function strongestIncomingYear(overview) { const years = Array.isArray(overview.yearly_breakdown) ? overview.yearly_breakdown : []; const sorted = years .map((item) => toRecordObject(item)) .filter((item) => { if (!item) { return false; } return Number(item.incoming_total_amount) > 0; }) .sort((left, right) => { const amountDelta = Number(right.incoming_total_amount) - Number(left.incoming_total_amount); if (amountDelta !== 0) { return amountDelta; } return String(left.year_bucket ?? "").localeCompare(String(right.year_bucket ?? "")); }); return sorted[0] ?? null; } function strongestNetYear(overview) { const years = Array.isArray(overview.yearly_breakdown) ? overview.yearly_breakdown : []; const sorted = years .map((item) => toRecordObject(item)) .filter((item) => { if (!item) { return false; } return Number(item.net_amount) !== 0; }) .sort((left, right) => { const amountDelta = Number(right.net_amount) - Number(left.net_amount); if (amountDelta !== 0) { return amountDelta; } return String(left.year_bucket ?? "").localeCompare(String(right.year_bucket ?? "")); }); return sorted[0] ?? null; } function businessOverviewCoverageLimitLine(overview) { const incoming = toRecordObject(overview.incoming_customer_revenue); const outgoing = toRecordObject(overview.outgoing_supplier_payout); const limited = []; if (incoming?.coverage_limited_by_probe_limit === true) { limited.push("входящие"); } if (outgoing?.coverage_limited_by_probe_limit === true) { limited.push("исходящие"); } const continuation = "Если нужен полный сквозной ответ, безопасный следующий шаг — выбрать конкретный год или квартал для дозапроса: тогда широкий срез можно собрать частями без выдачи непроверенного итога."; return limited.length > 0 ? `Важно: по направлению ${limited.join(" и ")} проверка достигла лимита строк; это расширенный проверенный срез найденных строк, но не гарантия полного бухгалтерского оборота без отдельной полной выгрузки. ${continuation}` : null; } function joinBusinessReplyLines(lines) { const reply = userFacingLines(lines.map(localizeLine)).join("\n").trim(); return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null; } function businessOverviewYearRowsLine(overview) { const years = Array.isArray(overview.yearly_breakdown) ? overview.yearly_breakdown : []; const values = years .map((item) => toRecordObject(item)) .filter((item) => Boolean(item)) .slice(0, 6) .map((item) => { const year = toNonEmptyString(item.year_bucket); const incoming = moneyText(item.incoming_total_amount_human_ru); const net = moneyText(item.net_amount_human_ru); const direction = item.net_direction === "net_outgoing" ? "нетто в минус" : "нетто в плюс"; return year && incoming && net ? `${year}: входящие ${incoming}, ${direction} ${net}` : null; }) .filter((item) => Boolean(item)); const joined = values.join("; "); return values.length > 0 ? `По годам: ${sentenceAmount(joined) ?? joined}.` : null; } function firstOverviewAxisLabel(rows, amountKey = "total_amount_human_ru") { const first = toRecordObject(Array.isArray(rows) ? rows[0] : null); const label = toNonEmptyString(first?.axis_value); const amount = moneyText(first?.[amountKey]); return label && amount ? `${label} — ${sentenceAmount(amount) ?? amount}` : null; } function firstNonFinancialOverviewAxisLabel(rows, amountKey = "total_amount_human_ru") { if (!Array.isArray(rows)) { return null; } for (const row of rows) { const item = toRecordObject(row); const label = toNonEmptyString(item?.axis_value); if (!label || (0, counterpartyRoleHeuristics_1.isLikelyFinancialInstitutionCounterparty)(label)) { continue; } const amount = moneyText(item?.[amountKey]); if (amount) { return `${label} — ${sentenceAmount(amount) ?? amount}`; } } return null; } function overviewAxisLooksFinancial(row) { if (!row) { return false; } return (row.counterparty_role_hint === "bank_or_financial_institution" || (0, counterpartyRoleHeuristics_1.isLikelyFinancialInstitutionCounterparty)(row.axis_value)); } function financialFlowHintTextRuFromRecord(row) { const hint = toNonEmptyString(row?.financial_flow_hint); const rows = typeof row?.financial_flow_hint_rows === "number" && Number.isFinite(row.financial_flow_hint_rows) ? ` (${row.financial_flow_hint_rows} строк)` : ""; if (hint === "loan_or_credit") { return `По полям банковского документа доминирует кредитный/заемный признак${rows}; это не обычная поставка и не клиентская выручка без отдельной проверки назначения.`; } if (hint === "bank_fee_or_service") { return `По полям банковского документа доминирует признак банковской комиссии/услуг банка${rows}; это не обычный поставщик товаров/услуг без отдельной проверки договора.`; } if (hint === "tax_or_budget") { return `По полям банковского документа доминирует налоговый/бюджетный признак${rows}; это не поставщик и не клиентская выручка.`; } if (hint === "payroll_or_social") { return `По полям банковского документа доминирует зарплатный/социальный признак${rows}; это не поставщик и не клиентская выручка.`; } if (hint === "supplier_payment") { return `По полям банковского документа доминирует признак оплаты поставщику${rows}; если получатель по названию является банком, это все равно требует осторожной трактовки.`; } return null; } function businessOverviewTaxLine(overview) { const tax = toRecordObject(overview.tax_position); if (!tax) { return null; } const salesVat = moneyText(tax.sales_vat_amount_human_ru); const purchaseVat = moneyText(tax.purchase_vat_amount_human_ru); const netVat = moneyText(tax.net_vat_amount_human_ru); if (!salesVat && !purchaseVat && !netVat) { return null; } const direction = tax.net_vat_direction === "vat_to_pay" ? "НДС к уплате" : tax.net_vat_direction === "vat_to_recover_or_offset" ? "НДС к возмещению/зачету" : "чистая НДС-позиция"; return `НДС: продажи ${salesVat ?? "0 руб."}, покупки ${purchaseVat ?? "0 руб."}, ${direction} ${sentenceAmount(netVat) ?? netVat ?? "0 руб."}.`; } function businessOverviewDebtLine(overview) { const debt = toRecordObject(overview.debt_position); if (!debt) { return null; } const receivables = moneyText(toRecordObject(debt.receivables)?.total_amount_human_ru); const payables = moneyText(toRecordObject(debt.payables)?.total_amount_human_ru); const net = moneyText(debt.net_debt_position_amount_human_ru); if (!receivables && !payables && !net) { return null; } const direction = debt.net_debt_position_direction === "net_payable" ? "кредиторка больше дебиторки" : "дебиторка больше кредиторки"; return `Долги: дебиторка ${receivables ?? "0 руб."}, кредиторка ${payables ?? "0 руб."}, нетто ${sentenceAmount(net) ?? net ?? "0 руб."} (${direction}).`; } function businessOverviewInventoryLine(overview) { const inventory = toRecordObject(overview.inventory_position); if (!inventory) { return null; } const amount = moneyText(inventory.total_amount_human_ru); const rows = Number(inventory.rows_matched); const quantity = Number(inventory.total_quantity); if (!amount && !Number.isFinite(rows)) { return null; } const pieces = [ Number.isFinite(rows) ? `${rows} позиций` : null, amount ? `на ${sentenceAmount(amount) ?? amount}` : null, Number.isFinite(quantity) && quantity > 0 ? `количество ${quantity}` : null ].filter((item) => Boolean(item)); return pieces.length > 0 ? `Склад: ${pieces.join(", ")}.` : null; } function rowCountText(value) { const count = Number(value); return Number.isFinite(count) ? String(count) : null; } function sideRowsText(side) { const rowsWithAmount = rowCountText(side?.rows_with_amount); const rowsMatched = rowCountText(side?.rows_matched); if (rowsWithAmount && rowsMatched) { return `${rowsWithAmount} из ${rowsMatched}`; } return rowsWithAmount ?? rowsMatched; } function sideDateText(side) { const first = toNonEmptyString(side?.first_movement_date); const latest = toNonEmptyString(side?.latest_movement_date); if (first && latest) { return first === latest ? `дата ${first}` : `даты ${first}..${latest}`; } return first ? `первая дата ${first}` : latest ? `последняя дата ${latest}` : null; } function bidirectionalNetLabel(direction) { if (direction === "net_outgoing") { return "нетто в сторону контрагента"; } if (direction === "balanced") { return "нетто около нуля"; } return "нетто в нашу сторону"; } function buildCompactBidirectionalValueFlowReply(entryPoint, draft) { const turnInput = toRecordObject(entryPoint.turn_input); const turnMeaning = toRecordObject(turnInput?.turn_meaning_ref); const bridge = toRecordObject(entryPoint.bridge); const pilot = toRecordObject(bridge?.pilot); const flow = toRecordObject(pilot?.derived_bidirectional_value_flow); if (!flow) { return null; } const incoming = toRecordObject(flow.incoming_customer_revenue); const outgoing = toRecordObject(flow.outgoing_supplier_payout); const incomingAmount = moneyText(incoming?.total_amount_human_ru); const outgoingAmount = moneyText(outgoing?.total_amount_human_ru); const netAmount = moneyText(flow.net_amount_human_ru); if (!incomingAmount && !outgoingAmount && !netAmount) { return null; } const counterparty = toNonEmptyString(flow.counterparty); const organizationScope = toNonEmptyString(turnMeaning?.explicit_organization_scope); const subjectLead = counterparty ? `по контрагенту ${counterparty}` : organizationScope ? `по компании ${organizationScope}` : "по выбранному контуру"; const period = toNonEmptyString(flow.period_scope); const periodText = period ? ` за период ${period}` : " в проверенном окне"; const incomingRows = sideRowsText(incoming); const outgoingRows = sideRowsText(outgoing); const incomingDates = sideDateText(incoming); const outgoingDates = sideDateText(outgoing); const netLabel = bidirectionalNetLabel(flow.net_direction); const lines = [ `Коротко: ${subjectLead}${periodText} по найденным строкам 1С получили ${incomingAmount ?? "0 руб."}, заплатили ${outgoingAmount ?? "0 руб."}; расчетное ${netLabel}: ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}.` ]; const basis = []; if (incomingRows) { basis.push(`входящих строк с суммой ${incomingRows}${incomingDates ? ` (${incomingDates})` : ""}`); } if (outgoingRows) { basis.push(`исходящих строк с суммой ${outgoingRows}${outgoingDates ? ` (${outgoingDates})` : ""}`); } if (basis.length > 0) { lines.push(`Основа: ${basis.join("; ")}.`); } if (flow.coverage_limited_by_probe_limit === true) { lines.push("Важно: часть проверки достигла лимита строк, поэтому это проверенный срез найденных движений, а не гарантия полного периода."); } lines.push("Метод: нетто рассчитано как подтвержденные входящие строки 1С минус подтвержденные исходящие строки; это не полное бухгалтерское сальдо вне проверенного окна."); const fallbackNextStep = toNonEmptyString(draft.next_step_line); if (fallbackNextStep) { lines.push(`Следующий шаг: ${localizeLine(fallbackNextStep)}`); } return joinBusinessReplyLines(lines); } function compactComparable(value) { return String(value ?? "") .toLowerCase() .replace(/[«»"']/g, "") .replace(/\s+/g, " ") .trim(); } function businessOverviewSeparateSubjectLabel(graph, turnMeaning, organizationScope) { const candidates = uniqueStrings([ ...toStringList(turnMeaning?.business_overview_separate_entity_candidates), ...toStringList(graph?.subject_candidates), ...toStringList(turnMeaning?.explicit_entity_candidates) ]); const organizationComparable = compactComparable(organizationScope); for (const candidate of candidates) { const text = toNonEmptyString(candidate); if (!text) { continue; } const comparable = compactComparable(text); if (organizationComparable && comparable === organizationComparable) { continue; } return text; } return null; } function sameBusinessSubject(left, right) { const leftComparable = compactComparable(left); const rightComparable = compactComparable(right); return Boolean(leftComparable && rightComparable && leftComparable === rightComparable); } function previousDocumentSummaryLine(bundle, separateSubject) { if (!bundle || !sameBusinessSubject(toNonEmptyString(bundle.counterparty), separateSubject)) { return null; } const count = Number(bundle.document_count); if (!Number.isFinite(count) || count <= 0) { return null; } return `документы по цепочке: найдено ${count}`; } function buildPreviousCounterpartyValueFlowSummary(flow, separateSubject, documentBundle) { if (!flow || !separateSubject || !sameBusinessSubject(toNonEmptyString(flow.counterparty), separateSubject)) { return null; } const incoming = toRecordObject(flow.incoming_customer_revenue); const outgoing = toRecordObject(flow.outgoing_supplier_payout); const incomingAmount = moneyText(incoming?.total_amount_human_ru); const outgoingAmount = moneyText(outgoing?.total_amount_human_ru); const netAmount = moneyText(flow.net_amount_human_ru); if (!incomingAmount && !outgoingAmount && !netAmount) { return null; } const counterparty = toNonEmptyString(flow.counterparty) ?? separateSubject; const netLabel = bidirectionalNetLabel(flow.net_direction); const lead = `; отдельно по ${counterparty}: получили ${incomingAmount ?? "0 руб."}, заплатили ${outgoingAmount ?? "0 руб."}, ` + `${netLabel} ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}`; const basis = []; const incomingRows = sideRowsText(incoming); const outgoingRows = sideRowsText(outgoing); const incomingDates = sideDateText(incoming); const outgoingDates = sideDateText(outgoing); if (incomingRows) { basis.push(`входящие строки ${incomingRows}${incomingDates ? ` (${incomingDates})` : ""}`); } if (outgoingRows) { basis.push(`исходящие строки ${outgoingRows}${outgoingDates ? ` (${outgoingDates})` : ""}`); } const documents = previousDocumentSummaryLine(documentBundle, counterparty); if (documents) { basis.push(documents); } const basisText = basis.length > 0 ? ` Основа: ${basis.join("; ")}.` : ""; return { lead, line: `Отдельно по контрагенту ${counterparty}: подтверждено получили ${incomingAmount ?? "0 руб."}, ` + `заплатили ${outgoingAmount ?? "0 руб."}, расчетное ${netLabel} ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}.` + `${basisText} Это не перенос сумм компании на контрагента, а отдельный ранее подтвержденный контрагентский срез.` }; } function buildCompactBusinessOverviewReply(entryPoint, draft) { const turnInput = toRecordObject(entryPoint.turn_input); const turnMeaning = toRecordObject(turnInput?.turn_meaning_ref); const graph = toRecordObject(turnInput?.data_need_graph); const bridge = toRecordObject(entryPoint.bridge); const pilot = toRecordObject(bridge?.pilot); const overview = toRecordObject(pilot?.derived_business_overview); const isBusinessOverview = toNonEmptyString(graph?.business_fact_family) === "business_overview" || toNonEmptyString(pilot?.pilot_scope) === "business_overview_route_template_v1"; const rankingNeed = toNonEmptyString(graph?.ranking_need); if (!isBusinessOverview || !overview) { return null; } const incoming = toRecordObject(overview.incoming_customer_revenue); const outgoing = toRecordObject(overview.outgoing_supplier_payout); const incomingAmount = moneyText(incoming?.total_amount_human_ru); const outgoingAmount = moneyText(outgoing?.total_amount_human_ru); const netAmount = moneyText(overview.net_amount_human_ru); const netDirection = overview.net_direction === "net_outgoing" ? "операционное нетто в минус" : "расчетное операционное нетто"; const period = businessOverviewPeriodText(overview); const limitLine = businessOverviewCoverageLimitLine(overview); const organizationScope = toNonEmptyString(turnMeaning?.explicit_organization_scope); const separateSubject = businessOverviewSeparateSubjectLabel(graph, turnMeaning, organizationScope); const previousCounterpartySummary = buildPreviousCounterpartyValueFlowSummary(toRecordObject(turnMeaning?.previous_counterparty_value_flow_bundle), separateSubject, toRecordObject(turnMeaning?.previous_counterparty_document_bundle)); const organizationPrefix = organizationScope ? `по компании ${organizationScope} ` : ""; const separateSubjectLead = separateSubject ? previousCounterpartySummary?.lead ?? `; по контрагенту ${separateSubject} суммы компании не переношу, это отдельный контур без подтвержденного итога в этой строке` : ""; const topCustomer = toRecordObject(Array.isArray(overview.top_customers) ? overview.top_customers[0] : null); const customerName = toNonEmptyString(topCustomer?.axis_value); const customerAmount = moneyText(topCustomer?.total_amount_human_ru); const topCustomerLooksFinancial = overviewAxisLooksFinancial(topCustomer); const nonFinancialCustomer = firstNonFinancialOverviewAxisLabel(topCustomerLooksFinancial ? overview.top_customers : []); const topCustomerLead = customerName && customerAmount ? topCustomerLooksFinancial ? `; крупнейший входящий денежный источник: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount} (похоже на банк/финорганизацию, не называю это клиентской выручкой без назначения платежа)${nonFinancialCustomer ? `; крупнейший небанковский входящий контрагент: ${nonFinancialCustomer}` : ""}` : `; крупнейший источник входящих денег: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}` : ""; const topSupplierRecord = toRecordObject(Array.isArray(overview.top_suppliers) ? overview.top_suppliers[0] : null); const topSupplier = firstOverviewAxisLabel(overview.top_suppliers); const topSupplierLooksFinancial = overviewAxisLooksFinancial(topSupplierRecord); const nonFinancialSupplier = firstNonFinancialOverviewAxisLabel(topSupplierLooksFinancial ? overview.top_suppliers : []); const topSupplierLead = topSupplier ? topSupplierLooksFinancial ? `; крупнейший получатель исходящих денег: ${topSupplier} (похоже на банк/финорганизацию, не называю это обычным поставщиком без назначения платежа/договора)${nonFinancialSupplier ? `; крупнейший небанковский получатель исходящих денег: ${nonFinancialSupplier}` : ""}` : `; крупнейший получатель исходящих денег: ${topSupplier}` : ""; const roleBoundaryLead = topCustomer || topSupplier ? "; клиент/поставщик как бизнес-роли этим денежным срезом не подтверждены" : ""; const financialBoundaryRequested = requestsFinancialCounterpartyBoundary(turnMeaning, graph); const requestedFinancialBoundaryLine = financialBoundaryRequested ? topCustomerLooksFinancial || topSupplierLooksFinancial ? "Отдельно по банкам: если денежный топ ведет банк/финансовая организация, это нельзя автоматически читать как обычного клиента или поставщика; нужны назначение платежа, вид операции и договор. Поэтому такой поток не является доказанной клиентской выручкой, обычной поставкой или чистой прибылью без отдельной проверки." : "Отдельно по банкам: банк/финансовую организацию в денежных топах нельзя автоматически читать как обычного клиента или поставщика; нужны назначение платежа, вид операции и договор. Поэтому такой поток не является доказанной клиентской выручкой, обычной поставкой или чистой прибылью без отдельной проверки." : null; const graphReasonCodes = toStringList(graph?.reason_codes); const directMoneyAnswer = graphReasonCodes.includes("data_need_graph_business_overview_direct_money_answer"); const crossScopeExecutiveSummary = Boolean(separateSubject && previousCounterpartySummary); const lines = []; const actionFamily = toNonEmptyString(turnMeaning?.asked_action_family); const unsupportedFamily = toNonEmptyString(turnMeaning?.unsupported_but_understood_family); const profitMarginBoundary = actionFamily === "profit_margin_boundary" || unsupportedFamily === "profit_margin_boundary"; const debtDueDateBoundary = actionFamily === "debt_due_date_boundary" || unsupportedFamily === "debt_due_date_boundary"; const vendorRiskBoundary = actionFamily === "vendor_risk_procurement_boundary" || unsupportedFamily === "vendor_risk_procurement_boundary"; const inventoryReserveBoundary = actionFamily === "inventory_reserve_boundary" || unsupportedFamily === "inventory_reserve_liquidation_boundary"; if (profitMarginBoundary) { const accountingFinancialResult = toRecordObject(overview.accounting_financial_result); if (accountingFinancialResult) { const direction = toNonEmptyString(accountingFinancialResult.final_result_direction); const amount = moneyText(accountingFinancialResult.final_result_amount_human_ru); const periodScope = toNonEmptyString(accountingFinancialResult.period_scope) ?? period; const marginPct = typeof accountingFinancialResult.net_margin_to_revenue_pct === "number" && Number.isFinite(accountingFinancialResult.net_margin_to_revenue_pct) ? `${accountingFinancialResult.net_margin_to_revenue_pct}%` : null; const directionText = direction === "profit" ? "учетная прибыль" : direction === "loss" ? "учетный убыток" : "нулевой учетный финрезультат"; const amountText = amount ? direction === "loss" ? `минус ${amount}` : amount : "сумма не распознана"; lines.push(`Коротко: нет, денежное операционное нетто не стоит считать чистой прибылью. Отдельно по закрытию счетов 90/91/99 в 1С за ${periodScope} подтвержден ${directionText}: ${amountText}${marginPct ? `; маржа к подтвержденной выручке ${marginPct}` : "; маржа к подтвержденной выручке не рассчитана"}.`); lines.push("Это учетный финрезультат по найденным строкам закрытия периода в 1С, а не внешний аудит и не юридически подтвержденная отчетность."); return joinBusinessReplyLines(lines); } const headline = toNonEmptyString(draft.headline); const cleanHeadline = headline?.replace(/^Коротко:\s*/iu, "").trim(); lines.push(cleanHeadline ? `Коротко: ${localizeLine(cleanHeadline)}` : "Коротко: нельзя точно подтвердить чистую прибыль и маржу по текущему срезу 1С; есть только ограниченный операционный денежный/товарный сигнал, а не полный отчет о прибыли и не бухгалтерский финансовый результат."); const boundaryLines = userFacingLines([ ...toStringList(draft.confirmed_lines), ...toStringList(draft.inference_lines), ...toStringList(draft.unknown_lines) ]) .filter((line) => /(?:прибыл|марж|финанс|p\s*&\s*l|p&l|расход|себестоим|закрыт|profit|margin|financial)/iu.test(line)) .slice(0, 2); if (boundaryLines.length > 0) { lines.push(...boundaryLines.map(localizeLine)); } lines.push("Для точного отчета о прибыли нужны отдельная проверка себестоимости, расходов, закрытия периода и финрезультата; текущий ограниченный сигнал нельзя выдавать за подтвержденную чистую прибыль или маржу."); if (limitLine) { lines.push(limitLine); } return joinBusinessReplyLines(lines); } if (debtDueDateBoundary) { const dueDateAging = toRecordObject(overview.debt_due_date_aging); if (dueDateAging) { const status = toNonEmptyString(dueDateAging.evidence_status); const asOfDate = toNonEmptyString(dueDateAging.as_of_date) ?? "проверенную дату"; const overdueAmount = moneyText(dueDateAging.overdue_amount_human_ru); const grossAmount = moneyText(dueDateAging.gross_open_amount_human_ru); const rowsWithPaymentTerms = typeof dueDateAging.rows_with_payment_terms === "number" && Number.isFinite(dueDateAging.rows_with_payment_terms) ? dueDateAging.rows_with_payment_terms : null; const rowsWithAmount = typeof dueDateAging.rows_with_amount === "number" && Number.isFinite(dueDateAging.rows_with_amount) ? dueDateAging.rows_with_amount : null; const dueDateScopePrefix = organizationScope ? `по компании ${organizationScope} ` : ""; if (status === "confirmed_overdue") { lines.push(`Коротко: ${dueDateScopePrefix}на ${asOfDate} подтвержденная просрочка есть: ${overdueAmount ?? "сумма не распознана"} по ${dueDateAging.overdue_rows ?? "найденным"} строкам.`); lines.push("Основа ответа: открытые расчеты 60/62/76, договорный срок оплаты и дата расчетного документа; это проверка просрочки по срокам оплаты, а не просто возраст договора."); } else if (status === "no_payment_terms_configured") { lines.push(`Коротко: ${dueDateScopePrefix}на ${asOfDate} подтвержденной просрочки нет: открытые расчеты проверены${grossAmount ? ` на ${grossAmount}` : ""}, но в найденных договорах срок оплаты не установлен.`); lines.push(rowsWithAmount !== null ? `Проверено строк с суммой: ${rowsWithAmount}. Без установленного срока оплаты нельзя честно назвать эти остатки просрочкой.` : "Без установленного срока оплаты нельзя честно назвать эти остатки просрочкой."); } else if (status === "insufficient_due_date_basis") { lines.push(`Коротко: ${dueDateScopePrefix}на ${asOfDate} просрочка не подтверждена: по строкам с установленным сроком оплаты не хватило даты расчетного документа.`); if (rowsWithPaymentTerms !== null) { lines.push(`Строк с установленным сроком оплаты: ${rowsWithPaymentTerms}; нужен документ-основание с датой, чтобы посчитать договорный срок оплаты.`); } } else { lines.push(`Коротко: ${dueDateScopePrefix}на ${asOfDate} проверка просрочки по срокам оплаты выполнена, подтвержденной просрочки не найдено${rowsWithPaymentTerms !== null ? `; строк с установленным сроком оплаты ${rowsWithPaymentTerms}` : ""}.`); } return joinBusinessReplyLines(lines); } const headline = toNonEmptyString(draft.headline); const cleanHeadline = headline?.replace(/^Коротко:\s*/iu, "").trim(); lines.push(cleanHeadline ? `Коротко: ${localizeLine(cleanHeadline)}` : "Коротко: нельзя точно определить, какая дебиторка просрочена, по текущему срезу 1С; есть только ограниченный долговой сигнал, но нет проверки договорных сроков оплаты."); lines.push("Проверить нужно отдельно: договоры, сроки оплаты, погашение и закрытие задолженности; без этого нельзя доказать просрочку по договорным срокам."); return joinBusinessReplyLines(lines); } if (vendorRiskBoundary) { const vendorProcurementQuality = toRecordObject(overview.vendor_procurement_quality); if (vendorProcurementQuality) { const status = toNonEmptyString(vendorProcurementQuality.evidence_status); const totalOutgoing = moneyText(vendorProcurementQuality.total_outgoing_amount_human_ru); const topOutgoingRecord = toRecordObject(vendorProcurementQuality.top_outgoing_counterparty); const topOutgoingName = toNonEmptyString(topOutgoingRecord?.axis_value); const topOutgoingAmount = moneyText(topOutgoingRecord?.total_amount_human_ru); const topOutgoingShare = typeof vendorProcurementQuality.top_outgoing_share_pct === "number" && Number.isFinite(vendorProcurementQuality.top_outgoing_share_pct) ? `${vendorProcurementQuality.top_outgoing_share_pct}%` : null; const nonFinancialRecord = toRecordObject(vendorProcurementQuality.top_non_financial_supplier); const nonFinancialName = toNonEmptyString(nonFinancialRecord?.axis_value); const nonFinancialAmount = moneyText(nonFinancialRecord?.total_amount_human_ru); const nonFinancialShare = typeof vendorProcurementQuality.top_non_financial_supplier_share_pct === "number" && Number.isFinite(vendorProcurementQuality.top_non_financial_supplier_share_pct) ? `${vendorProcurementQuality.top_non_financial_supplier_share_pct}%` : null; const periodScope = toNonEmptyString(vendorProcurementQuality.period_scope) ?? period; const totalText = totalOutgoing ? `; всего исходящих платежей в проверенном срезе ${totalOutgoing}` : ""; if (status === "financial_institution_leads_outgoing_cash") { lines.push(`Коротко: проверка концентрации закупок/исходящих платежей за ${periodScope} не подтверждает зависимость от обычного поставщика: крупнейший получатель исходящих денег ${topOutgoingName ?? "не распознан"}${topOutgoingShare ? ` держит около ${topOutgoingShare}` : ""}${topOutgoingAmount ? ` (${topOutgoingAmount})` : ""}, но по названию это банк/финансовая организация${totalText}.`); const financialHintText = financialFlowHintTextRuFromRecord(topOutgoingRecord); if (financialHintText) { lines.push(financialHintText); } if (nonFinancialName) { lines.push(`Крупнейший небанковский получатель исходящих денег: ${nonFinancialName}${nonFinancialShare ? `, около ${nonFinancialShare}` : ""}${nonFinancialAmount ? ` (${nonFinancialAmount})` : ""}. Это уже сигнал закупочной/исходящей концентрации, но не аудит надежности поставщика.`); } } else if (status === "reviewed_procurement_concentration") { lines.push(`Коротко: точный риск зависимости от одного поставщика не подтвержден полностью; проверка концентрации закупок/исходящих платежей за ${periodScope} нашла крупнейшего получателя исходящего потока: ${topOutgoingName ?? nonFinancialName ?? "получатель не распознан"}${topOutgoingShare ? ` держит около ${topOutgoingShare}` : nonFinancialShare ? ` держит около ${nonFinancialShare}` : ""}${topOutgoingAmount ? ` (${topOutgoingAmount})` : nonFinancialAmount ? ` (${nonFinancialAmount})` : ""}${totalText}.`); } else { lines.push(`Коротко: проверка концентрации закупок/исходящих платежей за ${periodScope} выполнена, но надежной небанковской концентрации поставщика по найденным исходящим платежам не хватает${totalText}.`); } const contractText = typeof vendorProcurementQuality.used_contracts === "number" && Number.isFinite(vendorProcurementQuality.used_contracts) ? typeof vendorProcurementQuality.total_contracts === "number" && Number.isFinite(vendorProcurementQuality.total_contracts) ? ` Договорный профиль: используется ${vendorProcurementQuality.used_contracts}/${vendorProcurementQuality.total_contracts} договоров${typeof vendorProcurementQuality.used_contract_share_pct === "number" && Number.isFinite(vendorProcurementQuality.used_contract_share_pct) ? ` (${vendorProcurementQuality.used_contract_share_pct}%)` : ""}.` : ` Договорный профиль: используется ${vendorProcurementQuality.used_contracts} договоров.` : ""; lines.push(`Что не доказано этим срезом: надежность поставщика, качество поставок, договорные условия, назначение каждого платежа и полная структура всех расходов.${contractText}`); return joinBusinessReplyLines(lines); } const supplierBasis = topSupplier ? topSupplierLooksFinancial ? `крупнейший получатель исходящих денег: ${topSupplier}; по названию это банк/финансовая организация, поэтому это не доказанная зависимость от обычного поставщика${nonFinancialSupplier ? `; крупнейший небанковский получатель исходящих денег: ${nonFinancialSupplier}` : ""}` : `крупнейший подтвержденный поставщик/получатель исходящих платежей: ${topSupplier}` : outgoingAmount ? `исходящие платежи/закупочный поток в проверенном срезе: ${outgoingAmount}` : "есть только ограниченный срез исходящих платежей без полного профиля поставщицкого риска"; const proxyLabel = topSupplierLooksFinancial ? "сигнал концентрации исходящих денег" : "сигнал концентрации закупок/исходящих платежей"; lines.push(`Коротко: точный риск зависимости от одного поставщика по текущим данным не подтвержден; есть только ${proxyLabel}: ${supplierBasis}.`); lines.push("Это сигнал концентрации закупок/исходящих платежей, а не полный аудит надежности поставщиков, условий, качества и структуры всех расходов."); lines.push("Для точного вывода нужна отдельная проверка поставщицкого риска: поставщики, договорные условия, качество поставок, сроки, доля в закупках и полная структура расходов."); return joinBusinessReplyLines(lines); } if (inventoryReserveBoundary) { const headline = toNonEmptyString(draft.headline); const inventoryQualityEvents = toRecordObject(overview.inventory_quality_events); const cleanHeadline = headline?.replace(/^Коротко:\s*/iu, "").trim(); const reserveBasis = cleanHeadline ? localizeLine(cleanHeadline).replace(/^проверил/iu, "Проверены") : null; lines.push(reserveBasis ? `Коротко: точно подтвердить резерв под неликвиды нельзя. ${reserveBasis}` : "Коротко: точно подтвердить резерв под неликвиды по текущим данным нельзя."); if (inventoryQualityEvents) { return joinBusinessReplyLines(lines); } const boundaryLines = userFacingLines([ ...toStringList(draft.unknown_lines), ...toStringList(draft.limitation_lines) ]) .filter((line) => /(?:резерв|неликвид|склад|товар|reserve|obsolete|inventory|stock)/iu.test(line)) .slice(0, 2); if (boundaryLines.length > 0) { lines.push(...boundaryLines.map(localizeLine)); } lines.push("Проверить нужно отдельно: складской срез на дату, учетную политику резервов, списания и ликвидационную стоимость; косвенные признаки нельзя выдавать за доказанный факт резерва."); return joinBusinessReplyLines(lines); } if (crossScopeExecutiveSummary && separateSubject && previousCounterpartySummary && (incomingAmount || outgoingAmount || netAmount)) { lines.push(`Коротко: по компании ${organizationScope ?? "в выбранном контуре"} ${period} подтвержден денежный срез: получили ${incomingAmount ?? "0 руб."}, исходящие платежи/списания ${outgoingAmount ?? "0 руб."}, ${netDirection} ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}${previousCounterpartySummary.lead}; можно утверждать только эти подтвержденные срезы, нельзя называть это чистой прибылью, полным оборотом или доказанной ролью главного клиента/поставщика.`); lines.push(previousCounterpartySummary.line); lines.push(`Можно утверждать: по компании подтвержден операционный денежный сигнал по найденным строкам 1С; по ${separateSubject} отдельно подтверждены входящие/исходящие строки, расчетное нетто и документы из предыдущего контрагентского среза.`); lines.push(`Нельзя утверждать: это не чистая прибыль, не полный бухгалтерский оборот вне проверенного окна и не доказательство, что ${separateSubject} является главным клиентом или поставщиком как бизнес-роль.`); if (limitLine) { lines.push(limitLine); } return joinBusinessReplyLines(lines); } if (rankingNeed) { const incomingLeader = strongestIncomingYear(overview); const netLeader = strongestNetYear(overview); const leaderYear = toNonEmptyString(incomingLeader?.year_bucket); const leaderAmount = moneyText(incomingLeader?.incoming_total_amount_human_ru); const leaderRows = Number(incomingLeader?.incoming_rows_with_amount); if (!leaderYear || !leaderAmount) { return null; } lines.push(`Коротко: ${organizationPrefix}в доступном проверенном срезе 1С по входящим денежным строкам лидирует ${leaderYear}: ${leaderAmount}${Number.isFinite(leaderRows) && leaderRows > 0 ? ` по ${leaderRows} строкам с суммой` : ""}; это не полный бухгалтерский рейтинг доходности.`); const netYear = toNonEmptyString(netLeader?.year_bucket); const netYearAmount = moneyText(netLeader?.net_amount_human_ru); if (netYear && netYearAmount) { const netLabel = netLeader?.net_direction === "net_outgoing" ? "нетто в минус" : "нетто в плюс"; lines.push(`По расчетному операционному нетто лучший год: ${netYear}, ${netLabel} ${sentenceAmount(netYearAmount) ?? netYearAmount}.`); } lines.push('Метод: "доходный" здесь трактую как подтвержденные входящие поступления/выручку по найденным строкам 1С, не как чистую бухгалтерскую прибыль.'); if (incomingAmount && outgoingAmount && netAmount) { lines.push(`Сверка по окну: входящие ${incomingAmount}, исходящие ${outgoingAmount}, ${netDirection} ${sentenceAmount(netAmount) ?? netAmount}.`); } if (requestedFinancialBoundaryLine) { lines.push(requestedFinancialBoundaryLine); } const yearRows = businessOverviewYearRowsLine(overview); if (yearRows) { lines.push(yearRows); } } else if (incomingAmount || outgoingAmount || netAmount) { lines.push(`Коротко: ${organizationPrefix}${period} по подтвержденным строкам 1С получили ${incomingAmount ?? "0 руб."}; исходящие платежи/списания ${outgoingAmount ?? "0 руб."}; ${netDirection} ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб"}${topCustomerLead}${topSupplierLead}${roleBoundaryLead}${separateSubjectLead}.`); lines.push('Метод: "заработали" здесь считаю как операционный денежный показатель по 1С; это не чистая прибыль и не финрезультат.'); if (!directMoneyAnswer && customerName && customerAmount) { lines.push(topCustomerLooksFinancial ? `Крупнейший входящий денежный источник в этом срезе: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}. По названию это банк/финансовая организация, поэтому без назначения платежа не называю это клиентской выручкой.${nonFinancialCustomer ? ` Крупнейший небанковский входящий контрагент: ${nonFinancialCustomer}.` : ""}` : `Крупнейший подтвержденный источник входящих денег в этом срезе: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}.`); } if (requestedFinancialBoundaryLine) { lines.push(requestedFinancialBoundaryLine); } } else { return null; } if (separateSubject) { lines.push(previousCounterpartySummary?.line ?? `Отдельно по контрагенту ${separateSubject}: этот итог не переносит суммы компании на контрагента. Можно утверждать только разделение контура; нельзя делать вывод о выручке, долге или прибыльности ${separateSubject} без отдельного контрагентского среза документов и движений.`); } if (!directMoneyAnswer && topSupplier) { lines.push(topSupplierLooksFinancial ? `Крупнейший получатель исходящих денег: ${topSupplier}. По названию это банк/финансовая организация, поэтому без назначения платежа/договора не считаю это обычным поставщиком.${nonFinancialSupplier ? ` Крупнейший небанковский получатель исходящих денег: ${nonFinancialSupplier}.` : ""}` : `Крупнейший подтвержденный получатель исходящих денег: ${topSupplier}.`); } if (!directMoneyAnswer && (topCustomer || topSupplier)) { lines.push(topCustomerLooksFinancial || topSupplierLooksFinancial ? "Важно по ролям: текущий денежный срез подтверждает источники и получателей денег, но банковские контрагенты требуют проверки назначения платежа/счетов и не доказывают роль клиента или поставщика." : "Важно по ролям: текущий денежный срез подтверждает денежные источники и получателей, но не доказывает, что это главный клиент или главный поставщик как бизнес-роль."); } if (!directMoneyAnswer) { lines.push(`Что подтверждено: денежный срез по компании${organizationScope ? ` ${organizationScope}` : ""}${period ? ` ${period}` : ""}${topCustomer ? ", крупнейший источник входящих денег" : ""}${topSupplier ? ", крупнейший получатель исходящих денег" : ""}.`); const taxLine = businessOverviewTaxLine(overview); if (taxLine) { lines.push(taxLine); } const debtLine = businessOverviewDebtLine(overview); if (debtLine) { lines.push(debtLine); } const inventoryLine = businessOverviewInventoryLine(overview); if (inventoryLine) { lines.push(inventoryLine); } const missingOverviewFamilies = []; if (!taxLine) { missingOverviewFamilies.push("общая НДС/налоговая позиция без отдельного точного расчета"); } if (!debtLine) { missingOverviewFamilies.push("долги без даты среза"); } if (!inventoryLine) { missingOverviewFamilies.push("склад без даты среза"); } if (missingOverviewFamilies.length > 0) { lines.push(`Что не подтверждено в этом срезе: ${missingOverviewFamilies.join(", ")}.`); } lines.push("Что нельзя утверждать: чистую прибыль, полноценный финрезультат, юридические бизнес-роли клиентов/поставщиков и общую налоговую позицию без отдельного точного расчета."); } if (limitLine) { lines.push(limitLine); } lines.push("Для ответа именно про чистую прибыль нужно отдельно считать себестоимость, расходы и закрытие периода."); return joinBusinessReplyLines(lines); } function statusFrom(entryPoint) { if (!entryPoint || entryPoint.entry_status === "skipped_not_applicable") { return "not_applicable"; } if (entryPoint.entry_status === "skipped_needs_more_context") { return "clarification_candidate"; } const bridgeStatus = toNonEmptyString(toRecordObject(entryPoint.bridge)?.bridge_status); if (bridgeStatus === "answer_draft_ready") { return "ready_for_guarded_use"; } if (bridgeStatus === "needs_clarification") { return "clarification_candidate"; } if (bridgeStatus === "checked_sources_only") { return "checked_sources_only_candidate"; } if (bridgeStatus === "blocked") { return "blocked"; } if (bridgeStatus === "unsupported") { return "unsupported"; } return "not_applicable"; } function replyTypeFor(status) { if (status === "clarification_candidate") { return "clarification_required"; } if (status === "blocked" || status === "unsupported" || status === "not_applicable") { return "no_grounded_answer"; } return "partial_coverage"; } function buildReplyText(entryPoint, status) { const bridge = toRecordObject(entryPoint.bridge); const draft = toRecordObject(bridge?.answer_draft); if (!draft) { if (status === "clarification_candidate") { return "Нужно уточнить контекст перед поиском в 1С: контрагента, период или организацию."; } return null; } const compactBidirectionalValueFlowReply = buildCompactBidirectionalValueFlowReply(entryPoint, draft); if (compactBidirectionalValueFlowReply) { return compactBidirectionalValueFlowReply; } const compactBusinessOverviewReply = buildCompactBusinessOverviewReply(entryPoint, draft); if (compactBusinessOverviewReply) { return compactBusinessOverviewReply; } const blocks = [ toNonEmptyString(draft.headline) ? `Коротко: ${localizeLine(String(draft.headline))}` : null, section("Что подтверждено:", toStringList(draft.confirmed_lines)), section("Что можно сказать только как вывод:", toStringList(draft.inference_lines)), section("Что не подтверждено:", toStringList(draft.unknown_lines)), section("Ограничения проверки:", toStringList(draft.limitation_lines)), toNonEmptyString(draft.next_step_line) ? `Следующий шаг: ${localizeLine(String(draft.next_step_line))}` : null ].filter((item) => Boolean(item)); const reply = blocks.join("\n\n").trim(); return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null; } function buildAssistantMcpDiscoveryResponseCandidate(entryPoint) { const entry = entryPoint ?? null; const status = statusFrom(entry); const reasonCodes = uniqueStrings(entry?.reason_codes ?? []); pushReason(reasonCodes, `mcp_discovery_response_candidate_${status}`); pushReason(reasonCodes, "mcp_discovery_response_candidate_not_hot_wired"); const replyText = entry && (status === "ready_for_guarded_use" || status === "checked_sources_only_candidate" || status === "clarification_candidate") ? buildReplyText(entry, status) : null; return { schema_version: exports.ASSISTANT_MCP_DISCOVERY_RESPONSE_CANDIDATE_SCHEMA_VERSION, policy_owner: "assistantMcpDiscoveryResponseCandidate", candidate_status: replyText ? status : status === "clarification_candidate" ? status : status, hot_runtime_wired: false, reply_type: replyTypeFor(status), reply_text: replyText, eligible_for_future_hot_runtime: Boolean(replyText) && (status === "ready_for_guarded_use" || status === "checked_sources_only_candidate" || status === "clarification_candidate"), must_keep_internal_mechanics_hidden: true, reason_codes: reasonCodes }; }