NODEDC_1C/llm_normalizer/backend/src/services/assistantMcpDiscoveryRespon...

1337 lines
81 KiB
TypeScript
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.

import type { AssistantMcpDiscoveryRuntimeEntryPointContract } from "./assistantMcpDiscoveryRuntimeEntryPoint";
import { isLikelyFinancialInstitutionCounterparty } from "./counterpartyRoleHeuristics";
export const ASSISTANT_MCP_DISCOVERY_RESPONSE_CANDIDATE_SCHEMA_VERSION =
"assistant_mcp_discovery_response_candidate_v1" as const;
export type AssistantMcpDiscoveryResponseCandidateStatus =
| "ready_for_guarded_use"
| "clarification_candidate"
| "checked_sources_only_candidate"
| "not_applicable"
| "blocked"
| "unsupported";
export interface AssistantMcpDiscoveryResponseCandidateContract {
schema_version: typeof ASSISTANT_MCP_DISCOVERY_RESPONSE_CANDIDATE_SCHEMA_VERSION;
policy_owner: "assistantMcpDiscoveryResponseCandidate";
candidate_status: AssistantMcpDiscoveryResponseCandidateStatus;
hot_runtime_wired: false;
reply_type: "partial_coverage" | "clarification_required" | "no_grounded_answer";
reply_text: string | null;
eligible_for_future_hot_runtime: boolean;
must_keep_internal_mechanics_hidden: true;
reason_codes: string[];
}
function toRecordObject(value: unknown): Record<string, unknown> | null {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return null;
}
return value as Record<string, unknown>;
}
function toNonEmptyString(value: unknown): string | null {
if (value === null || value === undefined) {
return null;
}
const text = String(value).trim();
return text.length > 0 ? text : null;
}
function normalizeQuestionText(value: unknown): string {
return String(value ?? "")
.toLowerCase()
.replace(/ё/g, "е")
.replace(/\s+/g, " ")
.trim();
}
function requestsFinancialCounterpartyBoundary(turnMeaning: Record<string, unknown> | null, graph: Record<string, unknown> | null): boolean {
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: unknown): string[] {
if (!Array.isArray(value)) {
return [];
}
return value.map((item) => toNonEmptyString(item)).filter((item): item is string => Boolean(item));
}
function normalizeReasonCode(value: string): string | null {
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: string[], value: string): void {
const normalized = normalizeReasonCode(value);
if (normalized && !target.includes(normalized)) {
target.push(normalized);
}
}
function uniqueStrings(values: string[]): string[] {
const result: string[] = [];
for (const value of values) {
const text = String(value ?? "").trim();
if (text && !result.includes(text)) {
result.push(text);
}
}
return result;
}
function hasInternalMechanics(value: string): boolean {
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: string[]): string[] {
return uniqueStrings(values).filter((line) => !hasInternalMechanics(line));
}
function sanitizeUserFacingMechanics(value: string): string {
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: Array<[RegExp, string]> = [
[/\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: string): string {
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: string, lines: string[]): string | null {
const clean = userFacingLines(lines.map(localizeLine));
if (clean.length === 0) {
return null;
}
return `${title}\n${clean.map((line) => `- ${line}`).join("\n")}`;
}
function readStringArray(value: unknown): string[] {
return Array.isArray(value)
? value.map((item) => toNonEmptyString(item)).filter((item): item is string => Boolean(item))
: [];
}
function moneyText(value: unknown): string | null {
const text = toNonEmptyString(value);
if (!text) {
return null;
}
return text.replace(/\s*руб\.$/u, " руб.").replace(/\s+/gu, " ");
}
function sentenceAmount(value: string | null): string | null {
return value ? value.replace(/[.]+$/u, "") : null;
}
function businessOverviewPeriodText(overview: Record<string, unknown>): string {
const period = toNonEmptyString(overview.period_scope);
return period ? `за ${period}` : "за все доступное проверенное окно";
}
function strongestIncomingYear(overview: Record<string, unknown>): Record<string, unknown> | null {
const years = Array.isArray(overview.yearly_breakdown) ? overview.yearly_breakdown : [];
const sorted = years
.map((item) => toRecordObject(item))
.filter((item): item is Record<string, unknown> => {
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: Record<string, unknown>): Record<string, unknown> | null {
const years = Array.isArray(overview.yearly_breakdown) ? overview.yearly_breakdown : [];
const sorted = years
.map((item) => toRecordObject(item))
.filter((item): item is Record<string, unknown> => {
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: Record<string, unknown>): string | null {
const incoming = toRecordObject(overview.incoming_customer_revenue);
const outgoing = toRecordObject(overview.outgoing_supplier_payout);
const limited: string[] = [];
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: string[]): string | null {
const reply = userFacingLines(lines.map(localizeLine)).join("\n").trim();
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
}
function businessOverviewYearRowsLine(overview: Record<string, unknown>): string | null {
const years = Array.isArray(overview.yearly_breakdown) ? overview.yearly_breakdown : [];
const values = years
.map((item) => toRecordObject(item))
.filter((item): item is Record<string, unknown> => 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): item is string => Boolean(item));
const joined = values.join("; ");
return values.length > 0 ? `По годам: ${sentenceAmount(joined) ?? joined}.` : null;
}
function firstOverviewAxisLabel(rows: unknown, amountKey = "total_amount_human_ru"): string | null {
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: unknown, amountKey = "total_amount_human_ru"): string | null {
if (!Array.isArray(rows)) {
return null;
}
for (const row of rows) {
const item = toRecordObject(row);
const label = toNonEmptyString(item?.axis_value);
if (!label || isLikelyFinancialInstitutionCounterparty(label)) {
continue;
}
const amount = moneyText(item?.[amountKey]);
if (amount) {
return `${label}${sentenceAmount(amount) ?? amount}`;
}
}
return null;
}
function overviewAxisLooksFinancial(row: Record<string, unknown> | null): boolean {
if (!row) {
return false;
}
return (
row.counterparty_role_hint === "bank_or_financial_institution" ||
isLikelyFinancialInstitutionCounterparty(row.axis_value)
);
}
function financialFlowHintTextRuFromRecord(row: Record<string, unknown> | null): string | null {
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: Record<string, unknown>): string | null {
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: Record<string, unknown>): string | null {
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: Record<string, unknown>): string | null {
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): item is string => Boolean(item));
return pieces.length > 0 ? `Склад: ${pieces.join(", ")}.` : null;
}
function rowCountText(value: unknown): string | null {
const count = Number(value);
return Number.isFinite(count) ? String(count) : null;
}
function sideRowsText(side: Record<string, unknown> | null): string | null {
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: Record<string, unknown> | null): string | null {
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: unknown): string {
if (direction === "net_outgoing") {
return "нетто в сторону контрагента";
}
if (direction === "balanced") {
return "нетто около нуля";
}
return "нетто в нашу сторону";
}
function buildCompactBidirectionalValueFlowReply(
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract,
draft: Record<string, unknown>
): string | null {
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: string[] = [];
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: string | null): string {
return String(value ?? "")
.toLowerCase()
.replace(/[«»"']/g, "")
.replace(/\s+/g, " ")
.trim();
}
function businessOverviewSeparateSubjectLabel(
graph: Record<string, unknown> | null,
turnMeaning: Record<string, unknown> | null,
organizationScope: string | null
): string | null {
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: string | null, right: string | null): boolean {
const leftComparable = compactComparable(left);
const rightComparable = compactComparable(right);
return Boolean(leftComparable && rightComparable && leftComparable === rightComparable);
}
function previousDocumentSummaryLine(
bundle: Record<string, unknown> | null,
separateSubject: string | null
): string | null {
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: Record<string, unknown> | null,
separateSubject: string | null,
documentBundle: Record<string, unknown> | null
): { lead: string; line: string } | null {
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: string[] = [];
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: AssistantMcpDiscoveryRuntimeEntryPointContract,
draft: Record<string, unknown>
): string | null {
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: string[] = [];
const actionFamily = toNonEmptyString(turnMeaning?.asked_action_family);
const unsupportedFamily = toNonEmptyString(turnMeaning?.unsupported_but_understood_family);
const profitMarginBoundary = actionFamily === "profit_margin_boundary" || unsupportedFamily === "profit_margin_boundary";
const debtDueDateBoundary = actionFamily === "debt_due_date_boundary" || unsupportedFamily === "debt_due_date_boundary";
const vendorRiskBoundary =
actionFamily === "vendor_risk_procurement_boundary" || unsupportedFamily === "vendor_risk_procurement_boundary";
const inventoryReserveBoundary =
actionFamily === "inventory_reserve_boundary" || unsupportedFamily === "inventory_reserve_liquidation_boundary";
if (profitMarginBoundary) {
const accountingFinancialResult = toRecordObject(overview.accounting_financial_result);
if (accountingFinancialResult) {
const direction = toNonEmptyString(accountingFinancialResult.final_result_direction);
const amount = moneyText(accountingFinancialResult.final_result_amount_human_ru);
const periodScope = toNonEmptyString(accountingFinancialResult.period_scope) ?? period;
const marginPct =
typeof accountingFinancialResult.net_margin_to_revenue_pct === "number" &&
Number.isFinite(accountingFinancialResult.net_margin_to_revenue_pct)
? `${accountingFinancialResult.net_margin_to_revenue_pct}%`
: null;
const directionText =
direction === "profit"
? "учетная прибыль"
: direction === "loss"
? "учетный убыток"
: "нулевой учетный финрезультат";
const amountText = amount
? direction === "loss"
? `минус ${amount}`
: amount
: "сумма не распознана";
lines.push(
`Коротко: нет, денежное операционное нетто не стоит считать чистой прибылью. Отдельно по закрытию счетов 90/91/99 в 1С за ${periodScope} подтвержден ${directionText}: ${amountText}${marginPct ? `; маржа к подтвержденной выручке ${marginPct}` : "; маржа к подтвержденной выручке не рассчитана"}.`
);
lines.push(
"Это учетный финрезультат по найденным строкам закрытия периода в 1С, а не внешний аудит и не юридически подтвержденная отчетность."
);
return joinBusinessReplyLines(lines);
}
const headline = toNonEmptyString(draft.headline);
const cleanHeadline = headline?.replace(/^Коротко:\s*/iu, "").trim();
lines.push(
cleanHeadline
? `Коротко: ${localizeLine(cleanHeadline)}`
: "Коротко: нельзя точно подтвердить чистую прибыль и маржу по текущему срезу 1С; есть только ограниченный операционный денежный/товарный сигнал, а не полный отчет о прибыли и не бухгалтерский финансовый результат."
);
const boundaryLines = userFacingLines([
...toStringList(draft.confirmed_lines),
...toStringList(draft.inference_lines),
...toStringList(draft.unknown_lines)
])
.filter((line) => /(?:прибыл|марж|финанс|p\s*&\s*l|p&l|расход|себестоим|закрыт|profit|margin|financial)/iu.test(line))
.slice(0, 2);
if (boundaryLines.length > 0) {
lines.push(...boundaryLines.map(localizeLine));
}
lines.push(
"Для точного отчета о прибыли нужны отдельная проверка себестоимости, расходов, закрытия периода и финрезультата; текущий ограниченный сигнал нельзя выдавать за подтвержденную чистую прибыль или маржу."
);
if (limitLine) {
lines.push(limitLine);
}
return joinBusinessReplyLines(lines);
}
if (debtDueDateBoundary) {
const dueDateAging = toRecordObject(overview.debt_due_date_aging);
if (dueDateAging) {
const status = toNonEmptyString(dueDateAging.evidence_status);
const asOfDate = toNonEmptyString(dueDateAging.as_of_date) ?? "проверенную дату";
const overdueAmount = moneyText(dueDateAging.overdue_amount_human_ru);
const grossAmount = moneyText(dueDateAging.gross_open_amount_human_ru);
const rowsWithPaymentTerms =
typeof dueDateAging.rows_with_payment_terms === "number" && Number.isFinite(dueDateAging.rows_with_payment_terms)
? dueDateAging.rows_with_payment_terms
: null;
const rowsWithAmount =
typeof dueDateAging.rows_with_amount === "number" && Number.isFinite(dueDateAging.rows_with_amount)
? dueDateAging.rows_with_amount
: null;
const dueDateScopePrefix = organizationScope ? `по компании ${organizationScope} ` : "";
if (status === "confirmed_overdue") {
lines.push(
`Коротко: ${dueDateScopePrefix}на ${asOfDate} подтвержденная просрочка есть: ${overdueAmount ?? "сумма не распознана"} по ${dueDateAging.overdue_rows ?? "найденным"} строкам.`
);
lines.push("Основа ответа: открытые расчеты 60/62/76, договорный срок оплаты и дата расчетного документа; это проверка просрочки по срокам оплаты, а не просто возраст договора.");
} else if (status === "no_payment_terms_configured") {
lines.push(
`Коротко: ${dueDateScopePrefix}на ${asOfDate} подтвержденной просрочки нет: открытые расчеты проверены${grossAmount ? ` на ${grossAmount}` : ""}, но в найденных договорах срок оплаты не установлен.`
);
lines.push(
rowsWithAmount !== null
? `Проверено строк с суммой: ${rowsWithAmount}. Без установленного срока оплаты нельзя честно назвать эти остатки просрочкой.`
: "Без установленного срока оплаты нельзя честно назвать эти остатки просрочкой."
);
} else if (status === "insufficient_due_date_basis") {
lines.push(
`Коротко: ${dueDateScopePrefix}на ${asOfDate} просрочка не подтверждена: по строкам с установленным сроком оплаты не хватило даты расчетного документа.`
);
if (rowsWithPaymentTerms !== null) {
lines.push(`Строк с установленным сроком оплаты: ${rowsWithPaymentTerms}; нужен документ-основание с датой, чтобы посчитать договорный срок оплаты.`);
}
} else {
lines.push(
`Коротко: ${dueDateScopePrefix}на ${asOfDate} проверка просрочки по срокам оплаты выполнена, подтвержденной просрочки не найдено${rowsWithPaymentTerms !== null ? `; строк с установленным сроком оплаты ${rowsWithPaymentTerms}` : ""}.`
);
}
return joinBusinessReplyLines(lines);
}
const headline = toNonEmptyString(draft.headline);
const cleanHeadline = headline?.replace(/^Коротко:\s*/iu, "").trim();
lines.push(
cleanHeadline
? `Коротко: ${localizeLine(cleanHeadline)}`
: "Коротко: нельзя точно определить, какая дебиторка просрочена, по текущему срезу 1С; есть только ограниченный долговой сигнал, но нет проверки договорных сроков оплаты."
);
lines.push(
"Проверить нужно отдельно: договоры, сроки оплаты, погашение и закрытие задолженности; без этого нельзя доказать просрочку по договорным срокам."
);
return joinBusinessReplyLines(lines);
}
if (vendorRiskBoundary) {
const vendorProcurementQuality = toRecordObject(overview.vendor_procurement_quality);
if (vendorProcurementQuality) {
const status = toNonEmptyString(vendorProcurementQuality.evidence_status);
const totalOutgoing = moneyText(vendorProcurementQuality.total_outgoing_amount_human_ru);
const topOutgoingRecord = toRecordObject(vendorProcurementQuality.top_outgoing_counterparty);
const topOutgoingName = toNonEmptyString(topOutgoingRecord?.axis_value);
const topOutgoingAmount = moneyText(topOutgoingRecord?.total_amount_human_ru);
const topOutgoingShare =
typeof vendorProcurementQuality.top_outgoing_share_pct === "number" &&
Number.isFinite(vendorProcurementQuality.top_outgoing_share_pct)
? `${vendorProcurementQuality.top_outgoing_share_pct}%`
: null;
const nonFinancialRecord = toRecordObject(vendorProcurementQuality.top_non_financial_supplier);
const nonFinancialName = toNonEmptyString(nonFinancialRecord?.axis_value);
const nonFinancialAmount = moneyText(nonFinancialRecord?.total_amount_human_ru);
const nonFinancialShare =
typeof vendorProcurementQuality.top_non_financial_supplier_share_pct === "number" &&
Number.isFinite(vendorProcurementQuality.top_non_financial_supplier_share_pct)
? `${vendorProcurementQuality.top_non_financial_supplier_share_pct}%`
: null;
const periodScope = toNonEmptyString(vendorProcurementQuality.period_scope) ?? period;
const totalText = totalOutgoing ? `; всего исходящих платежей в проверенном срезе ${totalOutgoing}` : "";
if (status === "financial_institution_leads_outgoing_cash") {
lines.push(
`Коротко: проверка концентрации закупок/исходящих платежей за ${periodScope} не подтверждает зависимость от обычного поставщика: крупнейший получатель исходящих денег ${topOutgoingName ?? "не распознан"}${topOutgoingShare ? ` держит около ${topOutgoingShare}` : ""}${topOutgoingAmount ? ` (${topOutgoingAmount})` : ""}, но по названию это банк/финансовая организация${totalText}.`
);
const financialHintText = financialFlowHintTextRuFromRecord(topOutgoingRecord);
if (financialHintText) {
lines.push(financialHintText);
}
if (nonFinancialName) {
lines.push(
`Крупнейший небанковский получатель исходящих денег: ${nonFinancialName}${nonFinancialShare ? `, около ${nonFinancialShare}` : ""}${nonFinancialAmount ? ` (${nonFinancialAmount})` : ""}. Это уже сигнал закупочной/исходящей концентрации, но не аудит надежности поставщика.`
);
}
} else if (status === "reviewed_procurement_concentration") {
lines.push(
`Коротко: точный риск зависимости от одного поставщика не подтвержден полностью; проверка концентрации закупок/исходящих платежей за ${periodScope} нашла крупнейшего получателя исходящего потока: ${topOutgoingName ?? nonFinancialName ?? "получатель не распознан"}${topOutgoingShare ? ` держит около ${topOutgoingShare}` : nonFinancialShare ? ` держит около ${nonFinancialShare}` : ""}${topOutgoingAmount ? ` (${topOutgoingAmount})` : nonFinancialAmount ? ` (${nonFinancialAmount})` : ""}${totalText}.`
);
} else {
lines.push(
`Коротко: проверка концентрации закупок/исходящих платежей за ${periodScope} выполнена, но надежной небанковской концентрации поставщика по найденным исходящим платежам не хватает${totalText}.`
);
}
const contractText =
typeof vendorProcurementQuality.used_contracts === "number" && Number.isFinite(vendorProcurementQuality.used_contracts)
? typeof vendorProcurementQuality.total_contracts === "number" && Number.isFinite(vendorProcurementQuality.total_contracts)
? ` Договорный профиль: используется ${vendorProcurementQuality.used_contracts}/${vendorProcurementQuality.total_contracts} договоров${typeof vendorProcurementQuality.used_contract_share_pct === "number" && Number.isFinite(vendorProcurementQuality.used_contract_share_pct) ? ` (${vendorProcurementQuality.used_contract_share_pct}%)` : ""}.`
: ` Договорный профиль: используется ${vendorProcurementQuality.used_contracts} договоров.`
: "";
lines.push(
`Что не доказано этим срезом: надежность поставщика, качество поставок, договорные условия, назначение каждого платежа и полная структура всех расходов.${contractText}`
);
return joinBusinessReplyLines(lines);
}
const supplierBasis = topSupplier
? topSupplierLooksFinancial
? `крупнейший получатель исходящих денег: ${topSupplier}; по названию это банк/финансовая организация, поэтому это не доказанная зависимость от обычного поставщика${nonFinancialSupplier ? `; крупнейший небанковский получатель исходящих денег: ${nonFinancialSupplier}` : ""}`
: `крупнейший подтвержденный поставщик/получатель исходящих платежей: ${topSupplier}`
: outgoingAmount
? `исходящие платежи/закупочный поток в проверенном срезе: ${outgoingAmount}`
: "есть только ограниченный срез исходящих платежей без полного профиля поставщицкого риска";
const proxyLabel = topSupplierLooksFinancial
? "сигнал концентрации исходящих денег"
: "сигнал концентрации закупок/исходящих платежей";
lines.push(
`Коротко: точный риск зависимости от одного поставщика по текущим данным не подтвержден; есть только ${proxyLabel}: ${supplierBasis}.`
);
lines.push(
"Это сигнал концентрации закупок/исходящих платежей, а не полный аудит надежности поставщиков, условий, качества и структуры всех расходов."
);
lines.push(
"Для точного вывода нужна отдельная проверка поставщицкого риска: поставщики, договорные условия, качество поставок, сроки, доля в закупках и полная структура расходов."
);
return joinBusinessReplyLines(lines);
}
if (inventoryReserveBoundary) {
const headline = toNonEmptyString(draft.headline);
const inventoryQualityEvents = toRecordObject(overview.inventory_quality_events);
const cleanHeadline = headline?.replace(/^Коротко:\s*/iu, "").trim();
const reserveBasis = cleanHeadline ? localizeLine(cleanHeadline).replace(/^проверил/iu, "Проверены") : null;
lines.push(
reserveBasis
? `Коротко: точно подтвердить резерв под неликвиды нельзя. ${reserveBasis}`
: "Коротко: точно подтвердить резерв под неликвиды по текущим данным нельзя."
);
if (inventoryQualityEvents) {
return joinBusinessReplyLines(lines);
}
const boundaryLines = userFacingLines([
...toStringList(draft.unknown_lines),
...toStringList(draft.limitation_lines)
])
.filter((line) => /(?:резерв|неликвид|склад|товар|reserve|obsolete|inventory|stock)/iu.test(line))
.slice(0, 2);
if (boundaryLines.length > 0) {
lines.push(...boundaryLines.map(localizeLine));
}
lines.push(
"Проверить нужно отдельно: складской срез на дату, учетную политику резервов, списания и ликвидационную стоимость; косвенные признаки нельзя выдавать за доказанный факт резерва."
);
return joinBusinessReplyLines(lines);
}
if (crossScopeExecutiveSummary && separateSubject && previousCounterpartySummary && (incomingAmount || outgoingAmount || netAmount)) {
lines.push(
`Коротко: по компании ${organizationScope ?? "в выбранном контуре"} ${period} подтвержден денежный срез: получили ${incomingAmount ?? "0 руб."}, исходящие платежи/списания ${outgoingAmount ?? "0 руб."}, ${netDirection} ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}${previousCounterpartySummary.lead}; можно утверждать только эти подтвержденные срезы, нельзя называть это чистой прибылью, полным оборотом или доказанной ролью главного клиента/поставщика.`
);
lines.push(previousCounterpartySummary.line);
lines.push(
`Можно утверждать: по компании подтвержден операционный денежный сигнал по найденным строкам 1С; по ${separateSubject} отдельно подтверждены входящие/исходящие строки, расчетное нетто и документы из предыдущего контрагентского среза.`
);
lines.push(
`Нельзя утверждать: это не чистая прибыль, не полный бухгалтерский оборот вне проверенного окна и не доказательство, что ${separateSubject} является главным клиентом или поставщиком как бизнес-роль.`
);
if (limitLine) {
lines.push(limitLine);
}
return joinBusinessReplyLines(lines);
}
if (rankingNeed) {
const incomingLeader = strongestIncomingYear(overview);
const netLeader = strongestNetYear(overview);
const leaderYear = toNonEmptyString(incomingLeader?.year_bucket);
const leaderAmount = moneyText(incomingLeader?.incoming_total_amount_human_ru);
const leaderRows = Number(incomingLeader?.incoming_rows_with_amount);
if (!leaderYear || !leaderAmount) {
return null;
}
lines.push(
`Коротко: ${organizationPrefix}в доступном проверенном срезе 1С по входящим денежным строкам лидирует ${leaderYear}: ${leaderAmount}${Number.isFinite(leaderRows) && leaderRows > 0 ? ` по ${leaderRows} строкам с суммой` : ""}; это не полный бухгалтерский рейтинг доходности.`
);
const netYear = toNonEmptyString(netLeader?.year_bucket);
const netYearAmount = moneyText(netLeader?.net_amount_human_ru);
if (netYear && netYearAmount) {
const netLabel = netLeader?.net_direction === "net_outgoing" ? "нетто в минус" : "нетто в плюс";
lines.push(`По расчетному операционному нетто лучший год: ${netYear}, ${netLabel} ${sentenceAmount(netYearAmount) ?? netYearAmount}.`);
}
lines.push('Метод: "доходный" здесь трактую как подтвержденные входящие поступления/выручку по найденным строкам 1С, не как чистую бухгалтерскую прибыль.');
if (incomingAmount && outgoingAmount && netAmount) {
lines.push(`Сверка по окну: входящие ${incomingAmount}, исходящие ${outgoingAmount}, ${netDirection} ${sentenceAmount(netAmount) ?? netAmount}.`);
}
if (requestedFinancialBoundaryLine) {
lines.push(requestedFinancialBoundaryLine);
}
const yearRows = businessOverviewYearRowsLine(overview);
if (yearRows) {
lines.push(yearRows);
}
} else if (incomingAmount || outgoingAmount || netAmount) {
lines.push(
`Коротко: ${organizationPrefix}${period} по подтвержденным строкам 1С получили ${incomingAmount ?? "0 руб."}; исходящие платежи/списания ${outgoingAmount ?? "0 руб."}; ${netDirection} ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб"}${topCustomerLead}${topSupplierLead}${roleBoundaryLead}${separateSubjectLead}.`
);
lines.push('Метод: "заработали" здесь считаю как операционный денежный показатель по 1С; это не чистая прибыль и не финрезультат.');
if (!directMoneyAnswer && customerName && customerAmount) {
lines.push(
topCustomerLooksFinancial
? `Крупнейший входящий денежный источник в этом срезе: ${customerName}${sentenceAmount(customerAmount) ?? customerAmount}. По названию это банк/финансовая организация, поэтому без назначения платежа не называю это клиентской выручкой.${nonFinancialCustomer ? ` Крупнейший небанковский входящий контрагент: ${nonFinancialCustomer}.` : ""}`
: `Крупнейший подтвержденный источник входящих денег в этом срезе: ${customerName}${sentenceAmount(customerAmount) ?? customerAmount}.`
);
}
if (requestedFinancialBoundaryLine) {
lines.push(requestedFinancialBoundaryLine);
}
} else {
return null;
}
if (separateSubject) {
lines.push(
previousCounterpartySummary?.line ??
`Отдельно по контрагенту ${separateSubject}: этот итог не переносит суммы компании на контрагента. Можно утверждать только разделение контура; нельзя делать вывод о выручке, долге или прибыльности ${separateSubject} без отдельного контрагентского среза документов и движений.`
);
}
if (!directMoneyAnswer && topSupplier) {
lines.push(
topSupplierLooksFinancial
? `Крупнейший получатель исходящих денег: ${topSupplier}. По названию это банк/финансовая организация, поэтому без назначения платежа/договора не считаю это обычным поставщиком.${nonFinancialSupplier ? ` Крупнейший небанковский получатель исходящих денег: ${nonFinancialSupplier}.` : ""}`
: `Крупнейший подтвержденный получатель исходящих денег: ${topSupplier}.`
);
}
if (!directMoneyAnswer && (topCustomer || topSupplier)) {
lines.push(
topCustomerLooksFinancial || topSupplierLooksFinancial
? "Важно по ролям: текущий денежный срез подтверждает источники и получателей денег, но банковские контрагенты требуют проверки назначения платежа/счетов и не доказывают роль клиента или поставщика."
: "Важно по ролям: текущий денежный срез подтверждает денежные источники и получателей, но не доказывает, что это главный клиент или главный поставщик как бизнес-роль."
);
}
if (!directMoneyAnswer) {
lines.push(
`Что подтверждено: денежный срез по компании${organizationScope ? ` ${organizationScope}` : ""}${period ? ` ${period}` : ""}${topCustomer ? ", крупнейший источник входящих денег" : ""}${topSupplier ? ", крупнейший получатель исходящих денег" : ""}.`
);
const taxLine = businessOverviewTaxLine(overview);
if (taxLine) {
lines.push(taxLine);
}
const debtLine = businessOverviewDebtLine(overview);
if (debtLine) {
lines.push(debtLine);
}
const inventoryLine = businessOverviewInventoryLine(overview);
if (inventoryLine) {
lines.push(inventoryLine);
}
const missingOverviewFamilies: string[] = [];
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: AssistantMcpDiscoveryRuntimeEntryPointContract | null): AssistantMcpDiscoveryResponseCandidateStatus {
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: AssistantMcpDiscoveryResponseCandidateStatus
): AssistantMcpDiscoveryResponseCandidateContract["reply_type"] {
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: AssistantMcpDiscoveryRuntimeEntryPointContract, status: AssistantMcpDiscoveryResponseCandidateStatus): string | null {
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): item is string => Boolean(item));
const reply = blocks.join("\n\n").trim();
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
}
export function buildAssistantMcpDiscoveryResponseCandidate(
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null | undefined
): AssistantMcpDiscoveryResponseCandidateContract {
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: 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
};
}