"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ASSISTANT_MCP_DISCOVERY_RESPONSE_CANDIDATE_SCHEMA_VERSION = void 0; exports.buildAssistantMcpDiscoveryResponseCandidate = buildAssistantMcpDiscoveryResponseCandidate; 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 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 localizeLine(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 "Запрошенный период уперся в лимит строк MCP; доступного бюджета помесячных дозапросов не хватило, чтобы покрыть все подпериоды."; } 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 value; } 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("исходящие"); } return limited.length > 0 ? `Важно: ${limited.join(" и ")} уперлись в лимит выборки MCP, поэтому это проверенный срез найденных строк, а не гарантированно полный бухгалтерский оборот.` : 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 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 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 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 = [ `Коротко: по контрагенту ${counterparty}${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)}`); } const reply = lines.join("\n").trim(); return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null; } 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 topCustomerLead = customerName && customerAmount ? `; крупнейший источник входящих денег: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}` : ""; const topSupplier = firstOverviewAxisLabel(overview.top_suppliers); const topSupplierLead = topSupplier ? `; крупнейший получатель исходящих денег: ${topSupplier}` : ""; const roleBoundaryLead = topCustomer || topSupplier ? "; клиент/поставщик как бизнес-роли этим денежным срезом не подтверждены" : ""; 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 = []; 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(`Можно утверждать: по компании подтвержден operating-flow proxy по найденным строкам 1С; по ${separateSubject} отдельно подтверждены входящие/исходящие строки, расчетное нетто и документы из предыдущего контрагентского среза.`); lines.push(`Нельзя утверждать: это не чистая прибыль, не полный бухгалтерский оборот вне проверенного окна и не доказательство, что ${separateSubject} является главным клиентом или поставщиком как бизнес-роль.`); if (limitLine) { lines.push(limitLine); } const reply = lines.join("\n").trim(); return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null; } 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(`Коротко: в доступном проверенном MCP-срезе по входящим денежным строкам лидирует ${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}.`); } 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('Метод: "заработали" здесь считаю как денежный operating-flow proxy по 1С; это не чистая прибыль и не финрезультат.'); if (!directMoneyAnswer && customerName && customerAmount) { lines.push(`Крупнейший подтвержденный источник входящих денег в этом срезе: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}.`); } } else { return null; } if (separateSubject) { lines.push(previousCounterpartySummary?.line ?? `Отдельно по контрагенту ${separateSubject}: этот итог не переносит суммы компании на контрагента. Можно утверждать только разделение контура; нельзя делать вывод о выручке, долге или прибыльности ${separateSubject} без отдельного контрагентского среза документов и движений.`); } if (!directMoneyAnswer && topSupplier) { lines.push(`Крупнейший подтвержденный получатель исходящих денег: ${topSupplier}.`); } if (!directMoneyAnswer && (topCustomer || topSupplier)) { lines.push("Важно по ролям: текущий денежный срез подтверждает денежные источники и получателей, но не доказывает, что это главный клиент или главный поставщик как бизнес-роль."); } 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("Для ответа именно про чистую прибыль нужно отдельно считать себестоимость, расходы и закрытие периода."); const reply = lines.join("\n").trim(); return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null; } 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 }; }