NODEDC_1C/llm_normalizer/backend/dist/services/assistantMcpDiscoveryRespon...

798 lines
51 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.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
};
}