1123 lines
81 KiB
JavaScript
1123 lines
81 KiB
JavaScript
"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 canRankYearlyNet = !limitLine;
|
||
const netLeader = canRankYearlyNet ? strongestNetYear(overview) : null;
|
||
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);
|
||
}
|
||
if (!canRankYearlyNet && Array.isArray(overview.yearly_breakdown) && overview.yearly_breakdown.length > 0) {
|
||
lines.push("Годовое операционное нетто в широком срезе не ранжирую: по одному из направлений достигнут лимит строк, поэтому безопаснее дозапросить конкретный год или квартал.");
|
||
}
|
||
const yearRows = canRankYearlyNet ? businessOverviewYearRowsLine(overview) : null;
|
||
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
|
||
};
|
||
}
|