Закрепить reviewed routes и защиту бизнес-ответов

This commit is contained in:
dctouch 2026-05-12 15:59:23 +03:00
parent b625f9af5b
commit 4fcf349894
47 changed files with 5341 additions and 416 deletions

View File

@ -77,7 +77,7 @@ exports.FEATURE_ASSISTANT_LIVING_CHAT_ROUTER_V1 = toBooleanFlag(process.env.FEAT
exports.ASSISTANT_MCP_PROXY_URL = (process.env.ASSISTANT_MCP_PROXY_URL ?? "http://127.0.0.1:6003").replace(/\/+$/, ""); exports.ASSISTANT_MCP_PROXY_URL = (process.env.ASSISTANT_MCP_PROXY_URL ?? "http://127.0.0.1:6003").replace(/\/+$/, "");
exports.ASSISTANT_MCP_CHANNEL = process.env.ASSISTANT_MCP_CHANNEL ?? "default"; exports.ASSISTANT_MCP_CHANNEL = process.env.ASSISTANT_MCP_CHANNEL ?? "default";
exports.ASSISTANT_MCP_TIMEOUT_MS = toNumberFlag(process.env.ASSISTANT_MCP_TIMEOUT_MS, 6000); exports.ASSISTANT_MCP_TIMEOUT_MS = toNumberFlag(process.env.ASSISTANT_MCP_TIMEOUT_MS, 6000);
exports.ASSISTANT_MCP_LIVE_LIMIT = Math.max(1, Math.trunc(toNumberFlag(process.env.ASSISTANT_MCP_LIVE_LIMIT, 24))); exports.ASSISTANT_MCP_LIVE_LIMIT = Math.max(1, Math.trunc(toNumberFlag(process.env.ASSISTANT_MCP_LIVE_LIMIT, 128)));
exports.VAT_PAYABLE_68_PREFIXES = toStringListFlag(process.env.VAT_PAYABLE_68_PREFIXES, ["68.02"]); exports.VAT_PAYABLE_68_PREFIXES = toStringListFlag(process.env.VAT_PAYABLE_68_PREFIXES, ["68.02"]);
exports.VAT_PAYABLE_19_PREFIXES = toStringListFlag(process.env.VAT_PAYABLE_19_PREFIXES, ["19"]); exports.VAT_PAYABLE_19_PREFIXES = toStringListFlag(process.env.VAT_PAYABLE_19_PREFIXES, ["19"]);
exports.DATA_DIR = process.env.DATA_DIR ?? path_1.default.resolve(exports.MODULE_ROOT, "data"); exports.DATA_DIR = process.env.DATA_DIR ?? path_1.default.resolve(exports.MODULE_ROOT, "data");

View File

@ -1733,8 +1733,7 @@ function extractAddressFilters(userMessage, intent) {
const periodWasDerivedHeuristically = warnings.includes("period_derived_from_month_phrase") || const periodWasDerivedHeuristically = warnings.includes("period_derived_from_month_phrase") ||
warnings.includes("period_derived_from_year_range_phrase") || warnings.includes("period_derived_from_year_range_phrase") ||
warnings.includes("period_derived_from_year_phrase"); warnings.includes("period_derived_from_year_phrase");
const preserveDerivedPeriodWindow = usesAsOfPrimaryWindow(intent) || const preserveDerivedPeriodWindow = intent === "inventory_on_hand_as_of_date" ||
intent === "inventory_on_hand_as_of_date" ||
intent === "inventory_supplier_stock_overlap_as_of_date"; intent === "inventory_supplier_stock_overlap_as_of_date";
if (periodWasDerivedHeuristically && !warnings.includes("exact_historical_period_window_requested")) { if (periodWasDerivedHeuristically && !warnings.includes("exact_historical_period_window_requested")) {
warnings.push("exact_historical_period_window_requested"); warnings.push("exact_historical_period_window_requested");

View File

@ -1672,7 +1672,8 @@ function hasVatPeriodInspectionBridgeSignal(text) {
const hasPeriodCue = /(?:\b(?:19|20)\d{2}\b|за\s+(?:\d{4}|год|период|квартал|месяц|январ|феврал|март|апрел|ма[йя]|июн|июл|август|сентябр|октябр|ноябр|декабр)|\b[1-4]\s*(?:кв|квартал))/iu.test(normalized); const hasPeriodCue = /(?:\b(?:19|20)\d{2}\b|за\s+(?:\d{4}|год|период|квартал|месяц|январ|феврал|март|апрел|ма[йя]|июн|июл|август|сентябр|октябр|ноябр|декабр)|\b[1-4]\s*(?:кв|квартал))/iu.test(normalized);
const hasInspectionCue = /(?:что\s+с|позици|основан|не\s+хватает|налогов[а-яё]*\s+вывод|вывод|декларац|книга\s+(?:продаж|покупок)|расшифр|разбор)/iu.test(normalized); const hasInspectionCue = /(?:что\s+с|позици|основан|не\s+хватает|налогов[а-яё]*\s+вывод|вывод|декларац|книга\s+(?:продаж|покупок)|расшифр|разбор)/iu.test(normalized);
const forecastOnlyCue = /(?:прогноз|план|примерн|ориентировочн)/iu.test(normalized) && !hasInspectionCue; const forecastOnlyCue = /(?:прогноз|план|примерн|ориентировочн)/iu.test(normalized) && !hasInspectionCue;
return hasPeriodCue && hasInspectionCue && !forecastOnlyCue; const hasVatMovementInspectionCue = /(?:покаж|движен|операц|по\s+сч(?:е|ё)т|покаж|движен|операц|РїРѕ\s+СЃС‡(?:Рµ|С)С|show|movement|movements|operation|operations|account)/iu.test(normalized);
return hasPeriodCue && (hasInspectionCue || hasVatMovementInspectionCue) && !forecastOnlyCue;
} }
function resolveUnicodeAddressIntentBridge(text) { function resolveUnicodeAddressIntentBridge(text) {
const normalized = String(text ?? "").trim().toLowerCase(); const normalized = String(text ?? "").trim().toLowerCase();
@ -2044,6 +2045,16 @@ function resolveAddressIntent(userMessage) {
reasons reasons
}; };
} }
const hasExplicitVatLiabilityPeriodBridge = /(?:\u043d\u0434\u0441|vat)/iu.test(text) &&
/(?:\b(?:19|20)\d{2}\b|\u0437\u0430\s+(?:\d{4}|\u0433\u043e\u0434|\u043f\u0435\u0440\u0438\u043e\u0434|\u043a\u0432\u0430\u0440\u0442\u0430\u043b|\u043c\u0435\u0441\u044f\u0446))/iu.test(text) &&
/(?:\u043a\u0430\u043a\u043e\u0439|\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u043d\u0430\u0447\u0438\u0441\u043b|\u0443\u043f\u043b\u0430\u0447|\u0443\u043f\u043b\u0430\u0442|\u043f\u0440\u043e\u0434\u0430\u0436|\u043f\u043e\u043a\u0443\u043f|\u0432\u044b\u0447\u0435\u0442|\u043a\s+\u0443\u043f\u043b\u0430\u0442|\u043a\s+\u0432\u043e\u0437\u043c\u0435\u0449|\u043f\u043e\u0437\u0438\u0446|liability|payable|charged|paid|sales|purchase|deduction|position)/iu.test(text);
if (hasExplicitVatLiabilityPeriodBridge) {
return {
intent: "vat_liability_confirmed_for_tax_period",
confidence: "high",
reasons: ["vat_liability_explicit_period_bridge_signal_detected"]
};
}
const hasLooseVatPayableBridge = /(?:\u043d\u0434\u0441|vat)/iu.test(text) && const hasLooseVatPayableBridge = /(?:\u043d\u0434\u0441|vat)/iu.test(text) &&
/(?:\u043a\u0430\u043a\u043e\u0439\s+\u043d\u0434\u0441\s+(?:(?:\u043d\u0430\u043c|(?:\u043c\u044b\s+)?\u0434\u043e\u043b\u0436\u043d\u044b)\s+)?(?:\u043d\u0430\u0434\u043e|\u043d\u0443\u0436\u043d\u043e|\u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e)|(?:\u043d\u0430\u043c|\u043c\u044b\s+)?\u043d\u0430\u0434\u043e\s+(?:\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u0442\u044c|\u0441\u0433\u0440\u0443\u0437\u0438\u0442\u044c)|(?:\u043d\u0430\u043c|\u043c\u044b\s+)?\u043d\u0443\u0436\u043d\u043e\s+(?:\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u0442\u044c|\u0441\u0433\u0440\u0443\u0437\u0438\u0442\u044c)|\u043c\u044b\s+\u0434\u043e\u043b\u0436\u043d\u044b\s+(?:\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u0442\u044c|\u0441\u0433\u0440\u0443\u0437\u0438\u0442\u044c)|\u043d\u0434\u0441\s+\u043a\s+\u0443\u043f\u043b\u0430\u0442\u0435)/iu.test(text) && /(?:\u043a\u0430\u043a\u043e\u0439\s+\u043d\u0434\u0441\s+(?:(?:\u043d\u0430\u043c|(?:\u043c\u044b\s+)?\u0434\u043e\u043b\u0436\u043d\u044b)\s+)?(?:\u043d\u0430\u0434\u043e|\u043d\u0443\u0436\u043d\u043e|\u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e)|(?:\u043d\u0430\u043c|\u043c\u044b\s+)?\u043d\u0430\u0434\u043e\s+(?:\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u0442\u044c|\u0441\u0433\u0440\u0443\u0437\u0438\u0442\u044c)|(?:\u043d\u0430\u043c|\u043c\u044b\s+)?\u043d\u0443\u0436\u043d\u043e\s+(?:\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u0442\u044c|\u0441\u0433\u0440\u0443\u0437\u0438\u0442\u044c)|\u043c\u044b\s+\u0434\u043e\u043b\u0436\u043d\u044b\s+(?:\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u0442\u044c|\u0441\u0433\u0440\u0443\u0437\u0438\u0442\u044c)|\u043d\u0434\u0441\s+\u043a\s+\u0443\u043f\u043b\u0430\u0442\u0435)/iu.test(text) &&
/(?:\u0437\u0430\s+(?:\d{4}|(?:\u044f\u043d\u0432\u0430\u0440|\u0444\u0435\u0432\u0440\u0430\u043b|\u043c\u0430\u0440\u0442|\u0430\u043f\u0440\u0435\u043b|\u043c\u0430[\u0439\u044f]|\u0438\u044e\u043d|\u0438\u044e\u043b|\u0430\u0432\u0433\u0443\u0441\u0442|\u0441\u0435\u043d\u0442\u044f\u0431\u0440|\u043e\u043a\u0442\u044f\u0431\u0440|\u043d\u043e\u044f\u0431\u0440|\u0434\u0435\u043a\u0430\u0431\u0440)\S*(?:\s+(?:19|20)\d{2})?)|\u043d\u0430\s+(?:\u044f\u043d\u0432\u0430\u0440|\u0444\u0435\u0432\u0440\u0430\u043b|\u043c\u0430\u0440\u0442|\u0430\u043f\u0440\u0435\u043b|\u043c\u0430[\u0439\u044f]|\u0438\u044e\u043d|\u0438\u044e\u043b|\u0430\u0432\u0433\u0443\u0441\u0442|\u0441\u0435\u043d\u0442\u044f\u0431\u0440|\u043e\u043a\u0442\u044f\u0431\u0440|\u043d\u043e\u044f\u0431\u0440|\u0434\u0435\u043a\u0430\u0431\u0440)\S*(?:\s+(?:19|20)\d{2})?|\u0432\s+(?:\u044f\u043d\u0432\u0430\u0440|\u0444\u0435\u0432\u0440\u0430\u043b|\u043c\u0430\u0440\u0442|\u0430\u043f\u0440\u0435\u043b|\u043c\u0430[\u0439\u044f]|\u0438\u044e\u043d|\u0438\u044e\u043b|\u0430\u0432\u0433\u0443\u0441\u0442|\u0441\u0435\u043d\u0442\u044f\u0431\u0440|\u043e\u043a\u0442\u044f\u0431\u0440|\u043d\u043e\u044f\u0431\u0440|\u0434\u0435\u043a\u0430\u0431\u0440)\S*(?:\s+(?:19|20)\d{2})?|\b[1-4]\s*(?:\u043a\u0432\u0430\u0440\u0442\u0430\u043b|\u043a\u0432\.?)\b)/iu.test(text); /(?:\u0437\u0430\s+(?:\d{4}|(?:\u044f\u043d\u0432\u0430\u0440|\u0444\u0435\u0432\u0440\u0430\u043b|\u043c\u0430\u0440\u0442|\u0430\u043f\u0440\u0435\u043b|\u043c\u0430[\u0439\u044f]|\u0438\u044e\u043d|\u0438\u044e\u043b|\u0430\u0432\u0433\u0443\u0441\u0442|\u0441\u0435\u043d\u0442\u044f\u0431\u0440|\u043e\u043a\u0442\u044f\u0431\u0440|\u043d\u043e\u044f\u0431\u0440|\u0434\u0435\u043a\u0430\u0431\u0440)\S*(?:\s+(?:19|20)\d{2})?)|\u043d\u0430\s+(?:\u044f\u043d\u0432\u0430\u0440|\u0444\u0435\u0432\u0440\u0430\u043b|\u043c\u0430\u0440\u0442|\u0430\u043f\u0440\u0435\u043b|\u043c\u0430[\u0439\u044f]|\u0438\u044e\u043d|\u0438\u044e\u043b|\u0430\u0432\u0433\u0443\u0441\u0442|\u0441\u0435\u043d\u0442\u044f\u0431\u0440|\u043e\u043a\u0442\u044f\u0431\u0440|\u043d\u043e\u044f\u0431\u0440|\u0434\u0435\u043a\u0430\u0431\u0440)\S*(?:\s+(?:19|20)\d{2})?|\u0432\s+(?:\u044f\u043d\u0432\u0430\u0440|\u0444\u0435\u0432\u0440\u0430\u043b|\u043c\u0430\u0440\u0442|\u0430\u043f\u0440\u0435\u043b|\u043c\u0430[\u0439\u044f]|\u0438\u044e\u043d|\u0438\u044e\u043b|\u0430\u0432\u0433\u0443\u0441\u0442|\u0441\u0435\u043d\u0442\u044f\u0431\u0440|\u043e\u043a\u0442\u044f\u0431\u0440|\u043d\u043e\u044f\u0431\u0440|\u0434\u0435\u043a\u0430\u0431\u0440)\S*(?:\s+(?:19|20)\d{2})?|\b[1-4]\s*(?:\u043a\u0432\u0430\u0440\u0442\u0430\u043b|\u043a\u0432\.?)\b)/iu.test(text);

View File

@ -197,6 +197,51 @@ const OPEN_CONTRACTS_CONFIRMED_AS_OF_QUERY_TEMPLATE = `
УПОРЯДОЧИТЬ ПО УПОРЯДОЧИТЬ ПО
Сумма __ORDER_DIRECTION__ Сумма __ORDER_DIRECTION__
`; `;
const DEBT_DUE_DATE_AGING_QUERY_TEMPLATE = `
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
__AS_OF_EXPR__ КАК Период,
"DUE_DATE_OPEN_BALANCE" КАК Регистратор,
ПРЕДСТАВЛЕНИЕ(Остатки.Счет) КАК СчетДт,
"" КАК СчетКт,
Остатки.СуммаРазвернутыйОстатокДт КАК Сумма,
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто1) КАК СубконтоДт1,
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто2) КАК СубконтоДт2,
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто3) КАК СубконтоДт3,
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто1) КАК Контрагент,
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто2) КАК Договор,
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто3) КАК ДокументРасчетов,
ПРЕДСТАВЛЕНИЕ(Остатки.Организация) КАК Организация,
ВЫРАЗИТЬ(Остатки.Субконто2 КАК Справочник.ДоговорыКонтрагентов).Дата КАК ДатаДоговора,
ВЫРАЗИТЬ(Остатки.Субконто2 КАК Справочник.ДоговорыКонтрагентов).УстановленСрокОплаты КАК УстановленСрокОплаты,
ВЫРАЗИТЬ(Остатки.Субконто2 КАК Справочник.ДоговорыКонтрагентов).СрокОплаты КАК СрокОплаты,
"debit_open_balance" КАК НаправлениеОстатка
ИЗ
РегистрБухгалтерии.Хозрасчетный.Остатки(__AS_OF_EXPR__, , , ) КАК Остатки
__WHERE_DT__
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
__AS_OF_EXPR__ КАК Период,
"DUE_DATE_OPEN_BALANCE" КАК Регистратор,
"" КАК СчетДт,
ПРЕДСТАВЛЕНИЕ(Остатки.Счет) КАК СчетКт,
Остатки.СуммаРазвернутыйОстатокКт КАК Сумма,
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто1) КАК СубконтоДт1,
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто2) КАК СубконтоДт2,
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто3) КАК СубконтоДт3,
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто1) КАК Контрагент,
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто2) КАК Договор,
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто3) КАК ДокументРасчетов,
ПРЕДСТАВЛЕНИЕ(Остатки.Организация) КАК Организация,
ВЫРАЗИТЬ(Остатки.Субконто2 КАК Справочник.ДоговорыКонтрагентов).Дата КАК ДатаДоговора,
ВЫРАЗИТЬ(Остатки.Субконто2 КАК Справочник.ДоговорыКонтрагентов).УстановленСрокОплаты КАК УстановленСрокОплаты,
ВЫРАЗИТЬ(Остатки.Субконто2 КАК Справочник.ДоговорыКонтрагентов).СрокОплаты КАК СрокОплаты,
"credit_open_balance" КАК НаправлениеОстатка
ИЗ
РегистрБухгалтерии.Хозрасчетный.Остатки(__AS_OF_EXPR__, , , ) КАК Остатки
__WHERE_KT__
УПОРЯДОЧИТЬ ПО
Сумма __ORDER_DIRECTION__
`;
const VAT_PAYABLE_CONFIRMED_AS_OF_QUERY_TEMPLATE = ` const VAT_PAYABLE_CONFIRMED_AS_OF_QUERY_TEMPLATE = `
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__ ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
__AS_OF_EXPR__ КАК Период, __AS_OF_EXPR__ КАК Период,
@ -723,7 +768,7 @@ const BASE_RECIPES = [
purpose: "Build customer value ranking and incoming deal profile from bank inflow docs", purpose: "Build customer value ranking and incoming deal profile from bank inflow docs",
required_filters: [], required_filters: [],
optional_filters: ["period_from", "period_to", "organization", "counterparty", "contract", "limit", "sort"], optional_filters: ["period_from", "period_to", "organization", "counterparty", "contract", "limit", "sort"],
default_limit: 20, default_limit: 200,
account_scope_mode: "preferred", account_scope_mode: "preferred",
query_template: "customer_revenue_profile" query_template: "customer_revenue_profile"
}, },
@ -733,7 +778,7 @@ const BASE_RECIPES = [
purpose: "Build supplier payout ranking and outgoing deal profile from bank outflow docs", purpose: "Build supplier payout ranking and outgoing deal profile from bank outflow docs",
required_filters: [], required_filters: [],
optional_filters: ["period_from", "period_to", "organization", "counterparty", "contract", "limit", "sort"], optional_filters: ["period_from", "period_to", "organization", "counterparty", "contract", "limit", "sort"],
default_limit: 20, default_limit: 200,
account_scope_mode: "preferred", account_scope_mode: "preferred",
query_template: "supplier_payout_profile" query_template: "supplier_payout_profile"
}, },
@ -778,6 +823,17 @@ const BASE_RECIPES = [
account_scope_mode: "preferred", account_scope_mode: "preferred",
query_template: "vat_liability_confirmed_tax_period_profile" query_template: "vat_liability_confirmed_tax_period_profile"
}, },
{
recipe_id: "address_accounting_financial_result_for_organization_v1",
intent: "accounting_financial_result_for_organization",
purpose: "Build reviewed accounting financial-result aggregate from 90/91/99 period-close movements",
required_filters: ["period_from", "period_to"],
optional_filters: ["organization", "limit", "sort"],
default_limit: 32,
account_scope: ["90", "91", "99"],
account_scope_mode: "strict",
query_template: "accounting_financial_result_profile"
},
{ {
recipe_id: "address_inventory_on_hand_as_of_date_v1", recipe_id: "address_inventory_on_hand_as_of_date_v1",
intent: "inventory_on_hand_as_of_date", intent: "inventory_on_hand_as_of_date",
@ -888,6 +944,17 @@ const BASE_RECIPES = [
account_scope_mode: "strict", account_scope_mode: "strict",
query_template: "open_contracts_confirmed_as_of_balance_profile" query_template: "open_contracts_confirmed_as_of_balance_profile"
}, },
{
recipe_id: "address_debt_due_date_aging_for_organization_v1",
intent: "debt_due_date_aging_for_organization",
purpose: "Check open 60/62/76 settlements against contract payment-term fields and settlement document dates before claiming overdue debt",
required_filters: ["as_of_date"],
optional_filters: ["period_from", "period_to", "organization", "counterparty", "contract", "limit", "sort"],
default_limit: 400,
account_scope: ["60", "62", "76"],
account_scope_mode: "strict",
query_template: "debt_due_date_aging_profile"
},
{ {
recipe_id: "address_contracts_by_counterparty_v1", recipe_id: "address_contracts_by_counterparty_v1",
intent: "list_contracts_by_counterparty", intent: "list_contracts_by_counterparty",
@ -1093,6 +1160,32 @@ function buildContractValueWhereClause(filters, fieldPath, contractFieldPath) {
`${contractFieldPath} <> ЗНАЧЕНИЕ(Справочник.ДоговорыКонтрагентов.ПустаяСсылка)` `${contractFieldPath} <> ЗНАЧЕНИЕ(Справочник.ДоговорыКонтрагентов.ПустаяСсылка)`
]); ]);
} }
function buildContractReferenceCondition(filters, fieldPaths) {
const contract = typeof filters.contract === "string" ? filters.contract.trim() : "";
if (!contract) {
return null;
}
const contractTokens = Array.from(new Set(contract
.split(/[^A-Za-zА-Яа-яЁё0-9]+/u)
.map((token) => token.trim())
.filter((token) => token.length >= 3)
.filter((token) => !["договор", "дог"].includes(token.toLowerCase()))));
const tokens = contractTokens.length > 0 ? contractTokens : [contract];
const clauses = fieldPaths
.map((fieldPath) => String(fieldPath ?? "").trim())
.filter((fieldPath) => fieldPath.length > 0)
.map((fieldPath) => {
const tokenConditions = tokens.map((token) => {
const escapedToken = toQueryStringLiteral(token);
return `${fieldPath}.Наименование ПОДОБНО "%${escapedToken}%"`;
});
return tokenConditions.length === 1 ? tokenConditions[0] : `(${tokenConditions.join(" И ")})`;
});
if (clauses.length === 0) {
return null;
}
return clauses.length === 1 ? clauses[0] : `(${clauses.join(" ИЛИ ")})`;
}
function normalizeAccountTokenForQuery(value) { function normalizeAccountTokenForQuery(value) {
const source = String(value ?? "").trim().replace(",", "."); const source = String(value ?? "").trim().replace(",", ".");
const match = source.match(/^(\d{2})(?:\.(\d{1,2}))?/); const match = source.match(/^(\d{2})(?:\.(\d{1,2}))?/);
@ -1178,6 +1271,35 @@ function buildAccountPrefixPredicate(fieldPath, prefixes) {
const clauses = normalizedPrefixes.map((prefix) => `ПОДСТРОКА(ЕСТЬNULL(${fieldPath}.Код, ""), 1, ${prefix.length}) = "${prefix}"`); const clauses = normalizedPrefixes.map((prefix) => `ПОДСТРОКА(ЕСТЬNULL(${fieldPath}.Код, ""), 1, ${prefix.length}) = "${prefix}"`);
return clauses.length === 1 ? clauses[0] : `(${clauses.join(" ИЛИ ")})`; return clauses.length === 1 ? clauses[0] : `(${clauses.join(" ИЛИ ")})`;
} }
function buildDebtDueDateAgingWhereClause(filters, amountFieldPath, accountPredicate) {
const conditions = [
`${amountFieldPath} > 0`,
`(${accountPredicate})`,
buildOrganizationReferenceCondition(filters, ["Остатки.Организация"]),
buildCounterpartyReferenceCondition(filters, ["Остатки.Субконто1"]),
buildContractReferenceCondition(filters, ["Остатки.Субконто2"])
].filter((item) => Boolean(item));
return `ГДЕ\n ${conditions.join("\n И ")}`;
}
function buildDebtDueDateAgingQuery(filters, resolvedLimit) {
const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
? toDateTimeExpr(filters.as_of_date, true)
: null) ??
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0
? toDateTimeExpr(filters.period_to, true)
: null) ??
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0
? toDateTimeExpr(filters.period_from, true)
: null) ??
"ТЕКУЩАЯДАТА()";
const accountPredicate = buildAccountPrefixPredicate("Остатки.Счет", ["60", "62", "76"]);
return DEBT_DUE_DATE_AGING_QUERY_TEMPLATE
.replaceAll("__LIMIT__", String(resolvedLimit))
.replaceAll("__AS_OF_EXPR__", asOfExpr)
.replaceAll("__WHERE_DT__", buildDebtDueDateAgingWhereClause(filters, "Остатки.СуммаРазвернутыйОстатокДт", accountPredicate))
.replaceAll("__WHERE_KT__", buildDebtDueDateAgingWhereClause(filters, "Остатки.СуммаРазвернутыйОстатокКт", accountPredicate))
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
}
function buildInventoryMovementQuery(filters, resolvedLimit, side) { function buildInventoryMovementQuery(filters, resolvedLimit, side) {
const debitPredicate = buildAccountPrefixPredicate("Движения.СчетДт", ["41.01"]); const debitPredicate = buildAccountPrefixPredicate("Движения.СчетДт", ["41.01"]);
const creditPredicate = buildAccountPrefixPredicate("Движения.СчетКт", ["41.01"]); const creditPredicate = buildAccountPrefixPredicate("Движения.СчетКт", ["41.01"]);
@ -1246,6 +1368,148 @@ function buildCounterpartyReferenceCondition(filters, fieldPaths) {
} }
return clauses.length === 1 ? clauses[0] : `(${clauses.join(" ИЛИ ")})`; return clauses.length === 1 ? clauses[0] : `(${clauses.join(" ИЛИ ")})`;
} }
const ORGANIZATION_REFERENCE_STOP_WORDS = new Set([
"ооо",
"зао",
"оао",
"ао",
"пао",
"ип",
"на",
"за",
"по",
"конец",
"начало",
"год",
"года",
"период",
"можно",
"точно",
"понять",
"какая",
"какой",
"какие",
"какую",
"компания",
"компании",
"организация",
"организации",
"дебиторка",
"дебиторки",
"кредиторка",
"кредиторки",
"просрочена",
"просроченные",
"просрочка",
"срок",
"оплаты",
"прибыль",
"маржа",
"ндс"
]);
const ORGANIZATION_REFERENCE_BOUNDARY_WORDS = new Set([
"на",
"за",
"конец",
"начало",
"можно",
"точно",
"понять",
"какая",
"какой",
"какие",
"какую",
"дебиторка",
"дебиторки",
"кредиторка",
"кредиторки",
"просрочена",
"просроченные",
"просрочка",
"прибыль",
"маржа",
"ндс"
]);
function organizationReferenceTokens(organization) {
const rawTokens = organization
.split(/[^A-Za-zА-Яа-яЁё0-9]+/u)
.map((token) => token.trim())
.filter((token) => token.length > 0);
const boundaryIndex = rawTokens.findIndex((token) => {
const lower = token.toLowerCase();
return /^\d+$/.test(token) || ORGANIZATION_REFERENCE_BOUNDARY_WORDS.has(lower);
});
const scopedTokens = boundaryIndex > 0 ? rawTokens.slice(0, boundaryIndex) : rawTokens;
return Array.from(new Set(scopedTokens
.filter((token) => token.length >= 3)
.filter((token) => !/^\d+$/.test(token))
.filter((token) => !ORGANIZATION_REFERENCE_STOP_WORDS.has(token.toLowerCase())))).slice(0, 4);
}
function buildOrganizationReferenceCondition(filters, fieldPaths) {
const organization = typeof filters.organization === "string" ? filters.organization.trim() : "";
if (!organization) {
return null;
}
const organizationTokens = organizationReferenceTokens(organization);
const tokens = organizationTokens.length > 0 ? organizationTokens : [organization];
const clauses = fieldPaths
.map((fieldPath) => String(fieldPath ?? "").trim())
.filter((fieldPath) => fieldPath.length > 0)
.map((fieldPath) => {
const tokenConditions = tokens.map((token) => {
const escapedToken = toQueryStringLiteral(token);
return `(Организации.Наименование ПОДОБНО "%${escapedToken}%" ИЛИ Организации.НаименованиеПолное ПОДОБНО "%${escapedToken}%")`;
});
const referenceSubquery = `(ВЫБРАТЬ Организации.Ссылка ИЗ Справочник.Организации КАК Организации ` +
`ГДЕ ${tokenConditions.length === 1 ? tokenConditions[0] : tokenConditions.join(" И ")})`;
return `${fieldPath} В ${referenceSubquery}`;
});
if (clauses.length === 0) {
return null;
}
return clauses.length === 1 ? clauses[0] : `(${clauses.join(" ИЛИ ")})`;
}
function buildAccountingFinancialResultAggregateSelect(filters, marker, debitLabel, creditLabel, debitPrefixes, creditPrefixes) {
const whereClause = buildWhereClause(filters, "Движения.Период", [
debitPrefixes.length > 0 ? buildAccountPrefixPredicate("Движения.СчетДт", debitPrefixes) : null,
creditPrefixes.length > 0 ? buildAccountPrefixPredicate("Движения.СчетКт", creditPrefixes) : null,
buildOrganizationReferenceCondition(filters, ["Движения.Организация"])
].filter((item) => Boolean(item)));
return `
ВЫБРАТЬ
ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0) КАК Период,
"${marker}" КАК Регистратор,
"${debitLabel}" КАК СчетДт,
"${creditLabel}" КАК СчетКт,
ЕСТЬNULL(СУММА(Движения.Сумма), 0) КАК Сумма,
"" КАК СубконтоДт1,
"" КАК СубконтоДт2,
"" КАК СубконтоДт3,
"" КАК СубконтоКт1,
"" КАК СубконтоКт2,
"" КАК СубконтоКт3,
"" КАК Организация
ИЗ
РегистрБухгалтерии.Хозрасчетный.ДвиженияССубконто КАК Движения
${whereClause}`;
}
function buildAccountingFinancialResultQuery(filters) {
const rows = [
["ACC90_REVENUE_KT", "ANY", "90.01", [], ["90.01"]],
["ACC90_COST_DT", "90.02", "ANY", ["90.02"], []],
["ACC90_SELLING_DT", "90.07", "ANY", ["90.07"], []],
["ACC90_ADMIN_DT", "90.08", "ANY", ["90.08"], []],
["ACC90_RESULT_TO_99_PROFIT", "90.09", "99", ["90.09"], ["99"]],
["ACC90_RESULT_FROM_99_LOSS", "99", "90.09", ["99"], ["90.09"]],
["ACC91_RESULT_TO_99_PROFIT", "91.09", "99", ["91.09"], ["99"]],
["ACC91_RESULT_FROM_99_LOSS", "99", "91.09", ["99"], ["91.09"]],
["ACC99_TO84_PROFIT_TRANSFER", "99", "84", ["99"], ["84"]],
["ACC84_TO99_LOSS_TRANSFER", "84", "99", ["84"], ["99"]]
];
return rows
.map(([marker, debitLabel, creditLabel, debitPrefixes, creditPrefixes]) => buildAccountingFinancialResultAggregateSelect(filters, marker, debitLabel, creditLabel, [...debitPrefixes], [...creditPrefixes]).trim())
.join("\nОБЪЕДИНИТЬ ВСЕ\n");
}
function buildInventorySaleDocumentQuery(filters, resolvedLimit) { function buildInventorySaleDocumentQuery(filters, resolvedLimit) {
const itemCondition = buildInventoryItemReferenceCondition(filters, ["Товары.Номенклатура"]); const itemCondition = buildInventoryItemReferenceCondition(filters, ["Товары.Номенклатура"]);
return INVENTORY_SALE_DOCUMENTS_QUERY_TEMPLATE return INVENTORY_SALE_DOCUMENTS_QUERY_TEMPLATE
@ -1324,6 +1588,8 @@ function maxLimitForIntent(intent) {
intent === "contract_usage_and_value" || intent === "contract_usage_and_value" ||
intent === "vat_payable_forecast" || intent === "vat_payable_forecast" ||
intent === "vat_liability_confirmed_for_tax_period" || intent === "vat_liability_confirmed_for_tax_period" ||
intent === "accounting_financial_result_for_organization" ||
intent === "debt_due_date_aging_for_organization" ||
intent === "inventory_on_hand_as_of_date" || intent === "inventory_on_hand_as_of_date" ||
intent === "inventory_purchase_provenance_for_item" || intent === "inventory_purchase_provenance_for_item" ||
intent === "inventory_purchase_documents_for_item" || intent === "inventory_purchase_documents_for_item" ||
@ -1374,7 +1640,8 @@ function buildAddressRecipePlan(recipe, filters) {
recipe.query_template === "counterparty_roles_profile" || recipe.query_template === "counterparty_roles_profile" ||
recipe.query_template === "contract_usage_profile" || recipe.query_template === "contract_usage_profile" ||
recipe.query_template === "vat_payable_forecast_profile" || recipe.query_template === "vat_payable_forecast_profile" ||
recipe.query_template === "vat_liability_confirmed_tax_period_profile"; recipe.query_template === "vat_liability_confirmed_tax_period_profile" ||
recipe.query_template === "accounting_financial_result_profile";
const baseLimit = typeof filters.limit === "number" && Number.isFinite(filters.limit) const baseLimit = typeof filters.limit === "number" && Number.isFinite(filters.limit)
? Math.max(1, Math.min(maxLimit, Math.trunc(filters.limit))) ? Math.max(1, Math.min(maxLimit, Math.trunc(filters.limit)))
: recipe.default_limit; : recipe.default_limit;
@ -1467,97 +1734,65 @@ function buildAddressRecipePlan(recipe, filters) {
.replaceAll("__WHERE_CLAUSE__", buildManagementWhereClause(filters, "Движения.Период")) .replaceAll("__WHERE_CLAUSE__", buildManagementWhereClause(filters, "Движения.Период"))
.replaceAll("__PERIOD_TO_EXPR__", periodToExpr); .replaceAll("__PERIOD_TO_EXPR__", periodToExpr);
})() })()
: recipe.query_template === "vat_payable_confirmed_as_of_balance_profile" : recipe.query_template === "accounting_financial_result_profile"
? (() => { ? buildAccountingFinancialResultQuery(filters)
const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0 : recipe.query_template === "debt_due_date_aging_profile"
? toDateTimeExpr(filters.as_of_date, true) ? buildDebtDueDateAgingQuery(filters, resolvedLimit)
: null) ?? : recipe.query_template === "vat_payable_confirmed_as_of_balance_profile"
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0 ? (() => {
? toDateTimeExpr(filters.period_to, true) const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
: null) ?? ? toDateTimeExpr(filters.as_of_date, true)
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0
? toDateTimeExpr(filters.period_from, true)
: null) ??
"ТЕКУЩАЯДАТА()";
return VAT_PAYABLE_CONFIRMED_AS_OF_QUERY_TEMPLATE
.replaceAll("__LIMIT__", String(resolvedLimit))
.replaceAll("__AS_OF_EXPR__", asOfExpr)
.replaceAll("__VAT_PAYABLE_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", config_1.VAT_PAYABLE_68_PREFIXES))
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
})()
: recipe.query_template === "inventory_on_hand_as_of_balance_profile"
? (() => {
const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
? toDateTimeExpr(filters.as_of_date, true)
: null) ??
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0
? toDateTimeExpr(filters.period_to, true)
: null) ?? : null) ??
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0 (typeof filters.period_to === "string" && filters.period_to.trim().length > 0
? toDateTimeExpr(filters.period_from, true) ? toDateTimeExpr(filters.period_to, true)
: null) ?? : null) ??
"ТЕКУЩАЯДАТА()"; (typeof filters.period_from === "string" && filters.period_from.trim().length > 0
return INVENTORY_ON_HAND_AS_OF_QUERY_TEMPLATE ? toDateTimeExpr(filters.period_from, true)
.replaceAll("__LIMIT__", String(resolvedLimit)) : null) ??
.replaceAll("__AS_OF_EXPR__", asOfExpr) "ТЕКУЩАЯДАТА()";
.replaceAll("__INVENTORY_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["41.01"])) return VAT_PAYABLE_CONFIRMED_AS_OF_QUERY_TEMPLATE
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort)); .replaceAll("__LIMIT__", String(resolvedLimit))
})() .replaceAll("__AS_OF_EXPR__", asOfExpr)
: recipe.query_template === "inventory_purchase_provenance_profile" .replaceAll("__VAT_PAYABLE_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", config_1.VAT_PAYABLE_68_PREFIXES))
? buildInventoryPurchaseDocumentQuery(filters, resolvedLimit) .replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
: recipe.query_template === "inventory_purchase_documents_profile" })()
? buildInventoryMovementQuery(filters, resolvedLimit, "dt") : recipe.query_template === "inventory_on_hand_as_of_balance_profile"
: recipe.query_template === "inventory_supplier_stock_overlap_profile" ? (() => {
? buildInventoryMovementQuery(filters, resolvedLimit, "dt") const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
: recipe.query_template === "inventory_sale_trace_profile" ? toDateTimeExpr(filters.as_of_date, true)
? buildInventorySaleDocumentQuery(filters, resolvedLimit) : null) ??
: recipe.query_template === "inventory_profitability_profile" (typeof filters.period_to === "string" && filters.period_to.trim().length > 0
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit) ? toDateTimeExpr(filters.period_to, true)
: recipe.query_template === "inventory_trading_margin_proxy_profile" : null) ??
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit) (typeof filters.period_from === "string" && filters.period_from.trim().length > 0
: recipe.query_template === "inventory_purchase_to_sale_chain_profile" ? toDateTimeExpr(filters.period_from, true)
: null) ??
"ТЕКУЩАЯДАТА()";
return INVENTORY_ON_HAND_AS_OF_QUERY_TEMPLATE
.replaceAll("__LIMIT__", String(resolvedLimit))
.replaceAll("__AS_OF_EXPR__", asOfExpr)
.replaceAll("__INVENTORY_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["41.01"]))
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
})()
: recipe.query_template === "inventory_purchase_provenance_profile"
? buildInventoryPurchaseDocumentQuery(filters, resolvedLimit)
: recipe.query_template === "inventory_purchase_documents_profile"
? buildInventoryMovementQuery(filters, resolvedLimit, "dt")
: recipe.query_template === "inventory_supplier_stock_overlap_profile"
? buildInventoryMovementQuery(filters, resolvedLimit, "dt")
: recipe.query_template === "inventory_sale_trace_profile"
? buildInventorySaleDocumentQuery(filters, resolvedLimit)
: recipe.query_template === "inventory_profitability_profile"
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit) ? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
: recipe.query_template === "inventory_aging_by_purchase_date_profile" : recipe.query_template === "inventory_trading_margin_proxy_profile"
? buildInventoryMovementQuery(filters, resolvedLimit, "dt") ? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
: recipe.query_template === "contracts_by_counterparty_profile" : recipe.query_template === "inventory_purchase_to_sale_chain_profile"
? CONTRACTS_BY_COUNTERPARTY_QUERY_TEMPLATE.replaceAll("__LIMIT__", String(resolvedLimit)) ? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
: recipe.query_template === "open_contracts_confirmed_as_of_balance_profile" : recipe.query_template === "inventory_aging_by_purchase_date_profile"
? (() => { ? buildInventoryMovementQuery(filters, resolvedLimit, "dt")
const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0 : recipe.query_template === "contracts_by_counterparty_profile"
? toDateTimeExpr(filters.as_of_date, true) ? CONTRACTS_BY_COUNTERPARTY_QUERY_TEMPLATE.replaceAll("__LIMIT__", String(resolvedLimit))
: null) ?? : recipe.query_template === "open_contracts_confirmed_as_of_balance_profile"
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0
? toDateTimeExpr(filters.period_to, true)
: null) ??
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0
? toDateTimeExpr(filters.period_from, true)
: null) ??
"ТЕКУЩАЯДАТА()";
return OPEN_CONTRACTS_CONFIRMED_AS_OF_QUERY_TEMPLATE
.replaceAll("__LIMIT__", String(resolvedLimit))
.replaceAll("__AS_OF_EXPR__", asOfExpr)
.replaceAll("__OPEN_CONTRACT_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["60", "62", "76"]))
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
})()
: recipe.query_template === "payables_confirmed_as_of_balance_profile"
? (() => {
const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
? toDateTimeExpr(filters.as_of_date, true)
: null) ??
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0
? toDateTimeExpr(filters.period_to, true)
: null) ??
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0
? toDateTimeExpr(filters.period_from, true)
: null) ??
"ТЕКУЩАЯДАТА()";
return PAYABLES_CONFIRMED_AS_OF_QUERY_TEMPLATE
.replaceAll("__LIMIT__", String(resolvedLimit))
.replaceAll("__AS_OF_EXPR__", asOfExpr)
.replaceAll("__PAYABLE_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["60", "76"]))
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
})()
: recipe.query_template === "receivables_confirmed_as_of_balance_profile"
? (() => { ? (() => {
const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0 const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
? toDateTimeExpr(filters.as_of_date, true) ? toDateTimeExpr(filters.as_of_date, true)
@ -1569,23 +1804,59 @@ function buildAddressRecipePlan(recipe, filters) {
? toDateTimeExpr(filters.period_from, true) ? toDateTimeExpr(filters.period_from, true)
: null) ?? : null) ??
"ТЕКУЩАЯДАТА()"; "ТЕКУЩАЯДАТА()";
return RECEIVABLES_CONFIRMED_AS_OF_QUERY_TEMPLATE return OPEN_CONTRACTS_CONFIRMED_AS_OF_QUERY_TEMPLATE
.replaceAll("__LIMIT__", String(resolvedLimit)) .replaceAll("__LIMIT__", String(resolvedLimit))
.replaceAll("__AS_OF_EXPR__", asOfExpr) .replaceAll("__AS_OF_EXPR__", asOfExpr)
.replaceAll("__RECEIVABLE_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["62", "76"])) .replaceAll("__OPEN_CONTRACT_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["60", "62", "76"]))
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort)); .replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
})() })()
: MOVEMENTS_QUERY_TEMPLATE : recipe.query_template === "payables_confirmed_as_of_balance_profile"
.replace("__LIMIT__", String(resolvedLimit)) ? (() => {
.replace("__WHERE_CLAUSE__", (() => { const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
const extraConditions = []; ? toDateTimeExpr(filters.as_of_date, true)
const accountCondition = buildMovementAccountCondition(filters); : null) ??
if (accountCondition) { (typeof filters.period_to === "string" && filters.period_to.trim().length > 0
extraConditions.push(accountCondition); ? toDateTimeExpr(filters.period_to, true)
} : null) ??
return buildWhereClause(filters, "Движения.Период", extraConditions); (typeof filters.period_from === "string" && filters.period_from.trim().length > 0
})()) ? toDateTimeExpr(filters.period_from, true)
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort)); : null) ??
"ТЕКУЩАЯДАТА()";
return PAYABLES_CONFIRMED_AS_OF_QUERY_TEMPLATE
.replaceAll("__LIMIT__", String(resolvedLimit))
.replaceAll("__AS_OF_EXPR__", asOfExpr)
.replaceAll("__PAYABLE_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["60", "76"]))
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
})()
: recipe.query_template === "receivables_confirmed_as_of_balance_profile"
? (() => {
const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
? toDateTimeExpr(filters.as_of_date, true)
: null) ??
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0
? toDateTimeExpr(filters.period_to, true)
: null) ??
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0
? toDateTimeExpr(filters.period_from, true)
: null) ??
"ТЕКУЩАЯДАТА()";
return RECEIVABLES_CONFIRMED_AS_OF_QUERY_TEMPLATE
.replaceAll("__LIMIT__", String(resolvedLimit))
.replaceAll("__AS_OF_EXPR__", asOfExpr)
.replaceAll("__RECEIVABLE_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["62", "76"]))
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
})()
: MOVEMENTS_QUERY_TEMPLATE
.replace("__LIMIT__", String(resolvedLimit))
.replace("__WHERE_CLAUSE__", (() => {
const extraConditions = [];
const accountCondition = buildMovementAccountCondition(filters);
if (accountCondition) {
extraConditions.push(accountCondition);
}
return buildWhereClause(filters, "Движения.Период", extraConditions);
})())
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
return { return {
recipe, recipe,
query, query,

View File

@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
exports.contractCandidatesFromRows = contractCandidatesFromRows; exports.contractCandidatesFromRows = contractCandidatesFromRows;
exports.composeFactualReply = composeFactualReply; exports.composeFactualReply = composeFactualReply;
exports.inferReplyType = inferReplyType; exports.inferReplyType = inferReplyType;
const assistantOrganizationMatcher_1 = require("../assistantOrganizationMatcher");
const replyPackaging_1 = require("./replyPackaging"); const replyPackaging_1 = require("./replyPackaging");
const counterpartyAnalyticsReplyBuilders_1 = require("./counterpartyAnalyticsReplyBuilders"); const counterpartyAnalyticsReplyBuilders_1 = require("./counterpartyAnalyticsReplyBuilders");
const inventoryReplyBuilders_1 = require("./inventoryReplyBuilders"); const inventoryReplyBuilders_1 = require("./inventoryReplyBuilders");
@ -515,10 +516,12 @@ function detectValueRankingFocus(userMessage) {
if (asksTotalMoneyEarned) { if (asksTotalMoneyEarned) {
return "total_flow"; return "total_flow";
} }
const hasCounterpartyRankingSubject = /(?:клиент|заказчик|покупател|контрагент|customer|client|counterpart|\u043a\u043b\u0438\u0435\u043d\u0442|\u0437\u0430\u043a\u0430\u0437\u0447\u0438\u043a|\u043f\u043e\u043a\u0443\u043f\u0430\u0442\u0435\u043b|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442)/iu.test(text);
const asksExplicitYearBreakdown = /(?:по\s+годам|за\s+какие\s+годы|динамик\w*\s+по\s+год|yearly\s+breakdown|by\s+year|\u043f\u043e\s+\u0433\u043e\u0434\u0430\u043c|\u0437\u0430\s+\u043a\u0430\u043a\u0438\u0435\s+\u0433\u043e\u0434\u044b|\u0434\u0438\u043d\u0430\u043c\u0438\u043a\w*\s+\u043f\u043e\s+\u0433\u043e\u0434)/iu.test(text);
const asksYearlyRevenueRanking = /(?:доходн|выручк|оборот|прибыл|деньг|денег|revenue|turnover|income)/iu.test(text) && const asksYearlyRevenueRanking = /(?:доходн|выручк|оборот|прибыл|деньг|денег|revenue|turnover|income)/iu.test(text) &&
/(?:год|года|годы|year|years|по\s+годам)/iu.test(text) && /(?:год|года|годы|year|years|по\s+годам)/iu.test(text) &&
/(?:сам(?:ый|ая|ое|ые)|топ|луч|best|max|наибольш|больше)/iu.test(text); /(?:сам(?:ый|ая|ое|ые)|топ|луч|best|max|наибольш|больше)/iu.test(text);
if (asksYearlyRevenueRanking) { if (asksYearlyRevenueRanking && (!hasCounterpartyRankingSubject || asksExplicitYearBreakdown)) {
return "top_years_by_total"; return "top_years_by_total";
} }
if (/(?:сам(?:ый|ая|ое|ые)\s+высок[а-яё]*|highest|largest)\s+чек|(?:max\s+check|чек\s+макс)/iu.test(text)) { if (/(?:сам(?:ый|ая|ое|ые)\s+высок[а-яё]*|highest|largest)\s+чек|(?:max\s+check|чек\s+макс)/iu.test(text)) {
@ -2516,7 +2519,7 @@ function composeFactualReplyBody(intent, rows, options = {}) {
const nonErrorProbeRows = orderedProbeRows.filter((item) => item.status !== "error"); const nonErrorProbeRows = orderedProbeRows.filter((item) => item.status !== "error");
const visibleProbeRows = (nonErrorProbeRows.length > 0 ? nonErrorProbeRows : orderedProbeRows).slice(0, 6); const visibleProbeRows = (nonErrorProbeRows.length > 0 ? nonErrorProbeRows : orderedProbeRows).slice(0, 6);
const erroredSources = vatProbe.probedSources.filter((item) => item.status === "error").length; const erroredSources = vatProbe.probedSources.filter((item) => item.status === "error").length;
lines.push("", "Покрытие VAT-источников через MCP:", `- Найдено VAT-объектов: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`, `- Прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`, `- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.`, `- Источников с ошибкой запроса: ${formatNumberWithDots(erroredSources)}.`); lines.push("", "Покрытие VAT-источников в 1С:", `- Найдено VAT-объектов: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`, `- Прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`, `- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.`, `- Источников с ошибкой запроса: ${formatNumberWithDots(erroredSources)}.`);
if (visibleProbeRows.length > 0) { if (visibleProbeRows.length > 0) {
lines.push(...visibleProbeRows.map((item, index) => { lines.push(...visibleProbeRows.map((item, index) => {
const name = item.synonym ? `${item.fullName} (${item.synonym})` : item.fullName; const name = item.synonym ? `${item.fullName} (${item.synonym})` : item.fullName;
@ -2536,7 +2539,7 @@ function composeFactualReplyBody(intent, rows, options = {}) {
lines.push("- Сумма прогноза выше рассчитана строго по оборотам 68.02*/19*; прямые VAT-источники показаны для проверки покрытия."); lines.push("- Сумма прогноза выше рассчитана строго по оборотам 68.02*/19*; прямые VAT-источники показаны для проверки покрытия.");
} }
else if (vatProbe && vatProbe.status === "error") { else if (vatProbe && vatProbe.status === "error") {
lines.push("", "Покрытие VAT-источников через MCP: probe завершился ошибкой, поэтому использован только базовый контур 68.02*/19*."); lines.push("", "Покрытие VAT-источников в 1С: дополнительная проверка завершилась ошибкой, поэтому использован только базовый контур 68.02*/19*.");
} }
if (!vatActivityDetected) { if (!vatActivityDetected) {
lines.push(`В выбранном окне не найдено движений по НДС-субсчетам 68.02*/19*; поэтому оперативный прогноз к уплате равен ${formatForecastMoney(0)}.`); lines.push(`В выбранном окне не найдено движений по НДС-субсчетам 68.02*/19*; поэтому оперативный прогноз к уплате равен ${formatForecastMoney(0)}.`);
@ -2588,12 +2591,15 @@ function composeFactualReplyBody(intent, rows, options = {}) {
const periodWindowLabel = options.periodFrom && options.periodTo ? `${formatDateRu(options.periodFrom)}..${formatDateRu(options.periodTo)}` : null; const periodWindowLabel = options.periodFrom && options.periodTo ? `${formatDateRu(options.periodFrom)}..${formatDateRu(options.periodTo)}` : null;
const formatConfirmedMoney = (value) => (options.useRubCurrency ? formatMoneyRub(value) : formatMoney(value)); const formatConfirmedMoney = (value) => (options.useRubCurrency ? formatMoneyRub(value) : formatMoney(value));
const vatProbe = options.vatDirectSourceProbe ?? null; const vatProbe = options.vatDirectSourceProbe ?? null;
const organizationLabel = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeValue)(options.organizationHint);
const organizationScopeLabel = organizationLabel ? ` по организации ${organizationLabel}` : "";
const lines = [ const lines = [
`Коротко: подтвержденный НДС к уплате за налоговый период — ${formatConfirmedMoney(vatToPay)}.`, `Коротко: подтвержденный НДС к уплате за налоговый период${organizationScopeLabel}${formatConfirmedMoney(vatToPay)}.`,
`Если смотреть на возможный перенос или переплату, получается ${formatConfirmedMoney(carryoverOrOverpayment)}.`, `Если смотреть на возможный перенос или переплату, получается ${formatConfirmedMoney(carryoverOrOverpayment)}.`,
"Это подтвержденный расчет по регистрам книг продаж и покупок, без surrogate-формулы 68/19.", "Это подтвержденный расчет по регистрам книг продаж и покупок, без surrogate-формулы 68/19.",
"", "",
"Что вошло в расчет:", "Что вошло в расчет:",
...(organizationLabel ? [`- Организация: ${organizationLabel}.`] : []),
`- Налоговый период расчета: ${periodWindowLabel ?? "не задан (нужен явный период)"}.`, `- Налоговый период расчета: ${periodWindowLabel ?? "не задан (нужен явный период)"}.`,
`- НДС по книге продаж: ${formatConfirmedMoney(salesVat)}.`, `- НДС по книге продаж: ${formatConfirmedMoney(salesVat)}.`,
`- НДС по книге покупок (вычеты): ${formatConfirmedMoney(purchaseVat)}.`, `- НДС по книге покупок (вычеты): ${formatConfirmedMoney(purchaseVat)}.`,
@ -2602,14 +2608,14 @@ function composeFactualReplyBody(intent, rows, options = {}) {
if (vatProbe && vatProbe.status === "ok") { if (vatProbe && vatProbe.status === "ok") {
const nonEmptySources = vatProbe.probedSources.filter((item) => item.status === "ok").length; const nonEmptySources = vatProbe.probedSources.filter((item) => item.status === "ok").length;
const erroredSources = vatProbe.probedSources.filter((item) => item.status === "error").length; const erroredSources = vatProbe.probedSources.filter((item) => item.status === "error").length;
lines.push("", "Покрытие VAT-источников через MCP:", `- Найдено VAT-объектов: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`, `- Прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`, `- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.`, `- Источников с ошибкой запроса: ${formatNumberWithDots(erroredSources)}.`); lines.push("", "Покрытие VAT-источников в 1С:", `- Найдено VAT-объектов: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`, `- Прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`, `- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.`, `- Источников с ошибкой запроса: ${formatNumberWithDots(erroredSources)}.`);
if (vatProbe.errors.length > 0) { if (vatProbe.errors.length > 0) {
lines.push(`- Ограничения probe: ${vatProbe.errors.slice(0, 2).join("; ")}.`); lines.push(`- Ограничения probe: ${vatProbe.errors.slice(0, 2).join("; ")}.`);
} }
lines.push("- Сумма расчета выше получена по книгам продаж/покупок; probe использован для контроля полноты VAT-источников."); lines.push("- Сумма расчета выше получена по книгам продаж/покупок; дополнительная проверка использована для контроля полноты VAT-источников.");
} }
else if (vatProbe && vatProbe.status === "error") { else if (vatProbe && vatProbe.status === "error") {
lines.push("", "Покрытие VAT-источников через MCP: дополнительный probe недоступен (например, timeout metadata).", "Итоговая сумма НДС выше рассчитана по основному маршруту книг продаж/покупок; probe влияет только на диагностику покрытия."); lines.push("", "Покрытие VAT-источников в 1С: дополнительная проверка недоступна, поэтому использован основной бухгалтерский срез.", "Итоговая сумма НДС выше рассчитана по основному маршруту книг продаж/покупок; probe влияет только на диагностику покрытия.");
if (vatProbe.errors.length > 0) { if (vatProbe.errors.length > 0) {
lines.push(`- Детали probe: ${vatProbe.errors.slice(0, 2).join("; ")}.`); lines.push(`- Детали probe: ${vatProbe.errors.slice(0, 2).join("; ")}.`);
} }
@ -2679,7 +2685,7 @@ function composeFactualReplyBody(intent, rows, options = {}) {
const vatProbe = options.vatDirectSourceProbe ?? null; const vatProbe = options.vatDirectSourceProbe ?? null;
if (vatProbe && vatProbe.status === "ok") { if (vatProbe && vatProbe.status === "ok") {
const nonEmptySources = vatProbe.probedSources.filter((item) => item.status === "ok").length; const nonEmptySources = vatProbe.probedSources.filter((item) => item.status === "ok").length;
lines.push("", "Блок 2.1. MCP-проверка VAT-источников", `- VAT-объектов в метаданных 1С: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`, `- Пробных прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`, `- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.`); lines.push("", "Блок 2.1. Проверка VAT-источников в 1С", `- VAT-объектов в метаданных 1С: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`, `- Пробных прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`, `- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.`);
if (vatProbe.probedSources.length > 0) { if (vatProbe.probedSources.length > 0) {
lines.push(...vatProbe.probedSources.slice(0, 4).map((item, index) => { lines.push(...vatProbe.probedSources.slice(0, 4).map((item, index) => {
const name = item.synonym ? `${item.fullName} (${item.synonym})` : item.fullName; const name = item.synonym ? `${item.fullName} (${item.synonym})` : item.fullName;
@ -2696,7 +2702,7 @@ function composeFactualReplyBody(intent, rows, options = {}) {
} }
} }
else if (vatProbe && vatProbe.status === "error") { else if (vatProbe && vatProbe.status === "error") {
lines.push("", "Блок 2.1. MCP-проверка VAT-источников", "- Probe VAT-источников завершился ошибкой, поэтому срез подтвержден по доступному бухгалтерскому источнику (68*)."); lines.push("", "Блок 2.1. Проверка VAT-источников в 1С", "- Дополнительная проверка VAT-источников завершилась ошибкой, поэтому срез подтвержден по доступному бухгалтерскому источнику (68*).");
} }
lines.push("", "Блок 3. Сводка", `- Строк в выборке: ${formatNumberWithDots(rows.length)}.`, `- Подтвержденных позиций по НДС: ${formatNumberWithDots(accountRows.length)}.`, "", "Блок 4. Подтвержденные позиции"); lines.push("", "Блок 3. Сводка", `- Строк в выборке: ${formatNumberWithDots(rows.length)}.`, `- Подтвержденных позиций по НДС: ${formatNumberWithDots(accountRows.length)}.`, "", "Блок 4. Подтвержденные позиции");
if (accountRows.length > 0) { if (accountRows.length > 0) {

View File

@ -48,9 +48,9 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
const includeTotal = focus === "full_profile" || focus === "total_only"; const includeTotal = focus === "full_profile" || focus === "total_only";
const includeRoles = focus === "full_profile" || focus === "roles_only"; const includeRoles = focus === "full_profile" || focus === "roles_only";
const directLead = focus === "suppliers_only" const directLead = focus === "suppliers_only"
? `Поставщиков (только supplier-роль): ${supplierOnly}.` ? `Поставщиков с ролью поставщика: ${supplierOnly}.`
: focus === "customers_only" : focus === "customers_only"
? `Заказчиков (только customer-роль): ${customerOnly}.` ? `Заказчиков с ролью покупателя: ${customerOnly}.`
: focus === "mixed_only" : focus === "mixed_only"
? `Контрагентов со смешанной ролью: ${mixedActive}.` ? `Контрагентов со смешанной ролью: ${mixedActive}.`
: includeTotal && totalCounterparties > 0 : includeTotal && totalCounterparties > 0
@ -74,9 +74,9 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
} }
if (includeRoles) { if (includeRoles) {
if (resolvedActive > 0 || activeCounterparties > 0) { if (resolvedActive > 0 || activeCounterparties > 0) {
lines.push("Роли контрагентов по активности:"); lines.push("Распределение ролей по активности:");
lines.push(`Заказчики (только customer-роль): ${customerOnly}.`); lines.push(`Заказчики с ролью покупателя: ${customerOnly}.`);
lines.push(`Поставщики (только supplier-роль): ${supplierOnly}.`); lines.push(`Поставщики с ролью поставщика: ${supplierOnly}.`);
lines.push(`Смешанные (и покупатель, и поставщик): ${mixedActive}.`); lines.push(`Смешанные (и покупатель, и поставщик): ${mixedActive}.`);
lines.push(`4. Всего активных контрагентов: ${activeCounterparties}.`); lines.push(`4. Всего активных контрагентов: ${activeCounterparties}.`);
if (otherCounterparties !== null) { if (otherCounterparties !== null) {
@ -88,10 +88,10 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
} }
} }
if (focus === "suppliers_only") { if (focus === "suppliers_only") {
lines.push(`Поставщиков (только supplier-роль): ${supplierOnly}.`); lines.push(`Поставщиков с ролью поставщика: ${supplierOnly}.`);
} }
if (focus === "customers_only") { if (focus === "customers_only") {
lines.push(`Заказчиков (только customer-роль): ${customerOnly}.`); lines.push(`Заказчиков с ролью покупателя: ${customerOnly}.`);
} }
if (focus === "mixed_only") { if (focus === "mixed_only") {
lines.push(`Контрагентов со смешанной ролью: ${mixedActive}.`); lines.push(`Контрагентов со смешанной ролью: ${mixedActive}.`);
@ -387,6 +387,11 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
const limit = deps.detectRankingLimit(options.userMessage, 20); const limit = deps.detectRankingLimit(options.userMessage, 20);
const minOpsForAvgCheck = deps.detectMinOpsForAvgCheck(options.userMessage); const minOpsForAvgCheck = deps.detectMinOpsForAvgCheck(options.userMessage);
const normalizedQuestion = deps.normalizeQuestionText(options.userMessage); const normalizedQuestion = deps.normalizeQuestionText(options.userMessage);
const asksSingleBestCounterparty = focus === "top_by_total" &&
/(?:какой|кто|which|who|какой|кто)/iu.test(normalizedQuestion) &&
/(?:больше\s+всего|сам(?:ый|ая|ое|ые)|наибольш|прин[её]с|highest|most|больше\s+всего|сам(?:ый|ая|РѕРµ|ые)|наибол|РїСЂРёРЅ[её]СЃ)/iu.test(normalizedQuestion) &&
!/(?:\btop\b|топ|рейтинг|список|первые|покажи\s+топ|дай\s+топ|покаж\w*\s+топ|дай\s+топ)/iu.test(normalizedQuestion);
const effectiveLimit = asksSingleBestCounterparty ? 1 : limit;
const byCounterparty = new Map(); const byCounterparty = new Map();
const byYear = new Map(); const byYear = new Map();
const deals = []; const deals = [];
@ -554,7 +559,7 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
? `Топ-${visible.length} поставщиков по максимальной разовой выплате:` ? `Топ-${visible.length} поставщиков по максимальной разовой выплате:`
: `Топ-${visible.length} заказчиков по максимальной сумме одной входящей операции:`; : `Топ-${visible.length} заказчиков по максимальной сумме одной входящей операции:`;
lines.unshift(heading); lines.unshift(heading);
lines.push(...visible.map((item, index) => `${index + 1}. ${item.name} | max single: ${item.maxSingle} | максимальная разовая сумма: ${deps.formatMoneyRub(item.maxSingle)} | сумма: ${deps.formatMoneyRub(item.total)} | операций: ${item.ops}`)); lines.push(...visible.map((item, index) => `${index + 1}. ${item.name} | максимальная разовая сумма: ${deps.formatMoneyRub(item.maxSingle)} | сумма: ${deps.formatMoneyRub(item.total)} | операций: ${item.ops}`));
return (0, replyContracts_1.buildFactualListReply)(lines); return (0, replyContracts_1.buildFactualListReply)(lines);
} }
if (focus === "top_by_avg_check_min_ops") { if (focus === "top_by_avg_check_min_ops") {
@ -592,8 +597,11 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
lines.push(...visible.map((item, index) => `${index + 1}. ${formatOptionalDate(item.period, deps.formatDateRu)} | ${item.counterparty} | ${item.registrator} | ${deps.formatMoneyRub(item.amount)}`)); lines.push(...visible.map((item, index) => `${index + 1}. ${formatOptionalDate(item.period, deps.formatDateRu)} | ${item.counterparty} | ${item.registrator} | ${deps.formatMoneyRub(item.amount)}`));
return (0, replyContracts_1.buildFactualListReply)(lines); return (0, replyContracts_1.buildFactualListReply)(lines);
} }
const visible = rankedByTotal.slice(0, limit); const visible = rankedByTotal.slice(0, effectiveLimit);
const singleCandidateOnly = rankedByTotal.length === 1; const singleCandidateOnly = rankedByTotal.length === 1;
const rankingPeriodLabel = options.periodFrom && options.periodTo
? `за период ${deps.formatDateRu(options.periodFrom)}..${deps.formatDateRu(options.periodTo)}`
: "за доступное время";
const heading = singleCandidateOnly const heading = singleCandidateOnly
? isSupplier ? isSupplier
? "Найденный поставщик по сумме выплат:" ? "Найденный поставщик по сумме выплат:"
@ -603,14 +611,17 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
: `Топ-${visible.length} заказчиков по сумме поступлений:`; : `Топ-${visible.length} заказчиков по сумме поступлений:`;
const leadingCounterparty = visible[0] ?? null; const leadingCounterparty = visible[0] ?? null;
lines.unshift(heading); lines.unshift(heading);
if (options.periodFrom && options.periodTo) {
lines.push(`Период рейтинга: ${rankingPeriodLabel}.`);
}
if (leadingCounterparty) { if (leadingCounterparty) {
const directAnswerLine = singleCandidateOnly const directAnswerLine = singleCandidateOnly
? isSupplier ? isSupplier
? `В выбранном срезе найден один поставщик: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг.` ? `В выбранном срезе найден один поставщик: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг.`
: `В выбранном срезе найден один клиент: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг; сумма является денежным потоком, а не чистой прибылью.` : `В выбранном срезе найден один клиент: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг; сумма является денежным потоком, а не чистой прибылью.`
: isSupplier : isSupplier
? `Крупнейший поставщик по подтвержденным выплатам за доступное время: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).` ? `Крупнейший поставщик по подтвержденным выплатам ${rankingPeriodLabel}: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).`
: `Самый доходный клиент за доступное время по подтвержденным поступлениям: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это денежный поток, а не чистая прибыль.`; : `Самый доходный клиент ${rankingPeriodLabel} по подтвержденным поступлениям: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это денежный поток, а не чистая прибыль.`;
lines.unshift(directAnswerLine); lines.unshift(directAnswerLine);
} }
lines.push(...visible.map((item, index) => { lines.push(...visible.map((item, index) => {

View File

@ -2,6 +2,7 @@
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION = void 0; exports.ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION = void 0;
exports.buildAssistantMcpDiscoveryAnswerDraft = buildAssistantMcpDiscoveryAnswerDraft; exports.buildAssistantMcpDiscoveryAnswerDraft = buildAssistantMcpDiscoveryAnswerDraft;
const counterpartyRoleHeuristics_1 = require("./counterpartyRoleHeuristics");
exports.ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION = "assistant_mcp_discovery_answer_draft_v1"; exports.ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION = "assistant_mcp_discovery_answer_draft_v1";
function normalizeReasonCode(value) { function normalizeReasonCode(value) {
const normalized = value const normalized = value
@ -371,6 +372,26 @@ function metadataRouteFamilyLabelRu(routeFamily) {
} }
return null; return null;
} }
function isInventoryReserveBoundaryTurn(pilot) {
const action = pilot.evidence.query_plan.turn_meaning_ref?.asked_action_family;
const unsupported = pilot.evidence.query_plan.turn_meaning_ref?.unsupported_but_understood_family;
return action === "inventory_reserve_boundary" || unsupported === "inventory_reserve_liquidation_boundary";
}
function isProfitMarginBoundaryTurn(pilot) {
const action = pilot.evidence.query_plan.turn_meaning_ref?.asked_action_family;
const unsupported = pilot.evidence.query_plan.turn_meaning_ref?.unsupported_but_understood_family;
return action === "profit_margin_boundary" || unsupported === "profit_margin_boundary";
}
function isDebtDueDateBoundaryTurn(pilot) {
const action = pilot.evidence.query_plan.turn_meaning_ref?.asked_action_family;
const unsupported = pilot.evidence.query_plan.turn_meaning_ref?.unsupported_but_understood_family;
return action === "debt_due_date_boundary" || unsupported === "debt_due_date_boundary";
}
function isVendorRiskBoundaryTurn(pilot) {
const action = pilot.evidence.query_plan.turn_meaning_ref?.asked_action_family;
const unsupported = pilot.evidence.query_plan.turn_meaning_ref?.unsupported_but_understood_family;
return action === "vendor_risk_procurement_boundary" || unsupported === "vendor_risk_procurement_boundary";
}
function businessOverviewInventoryUnknownLabel(overview) { function businessOverviewInventoryUnknownLabel(overview) {
if (overview.inventory_staleness_risk_proxy) { if (overview.inventory_staleness_risk_proxy) {
return "резервы/списания/ликвидационная стоимость склада"; return "резервы/списания/ликвидационная стоимость склада";
@ -433,6 +454,67 @@ function inlineBusinessOverviewAmount(value) {
.replace(/\s*руб\.$/u, " рублей") .replace(/\s*руб\.$/u, " рублей")
.replace(/[\s.]+$/u, ""); .replace(/[\s.]+$/u, "");
} }
function isFinancialInstitutionBucket(bucket) {
if (!bucket) {
return false;
}
return (bucket.counterparty_role_hint === "bank_or_financial_institution" ||
(0, counterpartyRoleHeuristics_1.isLikelyFinancialInstitutionCounterparty)(bucket.axis_value));
}
function firstNonFinancialInstitutionBucket(buckets) {
return (buckets ?? []).find((bucket) => !isFinancialInstitutionBucket(bucket)) ?? null;
}
function rankedBucketAmountLabel(bucket) {
return `${bucket.axis_value}${bucket.total_amount_human_ru}`;
}
function businessOverviewIncomingLeaderLine(overview) {
const leader = overview.top_customers[0];
if (!leader) {
return null;
}
if (!isFinancialInstitutionBucket(leader)) {
return `Самый крупный подтвержденный клиент в проверенном срезе: ${rankedBucketAmountLabel(leader)}.`;
}
const nonFinancial = firstNonFinancialInstitutionBucket(overview.top_customers.slice(1));
const nonFinancialText = nonFinancial
? ` Крупнейший небанковский входящий контрагент в этом же срезе: ${rankedBucketAmountLabel(nonFinancial)}.`
: "";
return (`Крупнейший входящий денежный источник в проверенном срезе: ${rankedBucketAmountLabel(leader)}. ` +
"По названию это банк/финансовая организация, поэтому без проверки назначения платежа не называю это клиентской выручкой или бизнес-заказчиком." +
nonFinancialText);
}
function businessOverviewOutgoingLeaderLine(overview) {
const leader = overview.top_suppliers?.[0];
if (!leader) {
return null;
}
if (!isFinancialInstitutionBucket(leader)) {
return `Самый крупный подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${rankedBucketAmountLabel(leader)}.`;
}
const nonFinancial = firstNonFinancialInstitutionBucket(overview.top_suppliers.slice(1));
const nonFinancialText = nonFinancial
? ` Крупнейший небанковский получатель исходящих денег в этом же срезе: ${rankedBucketAmountLabel(nonFinancial)}.`
: "";
return (`Крупнейший получатель исходящих денег в проверенном срезе: ${rankedBucketAmountLabel(leader)}. ` +
"По названию это банк/финансовая организация, поэтому без назначения платежа/договора не считаю это обычным поставщиком." +
nonFinancialText);
}
function businessOverviewSupplierBoundaryBasis(overview) {
const leader = overview.top_suppliers?.[0] ?? null;
if (!leader) {
return "есть только общий срез исходящих платежей без надежного vendor-risk профиля";
}
const share = percentText(leader.total_amount, overview.outgoing_supplier_payout.total_amount);
if (isFinancialInstitutionBucket(leader)) {
const base = share
? `крупнейший получатель исходящих денег ${leader.axis_value} держит около ${share} проверенного исходящего потока (${leader.total_amount_human_ru})`
: `крупнейший получатель исходящих денег: ${rankedBucketAmountLabel(leader)}`;
return `${base}; по названию это банк/финансовая организация, поэтому этот факт нельзя считать доказанной зависимостью от одного обычного поставщика`;
}
return share
? `крупнейший подтвержденный поставщик/получатель исходящих платежей ${leader.axis_value} держит около ${share} проверенного исходящего потока (${leader.total_amount_human_ru})`
: `крупнейший подтвержденный поставщик/получатель исходящих платежей: ${rankedBucketAmountLabel(leader)}`;
}
function businessOverviewHeadlineMetricsLine(overview) { function businessOverviewHeadlineMetricsLine(overview) {
const parts = []; const parts = [];
if (overview.incoming_customer_revenue.rows_with_amount > 0) { if (overview.incoming_customer_revenue.rows_with_amount > 0) {
@ -444,14 +526,72 @@ function businessOverviewHeadlineMetricsLine(overview) {
if (overview.incoming_customer_revenue.rows_with_amount > 0 || overview.outgoing_supplier_payout.rows_with_amount > 0) { if (overview.incoming_customer_revenue.rows_with_amount > 0 || overview.outgoing_supplier_payout.rows_with_amount > 0) {
parts.push(`расчетное операционное нетто ${inlineBusinessOverviewAmount(overview.net_amount_human_ru)}`); parts.push(`расчетное операционное нетто ${inlineBusinessOverviewAmount(overview.net_amount_human_ru)}`);
} }
if (overview.accounting_financial_result) {
const result = overview.accounting_financial_result;
const direction = result.final_result_direction === "profit"
? "учетная прибыль"
: result.final_result_direction === "loss"
? "учетный убыток"
: "нулевой учетный финрезультат";
const amount = result.final_result_direction === "loss"
? `минус ${inlineBusinessOverviewAmount(result.final_result_amount_human_ru)}`
: inlineBusinessOverviewAmount(result.final_result_amount_human_ru);
const margin = result.net_margin_to_revenue_pct === null
? "маржа к выручке 90.01 не рассчитана"
: `маржа к выручке 90.01 ${result.net_margin_to_revenue_pct}%`;
parts.push(`${direction} 90/91/99 ${amount}; ${margin}`);
}
const strongestIncomingYear = businessOverviewStrongestIncomingYear(overview); const strongestIncomingYear = businessOverviewStrongestIncomingYear(overview);
if (strongestIncomingYear) { if (strongestIncomingYear) {
parts.push(`самый сильный год по подтвержденным входящим поступлениям ${strongestIncomingYear.year_bucket}: ${inlineBusinessOverviewAmount(strongestIncomingYear.incoming_total_amount_human_ru)}`); parts.push(`самый сильный год по подтвержденным входящим поступлениям ${strongestIncomingYear.year_bucket}: ${inlineBusinessOverviewAmount(strongestIncomingYear.incoming_total_amount_human_ru)}`);
} }
return parts.length > 0 return parts.length > 0
? `${parts.join("; ")}. Это operating-flow proxy по найденным строкам, не бухгалтерская прибыль и не финрезультат` ? overview.accounting_financial_result
? `${parts.join("; ")}. Финрезультат ограничен найденными строками 1С и не является внешним аудитом или юридически подтвержденной отчетностью`
: `${parts.join("; ")}. Это operating-flow proxy по найденным строкам, не бухгалтерская прибыль и не финрезультат`
: null; : null;
} }
function businessOverviewAccountingFinancialResultText(overview) {
const result = overview.accounting_financial_result;
if (!result) {
return null;
}
const direction = result.final_result_direction === "profit"
? "учетная прибыль"
: result.final_result_direction === "loss"
? "учетный убыток"
: "нулевой учетный финрезультат";
const signedAmount = result.final_result_direction === "loss"
? `минус ${result.final_result_amount_human_ru}`
: result.final_result_amount_human_ru;
const marginText = result.net_margin_to_revenue_pct === null
? "маржа к выручке 90.01 не рассчитана"
: `маржа к выручке 90.01 ${result.net_margin_to_revenue_pct}%`;
const basis = result.final_transfer_basis === "account_99_to_84_period_close"
? "по закрытию 99 на 84"
: "по закрытию 90/91 на 99";
return `По бухгалтерскому маршруту 90/91/99 за ${result.period_scope} подтвержден ${direction}: ${signedAmount}; ${marginText}. Основа: ${basis}, ${result.period_close_rows_with_amount} строк(и) закрытия периода с суммой. Это учетный финрезультат по найденным строкам 1С, не внешний аудит и не юридически подтвержденная отчетность.`;
}
function businessOverviewDebtDueDateAgingText(overview) {
const aging = overview.debt_due_date_aging;
if (!aging) {
return null;
}
if (aging.evidence_status === "confirmed_overdue") {
const top = aging.top_overdue_items?.[0] ?? null;
const topText = top
? ` Самая старая строка: due date ${top.due_date}, просрочка ${top.overdue_days} дн., ${top.amount_human_ru}${top.contract ? ` по договору ${top.contract}` : ""}.`
: "";
return `Due-date aging на ${aging.as_of_date} проверен по срокам оплаты договоров и датам расчетных документов: подтверждено просроченных строк ${aging.overdue_rows}, сумма ${aging.overdue_amount_human_ru}.${topText}`;
}
if (aging.evidence_status === "no_payment_terms_configured") {
return `Due-date aging на ${aging.as_of_date} проверен по открытым расчетам: брутто ${aging.gross_open_amount_human_ru}, строк с суммой ${aging.rows_with_amount}, но в проверенных договорах срок оплаты не установлен. Подтвержденной просрочки по договорным срокам оплаты нет.`;
}
if (aging.evidence_status === "insufficient_due_date_basis") {
return `Due-date aging на ${aging.as_of_date} запускался, но по строкам с установленным сроком оплаты не хватило даты расчетного документа для честного расчета due date. Просрочка не подтверждена.`;
}
return `Due-date aging на ${aging.as_of_date} проверен: строк с установленным сроком оплаты ${aging.rows_with_payment_terms}, подтвержденной просрочки не найдено; не просрочено по расчету ${aging.not_yet_due_amount_human_ru}.`;
}
function headlineFor(mode, pilot) { function headlineFor(mode, pilot) {
const askedMonthlyBreakdown = pilot.derived_bidirectional_value_flow?.aggregation_axis === "month" || const askedMonthlyBreakdown = pilot.derived_bidirectional_value_flow?.aggregation_axis === "month" ||
pilot.derived_value_flow?.aggregation_axis === "month"; pilot.derived_value_flow?.aggregation_axis === "month";
@ -469,6 +609,35 @@ function headlineFor(mode, pilot) {
} }
if (isBusinessOverviewPilot(pilot) && pilot.derived_business_overview && mode === "confirmed_with_bounded_inference") { if (isBusinessOverviewPilot(pilot) && pilot.derived_business_overview && mode === "confirmed_with_bounded_inference") {
const overview = pilot.derived_business_overview; const overview = pilot.derived_business_overview;
if (isProfitMarginBoundaryTurn(pilot)) {
const accountingFinancialResultText = businessOverviewAccountingFinancialResultText(overview);
if (accountingFinancialResultText) {
return accountingFinancialResultText;
}
return "Нельзя точно подтвердить чистую прибыль и маржу по текущему срезу 1С; есть только bounded operating-flow/trading-margin proxy, не P&L и не бухгалтерский финрезультат.";
}
if (isDebtDueDateBoundaryTurn(pilot)) {
const dueDateText = businessOverviewDebtDueDateAgingText(overview);
if (dueDateText) {
return dueDateText;
}
return "Нельзя точно определить, какая дебиторка просрочена, по текущему срезу 1С; есть только debt-quality proxy, но нет проверенного due-date маршрута по договорам, срокам оплаты и погашению расчетов.";
}
if (isInventoryReserveBoundaryTurn(pilot)) {
const inventoryBasis = overview.inventory_staleness_risk_proxy
? "есть только складской staleness-risk proxy по найденным строкам"
: overview.inventory_position || overview.inventory_turnover_proxy
? "есть только ограниченные складские proxy-сигналы по найденным строкам"
: "нет отдельного складского среза на дату и проверки учетной политики резервов";
return `Коротко: точно подтвердить резерв под неликвиды по текущим данным нельзя; ${inventoryBasis}. Можно честно говорить только о необходимости отдельной проверки склада, списаний/резервов и ликвидационной стоимости, не превращая proxy в доказанный факт резерва.`;
}
if (isVendorRiskBoundaryTurn(pilot)) {
const supplierLeader = overview.top_suppliers?.[0] ?? null;
const proxyLabel = isFinancialInstitutionBucket(supplierLeader)
? "outgoing cash concentration proxy"
: "procurement concentration proxy";
return `Коротко: точный риск зависимости от одного поставщика по текущим данным не подтвержден; есть только ${proxyLabel}: ${businessOverviewSupplierBoundaryBasis(overview)}. Это сигнал концентрации исходящих платежей, а не полный аудит надежности поставщиков, условий, качества и структуры всех расходов.`;
}
const families = []; const families = [];
if (overview.incoming_customer_revenue.rows_with_amount > 0 || if (overview.incoming_customer_revenue.rows_with_amount > 0 ||
overview.outgoing_supplier_payout.rows_with_amount > 0) { overview.outgoing_supplier_payout.rows_with_amount > 0) {
@ -492,6 +661,9 @@ function headlineFor(mode, pilot) {
if (overview.tax_position) { if (overview.tax_position) {
families.push("НДС-позиция"); families.push("НДС-позиция");
} }
if (overview.accounting_financial_result) {
families.push("учетный финрезультат 90/91/99");
}
if (overview.trading_margin_proxy) { if (overview.trading_margin_proxy) {
families.push("торговый margin proxy"); families.push("торговый margin proxy");
} }
@ -507,6 +679,9 @@ function headlineFor(mode, pilot) {
if (overview.debt_staleness_risk_proxy) { if (overview.debt_staleness_risk_proxy) {
families.push("staleness risk proxy открытых расчетов"); families.push("staleness risk proxy открытых расчетов");
} }
if (overview.debt_due_date_aging) {
families.push("due-date aging открытых расчетов");
}
if (overview.inventory_position) { if (overview.inventory_position) {
families.push("складской срез на дату"); families.push("складской срез на дату");
} }
@ -516,18 +691,22 @@ function headlineFor(mode, pilot) {
if (overview.inventory_staleness_risk_proxy) { if (overview.inventory_staleness_risk_proxy) {
families.push("staleness risk proxy склада"); families.push("staleness risk proxy склада");
} }
const unknownFamilies = [overview.trading_margin_proxy ? "чистая прибыль/точная маржа" : "прибыль/маржа"]; const unknownFamilies = overview.accounting_financial_result
? ["аудированная/юридически подтвержденная прибыль"]
: [overview.trading_margin_proxy ? "чистая прибыль/точная маржа" : "прибыль/маржа"];
if (!overview.tax_position) { if (!overview.tax_position) {
unknownFamilies.push("НДС"); unknownFamilies.push("НДС");
} }
if (!overview.debt_position) { if (!overview.debt_position) {
unknownFamilies.push("долговой срез"); unknownFamilies.push("долговой срез");
} }
unknownFamilies.push(overview.debt_staleness_risk_proxy if (!overview.debt_due_date_aging) {
? "договорные сроки оплаты/due-date просрочка" unknownFamilies.push(overview.debt_staleness_risk_proxy
: overview.debt_open_settlement_quality ? "договорные сроки оплаты/due-date просрочка"
? "due-date просрочка" : overview.debt_open_settlement_quality
: "качество открытых расчетов"); ? "due-date просрочка"
: "качество открытых расчетов");
}
unknownFamilies.push(businessOverviewInventoryUnknownLabel(overview)); unknownFamilies.push(businessOverviewInventoryUnknownLabel(overview));
const metricLead = businessOverviewHeadlineMetricsLine(overview); const metricLead = businessOverviewHeadlineMetricsLine(overview);
if (metricLead) { if (metricLead) {
@ -725,9 +904,14 @@ function buildMustNotClaim(pilot) {
claims.push("Do not present a debt-position snapshot as debt aging, overdue debt, or credit-quality analysis."); claims.push("Do not present a debt-position snapshot as debt aging, overdue debt, or credit-quality analysis.");
claims.push("Do not present open-settlement concentration as contractual due-date aging or confirmed overdue debt."); claims.push("Do not present open-settlement concentration as contractual due-date aging or confirmed overdue debt.");
claims.push("Do not present business overview debt staleness risk proxy as confirmed overdue debt, contractual delinquency, credit risk, or due-date aging."); claims.push("Do not present business overview debt staleness risk proxy as confirmed overdue debt, contractual delinquency, credit risk, or due-date aging.");
claims.push("Do not claim contractual overdue debt unless the due-date aging route found configured payment terms and enough settlement-date evidence.");
claims.push("Do not present an inventory snapshot or purchase-date aging signal as turnover, obsolescence, liquidation value, or full inventory health."); claims.push("Do not present an inventory snapshot or purchase-date aging signal as turnover, obsolescence, liquidation value, or full inventory health.");
claims.push("Do not present business overview inventory turnover proxy as full inventory liquidity, FIFO turnover, obsolescence analysis, or liquidation value."); claims.push("Do not present business overview inventory turnover proxy as full inventory liquidity, FIFO turnover, obsolescence analysis, or liquidation value.");
claims.push("Do not present business overview inventory staleness risk proxy as confirmed obsolete stock, reserve, write-off, or liquidation value."); claims.push("Do not present business overview inventory staleness risk proxy as confirmed obsolete stock, reserve, write-off, or liquidation value.");
if (pilot.derived_business_overview?.top_customers?.some(isFinancialInstitutionBucket) ||
pilot.derived_business_overview?.top_suppliers?.some(isFinancialInstitutionBucket)) {
claims.push("Do not present bank-like counterparties as ordinary customers, suppliers, revenue, procurement dependency, or business quality evidence without payment-purpose/contract proof.");
}
if (pilot.derived_business_overview?.missing_proof_families?.length) { if (pilot.derived_business_overview?.missing_proof_families?.length) {
claims.push("Do not present business overview missing proof families as checked, executed, or confirmed routes."); claims.push("Do not present business overview missing proof families as checked, executed, or confirmed routes.");
} }
@ -736,6 +920,9 @@ function buildMustNotClaim(pilot) {
if (pilot.derived_ranked_value_flow) { if (pilot.derived_ranked_value_flow) {
claims.push("Do not present a bounded ranking as a complete all-time ranking outside the checked period and organization."); claims.push("Do not present a bounded ranking as a complete all-time ranking outside the checked period and organization.");
claims.push("Do not imply the top-ranked counterparty is globally final when probe-limit or scope boundaries still exist."); claims.push("Do not imply the top-ranked counterparty is globally final when probe-limit or scope boundaries still exist.");
if (pilot.derived_ranked_value_flow.ranked_values.some(isFinancialInstitutionBucket)) {
claims.push("Do not present bank-like counterparties as ordinary customers, suppliers, revenue, procurement dependency, or business quality evidence without payment-purpose/contract proof.");
}
} }
if (isDocumentPilot(pilot)) { if (isDocumentPilot(pilot)) {
claims.push("Do not claim full document history outside the checked period."); claims.push("Do not claim full document history outside the checked period.");
@ -885,24 +1072,38 @@ function derivedRankedValueFlowConfirmedLine(pilot) {
return null; return null;
} }
const leader = ranking.ranked_values[0]; const leader = ranking.ranked_values[0];
const leaderLooksFinancial = isFinancialInstitutionBucket(leader);
const organization = ranking.organization_scope ? ` по организации ${ranking.organization_scope}` : ""; const organization = ranking.organization_scope ? ` по организации ${ranking.organization_scope}` : "";
const period = ranking.period_scope ? ` за период ${ranking.period_scope}` : " в проверенном окне"; const period = ranking.period_scope ? ` за период ${ranking.period_scope}` : " в проверенном окне";
const roleCaveat = leaderLooksFinancial
? ranking.value_flow_direction === "outgoing_supplier_payout"
? " По названию это банк/финансовая организация, поэтому без назначения платежа/договора не называю это обычным поставщиком."
: " По названию это банк/финансовая организация, поэтому без назначения платежа не называю это клиентской выручкой или бизнес-заказчиком."
: "";
if (ranking.ranked_values.length === 1) { if (ranking.ranked_values.length === 1) {
const singleLead = ranking.value_flow_direction === "outgoing_supplier_payout" const singleLead = leaderLooksFinancial
? "В проверенных исходящих платежах найден один контрагент" ? ranking.value_flow_direction === "outgoing_supplier_payout"
: "В проверенных входящих поступлениях найден один контрагент"; ? "В проверенных исходящих платежах найден один банковский/финансовый получатель"
: "В проверенных входящих поступлениях найден один банковский/финансовый источник"
: ranking.value_flow_direction === "outgoing_supplier_payout"
? "В проверенных исходящих платежах найден один контрагент"
: "В проверенных входящих поступлениях найден один контрагент";
const limitCaveat = ranking.coverage_limited_by_probe_limit const limitCaveat = ranking.coverage_limited_by_probe_limit
? " Лимит строк проверки достигнут; сравнение с другими контрагентами может быть неполным." ? " Лимит строк проверки достигнут; сравнение с другими контрагентами может быть неполным."
: " Других контрагентов в этом проверенном срезе не найдено, поэтому это не полноценный сравнительный рейтинг."; : " Других контрагентов в этом проверенном срезе не найдено, поэтому это не полноценный сравнительный рейтинг.";
return `${singleLead} ${leader.axis_value}${organization}${period}: ${leader.total_amount_human_ru} по ${leader.rows_with_amount} строкам с суммой.${limitCaveat}`; return `${singleLead} ${leader.axis_value}${organization}${period}: ${leader.total_amount_human_ru} по ${leader.rows_with_amount} строкам с суммой.${roleCaveat}${limitCaveat}`;
} }
const directionLead = ranking.ranking_need === "bottom_asc" const directionLead = leaderLooksFinancial
? ranking.value_flow_direction === "outgoing_supplier_payout" ? ranking.value_flow_direction === "outgoing_supplier_payout"
? "Меньше всего заплатили контрагенту" ? "Крупнейший получатель исходящих денег"
: "Меньше всего денег принёс контрагент" : "Крупнейший входящий денежный источник"
: ranking.value_flow_direction === "outgoing_supplier_payout" : ranking.ranking_need === "bottom_asc"
? "Больше всего заплатили контрагенту" ? ranking.value_flow_direction === "outgoing_supplier_payout"
: "Больше всего денег принёс контрагент"; ? "Меньше всего заплатили контрагенту"
: "Меньше всего денег принёс контрагент"
: ranking.value_flow_direction === "outgoing_supplier_payout"
? "Больше всего заплатили контрагенту"
: "Больше всего денег принёс контрагент";
const tail = ranking.ranked_values const tail = ranking.ranked_values
.slice(1, 3) .slice(1, 3)
.map((bucket) => `${bucket.axis_value}${bucket.total_amount_human_ru}`) .map((bucket) => `${bucket.axis_value}${bucket.total_amount_human_ru}`)
@ -911,7 +1112,7 @@ function derivedRankedValueFlowConfirmedLine(pilot) {
const limitCaveat = ranking.coverage_limited_by_probe_limit const limitCaveat = ranking.coverage_limited_by_probe_limit
? " Лимит строк проверки достигнут; рейтинг может быть неполным." ? " Лимит строк проверки достигнут; рейтинг может быть неполным."
: ""; : "";
return `${directionLead} ${leader.axis_value}${organization}${period}: ${leader.total_amount_human_ru} по ${leader.rows_with_amount} строкам с суммой.${trail}${limitCaveat}`; return `${directionLead} ${leader.axis_value}${organization}${period}: ${leader.total_amount_human_ru} по ${leader.rows_with_amount} строкам с суммой.${roleCaveat}${trail}${limitCaveat}`;
} }
function derivedValueFlowConfirmedLine(pilot) { function derivedValueFlowConfirmedLine(pilot) {
const flow = pilot.derived_value_flow; const flow = pilot.derived_value_flow;
@ -1070,13 +1271,13 @@ function derivedBusinessOverviewConfirmedLines(pilot) {
if (strongestIncomingYear) { if (strongestIncomingYear) {
lines.push(`Самый сильный год по подтвержденным входящим поступлениям: ${strongestIncomingYear.year_bucket}${strongestIncomingYear.incoming_total_amount_human_ru} по ${strongestIncomingYear.incoming_rows_with_amount} строкам с суммой. Это не бухгалтерская прибыль.`); lines.push(`Самый сильный год по подтвержденным входящим поступлениям: ${strongestIncomingYear.year_bucket}${strongestIncomingYear.incoming_total_amount_human_ru} по ${strongestIncomingYear.incoming_rows_with_amount} строкам с суммой. Это не бухгалтерская прибыль.`);
} }
const leader = overview.top_customers[0]; const incomingLeaderLine = businessOverviewIncomingLeaderLine(overview);
if (leader) { if (incomingLeaderLine) {
lines.push(`Самый крупный подтвержденный клиент в проверенном срезе: ${leader.axis_value}${leader.total_amount_human_ru}.`); lines.push(incomingLeaderLine);
} }
const supplierLeader = overview.top_suppliers?.[0]; const outgoingLeaderLine = businessOverviewOutgoingLeaderLine(overview);
if (supplierLeader) { if (outgoingLeaderLine) {
lines.push(`Самый крупный подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${supplierLeader.axis_value}${supplierLeader.total_amount_human_ru}.`); lines.push(outgoingLeaderLine);
} }
if (overview.yearly_breakdown?.length) { if (overview.yearly_breakdown?.length) {
lines.push(`Годовая раскладка операционного денежного потока построена по подтвержденным строкам 1С за ${yearCountHumanRu(overview.yearly_breakdown.length)}.`); lines.push(`Годовая раскладка операционного денежного потока построена по подтвержденным строкам 1С за ${yearCountHumanRu(overview.yearly_breakdown.length)}.`);
@ -1124,6 +1325,12 @@ function derivedBusinessOverviewConfirmedLines(pilot) {
: "сбалансирован"; : "сбалансирован";
lines.push(`НДС-позиция за ${overview.tax_position.period_scope}: книга продаж ${overview.tax_position.sales_vat_amount_human_ru}, книга покупок/вычеты ${overview.tax_position.purchase_vat_amount_human_ru}, нетто ${taxDirection} ${overview.tax_position.net_vat_amount_human_ru}.`); lines.push(`НДС-позиция за ${overview.tax_position.period_scope}: книга продаж ${overview.tax_position.sales_vat_amount_human_ru}, книга покупок/вычеты ${overview.tax_position.purchase_vat_amount_human_ru}, нетто ${taxDirection} ${overview.tax_position.net_vat_amount_human_ru}.`);
} }
if (overview.accounting_financial_result) {
const accountingFinancialResultText = businessOverviewAccountingFinancialResultText(overview);
if (accountingFinancialResultText) {
lines.push(accountingFinancialResultText);
}
}
if (overview.trading_margin_proxy) { if (overview.trading_margin_proxy) {
const proxy = overview.trading_margin_proxy; const proxy = overview.trading_margin_proxy;
const marginText = proxy.margin_to_revenue_pct === null ? "не рассчитана" : `${proxy.margin_to_revenue_pct}%`; const marginText = proxy.margin_to_revenue_pct === null ? "не рассчитана" : `${proxy.margin_to_revenue_pct}%`;
@ -1156,6 +1363,10 @@ function derivedBusinessOverviewConfirmedLines(pilot) {
const counterpartyText = proxy.top_contract_counterparty ? ` / ${proxy.top_contract_counterparty}` : ""; const counterpartyText = proxy.top_contract_counterparty ? ` / ${proxy.top_contract_counterparty}` : "";
lines.push(`Staleness risk proxy открытых расчетов на ${proxy.as_of_date}: самый старый договорный сигнал ${proxy.oldest_contract_start_date}, возраст ${proxy.max_contract_age_days} дн.; старейший крупный договор ${proxy.top_contract}${counterpartyText} держит ${proxy.top_contract_amount_human_ru} (${proxy.top_contract_share_pct}% брутто открытых остатков); оценка ${debtStalenessRiskBandRu(proxy.risk_band)}. Это не подтвержденная просрочка, не кредитный риск и не due-date aging.`); lines.push(`Staleness risk proxy открытых расчетов на ${proxy.as_of_date}: самый старый договорный сигнал ${proxy.oldest_contract_start_date}, возраст ${proxy.max_contract_age_days} дн.; старейший крупный договор ${proxy.top_contract}${counterpartyText} держит ${proxy.top_contract_amount_human_ru} (${proxy.top_contract_share_pct}% брутто открытых остатков); оценка ${debtStalenessRiskBandRu(proxy.risk_band)}. Это не подтвержденная просрочка, не кредитный риск и не due-date aging.`);
} }
const dueDateText = businessOverviewDebtDueDateAgingText(overview);
if (dueDateText) {
lines.push(dueDateText);
}
if (overview.inventory_position) { if (overview.inventory_position) {
const leader = overview.inventory_position.top_items[0]; const leader = overview.inventory_position.top_items[0];
const leaderText = leader const leaderText = leader
@ -1203,6 +1414,13 @@ function businessOverviewCustomerConcentrationLine(overview) {
return null; return null;
} }
const share = percentText(leader.total_amount, overview.incoming_customer_revenue.total_amount); const share = percentText(leader.total_amount, overview.incoming_customer_revenue.total_amount);
if (isFinancialInstitutionBucket(leader)) {
const base = share
? `Крупнейший входящий денежный источник ${leader.axis_value} дает около ${share} проверенных входящих поступлений (${leader.total_amount_human_ru})`
: `Крупнейший входящий денежный источник в проверенном срезе: ${rankedBucketAmountLabel(leader)}`;
const nonFinancial = firstNonFinancialInstitutionBucket(overview.top_customers.slice(1));
return `${base}. По названию это банк/финансовая организация, поэтому это не доказывает клиентскую выручку или зависимость от клиента.${nonFinancial ? ` Крупнейший небанковский входящий контрагент: ${rankedBucketAmountLabel(nonFinancial)}.` : ""}`;
}
return share return share
? `Концентрация входящего потока: крупнейший подтвержденный клиент ${leader.axis_value} дает около ${share} проверенных входящих поступлений (${leader.total_amount_human_ru}). Это сигнал зависимости от клиента, а не полный customer-risk аудит.` ? `Концентрация входящего потока: крупнейший подтвержденный клиент ${leader.axis_value} дает около ${share} проверенных входящих поступлений (${leader.total_amount_human_ru}). Это сигнал зависимости от клиента, а не полный customer-risk аудит.`
: `Крупнейший подтвержденный клиент в проверенном срезе: ${leader.axis_value}${leader.total_amount_human_ru}.`; : `Крупнейший подтвержденный клиент в проверенном срезе: ${leader.axis_value}${leader.total_amount_human_ru}.`;
@ -1213,6 +1431,13 @@ function businessOverviewSupplierConcentrationLine(overview) {
return null; return null;
} }
const share = percentText(leader.total_amount, overview.outgoing_supplier_payout.total_amount); const share = percentText(leader.total_amount, overview.outgoing_supplier_payout.total_amount);
if (isFinancialInstitutionBucket(leader)) {
const base = share
? `Концентрация исходящего потока: крупнейший получатель исходящих денег ${leader.axis_value} держит около ${share} проверенных исходящих платежей (${leader.total_amount_human_ru})`
: `Крупнейший получатель исходящих денег в проверенном срезе: ${rankedBucketAmountLabel(leader)}`;
const nonFinancial = firstNonFinancialInstitutionBucket(overview.top_suppliers.slice(1));
return `${base}. По названию это банк/финансовая организация, поэтому это не доказательство зависимости от обычного поставщика без проверки назначения платежа/договора.${nonFinancial ? ` Крупнейший небанковский получатель исходящих денег: ${rankedBucketAmountLabel(nonFinancial)}.` : ""}`;
}
return share return share
? `Концентрация исходящего потока: крупнейший подтвержденный поставщик/получатель исходящих платежей ${leader.axis_value} держит около ${share} проверенных исходящих платежей (${leader.total_amount_human_ru}). Это сигнал procurement concentration по найденным строкам, а не полный vendor-risk аудит или структура всех расходов.` ? `Концентрация исходящего потока: крупнейший подтвержденный поставщик/получатель исходящих платежей ${leader.axis_value} держит около ${share} проверенных исходящих платежей (${leader.total_amount_human_ru}). Это сигнал procurement concentration по найденным строкам, а не полный vendor-risk аудит или структура всех расходов.`
: `Крупнейший подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${leader.axis_value}${leader.total_amount_human_ru}.`; : `Крупнейший подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${leader.axis_value}${leader.total_amount_human_ru}.`;
@ -1257,6 +1482,18 @@ function businessOverviewRiskSynthesisLine(overview) {
: `маржинальность proxy ${overview.trading_margin_proxy.margin_to_revenue_pct}%`; : `маржинальность proxy ${overview.trading_margin_proxy.margin_to_revenue_pct}%`;
signals.push(`торговый спред proxy ${overview.trading_margin_proxy.gross_spread_proxy_human_ru}, ${marginText}`); signals.push(`торговый спред proxy ${overview.trading_margin_proxy.gross_spread_proxy_human_ru}, ${marginText}`);
} }
if (overview.accounting_financial_result) {
const result = overview.accounting_financial_result;
const direction = result.final_result_direction === "profit"
? "учетная прибыль"
: result.final_result_direction === "loss"
? "учетный убыток"
: "нулевой учетный финрезультат";
const marginText = result.net_margin_to_revenue_pct === null
? "маржа к выручке 90.01 не рассчитана"
: `маржа к выручке 90.01 ${result.net_margin_to_revenue_pct}%`;
signals.push(`${direction} 90/91/99 ${result.final_result_amount_human_ru}, ${marginText}`);
}
if (overview.debt_position) { if (overview.debt_position) {
const debtDirection = overview.debt_position.net_debt_position_direction === "net_receivable" const debtDirection = overview.debt_position.net_debt_position_direction === "net_receivable"
? `дебиторка больше кредиторки на ${overview.debt_position.net_debt_position_amount_human_ru}` ? `дебиторка больше кредиторки на ${overview.debt_position.net_debt_position_amount_human_ru}`
@ -1275,6 +1512,16 @@ function businessOverviewRiskSynthesisLine(overview) {
if (overview.debt_staleness_risk_proxy) { if (overview.debt_staleness_risk_proxy) {
signals.push(`staleness risk proxy открытых расчетов: ${debtStalenessRiskBandRu(overview.debt_staleness_risk_proxy.risk_band)}, возраст ${overview.debt_staleness_risk_proxy.max_contract_age_days} дн., концентрация старейшего крупного договора ${overview.debt_staleness_risk_proxy.top_contract_share_pct}%`); signals.push(`staleness risk proxy открытых расчетов: ${debtStalenessRiskBandRu(overview.debt_staleness_risk_proxy.risk_band)}, возраст ${overview.debt_staleness_risk_proxy.max_contract_age_days} дн., концентрация старейшего крупного договора ${overview.debt_staleness_risk_proxy.top_contract_share_pct}%`);
} }
if (overview.debt_due_date_aging) {
const aging = overview.debt_due_date_aging;
signals.push(aging.evidence_status === "confirmed_overdue"
? `due-date aging: подтвержденная просрочка ${aging.overdue_amount_human_ru}, строк ${aging.overdue_rows}`
: aging.evidence_status === "no_payment_terms_configured"
? "due-date aging: проверено, но сроки оплаты в договорах не установлены; подтвержденной просрочки нет"
: aging.evidence_status === "insufficient_due_date_basis"
? "due-date aging: не хватило даты расчетного документа для честного расчета просрочки"
: `due-date aging: проверено, подтвержденной просрочки не найдено`);
}
if (overview.document_activity_profile) { if (overview.document_activity_profile) {
const topDocument = overview.document_activity_profile.top_document_types[0]; const topDocument = overview.document_activity_profile.top_document_types[0];
const topSection = overview.document_activity_profile.top_account_sections[0]; const topSection = overview.document_activity_profile.top_account_sections[0];
@ -1411,6 +1658,9 @@ function buildAssistantMcpDiscoveryAnswerDraft(pilot) {
if (pilot.derived_business_overview?.tax_position) { if (pilot.derived_business_overview?.tax_position) {
pushReason(reasonCodes, "answer_contains_business_overview_tax_position"); pushReason(reasonCodes, "answer_contains_business_overview_tax_position");
} }
if (pilot.derived_business_overview?.accounting_financial_result) {
pushReason(reasonCodes, "answer_contains_business_overview_accounting_financial_result");
}
if (pilot.derived_business_overview?.trading_margin_proxy) { if (pilot.derived_business_overview?.trading_margin_proxy) {
pushReason(reasonCodes, "answer_contains_business_overview_trading_margin_proxy"); pushReason(reasonCodes, "answer_contains_business_overview_trading_margin_proxy");
} }
@ -1441,6 +1691,10 @@ function buildAssistantMcpDiscoveryAnswerDraft(pilot) {
if (pilot.derived_business_overview?.debt_staleness_risk_proxy) { if (pilot.derived_business_overview?.debt_staleness_risk_proxy) {
pushReason(reasonCodes, "answer_contains_business_overview_debt_staleness_risk_proxy"); pushReason(reasonCodes, "answer_contains_business_overview_debt_staleness_risk_proxy");
} }
if (pilot.derived_business_overview?.debt_due_date_aging) {
pushReason(reasonCodes, "answer_contains_business_overview_debt_due_date_aging");
pushReason(reasonCodes, `answer_contains_business_overview_debt_due_date_aging_${pilot.derived_business_overview.debt_due_date_aging.evidence_status}`);
}
if (pilot.derived_business_overview?.inventory_position) { if (pilot.derived_business_overview?.inventory_position) {
pushReason(reasonCodes, "answer_contains_business_overview_inventory_position"); pushReason(reasonCodes, "answer_contains_business_overview_inventory_position");
} }

View File

@ -33,6 +33,11 @@ function isMcpDiscoveryEntryPointContract(value) {
return (record?.schema_version === "assistant_mcp_discovery_runtime_entry_point_v1" && return (record?.schema_version === "assistant_mcp_discovery_runtime_entry_point_v1" &&
record?.policy_owner === "assistantMcpDiscoveryRuntimeEntryPoint"); record?.policy_owner === "assistantMcpDiscoveryRuntimeEntryPoint");
} }
function isRouteCandidateContract(value) {
const record = toRecordObject(value);
return (record?.schema_version === "assistant_mcp_route_candidate_v1" &&
record?.policy_owner === "assistantMcpDiscoveryRuntimeBridge");
}
function resolveEntryPoint(input) { function resolveEntryPoint(input) {
if (isMcpDiscoveryEntryPointContract(input.entryPoint)) { if (isMcpDiscoveryEntryPointContract(input.entryPoint)) {
return input.entryPoint; return input.entryPoint;
@ -47,6 +52,7 @@ function buildAssistantMcpDiscoveryDebugAttachmentFields(input) {
const bridge = toRecordObject(entryPoint?.bridge); const bridge = toRecordObject(entryPoint?.bridge);
const planner = toRecordObject(bridge?.planner); const planner = toRecordObject(bridge?.planner);
const chainAlignment = toRecordObject(planner?.catalog_chain_template_alignment); const chainAlignment = toRecordObject(planner?.catalog_chain_template_alignment);
const routeCandidate = isRouteCandidateContract(bridge?.route_candidate) ? bridge.route_candidate : null;
const answerDraft = toRecordObject(bridge?.answer_draft); const answerDraft = toRecordObject(bridge?.answer_draft);
return { return {
assistant_mcp_discovery_entry_point_v1: entryPoint, assistant_mcp_discovery_entry_point_v1: entryPoint,
@ -59,6 +65,16 @@ function buildAssistantMcpDiscoveryDebugAttachmentFields(input) {
mcp_discovery_catalog_chain_alignment_status: toNonEmptyString(chainAlignment?.alignment_status), mcp_discovery_catalog_chain_alignment_status: toNonEmptyString(chainAlignment?.alignment_status),
mcp_discovery_catalog_chain_top_match: toNonEmptyString(chainAlignment?.top_chain_template_match), mcp_discovery_catalog_chain_top_match: toNonEmptyString(chainAlignment?.top_chain_template_match),
mcp_discovery_catalog_chain_selected_matches_top: chainAlignment?.selected_chain_matches_top === true, mcp_discovery_catalog_chain_selected_matches_top: chainAlignment?.selected_chain_matches_top === true,
mcp_discovery_route_candidate_v1: routeCandidate,
mcp_discovery_route_candidate_status: toNonEmptyString(routeCandidate?.candidate_status),
mcp_discovery_route_candidate_fact_family: toNonEmptyString(routeCandidate?.business_fact_family),
mcp_discovery_route_candidate_action_family: toNonEmptyString(routeCandidate?.action_family),
mcp_discovery_route_candidate_proof_expectation: toNonEmptyString(routeCandidate?.proof_expectation),
mcp_discovery_route_candidate_missing_axes: toStringArray(routeCandidate?.missing_axes),
mcp_discovery_route_candidate_provided_axes: toStringArray(routeCandidate?.provided_axes),
mcp_discovery_route_candidate_executable_now: routeCandidate?.executable_now === true,
mcp_discovery_route_candidate_enablement_reason: toNonEmptyString(routeCandidate?.enablement_reason),
mcp_discovery_route_candidate_next_action: toNonEmptyString(routeCandidate?.recommended_next_action),
mcp_discovery_answer_mode: toNonEmptyString(answerDraft?.answer_mode), mcp_discovery_answer_mode: toNonEmptyString(answerDraft?.answer_mode),
mcp_discovery_business_fact_answer_allowed: bridge?.business_fact_answer_allowed === true, mcp_discovery_business_fact_answer_allowed: bridge?.business_fact_answer_allowed === true,
mcp_discovery_user_facing_response_allowed: bridge?.user_facing_response_allowed === true, mcp_discovery_user_facing_response_allowed: bridge?.user_facing_response_allowed === true,

View File

@ -6,6 +6,7 @@ const addressMcpClient_1 = require("./addressMcpClient");
const assistantMcpDiscoveryRuntimeAdapter_1 = require("./assistantMcpDiscoveryRuntimeAdapter"); const assistantMcpDiscoveryRuntimeAdapter_1 = require("./assistantMcpDiscoveryRuntimeAdapter");
const assistantMcpDiscoveryPolicy_1 = require("./assistantMcpDiscoveryPolicy"); const assistantMcpDiscoveryPolicy_1 = require("./assistantMcpDiscoveryPolicy");
const addressRecipeCatalog_1 = require("./addressRecipeCatalog"); const addressRecipeCatalog_1 = require("./addressRecipeCatalog");
const counterpartyRoleHeuristics_1 = require("./counterpartyRoleHeuristics");
exports.ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION = "assistant_mcp_discovery_pilot_executor_v1"; exports.ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION = "assistant_mcp_discovery_pilot_executor_v1";
const DEFAULT_DEPS = { const DEFAULT_DEPS = {
executeAddressMcpQuery: addressMcpClient_1.executeAddressMcpQuery, executeAddressMcpQuery: addressMcpClient_1.executeAddressMcpQuery,
@ -200,6 +201,16 @@ function buildBusinessOverviewDebtFilters(planner) {
sort: "period_asc" sort: "period_asc"
}; };
} }
function shouldRunDebtDueDateAgingProbe(planner) {
const actionFamily = toNonEmptyString(planner.data_need_graph?.action_family);
const turnActionFamily = toNonEmptyString(planner.discovery_plan.turn_meaning_ref?.asked_action_family);
const unsupportedFamily = toNonEmptyString(planner.discovery_plan.turn_meaning_ref?.unsupported_but_understood_family);
const proofExpectation = toNonEmptyString(planner.data_need_graph?.proof_expectation);
const combined = [actionFamily, turnActionFamily, unsupportedFamily, proofExpectation]
.filter((item) => Boolean(item))
.join(" ");
return /(?:debt_due_date_boundary|due[-_ ]?date|overdue|aging|просроч|срок\s+оплат|дебиторк|кредиторск)/iu.test(combined);
}
function buildBusinessOverviewInventoryFilters(planner) { function buildBusinessOverviewInventoryFilters(planner) {
const meaning = planner.discovery_plan.turn_meaning_ref; const meaning = planner.discovery_plan.turn_meaning_ref;
const organization = toNonEmptyString(meaning?.explicit_organization_scope); const organization = toNonEmptyString(meaning?.explicit_organization_scope);
@ -231,6 +242,17 @@ function buildBusinessOverviewTradingMarginFilters(planner) {
sort: "period_asc" sort: "period_asc"
}; };
} }
function buildBusinessOverviewAccountingFinancialResultFilters(planner) {
const filters = buildBusinessOverviewTradingMarginFilters(planner);
if (!filters) {
return null;
}
return {
...filters,
limit: Math.max(32, planner.discovery_plan.execution_budget.max_rows_per_probe),
sort: "period_asc"
};
}
function buildInventoryExactFilters(planner) { function buildInventoryExactFilters(planner) {
const meaning = planner.discovery_plan.turn_meaning_ref; const meaning = planner.discovery_plan.turn_meaning_ref;
const subject = firstEntityCandidate(planner); const subject = firstEntityCandidate(planner);
@ -715,7 +737,8 @@ async function executeCoverageAwareValueFlowQuery(input) {
}); });
executedProbeCount += 1; executedProbeCount += 1;
probeResults.push(queryResultToProbeResult(input.primitiveId, broadResult)); probeResults.push(queryResultToProbeResult(input.primitiveId, broadResult));
const broadCoverageLimited = !broadResult.error && broadResult.matched_rows >= input.maxRowsPerProbe; const broadLimitThreshold = Math.max(1, Math.min(input.maxRowsPerProbe, broadRecipePlan.limit));
const broadCoverageLimited = !broadResult.error && broadResult.matched_rows >= broadLimitThreshold;
if (broadResult.error) { if (broadResult.error) {
pushUnique(queryLimitations, broadResult.error); pushUnique(queryLimitations, broadResult.error);
return { return {
@ -772,7 +795,8 @@ async function executeCoverageAwareValueFlowQuery(input) {
pushUnique(queryLimitations, chunkResult.error); pushUnique(queryLimitations, chunkResult.error);
continue; continue;
} }
if (chunkResult.matched_rows >= input.maxRowsPerProbe) { const chunkLimitThreshold = Math.max(1, Math.min(input.maxRowsPerProbe, chunkPlan.limit));
if (chunkResult.matched_rows >= chunkLimitThreshold) {
anyChunkLimited = true; anyChunkLimited = true;
} }
chunkResults.push(chunkResult); chunkResults.push(chunkResult);
@ -1604,6 +1628,13 @@ function extractContractDateFromText(value) {
if (!text || !/(?:договор|contract|дог\.)/iu.test(text)) { if (!text || !/(?:договор|contract|дог\.)/iu.test(text)) {
return null; return null;
} }
return extractAnyDateFromText(text);
}
function extractAnyDateFromText(value) {
const text = toNonEmptyString(value);
if (!text) {
return null;
}
const isoLikeMatch = text.match(/(\d{4})[-./](\d{1,2})[-./](\d{1,2})/); const isoLikeMatch = text.match(/(\d{4})[-./](\d{1,2})[-./](\d{1,2})/);
if (isoLikeMatch) { if (isoLikeMatch) {
return normalizeDateParts(isoLikeMatch[1], isoLikeMatch[2], isoLikeMatch[3]); return normalizeDateParts(isoLikeMatch[1], isoLikeMatch[2], isoLikeMatch[3]);
@ -1614,6 +1645,59 @@ function extractContractDateFromText(value) {
} }
return null; return null;
} }
function rowContractDateValue(row) {
const explicit = rowTextValue(row, ["ДатаДоговора", "ContractDate", "contract_date"]);
return extractAnyDateFromText(explicit) ?? rowOpenSettlementContractStartDateValue(row);
}
function rowSettlementDocumentDateValue(row) {
const explicit = rowTextValue(row, [
"ДатаДокументаРасчетов",
"SettlementDocumentDate",
"settlement_document_date"
]);
const settlementDocument = rowTextValue(row, [
"ДокументРасчетов",
"SettlementDocument",
"settlement_document"
]);
return extractAnyDateFromText(explicit) ?? extractAnyDateFromText(settlementDocument) ?? extractAnyDateFromText(rowDocumentValue(row));
}
function rowPaymentTermIsSetValue(row) {
const candidate = rowTextValue(row, ["УстановленСрокОплаты", "PaymentTermIsSet", "payment_term_is_set"]);
if (typeof row["УстановленСрокОплаты"] === "boolean") {
return row["УстановленСрокОплаты"] === true;
}
if (typeof row["PaymentTermIsSet"] === "boolean") {
return row["PaymentTermIsSet"] === true;
}
if (!candidate) {
return false;
}
return /^(?:true|истина|да|yes|1)$/iu.test(candidate.trim());
}
function rowPaymentTermDaysValue(row) {
const value = rowNumberValue(row, ["СрокОплаты", "PaymentTermDays", "payment_term_days"]);
if (value === null || !Number.isFinite(value) || value <= 0) {
return null;
}
return Math.trunc(value);
}
function addDaysToIsoDate(isoDate, days) {
const match = isoDate.match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (!match || !Number.isFinite(days)) {
return null;
}
const date = new Date(Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3])));
date.setUTCDate(date.getUTCDate() + Math.trunc(days));
if (Number.isNaN(date.getTime())) {
return null;
}
return [
String(date.getUTCFullYear()).padStart(4, "0"),
String(date.getUTCMonth() + 1).padStart(2, "0"),
String(date.getUTCDate()).padStart(2, "0")
].join("-");
}
function earlierIsoDate(left, right) { function earlierIsoDate(left, right) {
if (!left) { if (!left) {
return right; return right;
@ -2038,7 +2122,8 @@ function deriveRankedValueFlow(result, input) {
axis_value: axisValue, axis_value: axisValue,
rows_with_amount: bucket.rows_with_amount, rows_with_amount: bucket.rows_with_amount,
total_amount: bucket.total_amount, total_amount: bucket.total_amount,
total_amount_human_ru: formatAmountHumanRu(bucket.total_amount) total_amount_human_ru: formatAmountHumanRu(bucket.total_amount),
counterparty_role_hint: (0, counterpartyRoleHeuristics_1.counterpartyRoleHintForName)(axisValue)
})) }))
.sort((left, right) => { .sort((left, right) => {
const amountDelta = right.total_amount - left.total_amount; const amountDelta = right.total_amount - left.total_amount;
@ -2199,6 +2284,99 @@ function deriveBusinessOverviewTaxPosition(result, periodScope) {
inference_basis: "sales_book_minus_purchase_book_confirmed_1c_vat_rows" inference_basis: "sales_book_minus_purchase_book_confirmed_1c_vat_rows"
}; };
} }
function accountingFinancialResultMarkerAmount(result, marker) {
let total = 0;
for (const row of result.rows) {
if (String(rowDocumentValue(row) ?? "") !== marker) {
continue;
}
const amount = rowAmountValue(row);
if (amount !== null && Number.isFinite(amount)) {
total += amount;
}
}
return total;
}
function accountingFinancialResultNonZeroCount(values) {
return values.filter((value) => Math.abs(value) > 0).length;
}
function deriveBusinessOverviewAccountingFinancialResult(result, periodScope) {
if (!result || result.error || result.matched_rows <= 0 || !periodScope) {
return null;
}
const salesRevenueAccounting = accountingFinancialResultMarkerAmount(result, "ACC90_REVENUE_KT");
const costOfSalesAccounting = accountingFinancialResultMarkerAmount(result, "ACC90_COST_DT");
const sellingExpensesAccounting = accountingFinancialResultMarkerAmount(result, "ACC90_SELLING_DT");
const adminExpensesAccounting = accountingFinancialResultMarkerAmount(result, "ACC90_ADMIN_DT");
const salesProfitTo99 = accountingFinancialResultMarkerAmount(result, "ACC90_RESULT_TO_99_PROFIT");
const salesLossFrom99 = accountingFinancialResultMarkerAmount(result, "ACC90_RESULT_FROM_99_LOSS");
const otherProfitTo99 = accountingFinancialResultMarkerAmount(result, "ACC91_RESULT_TO_99_PROFIT");
const otherLossFrom99 = accountingFinancialResultMarkerAmount(result, "ACC91_RESULT_FROM_99_LOSS");
const profitTransferTo84 = accountingFinancialResultMarkerAmount(result, "ACC99_TO84_PROFIT_TRANSFER");
const lossTransferFrom84 = accountingFinancialResultMarkerAmount(result, "ACC84_TO99_LOSS_TRANSFER");
const amountSignals = [
salesRevenueAccounting,
costOfSalesAccounting,
sellingExpensesAccounting,
adminExpensesAccounting,
salesProfitTo99,
salesLossFrom99,
otherProfitTo99,
otherLossFrom99,
profitTransferTo84,
lossTransferFrom84
];
const rowsWithAmount = accountingFinancialResultNonZeroCount(amountSignals);
if (rowsWithAmount <= 0) {
return null;
}
const salesResultAmount = salesProfitTo99 - salesLossFrom99;
const otherResultAmount = otherProfitTo99 - otherLossFrom99;
const hasFinalTransfer = profitTransferTo84 > 0 || lossTransferFrom84 > 0;
const finalResultAmount = hasFinalTransfer
? profitTransferTo84 - lossTransferFrom84
: salesResultAmount + otherResultAmount;
const finalResultDirection = finalResultAmount > 0
? "profit"
: finalResultAmount < 0
? "loss"
: "balanced";
const netMarginToRevenuePct = salesRevenueAccounting > 0 ? percentageOfTotal(finalResultAmount, salesRevenueAccounting) : null;
const periodCloseRowsWithAmount = accountingFinancialResultNonZeroCount([
salesProfitTo99,
salesLossFrom99,
otherProfitTo99,
otherLossFrom99,
profitTransferTo84,
lossTransferFrom84
]);
return {
period_scope: periodScope,
rows_matched: result.matched_rows,
rows_with_amount: rowsWithAmount,
sales_revenue_accounting: salesRevenueAccounting,
sales_revenue_accounting_human_ru: formatAmountHumanRu(salesRevenueAccounting),
cost_of_sales_accounting: costOfSalesAccounting,
cost_of_sales_accounting_human_ru: formatAmountHumanRu(costOfSalesAccounting),
selling_expenses_accounting: sellingExpensesAccounting,
selling_expenses_accounting_human_ru: formatAmountHumanRu(sellingExpensesAccounting),
admin_expenses_accounting: adminExpensesAccounting,
admin_expenses_accounting_human_ru: formatAmountHumanRu(adminExpensesAccounting),
sales_result_amount: salesResultAmount,
sales_result_amount_human_ru: formatAmountHumanRu(Math.abs(salesResultAmount)),
other_result_amount: otherResultAmount,
other_result_amount_human_ru: formatAmountHumanRu(Math.abs(otherResultAmount)),
final_result_amount: finalResultAmount,
final_result_amount_human_ru: formatAmountHumanRu(Math.abs(finalResultAmount)),
final_result_direction: finalResultDirection,
net_margin_to_revenue_pct: netMarginToRevenuePct,
final_transfer_basis: hasFinalTransfer
? "account_99_to_84_period_close"
: "account_90_91_to_99_period_close",
period_close_rows_with_amount: periodCloseRowsWithAmount,
inference_basis: "account_90_91_99_period_close_aggregate_confirmed_1c_rows"
};
}
function deriveBusinessOverviewTradingMarginProxy(result, periodScope) { function deriveBusinessOverviewTradingMarginProxy(result, periodScope) {
if (!result || result.error || result.matched_rows <= 0 || !periodScope) { if (!result || result.error || result.matched_rows <= 0 || !periodScope) {
return null; return null;
@ -2554,6 +2732,121 @@ function deriveBusinessOverviewDebtStalenessRiskProxy(quality) {
inference_basis: "contract_date_age_and_open_balance_concentration_confirmed_1c_rows" inference_basis: "contract_date_age_and_open_balance_concentration_confirmed_1c_rows"
}; };
} }
function deriveBusinessOverviewDebtDueDateAging(input) {
if (!input.debtAsOfDate || !input.dueDateResult || input.dueDateResult.error || input.dueDateResult.matched_rows <= 0) {
return null;
}
const overdueItems = [];
let rowsWithAmount = 0;
let grossOpenAmount = 0;
let rowsWithPaymentTerms = 0;
let rowsWithoutPaymentTerms = 0;
let rowsWithoutDocumentDate = 0;
let overdueAmount = 0;
let notYetDueAmount = 0;
let notYetDueRows = 0;
for (const row of input.dueDateResult.rows) {
const amount = rowAmountValue(row);
if (amount === null) {
continue;
}
const absAmount = Math.abs(amount);
if (absAmount <= 0) {
continue;
}
rowsWithAmount += 1;
grossOpenAmount += absAmount;
const paymentTermIsSet = rowPaymentTermIsSetValue(row);
const paymentTermDays = rowPaymentTermDaysValue(row);
if (!paymentTermIsSet || paymentTermDays === null) {
rowsWithoutPaymentTerms += 1;
continue;
}
rowsWithPaymentTerms += 1;
const documentDate = rowSettlementDocumentDateValue(row) ?? rowContractDateValue(row);
if (!documentDate) {
rowsWithoutDocumentDate += 1;
continue;
}
const dueDate = addDaysToIsoDate(documentDate, paymentTermDays);
if (!dueDate) {
rowsWithoutDocumentDate += 1;
continue;
}
const overdueDays = dueDate < input.debtAsOfDate ? daysBetweenIsoDates(dueDate, input.debtAsOfDate) : null;
if (overdueDays !== null && overdueDays > 0) {
overdueAmount += absAmount;
overdueItems.push({
counterparty: rowCounterpartyValue(row),
contract: rowContractValue(row),
settlement_document: rowTextValue(row, [
"ДокументРасчетов",
"SettlementDocument",
"settlement_document"
]) ?? rowDocumentValue(row),
document_date: documentDate,
due_date: dueDate,
payment_term_days: paymentTermDays,
overdue_days: overdueDays,
amount: absAmount,
amount_human_ru: formatAmountHumanRu(absAmount)
});
}
else {
notYetDueRows += 1;
notYetDueAmount += absAmount;
}
}
if (rowsWithAmount <= 0) {
return null;
}
const topOverdueItems = overdueItems
.sort((left, right) => {
const daysDelta = right.overdue_days - left.overdue_days;
if (daysDelta !== 0) {
return daysDelta;
}
const amountDelta = right.amount - left.amount;
return amountDelta !== 0 ? amountDelta : String(left.contract ?? "").localeCompare(String(right.contract ?? ""), "ru");
})
.slice(0, 5)
.map((item) => ({
...item,
share_of_overdue_amount_pct: percentageOfTotal(item.amount, overdueAmount)
}));
const oldestDueDate = overdueItems
.map((item) => item.due_date)
.sort()[0] ?? null;
const maxOverdueDays = overdueItems.reduce((max, item) => max === null ? item.overdue_days : Math.max(max, item.overdue_days), null);
const evidenceStatus = overdueItems.length > 0
? "confirmed_overdue"
: rowsWithPaymentTerms <= 0
? "no_payment_terms_configured"
: rowsWithoutDocumentDate >= rowsWithPaymentTerms
? "insufficient_due_date_basis"
: "no_overdue_found";
return {
as_of_date: input.debtAsOfDate,
rows_matched: input.dueDateResult.matched_rows,
rows_with_amount: rowsWithAmount,
gross_open_amount: grossOpenAmount,
gross_open_amount_human_ru: formatAmountHumanRu(grossOpenAmount),
rows_with_payment_terms: rowsWithPaymentTerms,
rows_without_payment_terms: rowsWithoutPaymentTerms,
rows_without_document_date: rowsWithoutDocumentDate,
overdue_rows: overdueItems.length,
overdue_amount: overdueAmount,
overdue_amount_human_ru: formatAmountHumanRu(overdueAmount),
not_yet_due_rows: notYetDueRows,
not_yet_due_amount: notYetDueAmount,
not_yet_due_amount_human_ru: formatAmountHumanRu(notYetDueAmount),
oldest_due_date: oldestDueDate,
max_overdue_days: maxOverdueDays,
top_overdue_items: topOverdueItems,
evidence_status: evidenceStatus,
inference_basis: "contract_payment_terms_and_settlement_document_dates_from_open_balance_rows"
};
}
function debtStalenessRiskBandRu(riskBand) { function debtStalenessRiskBandRu(riskBand) {
if (riskBand === "high") { if (riskBand === "high") {
return "высокая зона внимания"; return "высокая зона внимания";
@ -2748,7 +3041,7 @@ function buildBusinessOverviewMissingProofFamilies(input) {
items.push(item); items.push(item);
} }
}; };
if (missing.has("profit_margin") || missing.has("accounting_profit_margin")) { if ((missing.has("profit_margin") || missing.has("accounting_profit_margin")) && !input.accountingFinancialResult) {
pushUnique({ pushUnique({
family: "accounting_profit_margin", family: "accounting_profit_margin",
current_status: input.tradingMarginProxy ? "proxy_only_currently" : "reviewed_route_not_wired", current_status: input.tradingMarginProxy ? "proxy_only_currently" : "reviewed_route_not_wired",
@ -2759,7 +3052,7 @@ function buildBusinessOverviewMissingProofFamilies(input) {
must_not_claim: "clean_profit_accounting_margin_or_full_pnl" must_not_claim: "clean_profit_accounting_margin_or_full_pnl"
}); });
} }
if (missing.has("debt_due_date_aging_quality") || missing.has("debt_open_settlement_quality")) { if ((missing.has("debt_due_date_aging_quality") || missing.has("debt_open_settlement_quality")) && !input.debtDueDateAging) {
pushUnique({ pushUnique({
family: "debt_due_date_aging_quality", family: "debt_due_date_aging_quality",
current_status: input.debtStalenessRiskProxy current_status: input.debtStalenessRiskProxy
@ -2830,6 +3123,7 @@ function deriveBusinessOverview(input) {
}); });
const activityPeriod = deriveActivityPeriod(input.lifecycleResult); const activityPeriod = deriveActivityPeriod(input.lifecycleResult);
const taxPosition = deriveBusinessOverviewTaxPosition(input.taxResult, input.periodScope); const taxPosition = deriveBusinessOverviewTaxPosition(input.taxResult, input.periodScope);
const accountingFinancialResult = deriveBusinessOverviewAccountingFinancialResult(input.accountingFinancialResultResult, input.periodScope);
const tradingMarginProxy = deriveBusinessOverviewTradingMarginProxy(input.tradingMarginResult, input.periodScope); const tradingMarginProxy = deriveBusinessOverviewTradingMarginProxy(input.tradingMarginResult, input.periodScope);
const debtPosition = deriveBusinessOverviewDebtPosition({ const debtPosition = deriveBusinessOverviewDebtPosition({
receivablesResult: input.receivablesResult, receivablesResult: input.receivablesResult,
@ -2844,6 +3138,10 @@ function deriveBusinessOverview(input) {
const counterpartyProfile = deriveBusinessOverviewCounterpartyProfile(input.counterpartyProfileResult, input.periodScope); const counterpartyProfile = deriveBusinessOverviewCounterpartyProfile(input.counterpartyProfileResult, input.periodScope);
const contractUsageProfile = deriveBusinessOverviewContractUsageProfile(input.contractUsageProfileResult, input.periodScope); const contractUsageProfile = deriveBusinessOverviewContractUsageProfile(input.contractUsageProfileResult, input.periodScope);
const debtStalenessRiskProxy = deriveBusinessOverviewDebtStalenessRiskProxy(debtOpenSettlementQuality); const debtStalenessRiskProxy = deriveBusinessOverviewDebtStalenessRiskProxy(debtOpenSettlementQuality);
const debtDueDateAging = deriveBusinessOverviewDebtDueDateAging({
dueDateResult: input.dueDateAgingResult,
debtAsOfDate: input.debtAsOfDate
});
const inventoryPosition = deriveBusinessOverviewInventoryPosition({ const inventoryPosition = deriveBusinessOverviewInventoryPosition({
inventoryOnHandResult: input.inventoryOnHandResult, inventoryOnHandResult: input.inventoryOnHandResult,
inventoryAgingResult: input.inventoryAgingResult, inventoryAgingResult: input.inventoryAgingResult,
@ -2862,10 +3160,12 @@ function deriveBusinessOverview(input) {
outgoing.rows_with_amount > 0, outgoing.rows_with_amount > 0,
Boolean(activityPeriod), Boolean(activityPeriod),
Boolean(taxPosition), Boolean(taxPosition),
Boolean(accountingFinancialResult),
Boolean(tradingMarginProxy), Boolean(tradingMarginProxy),
Boolean(debtPosition), Boolean(debtPosition),
Boolean(debtOpenSettlementQuality), Boolean(debtOpenSettlementQuality),
Boolean(debtStalenessRiskProxy), Boolean(debtStalenessRiskProxy),
Boolean(debtDueDateAging),
Boolean(documentActivityProfile), Boolean(documentActivityProfile),
Boolean(counterpartyProfile), Boolean(counterpartyProfile),
Boolean(contractUsageProfile), Boolean(contractUsageProfile),
@ -2879,9 +3179,9 @@ function deriveBusinessOverview(input) {
const netAmount = incoming.total_amount - outgoing.total_amount; const netAmount = incoming.total_amount - outgoing.total_amount;
const hasBusinessOverviewProfileSignal = Boolean(documentActivityProfile || counterpartyProfile || contractUsageProfile); const hasBusinessOverviewProfileSignal = Boolean(documentActivityProfile || counterpartyProfile || contractUsageProfile);
const missingSignalFamilies = [ const missingSignalFamilies = [
tradingMarginProxy ? "accounting_profit_margin" : "profit_margin", accountingFinancialResult ? null : tradingMarginProxy ? "accounting_profit_margin" : "profit_margin",
debtPosition ? null : "debt_position", debtPosition ? null : "debt_position",
debtOpenSettlementQuality ? "debt_due_date_aging_quality" : "debt_open_settlement_quality", debtDueDateAging ? null : debtOpenSettlementQuality ? "debt_due_date_aging_quality" : "debt_open_settlement_quality",
taxPosition ? null : "tax_position", taxPosition ? null : "tax_position",
inventoryPosition inventoryPosition
? inventoryStalenessRiskProxy ? inventoryStalenessRiskProxy
@ -2894,9 +3194,11 @@ function deriveBusinessOverview(input) {
].filter((item) => Boolean(item)); ].filter((item) => Boolean(item));
const missingProofFamilies = buildBusinessOverviewMissingProofFamilies({ const missingProofFamilies = buildBusinessOverviewMissingProofFamilies({
missingSignalFamilies, missingSignalFamilies,
accountingFinancialResult,
tradingMarginProxy, tradingMarginProxy,
debtOpenSettlementQuality, debtOpenSettlementQuality,
debtStalenessRiskProxy, debtStalenessRiskProxy,
debtDueDateAging,
inventoryPosition, inventoryPosition,
inventoryTurnoverProxy, inventoryTurnoverProxy,
inventoryStalenessRiskProxy, inventoryStalenessRiskProxy,
@ -2915,10 +3217,12 @@ function deriveBusinessOverview(input) {
yearly_breakdown: yearlyBreakdown, yearly_breakdown: yearlyBreakdown,
activity_period: activityPeriod, activity_period: activityPeriod,
tax_position: taxPosition, tax_position: taxPosition,
accounting_financial_result: accountingFinancialResult,
trading_margin_proxy: tradingMarginProxy, trading_margin_proxy: tradingMarginProxy,
debt_position: debtPosition, debt_position: debtPosition,
debt_open_settlement_quality: debtOpenSettlementQuality, debt_open_settlement_quality: debtOpenSettlementQuality,
debt_staleness_risk_proxy: debtStalenessRiskProxy, debt_staleness_risk_proxy: debtStalenessRiskProxy,
debt_due_date_aging: debtDueDateAging,
inventory_position: inventoryPosition, inventory_position: inventoryPosition,
inventory_turnover_proxy: inventoryTurnoverProxy, inventory_turnover_proxy: inventoryTurnoverProxy,
inventory_staleness_risk_proxy: inventoryStalenessRiskProxy, inventory_staleness_risk_proxy: inventoryStalenessRiskProxy,
@ -2929,9 +3233,9 @@ function deriveBusinessOverview(input) {
checked_signal_count: checkedSignalCount, checked_signal_count: checkedSignalCount,
missing_signal_families: missingSignalFamilies, missing_signal_families: missingSignalFamilies,
missing_proof_families: missingProofFamilies, missing_proof_families: missingProofFamilies,
inference_basis: hasBusinessOverviewProfileSignal || inventoryPosition inference_basis: hasBusinessOverviewProfileSignal || inventoryPosition || accountingFinancialResult
? "business_overview_from_confirmed_1c_multi_family_rows" ? "business_overview_from_confirmed_1c_multi_family_rows"
: debtOpenSettlementQuality : debtOpenSettlementQuality || debtDueDateAging
? "business_overview_from_confirmed_1c_multi_family_rows" ? "business_overview_from_confirmed_1c_multi_family_rows"
: taxPosition && debtPosition : taxPosition && debtPosition
? "business_overview_from_confirmed_1c_money_activity_tax_and_debt_rows" ? "business_overview_from_confirmed_1c_money_activity_tax_and_debt_rows"
@ -2956,6 +3260,9 @@ function summarizeBusinessOverviewRows(input) {
if (input.taxResult && !input.taxResult.error) { if (input.taxResult && !input.taxResult.error) {
parts.push(`${input.taxResult.fetched_rows} VAT/tax rows fetched, ${input.taxResult.matched_rows} matched`); parts.push(`${input.taxResult.fetched_rows} VAT/tax rows fetched, ${input.taxResult.matched_rows} matched`);
} }
if (input.accountingFinancialResultResult && !input.accountingFinancialResultResult.error) {
parts.push(`${input.accountingFinancialResultResult.fetched_rows} accounting financial-result aggregate rows fetched, ${input.accountingFinancialResultResult.matched_rows} matched`);
}
if (input.tradingMarginResult && !input.tradingMarginResult.error) { if (input.tradingMarginResult && !input.tradingMarginResult.error) {
parts.push(`${input.tradingMarginResult.fetched_rows} trading-margin document rows fetched, ${input.tradingMarginResult.matched_rows} matched`); parts.push(`${input.tradingMarginResult.fetched_rows} trading-margin document rows fetched, ${input.tradingMarginResult.matched_rows} matched`);
} }
@ -2968,6 +3275,9 @@ function summarizeBusinessOverviewRows(input) {
if (input.openContractsResult && !input.openContractsResult.error) { if (input.openContractsResult && !input.openContractsResult.error) {
parts.push(`${input.openContractsResult.fetched_rows} open-contract rows fetched, ${input.openContractsResult.matched_rows} matched`); parts.push(`${input.openContractsResult.fetched_rows} open-contract rows fetched, ${input.openContractsResult.matched_rows} matched`);
} }
if (input.dueDateAgingResult && !input.dueDateAgingResult.error) {
parts.push(`${input.dueDateAgingResult.fetched_rows} due-date aging rows fetched, ${input.dueDateAgingResult.matched_rows} matched`);
}
if (input.documentActivityProfileResult && !input.documentActivityProfileResult.error) { if (input.documentActivityProfileResult && !input.documentActivityProfileResult.error) {
parts.push(`${input.documentActivityProfileResult.fetched_rows} document/account-section profile rows fetched, ${input.documentActivityProfileResult.matched_rows} matched`); parts.push(`${input.documentActivityProfileResult.fetched_rows} document/account-section profile rows fetched, ${input.documentActivityProfileResult.matched_rows} matched`);
} }
@ -3000,11 +3310,29 @@ function buildBusinessOverviewConfirmedFacts(derived) {
} }
if (derived.top_customers.length > 0) { if (derived.top_customers.length > 0) {
const leader = derived.top_customers[0]; const leader = derived.top_customers[0];
facts.push(`Самый крупный подтвержденный клиент в проверенном срезе: ${leader.axis_value}${leader.total_amount_human_ru}.`); if ((0, counterpartyRoleHeuristics_1.isLikelyFinancialInstitutionCounterparty)(leader.axis_value)) {
facts.push(`Крупнейший входящий денежный источник в проверенном срезе: ${leader.axis_value}${leader.total_amount_human_ru}. По названию это банк/финансовая организация, поэтому без назначения платежа это не доказанный клиент или бизнес-выручка.`);
const nonFinancialLeader = derived.top_customers.slice(1).find((item) => !(0, counterpartyRoleHeuristics_1.isLikelyFinancialInstitutionCounterparty)(item.axis_value));
if (nonFinancialLeader) {
facts.push(`Крупнейший небанковский входящий контрагент в проверенном срезе: ${nonFinancialLeader.axis_value}${nonFinancialLeader.total_amount_human_ru}.`);
}
}
else {
facts.push(`Самый крупный подтвержденный клиент в проверенном срезе: ${leader.axis_value}${leader.total_amount_human_ru}.`);
}
} }
if (derived.top_suppliers.length > 0) { if (derived.top_suppliers.length > 0) {
const leader = derived.top_suppliers[0]; const leader = derived.top_suppliers[0];
facts.push(`Самый крупный подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${leader.axis_value}${leader.total_amount_human_ru}.`); if ((0, counterpartyRoleHeuristics_1.isLikelyFinancialInstitutionCounterparty)(leader.axis_value)) {
facts.push(`Крупнейший получатель исходящих денег в проверенном срезе: ${leader.axis_value}${leader.total_amount_human_ru}. По названию это банк/финансовая организация, поэтому без назначения платежа/договора это не доказанный обычный поставщик.`);
const nonFinancialLeader = derived.top_suppliers.slice(1).find((item) => !(0, counterpartyRoleHeuristics_1.isLikelyFinancialInstitutionCounterparty)(item.axis_value));
if (nonFinancialLeader) {
facts.push(`Крупнейший небанковский получатель исходящих денег в проверенном срезе: ${nonFinancialLeader.axis_value}${nonFinancialLeader.total_amount_human_ru}.`);
}
}
else {
facts.push(`Самый крупный подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${leader.axis_value}${leader.total_amount_human_ru}.`);
}
} }
if (derived.yearly_breakdown.length > 0) { if (derived.yearly_breakdown.length > 0) {
facts.push(`Годовая раскладка операционного денежного потока построена по подтвержденным строкам 1С за ${yearCountHumanRu(derived.yearly_breakdown.length)}.`); facts.push(`Годовая раскладка операционного денежного потока построена по подтвержденным строкам 1С за ${yearCountHumanRu(derived.yearly_breakdown.length)}.`);
@ -3087,6 +3415,25 @@ function buildBusinessOverviewConfirmedFacts(derived) {
const counterpartyText = proxy.top_contract_counterparty ? ` / ${proxy.top_contract_counterparty}` : ""; const counterpartyText = proxy.top_contract_counterparty ? ` / ${proxy.top_contract_counterparty}` : "";
facts.push(`Staleness risk proxy открытых расчетов на ${proxy.as_of_date}: самый старый договорный сигнал ${proxy.oldest_contract_start_date}, возраст ${proxy.max_contract_age_days} дн.; старейший крупный договор ${proxy.top_contract}${counterpartyText} держит ${proxy.top_contract_amount_human_ru} (${proxy.top_contract_share_pct}% брутто открытых остатков); оценка ${debtStalenessRiskBandRu(proxy.risk_band)}. Это не подтвержденная просрочка, не кредитный риск и не due-date aging.`); facts.push(`Staleness risk proxy открытых расчетов на ${proxy.as_of_date}: самый старый договорный сигнал ${proxy.oldest_contract_start_date}, возраст ${proxy.max_contract_age_days} дн.; старейший крупный договор ${proxy.top_contract}${counterpartyText} держит ${proxy.top_contract_amount_human_ru} (${proxy.top_contract_share_pct}% брутто открытых остатков); оценка ${debtStalenessRiskBandRu(proxy.risk_band)}. Это не подтвержденная просрочка, не кредитный риск и не due-date aging.`);
} }
if (derived.debt_due_date_aging) {
const aging = derived.debt_due_date_aging;
if (aging.evidence_status === "confirmed_overdue") {
const top = aging.top_overdue_items[0];
const topText = top
? ` Самая старая просрочка: ${top.due_date}, ${top.overdue_days} дн., ${top.amount_human_ru}${top.contract ? ` по договору ${top.contract}` : ""}.`
: "";
facts.push(`Due-date aging открытых расчетов на ${aging.as_of_date} проверен по срокам оплаты договоров и датам расчетных документов: подтверждено просроченных строк ${aging.overdue_rows}, сумма ${aging.overdue_amount_human_ru}.${topText}`);
}
else if (aging.evidence_status === "no_payment_terms_configured") {
facts.push(`Due-date aging открытых расчетов на ${aging.as_of_date} проверен по ${aging.rows_with_amount} строкам с суммой: брутто открытых остатков ${aging.gross_open_amount_human_ru}, но в проверенных договорах срок оплаты не установлен. Подтвержденной просрочки по договорным срокам оплаты нет.`);
}
else if (aging.evidence_status === "insufficient_due_date_basis") {
facts.push(`Due-date aging открытых расчетов на ${aging.as_of_date} запускался по ${aging.rows_with_payment_terms} строкам с установленным сроком оплаты, но в найденных строках не хватило даты расчетного документа для честного расчета due date. Просрочка не подтверждена.`);
}
else {
facts.push(`Due-date aging открытых расчетов на ${aging.as_of_date} проверен: строк с установленным сроком оплаты ${aging.rows_with_payment_terms}, подтвержденной просрочки не найдено; не просрочено по расчету ${aging.not_yet_due_amount_human_ru}.`);
}
}
if (derived.inventory_position) { if (derived.inventory_position) {
const leader = derived.inventory_position.top_items[0]; const leader = derived.inventory_position.top_items[0];
const leaderText = leader const leaderText = leader
@ -3133,6 +3480,9 @@ function buildBusinessOverviewInferredFacts(derived) {
const supplierSharePct = supplierLeader const supplierSharePct = supplierLeader
? percentageOfTotal(supplierLeader.total_amount, derived.outgoing_supplier_payout.total_amount) ? percentageOfTotal(supplierLeader.total_amount, derived.outgoing_supplier_payout.total_amount)
: null; : null;
const supplierLeaderIsFinancial = supplierLeader
? (0, counterpartyRoleHeuristics_1.isLikelyFinancialInstitutionCounterparty)(supplierLeader.axis_value)
: false;
const strongestIncomingYear = [...derived.yearly_breakdown] const strongestIncomingYear = [...derived.yearly_breakdown]
.filter((bucket) => bucket.incoming_total_amount > 0) .filter((bucket) => bucket.incoming_total_amount > 0)
.sort((left, right) => right.incoming_total_amount - left.incoming_total_amount || left.year_bucket.localeCompare(right.year_bucket))[0]; .sort((left, right) => right.incoming_total_amount - left.incoming_total_amount || left.year_bucket.localeCompare(right.year_bucket))[0];
@ -3142,9 +3492,13 @@ function buildBusinessOverviewInferredFacts(derived) {
return [ return [
`Расчетное нетто по найденным строкам: ${derived.net_amount_human_ru}; ${direction}.`, `Расчетное нетто по найденным строкам: ${derived.net_amount_human_ru}; ${direction}.`,
supplierLeader supplierLeader
? supplierSharePct !== null ? supplierLeaderIsFinancial
? `Крупнейший подтвержденный поставщик/получатель исходящих платежей ${supplierLeader.axis_value} держит около ${supplierSharePct}% проверенного исходящего потока (${supplierLeader.total_amount_human_ru}). Это procurement concentration proxy по найденным строкам, а не полный vendor-risk аудит.` ? supplierSharePct !== null
: `Крупнейший подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${supplierLeader.axis_value}${supplierLeader.total_amount_human_ru}.` ? `Крупнейший получатель исходящих денег ${supplierLeader.axis_value} держит около ${supplierSharePct}% проверенного исходящего потока (${supplierLeader.total_amount_human_ru}). По названию это банк/финансовая организация, поэтому это outgoing cash concentration proxy, а не доказанный vendor-risk по обычному поставщику.`
: `Крупнейший получатель исходящих денег в проверенном срезе: ${supplierLeader.axis_value}${supplierLeader.total_amount_human_ru}. По названию это банк/финансовая организация, поэтому это не доказанный обычный поставщик.`
: supplierSharePct !== null
? `Крупнейший подтвержденный поставщик/получатель исходящих платежей ${supplierLeader.axis_value} держит около ${supplierSharePct}% проверенного исходящего потока (${supplierLeader.total_amount_human_ru}). Это procurement concentration proxy по найденным строкам, а не полный vendor-risk аудит.`
: `Крупнейший подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${supplierLeader.axis_value}${supplierLeader.total_amount_human_ru}.`
: null, : null,
strongestIncomingYear strongestIncomingYear
? `Самый сильный год по подтвержденным входящим поступлениям: ${strongestIncomingYear.year_bucket} (${strongestIncomingYear.incoming_total_amount_human_ru}).` ? `Самый сильный год по подтвержденным входящим поступлениям: ${strongestIncomingYear.year_bucket} (${strongestIncomingYear.incoming_total_amount_human_ru}).`
@ -3803,10 +4157,12 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
let outgoingResult = null; let outgoingResult = null;
let lifecycleResult = null; let lifecycleResult = null;
let taxResult = null; let taxResult = null;
let accountingFinancialResultResult = null;
let tradingMarginResult = null; let tradingMarginResult = null;
let receivablesResult = null; let receivablesResult = null;
let payablesResult = null; let payablesResult = null;
let openContractsResult = null; let openContractsResult = null;
let dueDateAgingResult = null;
let documentActivityProfileResult = null; let documentActivityProfileResult = null;
let counterpartyProfileResult = null; let counterpartyProfileResult = null;
let contractUsageProfileResult = null; let contractUsageProfileResult = null;
@ -3816,8 +4172,10 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
const lifecycleFilters = buildLifecycleFilters(planner); const lifecycleFilters = buildLifecycleFilters(planner);
const profileFilters = buildBusinessOverviewProfileFilters(planner); const profileFilters = buildBusinessOverviewProfileFilters(planner);
const taxFilters = buildBusinessOverviewTaxFilters(planner); const taxFilters = buildBusinessOverviewTaxFilters(planner);
const accountingFinancialResultFilters = buildBusinessOverviewAccountingFinancialResultFilters(planner);
const tradingMarginFilters = buildBusinessOverviewTradingMarginFilters(planner); const tradingMarginFilters = buildBusinessOverviewTradingMarginFilters(planner);
const debtFilters = buildBusinessOverviewDebtFilters(planner); const debtFilters = buildBusinessOverviewDebtFilters(planner);
const debtDueDateAgingProbeEnabled = shouldRunDebtDueDateAgingProbe(planner);
const inventoryFilters = buildBusinessOverviewInventoryFilters(planner); const inventoryFilters = buildBusinessOverviewInventoryFilters(planner);
const debtAsOfDate = toNonEmptyString(debtFilters?.as_of_date); const debtAsOfDate = toNonEmptyString(debtFilters?.as_of_date);
const inventoryAsOfDate = toNonEmptyString(inventoryFilters?.as_of_date); const inventoryAsOfDate = toNonEmptyString(inventoryFilters?.as_of_date);
@ -3830,6 +4188,9 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
const taxSelection = taxFilters const taxSelection = taxFilters
? (0, addressRecipeCatalog_1.selectAddressRecipe)("vat_liability_confirmed_for_tax_period", taxFilters) ? (0, addressRecipeCatalog_1.selectAddressRecipe)("vat_liability_confirmed_for_tax_period", taxFilters)
: null; : null;
const accountingFinancialResultSelection = accountingFinancialResultFilters
? (0, addressRecipeCatalog_1.selectAddressRecipe)("accounting_financial_result_for_organization", accountingFinancialResultFilters)
: null;
const tradingMarginSelection = tradingMarginFilters const tradingMarginSelection = tradingMarginFilters
? (0, addressRecipeCatalog_1.selectAddressRecipe)("inventory_trading_margin_proxy_for_organization", tradingMarginFilters) ? (0, addressRecipeCatalog_1.selectAddressRecipe)("inventory_trading_margin_proxy_for_organization", tradingMarginFilters)
: null; : null;
@ -3842,6 +4203,9 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
const openContractsSelection = debtFilters const openContractsSelection = debtFilters
? (0, addressRecipeCatalog_1.selectAddressRecipe)("open_contracts_confirmed_as_of_date", debtFilters) ? (0, addressRecipeCatalog_1.selectAddressRecipe)("open_contracts_confirmed_as_of_date", debtFilters)
: null; : null;
const dueDateAgingSelection = debtFilters && debtDueDateAgingProbeEnabled
? (0, addressRecipeCatalog_1.selectAddressRecipe)("debt_due_date_aging_for_organization", debtFilters)
: null;
const inventoryOnHandSelection = inventoryFilters const inventoryOnHandSelection = inventoryFilters
? (0, addressRecipeCatalog_1.selectAddressRecipe)("inventory_on_hand_as_of_date", inventoryFilters) ? (0, addressRecipeCatalog_1.selectAddressRecipe)("inventory_on_hand_as_of_date", inventoryFilters)
: null; : null;
@ -3909,6 +4273,16 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
pushReason(reasonCodes, "pilot_business_overview_tax_recipe_not_available"); pushReason(reasonCodes, "pilot_business_overview_tax_recipe_not_available");
pushUnique(queryLimitations, "Business overview VAT/tax probe requires an executable tax-period recipe"); pushUnique(queryLimitations, "Business overview VAT/tax probe requires an executable tax-period recipe");
} }
if (accountingFinancialResultSelection?.selected_recipe) {
pushReason(reasonCodes, "pilot_business_overview_accounting_financial_result_recipe_selected");
}
else if (!accountingFinancialResultFilters) {
pushReason(reasonCodes, "pilot_business_overview_accounting_financial_result_probe_skipped_without_explicit_period");
}
else {
pushReason(reasonCodes, "pilot_business_overview_accounting_financial_result_recipe_not_available");
pushUnique(queryLimitations, "Business overview accounting financial-result probe requires an executable 90/91/99 period-close recipe");
}
if (tradingMarginSelection?.selected_recipe) { if (tradingMarginSelection?.selected_recipe) {
pushReason(reasonCodes, "pilot_business_overview_trading_margin_recipe_selected"); pushReason(reasonCodes, "pilot_business_overview_trading_margin_recipe_selected");
} }
@ -3939,6 +4313,19 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
pushReason(reasonCodes, "pilot_business_overview_open_contracts_recipe_not_available"); pushReason(reasonCodes, "pilot_business_overview_open_contracts_recipe_not_available");
pushUnique(queryLimitations, "Business overview open-settlement quality probe requires executable open-contracts as-of-date recipe"); pushUnique(queryLimitations, "Business overview open-settlement quality probe requires executable open-contracts as-of-date recipe");
} }
if (dueDateAgingSelection?.selected_recipe) {
pushReason(reasonCodes, "pilot_business_overview_debt_due_date_aging_recipe_selected");
}
else if (!debtFilters) {
pushReason(reasonCodes, "pilot_business_overview_debt_due_date_aging_probe_skipped_without_explicit_as_of_date");
}
else if (!debtDueDateAgingProbeEnabled) {
pushReason(reasonCodes, "pilot_business_overview_debt_due_date_aging_probe_skipped_without_boundary_need");
}
else {
pushReason(reasonCodes, "pilot_business_overview_debt_due_date_aging_recipe_not_available");
pushUnique(queryLimitations, "Business overview due-date aging probe requires executable contract payment-term/open-balance recipe");
}
if (inventoryOnHandSelection?.selected_recipe) { if (inventoryOnHandSelection?.selected_recipe) {
pushReason(reasonCodes, "pilot_business_overview_inventory_on_hand_recipe_selected"); pushReason(reasonCodes, "pilot_business_overview_inventory_on_hand_recipe_selected");
if (inventoryAgingSelection?.selected_recipe) { if (inventoryAgingSelection?.selected_recipe) {
@ -3982,6 +4369,14 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
account_scope: taxPlan.account_scope account_scope: taxPlan.account_scope
}); });
} }
if (accountingFinancialResultSelection?.selected_recipe) {
const accountingFinancialResultPlan = (0, addressRecipeCatalog_1.buildAddressRecipePlan)(accountingFinancialResultSelection.selected_recipe, accountingFinancialResultFilters);
accountingFinancialResultResult = await runtimeDeps.executeAddressMcpQuery({
query: accountingFinancialResultPlan.query,
limit: accountingFinancialResultPlan.limit,
account_scope: accountingFinancialResultPlan.account_scope
});
}
if (receivablesSelection?.selected_recipe && payablesSelection?.selected_recipe) { if (receivablesSelection?.selected_recipe && payablesSelection?.selected_recipe) {
const receivablesPlan = (0, addressRecipeCatalog_1.buildAddressRecipePlan)(receivablesSelection.selected_recipe, debtFilters); const receivablesPlan = (0, addressRecipeCatalog_1.buildAddressRecipePlan)(receivablesSelection.selected_recipe, debtFilters);
receivablesResult = await runtimeDeps.executeAddressMcpQuery({ receivablesResult = await runtimeDeps.executeAddressMcpQuery({
@ -4004,6 +4399,14 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
account_scope: openContractsPlan.account_scope account_scope: openContractsPlan.account_scope
}); });
} }
if (dueDateAgingSelection?.selected_recipe) {
const dueDateAgingPlan = (0, addressRecipeCatalog_1.buildAddressRecipePlan)(dueDateAgingSelection.selected_recipe, debtFilters);
dueDateAgingResult = await runtimeDeps.executeAddressMcpQuery({
query: dueDateAgingPlan.query,
limit: dueDateAgingPlan.limit,
account_scope: dueDateAgingPlan.account_scope
});
}
if (inventoryOnHandSelection?.selected_recipe) { if (inventoryOnHandSelection?.selected_recipe) {
const inventoryOnHandPlan = (0, addressRecipeCatalog_1.buildAddressRecipePlan)(inventoryOnHandSelection.selected_recipe, inventoryFilters); const inventoryOnHandPlan = (0, addressRecipeCatalog_1.buildAddressRecipePlan)(inventoryOnHandSelection.selected_recipe, inventoryFilters);
inventoryOnHandResult = await runtimeDeps.executeAddressMcpQuery({ inventoryOnHandResult = await runtimeDeps.executeAddressMcpQuery({
@ -4025,6 +4428,9 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
if (taxResult) { if (taxResult) {
probeResults.push(queryResultToProbeResult(step.primitive_id, taxResult)); probeResults.push(queryResultToProbeResult(step.primitive_id, taxResult));
} }
if (accountingFinancialResultResult) {
probeResults.push(queryResultToProbeResult(step.primitive_id, accountingFinancialResultResult));
}
if (receivablesResult) { if (receivablesResult) {
probeResults.push(queryResultToProbeResult(step.primitive_id, receivablesResult)); probeResults.push(queryResultToProbeResult(step.primitive_id, receivablesResult));
} }
@ -4034,6 +4440,9 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
if (openContractsResult) { if (openContractsResult) {
probeResults.push(queryResultToProbeResult(step.primitive_id, openContractsResult)); probeResults.push(queryResultToProbeResult(step.primitive_id, openContractsResult));
} }
if (dueDateAgingResult) {
probeResults.push(queryResultToProbeResult(step.primitive_id, dueDateAgingResult));
}
if (inventoryOnHandResult) { if (inventoryOnHandResult) {
probeResults.push(queryResultToProbeResult(step.primitive_id, inventoryOnHandResult)); probeResults.push(queryResultToProbeResult(step.primitive_id, inventoryOnHandResult));
} }
@ -4059,6 +4468,13 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
else if (taxResult) { else if (taxResult) {
pushReason(reasonCodes, "pilot_business_overview_tax_query_mcp_executed"); pushReason(reasonCodes, "pilot_business_overview_tax_query_mcp_executed");
} }
if (accountingFinancialResultResult?.error) {
pushUnique(queryLimitations, accountingFinancialResultResult.error);
pushReason(reasonCodes, "pilot_business_overview_accounting_financial_result_query_mcp_error");
}
else if (accountingFinancialResultResult) {
pushReason(reasonCodes, "pilot_business_overview_accounting_financial_result_query_mcp_executed");
}
if (receivablesResult?.error) { if (receivablesResult?.error) {
pushUnique(queryLimitations, receivablesResult.error); pushUnique(queryLimitations, receivablesResult.error);
pushReason(reasonCodes, "pilot_business_overview_receivables_query_mcp_error"); pushReason(reasonCodes, "pilot_business_overview_receivables_query_mcp_error");
@ -4084,6 +4500,13 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
else if (openContractsResult) { else if (openContractsResult) {
pushReason(reasonCodes, "pilot_business_overview_open_contracts_query_mcp_executed"); pushReason(reasonCodes, "pilot_business_overview_open_contracts_query_mcp_executed");
} }
if (dueDateAgingResult?.error) {
pushUnique(queryLimitations, dueDateAgingResult.error);
pushReason(reasonCodes, "pilot_business_overview_debt_due_date_aging_query_mcp_error");
}
else if (dueDateAgingResult) {
pushReason(reasonCodes, "pilot_business_overview_debt_due_date_aging_query_mcp_executed");
}
if (inventoryOnHandResult?.error) { if (inventoryOnHandResult?.error) {
pushUnique(queryLimitations, inventoryOnHandResult.error); pushUnique(queryLimitations, inventoryOnHandResult.error);
pushReason(reasonCodes, "pilot_business_overview_inventory_on_hand_query_mcp_error"); pushReason(reasonCodes, "pilot_business_overview_inventory_on_hand_query_mcp_error");
@ -4194,10 +4617,12 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
outgoingResult, outgoingResult,
lifecycleResult, lifecycleResult,
taxResult, taxResult,
accountingFinancialResultResult,
tradingMarginResult, tradingMarginResult,
receivablesResult, receivablesResult,
payablesResult, payablesResult,
openContractsResult, openContractsResult,
dueDateAgingResult,
documentActivityProfileResult, documentActivityProfileResult,
counterpartyProfileResult, counterpartyProfileResult,
contractUsageProfileResult, contractUsageProfileResult,
@ -4234,6 +4659,9 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
if (derivedBusinessOverview.tax_position) { if (derivedBusinessOverview.tax_position) {
pushReason(reasonCodes, "pilot_derived_business_overview_tax_position_from_confirmed_rows"); pushReason(reasonCodes, "pilot_derived_business_overview_tax_position_from_confirmed_rows");
} }
if (derivedBusinessOverview.accounting_financial_result) {
pushReason(reasonCodes, "pilot_derived_business_overview_accounting_financial_result_from_confirmed_rows");
}
if (derivedBusinessOverview.trading_margin_proxy) { if (derivedBusinessOverview.trading_margin_proxy) {
pushReason(reasonCodes, "pilot_derived_business_overview_trading_margin_proxy_from_confirmed_rows"); pushReason(reasonCodes, "pilot_derived_business_overview_trading_margin_proxy_from_confirmed_rows");
} }
@ -4249,6 +4677,10 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
if (derivedBusinessOverview.debt_staleness_risk_proxy) { if (derivedBusinessOverview.debt_staleness_risk_proxy) {
pushReason(reasonCodes, "pilot_derived_business_overview_debt_staleness_risk_proxy_from_confirmed_rows"); pushReason(reasonCodes, "pilot_derived_business_overview_debt_staleness_risk_proxy_from_confirmed_rows");
} }
if (derivedBusinessOverview.debt_due_date_aging) {
pushReason(reasonCodes, "pilot_derived_business_overview_debt_due_date_aging_from_confirmed_rows");
pushReason(reasonCodes, `pilot_derived_business_overview_debt_due_date_aging_${derivedBusinessOverview.debt_due_date_aging.evidence_status}`);
}
if (derivedBusinessOverview.inventory_position) { if (derivedBusinessOverview.inventory_position) {
pushReason(reasonCodes, "pilot_derived_business_overview_inventory_position_from_confirmed_rows"); pushReason(reasonCodes, "pilot_derived_business_overview_inventory_position_from_confirmed_rows");
} }
@ -4267,10 +4699,12 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
outgoingResult, outgoingResult,
lifecycleResult, lifecycleResult,
taxResult, taxResult,
accountingFinancialResultResult,
tradingMarginResult, tradingMarginResult,
receivablesResult, receivablesResult,
payablesResult, payablesResult,
openContractsResult, openContractsResult,
dueDateAgingResult,
documentActivityProfileResult, documentActivityProfileResult,
counterpartyProfileResult, counterpartyProfileResult,
contractUsageProfileResult, contractUsageProfileResult,

View File

@ -19,7 +19,7 @@ exports.ASSISTANT_MCP_DISCOVERY_PRIMITIVES = [
]; ];
const DEFAULT_DISCOVERY_BUDGET = { const DEFAULT_DISCOVERY_BUDGET = {
max_probe_count: 3, max_probe_count: 3,
max_rows_per_probe: 100 max_rows_per_probe: 200
}; };
const MAX_PROBE_COUNT = 36; const MAX_PROBE_COUNT = 36;
const MAX_ROWS_PER_PROBE = 500; const MAX_ROWS_PER_PROBE = 500;

View File

@ -2,6 +2,7 @@
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.ASSISTANT_MCP_DISCOVERY_RESPONSE_CANDIDATE_SCHEMA_VERSION = void 0; exports.ASSISTANT_MCP_DISCOVERY_RESPONSE_CANDIDATE_SCHEMA_VERSION = void 0;
exports.buildAssistantMcpDiscoveryResponseCandidate = buildAssistantMcpDiscoveryResponseCandidate; exports.buildAssistantMcpDiscoveryResponseCandidate = buildAssistantMcpDiscoveryResponseCandidate;
const counterpartyRoleHeuristics_1 = require("./counterpartyRoleHeuristics");
exports.ASSISTANT_MCP_DISCOVERY_RESPONSE_CANDIDATE_SCHEMA_VERSION = "assistant_mcp_discovery_response_candidate_v1"; exports.ASSISTANT_MCP_DISCOVERY_RESPONSE_CANDIDATE_SCHEMA_VERSION = "assistant_mcp_discovery_response_candidate_v1";
function toRecordObject(value) { function toRecordObject(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) { if (!value || typeof value !== "object" || Array.isArray(value)) {
@ -67,7 +68,26 @@ function hasInternalMechanics(value) {
function userFacingLines(values) { function userFacingLines(values) {
return uniqueStrings(values).filter((line) => !hasInternalMechanics(line)); return uniqueStrings(values).filter((line) => !hasInternalMechanics(line));
} }
function sanitizeUserFacingMechanics(value) {
return 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С";
});
}
function localizeLine(value) { function localizeLine(value) {
const sanitizedValue = sanitizeUserFacingMechanics(value);
if (/^1C activity rows were found for the requested counterparty scope$/i.test(value)) { if (/^1C activity rows were found for the requested counterparty scope$/i.test(value)) {
return "В 1С найдены строки активности в запрошенном срезе."; return "В 1С найдены строки активности в запрошенном срезе.";
} }
@ -88,7 +108,7 @@ function localizeLine(value) {
return `В 1С проверены входящие и исходящие денежные строки в запрошенном срезе: ${incoming}, ${outgoing}.`; 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)) { if (/^Requested period hit the MCP row limit, but the approved monthly recovery probe budget is smaller than the required subperiod count$/i.test(value)) {
return "Запрошенный период уперся в лимит строк MCP; доступного бюджета помесячных дозапросов не хватило, чтобы покрыть все подпериоды."; return "Запрошенный период достиг лимита строк; доступного бюджета помесячных дозапросов не хватило, чтобы покрыть все подпериоды.";
} }
const counterpartyMatch = value.match(/^1C activity rows were found for counterparty\s+(.+)$/i); const counterpartyMatch = value.match(/^1C activity rows were found for counterparty\s+(.+)$/i);
if (counterpartyMatch) { if (counterpartyMatch) {
@ -113,10 +133,10 @@ function localizeLine(value) {
} }
const movementRowsMatch = value.match(/^1C movement rows were found for counterparty\s+(.+)$/i); const movementRowsMatch = value.match(/^1C movement rows were found for counterparty\s+(.+)$/i);
if (movementRowsMatch) { if (movementRowsMatch) {
return `Р 1РЎ найдены строки движений РїРѕ контрагенту ${movementRowsMatch[1]}.`; return `В 1С найдены строки движений по контрагенту ${movementRowsMatch[1]}.`;
} }
if (/^1C movement rows were found for the requested scope$/i.test(value)) { if (/^1C movement rows were found for the requested scope$/i.test(value)) {
return "Р 1РЎ найдены строки движений РїРѕ запрошенному контуру."; return "В 1С найдены строки движений по запрошенному контуру.";
} }
const supplierPayoutMatch = value.match(/^1C supplier-payout rows were found for counterparty\s+(.+)$/i); const supplierPayoutMatch = value.match(/^1C supplier-payout rows were found for counterparty\s+(.+)$/i);
if (supplierPayoutMatch) { if (supplierPayoutMatch) {
@ -144,7 +164,7 @@ function localizeLine(value) {
return "Срез документов ограничен только подтвержденными строками документов в проверенном окне."; return "Срез документов ограничен только подтвержденными строками документов в проверенном окне.";
} }
if (/^Counterparty movement evidence is limited to confirmed 1C movement rows in the checked scope$/i.test(value)) { if (/^Counterparty movement evidence is limited to confirmed 1C movement rows in the checked scope$/i.test(value)) {
return "Срез движений ограничен только подтвержденными строками движений РІ проверенном РѕРєРЅРµ."; return "Срез движений ограничен только подтвержденными строками движений в проверенном окне.";
} }
if (/^Counterparty value-flow total was calculated from confirmed 1C movement rows$/i.test(value)) { if (/^Counterparty value-flow total was calculated from confirmed 1C movement rows$/i.test(value)) {
return "Сумма входящих поступлений рассчитана только по подтвержденным строкам поступлений в 1С."; return "Сумма входящих поступлений рассчитана только по подтвержденным строкам поступлений в 1С.";
@ -227,10 +247,10 @@ function localizeLine(value) {
return "Полный срез документов без явно проверенного периода не подтвержден."; return "Полный срез документов без явно проверенного периода не подтвержден.";
} }
if (/^Full movement history outside the checked period is not proven by this MCP discovery pilot$/i.test(value)) { if (/^Full movement history outside the checked period is not proven by this MCP discovery pilot$/i.test(value)) {
return "Полный исторический срез движений РІРЅРµ проверенного периода этим РїРѕРёСЃРєРѕРј РЅРµ подтвержден."; return "Полный исторический срез движений вне проверенного периода этим поиском не подтвержден.";
} }
if (/^Full movement history is not proven without an explicit checked period$/i.test(value)) { if (/^Full movement history is not proven without an explicit checked period$/i.test(value)) {
return "Полный срез движений без СЏРІРЅРѕ проверенного периода РЅРµ подтвержден."; return "Полный срез движений без явно проверенного периода не подтвержден.";
} }
if (/^Full supplier-payout amount outside the checked period is not proven by this MCP discovery pilot$/i.test(value)) { if (/^Full supplier-payout amount outside the checked period is not proven by this MCP discovery pilot$/i.test(value)) {
return "Полный объем исходящих платежей вне проверенного периода этим поиском не подтвержден."; return "Полный объем исходящих платежей вне проверенного периода этим поиском не подтвержден.";
@ -245,10 +265,10 @@ function localizeLine(value) {
return "Полный двусторонний денежный поток за все время без явно проверенного периода не подтвержден."; return "Полный двусторонний денежный поток за все время без явно проверенного периода не подтвержден.";
} }
if (/^Requested period coverage was recovered through monthly 1C value-flow probes after the broad probe hit the row limit$/i.test(value)) { 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С после того, как общая выборка уперлась в лимит строк."; 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)) { 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С после того, как общая выборка уперлась в лимит строк хотя бы по одной стороне."; return "Покрытие запрошенного периода по двустороннему денежному потоку восстановлено помесячными проверками 1С после того, как общая выборка достигла лимита строк хотя бы по одной стороне.";
} }
if (/^Requested period coverage was recovered through monthly 1C value-flow probes$/i.test(value)) { if (/^Requested period coverage was recovered through monthly 1C value-flow probes$/i.test(value)) {
return "Покрытие запрошенного периода восстановлено помесячными проверками 1С."; return "Покрытие запрошенного периода восстановлено помесячными проверками 1С.";
@ -268,7 +288,7 @@ function localizeLine(value) {
if (/^Complete requested-period coverage for bidirectional value-flow is not proven by the available checked rows$/i.test(value)) { if (/^Complete requested-period coverage for bidirectional value-flow is not proven by the available checked rows$/i.test(value)) {
return "Полное покрытие запрошенного периода по двустороннему денежному потоку не подтверждено доступными проверенными строками."; return "Полное покрытие запрошенного периода по двустороннему денежному потоку не подтверждено доступными проверенными строками.";
} }
return value; return sanitizedValue;
} }
function section(title, lines) { function section(title, lines) {
const clean = userFacingLines(lines.map(localizeLine)); const clean = userFacingLines(lines.map(localizeLine));
@ -345,7 +365,7 @@ function businessOverviewCoverageLimitLine(overview) {
limited.push("исходящие"); limited.push("исходящие");
} }
return limited.length > 0 return limited.length > 0
? `Важно: ${limited.join(" и ")} уперлись в лимит выборки MCP, поэтому это проверенный срез найденных строк, а не гарантированно полный бухгалтерский оборот.` ? `Важно: по направлению ${limited.join(" и ")} проверка достигла лимита строк; это расширенный проверенный срез найденных строк, но не гарантия полного бухгалтерского оборота без отдельной полной выгрузки.`
: null; : null;
} }
function businessOverviewYearRowsLine(overview) { function businessOverviewYearRowsLine(overview) {
@ -371,6 +391,30 @@ function firstOverviewAxisLabel(rows, amountKey = "total_amount_human_ru") {
const amount = moneyText(first?.[amountKey]); const amount = moneyText(first?.[amountKey]);
return label && amount ? `${label}${sentenceAmount(amount) ?? amount}` : null; 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 businessOverviewTaxLine(overview) { function businessOverviewTaxLine(overview) {
const tax = toRecordObject(overview.tax_position); const tax = toRecordObject(overview.tax_position);
if (!tax) { if (!tax) {
@ -487,7 +531,7 @@ function buildCompactBidirectionalValueFlowReply(entryPoint, draft) {
lines.push(`Основа: ${basis.join("; ")}.`); lines.push(`Основа: ${basis.join("; ")}.`);
} }
if (flow.coverage_limited_by_probe_limit === true) { if (flow.coverage_limited_by_probe_limit === true) {
lines.push("Важно: часть проверки уперлась в лимит строк, поэтому это проверенный срез найденных движений, а не гарантия полного периода."); lines.push("Важно: часть проверки достигла лимита строк, поэтому это проверенный срез найденных движений, а не гарантия полного периода.");
} }
lines.push("Метод: нетто рассчитано как подтвержденные входящие строки 1С минус подтвержденные исходящие строки; это не полное бухгалтерское сальдо вне проверенного окна."); lines.push("Метод: нетто рассчитано как подтвержденные входящие строки 1С минус подтвержденные исходящие строки; это не полное бухгалтерское сальдо вне проверенного окна.");
const fallbackNextStep = toNonEmptyString(draft.next_step_line); const fallbackNextStep = toNonEmptyString(draft.next_step_line);
@ -610,16 +654,158 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
const topCustomer = toRecordObject(Array.isArray(overview.top_customers) ? overview.top_customers[0] : null); const topCustomer = toRecordObject(Array.isArray(overview.top_customers) ? overview.top_customers[0] : null);
const customerName = toNonEmptyString(topCustomer?.axis_value); const customerName = toNonEmptyString(topCustomer?.axis_value);
const customerAmount = moneyText(topCustomer?.total_amount_human_ru); const customerAmount = moneyText(topCustomer?.total_amount_human_ru);
const topCustomerLooksFinancial = overviewAxisLooksFinancial(topCustomer);
const nonFinancialCustomer = firstNonFinancialOverviewAxisLabel(topCustomerLooksFinancial ? overview.top_customers : []);
const topCustomerLead = customerName && customerAmount const topCustomerLead = customerName && customerAmount
? `; крупнейший источник входящих денег: ${customerName}${sentenceAmount(customerAmount) ?? 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 topSupplier = firstOverviewAxisLabel(overview.top_suppliers);
const topSupplierLead = topSupplier ? `; крупнейший получатель исходящих денег: ${topSupplier}` : ""; const topSupplierLooksFinancial = overviewAxisLooksFinancial(topSupplierRecord);
const nonFinancialSupplier = firstNonFinancialOverviewAxisLabel(topSupplierLooksFinancial ? overview.top_suppliers : []);
const topSupplierLead = topSupplier
? topSupplierLooksFinancial
? `; крупнейший получатель исходящих денег: ${topSupplier} (похоже на банк/финорганизацию, не называю это обычным поставщиком без назначения платежа/договора)${nonFinancialSupplier ? `; крупнейший небанковский получатель исходящих денег: ${nonFinancialSupplier}` : ""}`
: `; крупнейший получатель исходящих денег: ${topSupplier}`
: "";
const roleBoundaryLead = topCustomer || topSupplier ? "; клиент/поставщик как бизнес-роли этим денежным срезом не подтверждены" : ""; const roleBoundaryLead = topCustomer || topSupplier ? "; клиент/поставщик как бизнес-роли этим денежным срезом не подтверждены" : "";
const graphReasonCodes = toStringList(graph?.reason_codes); const graphReasonCodes = toStringList(graph?.reason_codes);
const directMoneyAnswer = graphReasonCodes.includes("data_need_graph_business_overview_direct_money_answer"); const directMoneyAnswer = graphReasonCodes.includes("data_need_graph_business_overview_direct_money_answer");
const crossScopeExecutiveSummary = Boolean(separateSubject && previousCounterpartySummary); const crossScopeExecutiveSummary = Boolean(separateSubject && previousCounterpartySummary);
const lines = []; 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 за ${periodScope} подтвержден ${directionText}: ${amountText}${marginPct ? `; маржа к выручке 90.01 ${marginPct}` : "; маржа к выручке 90.01 не рассчитана"}.`);
lines.push("Это учетный финрезультат по найденным строкам закрытия периода в 1С, а не внешний аудит и не юридически подтвержденная отчетность.");
const reply = lines.join("\n").trim();
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
}
const headline = toNonEmptyString(draft.headline);
const cleanHeadline = headline?.replace(/^Коротко:\s*/iu, "").trim();
lines.push(cleanHeadline
? `Коротко: ${localizeLine(cleanHeadline)}`
: "Коротко: нельзя точно подтвердить чистую прибыль и маржу по текущему срезу 1С; есть только bounded operating-flow/trading-margin proxy, не P&L и не бухгалтерский финансовый результат.");
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("Для точного P&L нужны отдельный маршрут по себестоимости, расходам, закрытию периода и финрезультату; текущий proxy нельзя выдавать за подтвержденную чистую прибыль или маржу.");
if (limitLine) {
lines.push(limitLine);
}
const reply = lines.join("\n").trim();
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
}
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;
if (status === "confirmed_overdue") {
lines.push(`Коротко: на ${asOfDate} подтвержденная просрочка есть: ${overdueAmount ?? "сумма не распознана"} по ${dueDateAging.overdue_rows ?? "найденным"} строкам.`);
lines.push("Основа ответа: открытые расчеты 60/62/76, договорный срок оплаты и дата расчетного документа; это уже due-date route, не старение договора как proxy.");
}
else if (status === "no_payment_terms_configured") {
lines.push(`Коротко: на ${asOfDate} подтвержденной просрочки нет: открытые расчеты проверены${grossAmount ? ` на ${grossAmount}` : ""}, но в найденных договорах срок оплаты не установлен.`);
lines.push(rowsWithAmount !== null
? `Проверено строк с суммой: ${rowsWithAmount}. Без установленного срока оплаты нельзя честно назвать эти остатки просрочкой.`
: "Без установленного срока оплаты нельзя честно назвать эти остатки просрочкой.");
}
else if (status === "insufficient_due_date_basis") {
lines.push(`Коротко: due-date route запущен на ${asOfDate}, но просрочка не подтверждена: по строкам с установленным сроком оплаты не хватило даты расчетного документа.`);
if (rowsWithPaymentTerms !== null) {
lines.push(`Строк с установленным сроком оплаты: ${rowsWithPaymentTerms}; нужен документ-основание с датой для расчета due date.`);
}
}
else {
lines.push(`Коротко: due-date route на ${asOfDate} проверен, подтвержденной просрочки не найдено${rowsWithPaymentTerms !== null ? `; строк с установленным сроком оплаты ${rowsWithPaymentTerms}` : ""}.`);
}
const reply = lines.join("\n").trim();
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
}
const headline = toNonEmptyString(draft.headline);
const cleanHeadline = headline?.replace(/^Коротко:\s*/iu, "").trim();
lines.push(cleanHeadline
? `Коротко: ${localizeLine(cleanHeadline)}`
: "Коротко: нельзя точно определить, какая дебиторка просрочена, по текущему срезу 1С; есть только debt-quality proxy, но нет проверенного due-date маршрута.");
lines.push("Проверить нужно отдельно: договоры, сроки оплаты, погашение и закрытие задолженности; без этого нельзя доказать overdue/due-date aging.");
const reply = lines.join("\n").trim();
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
}
if (vendorRiskBoundary) {
const supplierBasis = topSupplier
? topSupplierLooksFinancial
? `крупнейший получатель исходящих денег: ${topSupplier}; по названию это банк/финансовая организация, поэтому это не доказанная зависимость от обычного поставщика${nonFinancialSupplier ? `; крупнейший небанковский получатель исходящих денег: ${nonFinancialSupplier}` : ""}`
: `крупнейший подтвержденный поставщик/получатель исходящих платежей: ${topSupplier}`
: outgoingAmount
? `исходящие платежи/закупочный поток в проверенном срезе: ${outgoingAmount}`
: "есть только ограниченный срез исходящих платежей без полного vendor-risk профиля";
const proxyLabel = topSupplierLooksFinancial ? "outgoing cash concentration proxy" : "procurement concentration proxy";
lines.push(`Коротко: точный риск зависимости от одного поставщика по текущим данным не подтвержден; есть только ${proxyLabel}: ${supplierBasis}.`);
lines.push("Это сигнал концентрации закупок/исходящих платежей, а не полный аудит надежности поставщиков, условий, качества и структуры всех расходов.");
lines.push("Для точного вывода нужен отдельный reviewed vendor-risk route: поставщики, договорные условия, качество поставок, сроки, доля в закупках и полная структура расходов.");
const reply = lines.join("\n").trim();
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
}
if (inventoryReserveBoundary) {
const headline = toNonEmptyString(draft.headline);
const cleanHeadline = headline?.replace(/^Коротко:\s*/iu, "").trim();
lines.push(cleanHeadline
? `Коротко: ${localizeLine(cleanHeadline)}`
: "Коротко: точно подтвердить резерв под неликвиды по текущим данным нельзя.");
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("Проверить нужно отдельно: складской срез на дату, учетную политику резервов, списания и ликвидационную стоимость; proxy-сигналы нельзя выдавать за доказанный факт резерва.");
const reply = lines.join("\n").trim();
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
}
if (crossScopeExecutiveSummary && separateSubject && previousCounterpartySummary && (incomingAmount || outgoingAmount || netAmount)) { if (crossScopeExecutiveSummary && separateSubject && previousCounterpartySummary && (incomingAmount || outgoingAmount || netAmount)) {
lines.push(`Коротко: по компании ${organizationScope ?? "в выбранном контуре"} ${period} подтвержден денежный срез: получили ${incomingAmount ?? "0 руб."}, исходящие платежи/списания ${outgoingAmount ?? "0 руб."}, ${netDirection} ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}${previousCounterpartySummary.lead}; можно утверждать только эти подтвержденные срезы, нельзя называть это чистой прибылью, полным оборотом или доказанной ролью главного клиента/поставщика.`); lines.push(`Коротко: по компании ${organizationScope ?? "в выбранном контуре"} ${period} подтвержден денежный срез: получили ${incomingAmount ?? "0 руб."}, исходящие платежи/списания ${outgoingAmount ?? "0 руб."}, ${netDirection} ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}${previousCounterpartySummary.lead}; можно утверждать только эти подтвержденные срезы, нельзя называть это чистой прибылью, полным оборотом или доказанной ролью главного клиента/поставщика.`);
lines.push(previousCounterpartySummary.line); lines.push(previousCounterpartySummary.line);
@ -640,7 +826,7 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
if (!leaderYear || !leaderAmount) { if (!leaderYear || !leaderAmount) {
return null; return null;
} }
lines.push(`Коротко: в доступном проверенном MCP-срезе по входящим денежным строкам лидирует ${leaderYear}: ${leaderAmount}${Number.isFinite(leaderRows) && leaderRows > 0 ? ` по ${leaderRows} строкам с суммой` : ""}; это не полный бухгалтерский рейтинг доходности.`); lines.push(`Коротко: в доступном проверенном срезе 1С по входящим денежным строкам лидирует ${leaderYear}: ${leaderAmount}${Number.isFinite(leaderRows) && leaderRows > 0 ? ` по ${leaderRows} строкам с суммой` : ""}; это не полный бухгалтерский рейтинг доходности.`);
const netYear = toNonEmptyString(netLeader?.year_bucket); const netYear = toNonEmptyString(netLeader?.year_bucket);
const netYearAmount = moneyText(netLeader?.net_amount_human_ru); const netYearAmount = moneyText(netLeader?.net_amount_human_ru);
if (netYear && netYearAmount) { if (netYear && netYearAmount) {
@ -660,7 +846,9 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
lines.push(`Коротко: ${organizationPrefix}${period} по подтвержденным строкам 1С получили ${incomingAmount ?? "0 руб."}; исходящие платежи/списания ${outgoingAmount ?? "0 руб."}; ${netDirection} ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб"}${topCustomerLead}${topSupplierLead}${roleBoundaryLead}${separateSubjectLead}.`); lines.push(`Коротко: ${organizationPrefix}${period} по подтвержденным строкам 1С получили ${incomingAmount ?? "0 руб."}; исходящие платежи/списания ${outgoingAmount ?? "0 руб."}; ${netDirection} ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб"}${topCustomerLead}${topSupplierLead}${roleBoundaryLead}${separateSubjectLead}.`);
lines.push('Метод: "заработали" здесь считаю как денежный operating-flow proxy по 1С; это не чистая прибыль и не финрезультат.'); lines.push('Метод: "заработали" здесь считаю как денежный operating-flow proxy по 1С; это не чистая прибыль и не финрезультат.');
if (!directMoneyAnswer && customerName && customerAmount) { if (!directMoneyAnswer && customerName && customerAmount) {
lines.push(`Крупнейший подтвержденный источник входящих денег в этом срезе: ${customerName}${sentenceAmount(customerAmount) ?? customerAmount}.`); lines.push(topCustomerLooksFinancial
? `Крупнейший входящий денежный источник в этом срезе: ${customerName}${sentenceAmount(customerAmount) ?? customerAmount}. По названию это банк/финансовая организация, поэтому без назначения платежа не называю это клиентской выручкой.${nonFinancialCustomer ? ` Крупнейший небанковский входящий контрагент: ${nonFinancialCustomer}.` : ""}`
: `Крупнейший подтвержденный источник входящих денег в этом срезе: ${customerName}${sentenceAmount(customerAmount) ?? customerAmount}.`);
} }
} }
else { else {
@ -671,10 +859,14 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
`Отдельно по контрагенту ${separateSubject}: этот итог не переносит суммы компании на контрагента. Можно утверждать только разделение контура; нельзя делать вывод о выручке, долге или прибыльности ${separateSubject} без отдельного контрагентского среза документов и движений.`); `Отдельно по контрагенту ${separateSubject}: этот итог не переносит суммы компании на контрагента. Можно утверждать только разделение контура; нельзя делать вывод о выручке, долге или прибыльности ${separateSubject} без отдельного контрагентского среза документов и движений.`);
} }
if (!directMoneyAnswer && topSupplier) { if (!directMoneyAnswer && topSupplier) {
lines.push(`Крупнейший подтвержденный получатель исходящих денег: ${topSupplier}.`); lines.push(topSupplierLooksFinancial
? `Крупнейший получатель исходящих денег: ${topSupplier}. По названию это банк/финансовая организация, поэтому без назначения платежа/договора не считаю это обычным поставщиком.${nonFinancialSupplier ? ` Крупнейший небанковский получатель исходящих денег: ${nonFinancialSupplier}.` : ""}`
: `Крупнейший подтвержденный получатель исходящих денег: ${topSupplier}.`);
} }
if (!directMoneyAnswer && (topCustomer || topSupplier)) { if (!directMoneyAnswer && (topCustomer || topSupplier)) {
lines.push("Важно по ролям: текущий денежный срез подтверждает денежные источники и получателей, но не доказывает, что это главный клиент или главный поставщик как бизнес-роль."); lines.push(topCustomerLooksFinancial || topSupplierLooksFinancial
? "Важно по ролям: текущий денежный срез подтверждает источники и получателей денег, но банковские контрагенты требуют проверки назначения платежа/счетов и не доказывают роль клиента или поставщика."
: "Важно по ролям: текущий денежный срез подтверждает денежные источники и получателей, но не доказывает, что это главный клиент или главный поставщик как бизнес-роль.");
} }
if (!directMoneyAnswer) { if (!directMoneyAnswer) {
lines.push(`Что подтверждено: денежный срез по компании${organizationScope ? ` ${organizationScope}` : ""}${period ? ` ${period}` : ""}${topCustomer ? ", крупнейший источник входящих денег" : ""}${topSupplier ? ", крупнейший получатель исходящих денег" : ""}.`); lines.push(`Что подтверждено: денежный срез по компании${organizationScope ? ` ${organizationScope}` : ""}${period ? ` ${period}` : ""}${topCustomer ? ", крупнейший источник входящих денег" : ""}${topSupplier ? ", крупнейший получатель исходящих денег" : ""}.`);

View File

@ -1,12 +1,13 @@
"use strict"; "use strict";
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
exports.ASSISTANT_MCP_DISCOVERY_LOOP_STATE_SCHEMA_VERSION = exports.ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION = void 0; exports.ASSISTANT_MCP_ROUTE_CANDIDATE_SCHEMA_VERSION = exports.ASSISTANT_MCP_DISCOVERY_LOOP_STATE_SCHEMA_VERSION = exports.ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION = void 0;
exports.runAssistantMcpDiscoveryRuntimeBridge = runAssistantMcpDiscoveryRuntimeBridge; exports.runAssistantMcpDiscoveryRuntimeBridge = runAssistantMcpDiscoveryRuntimeBridge;
const assistantMcpDiscoveryAnswerAdapter_1 = require("./assistantMcpDiscoveryAnswerAdapter"); const assistantMcpDiscoveryAnswerAdapter_1 = require("./assistantMcpDiscoveryAnswerAdapter");
const assistantMcpDiscoveryPilotExecutor_1 = require("./assistantMcpDiscoveryPilotExecutor"); const assistantMcpDiscoveryPilotExecutor_1 = require("./assistantMcpDiscoveryPilotExecutor");
const assistantMcpDiscoveryPlanner_1 = require("./assistantMcpDiscoveryPlanner"); const assistantMcpDiscoveryPlanner_1 = require("./assistantMcpDiscoveryPlanner");
exports.ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION = "assistant_mcp_discovery_runtime_bridge_v1"; exports.ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION = "assistant_mcp_discovery_runtime_bridge_v1";
exports.ASSISTANT_MCP_DISCOVERY_LOOP_STATE_SCHEMA_VERSION = "assistant_mcp_discovery_loop_state_v1"; exports.ASSISTANT_MCP_DISCOVERY_LOOP_STATE_SCHEMA_VERSION = "assistant_mcp_discovery_loop_state_v1";
exports.ASSISTANT_MCP_ROUTE_CANDIDATE_SCHEMA_VERSION = "assistant_mcp_route_candidate_v1";
function normalizeReasonCode(value) { function normalizeReasonCode(value) {
const normalized = value const normalized = value
.trim() .trim()
@ -58,6 +59,21 @@ function loopStatusFor(bridgeStatus) {
} }
return "ready_for_next_hop"; return "ready_for_next_hop";
} }
function routeCandidateStatusFor(bridgeStatus, pilot, missingProofFamily) {
if (bridgeStatus === "blocked" || pilot.pilot_status === "blocked") {
return "blocked";
}
if (bridgeStatus === "needs_clarification" || pilot.pilot_status === "skipped_needs_clarification") {
return "needs_user_scope";
}
if (bridgeStatus === "unsupported" || pilot.pilot_status === "unsupported") {
return "needs_route_enablement";
}
if (missingProofFamily) {
return "needs_route_enablement";
}
return "ready_for_reviewed_execution";
}
function flattenAxes(pilot, source) { function flattenAxes(pilot, source) {
const result = []; const result = [];
for (const step of pilot.dry_run.execution_steps) { for (const step of pilot.dry_run.execution_steps) {
@ -83,6 +99,119 @@ function entityCandidatesFromPlanner(planner) {
const values = planner.discovery_plan.turn_meaning_ref?.explicit_entity_candidates ?? []; const values = planner.discovery_plan.turn_meaning_ref?.explicit_entity_candidates ?? [];
return uniqueStrings(values); return uniqueStrings(values);
} }
function firstNonEmpty(values) {
for (const value of values) {
const text = String(value ?? "").trim();
if (text) {
return text;
}
}
return null;
}
function routeCandidateProofFamiliesFor(actionFamily, proofExpectation) {
const combined = `${actionFamily ?? ""} ${proofExpectation ?? ""}`.trim().toLowerCase();
const result = [];
const add = (family) => {
if (!result.includes(family)) {
result.push(family);
}
};
if (!combined || combined === "broad_evaluation bounded_inference") {
return result;
}
if (/(?:inventory|stock|warehouse|reserve|liquidation|write[-_ ]?off|obsolete|obsolescence)/iu.test(combined)) {
add("inventory_reserve_liquidation_quality");
}
if (/(?:debt|due[-_ ]?date|overdue|aging|credit[-_ ]?risk)/iu.test(combined)) {
add("debt_due_date_aging_quality");
}
if (/(?:vendor|supplier|procurement|sourcing)/iu.test(combined)) {
add("vendor_risk_procurement_quality");
}
if (/(?:profit|margin|pnl|p&l|financial[-_ ]?result)/iu.test(combined)) {
add("accounting_profit_margin");
}
return result;
}
function routeCandidateMissingProofFamily(planner, pilot) {
if (planner.data_need_graph?.business_fact_family !== "business_overview") {
return null;
}
const wantedFamilies = routeCandidateProofFamiliesFor(planner.data_need_graph?.action_family ?? null, planner.data_need_graph?.proof_expectation ?? null);
if (wantedFamilies.length <= 0) {
return null;
}
const missingProofFamilies = pilot.derived_business_overview?.missing_proof_families ?? [];
return missingProofFamilies.find((item) => wantedFamilies.includes(item.family)) ?? null;
}
function routeCandidateEnablementReason(status, pilot, missingAxes, missingProofFamily) {
if (status === "ready_for_reviewed_execution") {
return null;
}
if (status === "needs_user_scope") {
return missingAxes.length > 0
? `Missing scope axes: ${missingAxes.join(", ")}`
: "Selected chain needs user clarification before MCP execution";
}
if (missingProofFamily) {
return [
`Missing reviewed proof family: ${missingProofFamily.family}`,
`next_required_evidence=${missingProofFamily.next_required_evidence}`,
missingProofFamily.current_supported_evidence
? `current_supported_evidence=${missingProofFamily.current_supported_evidence}`
: null,
`must_not_claim=${missingProofFamily.must_not_claim}`
]
.filter((item) => Boolean(item))
.join("; ");
}
return firstNonEmpty([
...pilot.query_limitations,
...pilot.evidence.unknown_facts,
"Selected chain is not safely executable by the reviewed MCP runtime yet"
]);
}
function routeCandidateNextAction(status) {
if (status === "ready_for_reviewed_execution") {
return "Execute through the reviewed runtime bridge and truth gate.";
}
if (status === "needs_user_scope") {
return "Ask the user for the missing scope axes before MCP execution.";
}
if (status === "needs_route_enablement") {
return "Create or wire a reviewed exact route for the selected chain before treating the fact as answerable.";
}
return "Do not execute until the blocking reason is resolved.";
}
function buildRouteCandidate(planner, pilot, bridgeStatus) {
const plannerClarificationGaps = planner.discovery_plan.clarification_gaps ?? [];
const providedAxes = flattenAxes(pilot, "provided_axes");
const missingAxes = plannerClarificationGaps.length > 0 ? plannerClarificationGaps : flattenAxes(pilot, "missing_axis_options");
const missingProofFamily = routeCandidateMissingProofFamily(planner, pilot);
const candidateStatus = routeCandidateStatusFor(bridgeStatus, pilot, missingProofFamily);
return {
schema_version: exports.ASSISTANT_MCP_ROUTE_CANDIDATE_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryRuntimeBridge",
candidate_status: candidateStatus,
selected_chain_id: planner.selected_chain_id,
selected_chain_summary: planner.selected_chain_summary,
nearest_catalog_chain_template: planner.catalog_chain_template_alignment.top_chain_template_match,
catalog_alignment_status: planner.catalog_chain_template_alignment.alignment_status,
business_fact_family: planner.data_need_graph?.business_fact_family ?? null,
action_family: planner.data_need_graph?.action_family ?? null,
proof_expectation: planner.data_need_graph?.proof_expectation ?? null,
required_axes: [...planner.required_axes],
provided_axes: providedAxes,
missing_axes: missingAxes,
executable_now: candidateStatus === "ready_for_reviewed_execution",
enablement_reason: routeCandidateEnablementReason(candidateStatus, pilot, missingAxes, missingProofFamily),
recommended_next_action: routeCandidateNextAction(candidateStatus),
forbidden_overclaim_flags: uniqueStrings([
...(planner.data_need_graph?.forbidden_overclaim_flags ?? []),
...(missingProofFamily ? [missingProofFamily.must_not_claim] : [])
])
};
}
function buildLoopState(planner, pilot, bridgeStatus) { function buildLoopState(planner, pilot, bridgeStatus) {
const plannerClarificationGaps = planner.discovery_plan.clarification_gaps ?? []; const plannerClarificationGaps = planner.discovery_plan.clarification_gaps ?? [];
return { return {
@ -120,10 +249,13 @@ async function runAssistantMcpDiscoveryRuntimeBridge(input) {
const answerDraft = (0, assistantMcpDiscoveryAnswerAdapter_1.buildAssistantMcpDiscoveryAnswerDraft)(pilot); const answerDraft = (0, assistantMcpDiscoveryAnswerAdapter_1.buildAssistantMcpDiscoveryAnswerDraft)(pilot);
const bridgeStatus = bridgeStatusFor(pilot, answerDraft); const bridgeStatus = bridgeStatusFor(pilot, answerDraft);
const loopState = buildLoopState(planner, pilot, bridgeStatus); const loopState = buildLoopState(planner, pilot, bridgeStatus);
const routeCandidate = buildRouteCandidate(planner, pilot, bridgeStatus);
const reasonCodes = uniqueStrings([...planner.reason_codes, ...pilot.reason_codes, ...answerDraft.reason_codes]); const reasonCodes = uniqueStrings([...planner.reason_codes, ...pilot.reason_codes, ...answerDraft.reason_codes]);
pushReason(reasonCodes, `runtime_bridge_status_${bridgeStatus}`); pushReason(reasonCodes, `runtime_bridge_status_${bridgeStatus}`);
pushReason(reasonCodes, "runtime_bridge_not_wired_to_hot_assistant_answer"); pushReason(reasonCodes, "runtime_bridge_not_wired_to_hot_assistant_answer");
pushReason(reasonCodes, `runtime_bridge_loop_state_${loopState.loop_status}`); pushReason(reasonCodes, `runtime_bridge_loop_state_${loopState.loop_status}`);
pushReason(reasonCodes, "runtime_bridge_route_candidate_built");
pushReason(reasonCodes, `runtime_bridge_route_candidate_${routeCandidate.candidate_status}`);
return { return {
schema_version: exports.ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION, schema_version: exports.ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryRuntimeBridge", policy_owner: "assistantMcpDiscoveryRuntimeBridge",
@ -133,6 +265,7 @@ async function runAssistantMcpDiscoveryRuntimeBridge(input) {
pilot, pilot,
answer_draft: answerDraft, answer_draft: answerDraft,
loop_state: loopState, loop_state: loopState,
route_candidate: routeCandidate,
user_facing_response_allowed: bridgeStatus !== "blocked", user_facing_response_allowed: bridgeStatus !== "blocked",
business_fact_answer_allowed: businessFactAnswerAllowed(answerDraft), business_fact_answer_allowed: businessFactAnswerAllowed(answerDraft),
requires_user_clarification: bridgeStatus === "needs_clarification", requires_user_clarification: bridgeStatus === "needs_clarification",

View File

@ -130,6 +130,11 @@ function isGarbageSemanticAnchorCandidate(value) {
"всему", "всему",
"всей", "всей",
"всем", "всем",
"год",
"года",
"году",
"годом",
"годы",
"выводу", "выводу",
"выводам", "выводам",
"аудиту", "аудиту",
@ -626,6 +631,30 @@ function hasOrganizationLevelEarningsOverviewSignal(text) {
/(?:\u043a\u0430\u043a\w*|\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a\w*|\u043f\u043e\u043a\u0430\u0436|\u0434\u0430\u0439|\u0443\s+\u043d\u0430\u0441|\u043d\u0430\u0448\w*|\u043c\u044b\b|\u043a\u043e\u043c\u043f\u0430\u043d|\u0431\u0438\u0437\u043d\u0435\u0441|\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446|\u0432\s+\u0446\u0435\u043b\u043e\u043c|\u0432\u043e\u043e\u0431\u0449\u0435|(?:19|20)\d{2}|all\s+time|what|which|how\s+much|show|give|company|business|organization|our|we|us)/iu.test(text); /(?:\u043a\u0430\u043a\w*|\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a\w*|\u043f\u043e\u043a\u0430\u0436|\u0434\u0430\u0439|\u0443\s+\u043d\u0430\u0441|\u043d\u0430\u0448\w*|\u043c\u044b\b|\u043a\u043e\u043c\u043f\u0430\u043d|\u0431\u0438\u0437\u043d\u0435\u0441|\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446|\u0432\s+\u0446\u0435\u043b\u043e\u043c|\u0432\u043e\u043e\u0431\u0449\u0435|(?:19|20)\d{2}|all\s+time|what|which|how\s+much|show|give|company|business|organization|our|we|us)/iu.test(text);
return hasYearRankingCue || hasCompanyEarningsCue || hasCompanyProfitMarginCue; return hasYearRankingCue || hasCompanyEarningsCue || hasCompanyProfitMarginCue;
} }
function hasOrganizationLevelProfitMarginBoundaryOverviewSignal(text) {
if (!text) {
return false;
}
const hasProfitMarginCue = /(?:\u043f\u0440\u0438\u0431\u044b\u043b\w*|\u043c\u0430\u0440\u0436\w*|\u0440\u0435\u043d\u0442\u0430\u0431\w*|\u0444\u0438\u043d(?:\u0430\u043d\u0441\w*)?\s*[- ]?\s*\u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442|p\s*&\s*l|profit(?:ability)?|margin|financial\s+result)/iu.test(text);
const hasCompanyScopeCue = /(?:\u0443\s+\u043d\u0430\u0441|\u043d\u0430\u0448\w*|\u043c\u044b\b|\u043f\u043e\s+\u043a\u043e\u043c\u043f\u0430\u043d|\u043a\u043e\u043c\u043f\u0430\u043d|\u0431\u0438\u0437\u043d\u0435\u0441|\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446|\u0432\s+\u0446\u0435\u043b\u043e\u043c|\b(?:\u043e\u043e\u043e|\u0438\u043f|\u0430\u043e|\u043f\u0430\u043e|\u0437\u0430\u043e|\u043e\u0430\u043e)\b|(?:19|20)\d{2}|company|business|organization|our|we|us)/iu.test(text);
return hasProfitMarginCue && hasCompanyScopeCue;
}
function hasProfitMarginBoundaryFollowupSignal(text) {
if (!text) {
return false;
}
const hasProfitOrResultCue = /(?:\u043f\u0440\u0438\u0431\u044b\u043b\w*|\u0443\u0431\u044b\u0442\w*|\u043c\u0430\u0440\u0436\w*|\u0440\u0435\u043d\u0442\u0430\u0431\w*|\u0444\u0438\u043d(?:\u0430\u043d\u0441\w*)?\s*[- ]?\s*\u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442|p\s*&\s*l|profit|loss|margin|financial\s+result)/iu.test(text);
const hasFollowupShape = /(?:\u044d\u0442\u043e|\u0438\u0442\u043e\u0433|\u0438\u0442\u043e\u0433\u043e|\u043f\u043e\u043b\u0443\u0447\w*|\u043a\u043e\u0440\u043e\u0442\w*|\u0432\s+\u0438\u0442\u043e\u0433\u0435|\u043c\u043e\u0436\u043d\u043e\s+(?:\u043b\u0438\s+)?\u0441\u043a\u0430\u0437\u0430\u0442\u044c|is\s+it|result|short|brief)/iu.test(text);
return hasProfitOrResultCue && hasFollowupShape;
}
function hasDebtDueDateBoundaryFollowupSignal(text) {
if (!text) {
return false;
}
const hasDueDateCue = /(?:\u043f\u0440\u043e\u0441\u0440\u043e\u0447\w*|\u0441\u0440\u043e\u043a\w*\s+\u043e\u043f\u043b\u0430\u0442|\u0434\u043e\u043a\u0430\u0437\w*|\u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\w*|due[-\s]?date|overdue|debt\s+aging|aging)/iu.test(text);
const hasFollowupShape = /(?:\u0442\u043e\s+\u0435\u0441\u0442\u044c|\u043f\u043e\u0447\u0435\u043c\u0443|\u043a\u043e\u0440\u043e\u0442\w*|\u043d\u0435\u043b\u044c\u0437\u044f|\u043c\u043e\u0436\u043d\u043e\s+\u043b\u0438|\u0437\u043d\u0430\u0447\u0438\u0442|\u0432\u044b\u0445\u043e\u0434\u0438\u0442|why|short|brief|so)/iu.test(text);
return hasDueDateCue && hasFollowupShape;
}
function hasOrganizationLevelDebtDueDateOverviewSignal(text) { function hasOrganizationLevelDebtDueDateOverviewSignal(text) {
if (!text) { if (!text) {
return false; return false;
@ -711,6 +740,12 @@ function hasExplicitVatQuestionSignal(text) {
return (/(?:\u043d\u0434\u0441|vat)/iu.test(text) && return (/(?:\u043d\u0434\u0441|vat)/iu.test(text) &&
/(?:\u0437\u0430|\u043d\u0430|\u043f\u0435\u0440\u0438\u043e\u0434|\u043f\u043e\u0437\u0438\u0446|\u043a\s+\u0443\u043f\u043b\u0430\u0442|\u043a\s+\u0432\u043e\u0437\u043c\u0435\u0449|\u043e\u0441\u043d\u043e\u0432\u0430\u043d|\u043d\u0430\u043b\u043e\u0433\u043e\u0432\p{L}*\s+\u0432\u044b\u0432\u043e\u0434|tax\s+period|tax\s+position)/iu.test(text)); /(?:\u0437\u0430|\u043d\u0430|\u043f\u0435\u0440\u0438\u043e\u0434|\u043f\u043e\u0437\u0438\u0446|\u043a\s+\u0443\u043f\u043b\u0430\u0442|\u043a\s+\u0432\u043e\u0437\u043c\u0435\u0449|\u043e\u0441\u043d\u043e\u0432\u0430\u043d|\u043d\u0430\u043b\u043e\u0433\u043e\u0432\p{L}*\s+\u0432\u044b\u0432\u043e\u0434|tax\s+period|tax\s+position)/iu.test(text));
} }
function hasExplicitVatMovementEvidenceSignal(text) {
if (!/(?:\u043d\u0434\u0441|vat)/iu.test(text)) {
return false;
}
return hasMovementEvidenceFollowupSignal(text);
}
function hasBusinessOverviewSeparateCounterpartySignal(text) { function hasBusinessOverviewSeparateCounterpartySignal(text) {
if (!text) { if (!text) {
return false; return false;
@ -1102,6 +1137,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
const rawReferentialDocumentExclusionSignal = hasReferentialDocumentExclusionFollowupSignal(repairedUserText ?? rawUserText ?? ""); const rawReferentialDocumentExclusionSignal = hasReferentialDocumentExclusionFollowupSignal(repairedUserText ?? rawUserText ?? "");
const rawPrimaryBusinessOverviewSignal = hasBusinessOverviewSignal(rawText); const rawPrimaryBusinessOverviewSignal = hasBusinessOverviewSignal(rawText);
const explicitVatQuestionSignal = hasExplicitVatQuestionSignal(rawText); const explicitVatQuestionSignal = hasExplicitVatQuestionSignal(rawText);
const explicitVatMovementEvidenceSignal = hasExplicitVatMovementEvidenceSignal(rawText);
const explicitVatSuppressesBusinessOverviewContinuation = Boolean(explicitVatQuestionSignal && !rawPrimaryBusinessOverviewSignal); const explicitVatSuppressesBusinessOverviewContinuation = Boolean(explicitVatQuestionSignal && !rawPrimaryBusinessOverviewSignal);
const businessOverviewContinuationSignal = hasBusinessOverviewFollowupSeed(followupSeed) && const businessOverviewContinuationSignal = hasBusinessOverviewFollowupSeed(followupSeed) &&
hasBusinessOverviewContinuationSignal(rawText) && hasBusinessOverviewContinuationSignal(rawText) &&
@ -1114,6 +1150,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
(hasValueFlowSignal(rawText) || hasValueRankingSignal(rawText) || rawBidirectionalValueFlowSignal); (hasValueFlowSignal(rawText) || hasValueRankingSignal(rawText) || rawBidirectionalValueFlowSignal);
const rawMetadataSignal = !rawLifecycleSignal && const rawMetadataSignal = !rawLifecycleSignal &&
!rawValueFlowSignal && !rawValueFlowSignal &&
!explicitVatMovementEvidenceSignal &&
!rawReferentialDocumentExclusionSignal && !rawReferentialDocumentExclusionSignal &&
hasMetadataSignal(rawText); hasMetadataSignal(rawText);
const rawEntityResolutionSignal = !rawLifecycleSignal && !rawValueFlowSignal && !rawMetadataSignal && hasEntityResolutionSignal(rawText); const rawEntityResolutionSignal = !rawLifecycleSignal && !rawValueFlowSignal && !rawMetadataSignal && hasEntityResolutionSignal(rawText);
@ -1128,7 +1165,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
const explicitDateScopeLiteralDetected = hasExplicitDateScopeLiteral(dateScopeSignalText); const explicitDateScopeLiteralDetected = hasExplicitDateScopeLiteral(dateScopeSignalText);
const relativeCurrentDateHintDetected = hasRelativeCurrentDateHint(rawText); const relativeCurrentDateHintDetected = hasRelativeCurrentDateHint(rawText);
const rawDateScope = collectDateScopeFromRawText(dateScopeSignalText); const rawDateScope = collectDateScopeFromRawText(dateScopeSignalText);
const rawMetadataScopeHint = rawMetadataSignal ? metadataScopeHintFromRawText(rawText) : null; const rawMetadataScopeHint = rawMetadataSignal || explicitVatMovementEvidenceSignal ? metadataScopeHintFromRawText(rawText) : null;
const rawTopicSwitchSignal = hasExplicitTopicSwitchSignal(rawText); const rawTopicSwitchSignal = hasExplicitTopicSwitchSignal(rawText);
const rawEntityCandidate = rawEntityResolutionSignal ? rawEntityResolutionCandidate(rawEntitySourceText) : null; const rawEntityCandidate = rawEntityResolutionSignal ? rawEntityResolutionCandidate(rawEntitySourceText) : null;
const rawScopedEntityCandidate = !predecomposeEntities.counterparty && const rawScopedEntityCandidate = !predecomposeEntities.counterparty &&
@ -1147,11 +1184,41 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
const rawAggregationAxis = toNonEmptyString(assistantTurnMeaning?.asked_aggregation_axis); const rawAggregationAxis = toNonEmptyString(assistantTurnMeaning?.asked_aggregation_axis);
const unsupported = toNonEmptyString(assistantTurnMeaning?.unsupported_but_understood_family); const unsupported = toNonEmptyString(assistantTurnMeaning?.unsupported_but_understood_family);
const broadBusinessEvaluationUnsupported = unsupported === "broad_business_evaluation"; const broadBusinessEvaluationUnsupported = unsupported === "broad_business_evaluation";
const businessOverviewSignal = rawBusinessOverviewSignal || const seededBusinessOverviewSignal = broadBusinessEvaluationUnsupported ||
broadBusinessEvaluationUnsupported ||
rawDomain === "business_summary" || rawDomain === "business_summary" ||
rawDomain === "business_overview" || rawDomain === "business_overview" ||
rawAction === "broad_evaluation"; rawAction === "broad_evaluation";
const inventoryReserveBusinessOverviewSignal = (rawBusinessOverviewSignal || seededBusinessOverviewSignal) &&
hasOrganizationLevelInventoryReserveLiquidationOverviewSignal(rawText);
const debtDueDateFollowupBusinessOverviewSignal = businessOverviewContinuationSignal && hasDebtDueDateBoundaryFollowupSignal(rawText);
const debtDueDateBusinessOverviewSignal = ((rawBusinessOverviewSignal || seededBusinessOverviewSignal) &&
hasOrganizationLevelDebtDueDateOverviewSignal(rawText)) ||
debtDueDateFollowupBusinessOverviewSignal;
const supplierQualityBusinessOverviewSignal = (rawBusinessOverviewSignal || seededBusinessOverviewSignal) && hasOrganizationLevelSupplierQualityOverviewSignal(rawText);
const profitMarginFollowupBusinessOverviewSignal = businessOverviewContinuationSignal && hasProfitMarginBoundaryFollowupSignal(rawText);
const profitMarginBusinessOverviewSignal = ((rawBusinessOverviewSignal || seededBusinessOverviewSignal) &&
hasOrganizationLevelProfitMarginBoundaryOverviewSignal(rawText)) ||
profitMarginFollowupBusinessOverviewSignal;
const businessOverviewActionFamily = inventoryReserveBusinessOverviewSignal
? "inventory_reserve_boundary"
: debtDueDateBusinessOverviewSignal
? "debt_due_date_boundary"
: supplierQualityBusinessOverviewSignal
? "vendor_risk_procurement_boundary"
: profitMarginBusinessOverviewSignal
? "profit_margin_boundary"
: "broad_evaluation";
const businessOverviewUnsupportedFamily = inventoryReserveBusinessOverviewSignal
? "inventory_reserve_liquidation_boundary"
: debtDueDateBusinessOverviewSignal
? "debt_due_date_boundary"
: supplierQualityBusinessOverviewSignal
? "vendor_risk_procurement_boundary"
: profitMarginBusinessOverviewSignal
? "profit_margin_boundary"
: "broad_business_evaluation";
const businessOverviewSignal = rawBusinessOverviewSignal ||
seededBusinessOverviewSignal;
const businessOverviewSeparateCounterpartySignal = Boolean(businessOverviewSignal && hasBusinessOverviewSeparateCounterpartySignal(rawText)); const businessOverviewSeparateCounterpartySignal = Boolean(businessOverviewSignal && hasBusinessOverviewSeparateCounterpartySignal(rawText));
const businessOverviewSeparateCounterpartyCandidate = businessOverviewSeparateCounterpartySignal const businessOverviewSeparateCounterpartyCandidate = businessOverviewSeparateCounterpartySignal
? businessOverviewSeparateCounterpartyCandidateFromText(rawText) ? businessOverviewSeparateCounterpartyCandidateFromText(rawText)
@ -1176,7 +1243,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
: rawAssistantTurnMeaningOrganizationScope; : rawAssistantTurnMeaningOrganizationScope;
const rawOrganizationMentionSignal = hasOrganizationScopeSignalUtf8(rawText); const rawOrganizationMentionSignal = hasOrganizationScopeSignalUtf8(rawText);
const rawOrganizationScope = extractOrganizationScopeFromRawText(rawUserText ?? rawEffectiveText ?? rawSignalSourceText); const rawOrganizationScope = extractOrganizationScopeFromRawText(rawUserText ?? rawEffectiveText ?? rawSignalSourceText);
const currentTurnFreshOrganizationScope = rawOrganizationScope ?? predecomposeEntities.organization; const currentTurnFreshOrganizationScope = predecomposeEntities.organization ?? rawOrganizationScope;
const currentTurnOrganizationScope = currentTurnFreshOrganizationScope ?? assistantTurnMeaningOrganizationScope; const currentTurnOrganizationScope = currentTurnFreshOrganizationScope ?? assistantTurnMeaningOrganizationScope;
const followupCounterpartyIsMetadataOrganizationScope = Boolean(followupSeed.subjectResolutionOptional && const followupCounterpartyIsMetadataOrganizationScope = Boolean(followupSeed.subjectResolutionOptional &&
followupSeed.counterparty && followupSeed.counterparty &&
@ -1500,9 +1567,21 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
const semanticDataNeed = metadataAmbiguityLaneClarificationApplicable const semanticDataNeed = metadataAmbiguityLaneClarificationApplicable
? "metadata lane clarification" ? "metadata lane clarification"
: semanticNeedFor({ : semanticNeedFor({
domain: businessOverviewSignal ? "business_overview" : rawDomain ?? seededDomain, domain: explicitVatMovementEvidenceSignal
action: businessOverviewSignal ? "broad_evaluation" : rawAction ?? seededAction, ? "movements"
unsupported: businessOverviewSignal ? "broad_business_evaluation" : unsupported ?? seededUnsupported, : businessOverviewSignal
? "business_overview"
: rawDomain ?? seededDomain,
action: explicitVatMovementEvidenceSignal
? "list_movements"
: businessOverviewSignal
? businessOverviewActionFamily
: rawAction ?? seededAction,
unsupported: explicitVatMovementEvidenceSignal
? "movement_evidence"
: businessOverviewSignal
? businessOverviewUnsupportedFamily
: unsupported ?? seededUnsupported,
lifecycleSignal, lifecycleSignal,
valueFlowSignal, valueFlowSignal,
metadataSignal: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable, metadataSignal: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable,
@ -1530,7 +1609,9 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
followupSeed.discoveryEntity ?? followupSeed.discoveryEntity ??
followupSeed.metadataSelectedEntitySet ?? followupSeed.metadataSelectedEntitySet ??
null; null;
const metadataScopedLaneWithoutSubject = Boolean((metadataGroundedMovementLaneApplicable || metadataGroundedDocumentLaneApplicable) && const metadataScopedLaneWithoutSubject = Boolean((metadataGroundedMovementLaneApplicable ||
metadataGroundedDocumentLaneApplicable ||
explicitVatMovementEvidenceSignal) &&
!effectiveFollowupCounterparty && !effectiveFollowupCounterparty &&
metadataLaneCarryoverAvailable); metadataLaneCarryoverAvailable);
const groundedFollowupEntity = metadataScopedLaneWithoutSubject const groundedFollowupEntity = metadataScopedLaneWithoutSubject
@ -1711,17 +1792,19 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
? "counterparty_lifecycle" ? "counterparty_lifecycle"
: valueFlowSignal : valueFlowSignal
? "counterparty_value" ? "counterparty_value"
: metadataGroundedMovementLaneApplicable : explicitVatMovementEvidenceSignal
? "movements" ? "movements"
: metadataGroundedDocumentLaneApplicable : metadataGroundedMovementLaneApplicable
? "documents" ? "movements"
: entityResolutionSignal : metadataGroundedDocumentLaneApplicable
? "entity_resolution" ? "documents"
: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable : entityResolutionSignal
? "metadata" ? "entity_resolution"
: rawDomain ?? seededDomain, : rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
? "metadata"
: rawDomain ?? seededDomain,
asked_action_family: businessOverviewSignal asked_action_family: businessOverviewSignal
? "broad_evaluation" ? businessOverviewActionFamily
: lifecycleSignal : lifecycleSignal
? "activity_duration" ? "activity_duration"
: valueFlowSignal : valueFlowSignal
@ -1730,15 +1813,17 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
: payoutSignal : payoutSignal
? "payout" ? "payout"
: rawAction ?? seededAction ?? "turnover" : rawAction ?? seededAction ?? "turnover"
: metadataGroundedMovementLaneApplicable : explicitVatMovementEvidenceSignal
? "list_movements" ? "list_movements"
: metadataGroundedDocumentLaneApplicable : metadataGroundedMovementLaneApplicable
? "list_documents" ? "list_movements"
: entityResolutionSignal : metadataGroundedDocumentLaneApplicable
? "search_business_entity" ? "list_documents"
: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable : entityResolutionSignal
? metadataActionFromRawText(rawText) ?? seededAction ? "search_business_entity"
: rawAction ?? seededAction, : rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
? metadataActionFromRawText(rawText) ?? seededAction
: rawAction ?? seededAction,
asked_aggregation_axis: monthlyAggregationSignal ? "month" : rawAggregationAxis, asked_aggregation_axis: monthlyAggregationSignal ? "month" : rawAggregationAxis,
seeded_ranking_need: valueFlowSignal && followupSeed.rankingNeed && !rawEntitySearchOverridesStaleScope seeded_ranking_need: valueFlowSignal && followupSeed.rankingNeed && !rawEntitySearchOverridesStaleScope
? followupSeed.rankingNeed ? followupSeed.rankingNeed
@ -1757,7 +1842,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
explicit_date_scope: explicitDateScope, explicit_date_scope: explicitDateScope,
subject_resolution_optional: metadataScopedLaneWithoutSubject || undefined, subject_resolution_optional: metadataScopedLaneWithoutSubject || undefined,
unsupported_but_understood_family: businessOverviewSignal unsupported_but_understood_family: businessOverviewSignal
? "broad_business_evaluation" ? businessOverviewUnsupportedFamily
: unsupported ?? : unsupported ??
(lifecycleSignal (lifecycleSignal
? "counterparty_lifecycle" ? "counterparty_lifecycle"
@ -1771,20 +1856,23 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
? "movement_evidence" ? "movement_evidence"
: metadataGroundedDocumentLaneApplicable : metadataGroundedDocumentLaneApplicable
? "document_evidence" ? "document_evidence"
: metadataAmbiguityLaneClarificationApplicable : explicitVatMovementEvidenceSignal
? "metadata_lane_choice_clarification" ? "movement_evidence"
: entityResolutionSignal : metadataAmbiguityLaneClarificationApplicable
? "entity_resolution" ? "metadata_lane_choice_clarification"
: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable : entityResolutionSignal
? "1c_metadata_surface" ? "entity_resolution"
: followupDiscoverySeedApplicable : rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
? seededUnsupported ? "1c_metadata_surface"
: null), : followupDiscoverySeedApplicable
? seededUnsupported
: null),
stale_replay_forbidden: Boolean(assistantTurnMeaning?.stale_replay_forbidden || stale_replay_forbidden: Boolean(assistantTurnMeaning?.stale_replay_forbidden ||
businessOverviewSignal || businessOverviewSignal ||
unsupported || unsupported ||
lifecycleSignal || lifecycleSignal ||
valueFlowSignal || valueFlowSignal ||
explicitVatMovementEvidenceSignal ||
metadataGroundedMovementLaneApplicable || metadataGroundedMovementLaneApplicable ||
metadataGroundedDocumentLaneApplicable || metadataGroundedDocumentLaneApplicable ||
metadataAmbiguityLaneClarificationApplicable || metadataAmbiguityLaneClarificationApplicable ||
@ -1841,11 +1929,15 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
} }
const currentTurnValueFlowExactOverrideApplicable = Boolean(valueFlowSignal && const currentTurnValueFlowExactOverrideApplicable = Boolean(valueFlowSignal &&
explicitIntentCandidate && explicitIntentCandidate &&
rawValueFlowAggregateQuestionSignal && (rawValueFlowAggregateQuestionSignal || hasValueRankingSignal(rawText)) &&
semanticDataNeed && semanticDataNeed &&
(entityCandidates.length > 0 || explicitOrganizationScope || openScopeValueFlowWithoutResolvedCounterparty)); (entityCandidates.length > 0 || explicitOrganizationScope || openScopeValueFlowWithoutResolvedCounterparty));
const runDiscovery = shouldRunDiscovery({ const runDiscovery = shouldRunDiscovery({
unsupported: businessOverviewSignal ? "broad_business_evaluation" : unsupported ?? seededUnsupported, unsupported: explicitVatMovementEvidenceSignal
? "movement_evidence"
: businessOverviewSignal
? "broad_business_evaluation"
: unsupported ?? seededUnsupported,
lifecycleSignal, lifecycleSignal,
valueFlowSignal, valueFlowSignal,
metadataSignal: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable, metadataSignal: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable,
@ -1860,6 +1952,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
metadataGroundedDocumentLaneApplicable || metadataGroundedDocumentLaneApplicable ||
groundedValueFlowFollowupApplicable, groundedValueFlowFollowupApplicable,
forceDiscoveryOverExplicitIntent: businessOverviewSignal || forceDiscoveryOverExplicitIntent: businessOverviewSignal ||
explicitVatMovementEvidenceSignal ||
Boolean(entityResolutionClarificationCandidate) || Boolean(entityResolutionClarificationCandidate) ||
organizationClarificationFollowupApplicable || organizationClarificationFollowupApplicable ||
periodClarificationFollowupApplicable || periodClarificationFollowupApplicable ||
@ -1883,17 +1976,19 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
? "followup_context" ? "followup_context"
: metadataGroundedDocumentLaneApplicable : metadataGroundedDocumentLaneApplicable
? "followup_context" ? "followup_context"
: predecomposeContract : explicitVatMovementEvidenceSignal
? "predecompose_contract" ? "raw_text"
: lifecycleSignal : predecomposeContract
? "raw_text" ? "predecompose_contract"
: valueFlowSignal : lifecycleSignal
? "raw_text" ? "raw_text"
: entityResolutionSignal : valueFlowSignal
? "raw_text" ? "raw_text"
: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable : entityResolutionSignal
? "raw_text" ? "raw_text"
: "none"; : rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
? "raw_text"
: "none";
if (lifecycleSignal) { if (lifecycleSignal) {
pushReason(reasonCodes, "mcp_discovery_lifecycle_signal_detected"); pushReason(reasonCodes, "mcp_discovery_lifecycle_signal_detected");
} }
@ -1903,6 +1998,9 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
if (rawMetadataSignal) { if (rawMetadataSignal) {
pushReason(reasonCodes, "mcp_discovery_metadata_signal_detected"); pushReason(reasonCodes, "mcp_discovery_metadata_signal_detected");
} }
if (explicitVatMovementEvidenceSignal) {
pushReason(reasonCodes, "mcp_discovery_vat_movement_evidence_signal_detected");
}
if (entityResolutionSignal) { if (entityResolutionSignal) {
pushReason(reasonCodes, "mcp_discovery_entity_resolution_signal_detected"); pushReason(reasonCodes, "mcp_discovery_entity_resolution_signal_detected");
} }
@ -2026,6 +2124,12 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
if (businessOverviewContinuationSignal) { if (businessOverviewContinuationSignal) {
pushReason(reasonCodes, "mcp_discovery_business_overview_continuation_from_followup_context"); pushReason(reasonCodes, "mcp_discovery_business_overview_continuation_from_followup_context");
} }
if (profitMarginFollowupBusinessOverviewSignal) {
pushReason(reasonCodes, "mcp_discovery_business_overview_profit_margin_followup_boundary");
}
if (debtDueDateFollowupBusinessOverviewSignal) {
pushReason(reasonCodes, "mcp_discovery_business_overview_debt_due_date_followup_boundary");
}
if (explicitVatSuppressesBusinessOverviewContinuation) { if (explicitVatSuppressesBusinessOverviewContinuation) {
pushReason(reasonCodes, "mcp_discovery_business_overview_continuation_suppressed_by_explicit_vat_question"); pushReason(reasonCodes, "mcp_discovery_business_overview_continuation_suppressed_by_explicit_vat_question");
} }

View File

@ -313,6 +313,13 @@ function createAssistantRoutePolicy(deps) {
const hasTemporalCue = /(?:на\s+эту\s+же\s+дат[ауеы]|на\s+тот\s+же\s+период|за\s+этот\s+же\s+период|за\s+этот\s+период|март|апрел|ма[йя]|июн|июл|август|сентябр|октябр|ноябр|декабр|\b(?:19|20)\d{2}\b)/iu.test(normalized); const hasTemporalCue = /(?:на\s+эту\s+же\s+дат[ауеы]|на\s+тот\s+же\s+период|за\s+этот\s+же\s+период|за\s+этот\s+период|март|апрел|ма[йя]|июн|июл|август|сентябр|октябр|ноябр|декабр|\b(?:19|20)\d{2}\b)/iu.test(normalized);
return hasRequestCue && hasTemporalCue; return hasRequestCue && hasTemporalCue;
} }
function hasOrganizationClarificationTextCue(text) {
const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase());
if (!normalized) {
return false;
}
return /(?<!\p{L})(?:\u043e\u043e\u043e|\u0438\u043f|\u0430\u043e|\u043f\u0430\u043e|\u0437\u0430\u043e)(?!\p{L})|(?:\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446|\u043a\u043e\u043c\u043f\u0430\u043d|llc|company|organization)/iu.test(normalized);
}
function resolveAssistantOrchestrationDecision(input) { function resolveAssistantOrchestrationDecision(input) {
const rawUserMessage = String(input?.rawUserMessage ?? input?.userMessage ?? ""); const rawUserMessage = String(input?.rawUserMessage ?? input?.userMessage ?? "");
const effectiveAddressUserMessage = String(input?.effectiveAddressUserMessage ?? rawUserMessage); const effectiveAddressUserMessage = String(input?.effectiveAddressUserMessage ?? rawUserMessage);
@ -475,6 +482,27 @@ function createAssistantRoutePolicy(deps) {
const followupPreviousFilters = followupContext?.previous_filters && typeof followupContext.previous_filters === "object" const followupPreviousFilters = followupContext?.previous_filters && typeof followupContext.previous_filters === "object"
? followupContext.previous_filters ? followupContext.previous_filters
: null; : null;
const followupLoopStatus = toNonEmptyString(followupContext?.previous_discovery_loop_status);
const followupLoopSelectedChainId = toNonEmptyString(followupContext?.previous_discovery_loop_selected_chain_id);
const followupLoopPendingAxes = Array.isArray(followupContext?.previous_discovery_loop_pending_axes)
? followupContext.previous_discovery_loop_pending_axes.map((item) => toNonEmptyString(item)).filter(Boolean)
: [];
const currentTurnPredecomposeOrganization = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.entities?.organization) ??
(toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.semantics?.anchor_kind) === "organization"
? toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.semantics?.anchor_value)
: null);
const routeCandidateOrganizationClarificationDetected = Boolean(followupContext &&
followupLoopStatus === "awaiting_clarification" &&
followupLoopSelectedChainId &&
followupLoopPendingAxes.includes("organization") &&
(currentTurnPredecomposeOrganization ||
explicitOrganizationClarificationSelection ||
[
rawUserMessage,
repairedRawUserMessage,
effectiveAddressUserMessage,
repairedEffectiveAddressUserMessage
].some((message) => hasOrganizationClarificationTextCue(message))));
const protectedInventoryShortFollowup = Boolean(followupContext && const protectedInventoryShortFollowup = Boolean(followupContext &&
(isInventorySelectedObjectIntent(followupPreviousIntent) || (isInventorySelectedObjectIntent(followupPreviousIntent) ||
(followupPreviousIntent === "inventory_on_hand_as_of_date" && (followupPreviousIntent === "inventory_on_hand_as_of_date" &&
@ -524,6 +552,7 @@ function createAssistantRoutePolicy(deps) {
"net_value_flow" "net_value_flow"
].includes(String(toNonEmptyString(assistantTurnMeaning?.asked_action_family) ?? "")) || ].includes(String(toNonEmptyString(assistantTurnMeaning?.asked_action_family) ?? "")) ||
/(?:нетто|сальдо|сколько\s+мы\s+(?:получили|заплатили)|incoming|outgoing)/iu.test(analyticsSample))); /(?:нетто|сальдо|сколько\s+мы\s+(?:получили|заплатили)|incoming|outgoing)/iu.test(analyticsSample)));
const effectiveGroundedValueFlowFollowupContextDetected = groundedValueFlowFollowupContextDetected || routeCandidateOrganizationClarificationDetected;
const baseToolGatePreservesAddressLane = Boolean(baseToolGate?.runAddressLane && const baseToolGatePreservesAddressLane = Boolean(baseToolGate?.runAddressLane &&
[ [
"address_intent_resolver_detected", "address_intent_resolver_detected",
@ -533,14 +562,15 @@ function createAssistantRoutePolicy(deps) {
].includes(String(baseToolGate?.reason ?? ""))) || ].includes(String(baseToolGate?.reason ?? ""))) ||
Boolean(baseToolGate?.runAddressLane && Boolean(baseToolGate?.runAddressLane &&
String(baseToolGate?.reason ?? "") === "followup_context_detected" && String(baseToolGate?.reason ?? "") === "followup_context_detected" &&
groundedValueFlowFollowupContextDetected); effectiveGroundedValueFlowFollowupContextDetected);
const nonDomainQueryIndexed = Boolean(!llmFirstAddressCandidate && const nonDomainQueryIndexed = Boolean(!llmFirstAddressCandidate &&
deterministicNonDomainGuard && deterministicNonDomainGuard &&
(llmFirstUnsupportedCandidate || llmContractMode === null) && (llmFirstUnsupportedCandidate || llmContractMode === null) &&
!baseToolGatePreservesAddressLane && !baseToolGatePreservesAddressLane &&
!groundedValueFlowFollowupContextDetected && !effectiveGroundedValueFlowFollowupContextDetected &&
!protectedInventoryShortFollowup && !protectedInventoryShortFollowup &&
!organizationClarificationContinuationDetected); !organizationClarificationContinuationDetected &&
!routeCandidateOrganizationClarificationDetected);
const lastAddressAssistantDebug = sessionItems const lastAddressAssistantDebug = sessionItems
? findLastAddressAssistantItem(sessionItems)?.debug ?? null ? findLastAddressAssistantItem(sessionItems)?.debug ?? null
: null; : null;
@ -583,7 +613,7 @@ function createAssistantRoutePolicy(deps) {
!turnMeaningIntentCandidate && !turnMeaningIntentCandidate &&
!dataScopeMetaQuery && !dataScopeMetaQuery &&
!dangerOrCoercionSignal && !dangerOrCoercionSignal &&
!groundedValueFlowFollowupContextDetected && !effectiveGroundedValueFlowFollowupContextDetected &&
!organizationClarificationContinuationDetected); !organizationClarificationContinuationDetected);
const hardMetaMode = resolveHardMetaMode({ const hardMetaMode = resolveHardMetaMode({
dataScopeMetaQuery, dataScopeMetaQuery,
@ -748,7 +778,7 @@ function createAssistantRoutePolicy(deps) {
!dataScopeMetaQuery && !dataScopeMetaQuery &&
!capabilityMetaQuery && !capabilityMetaQuery &&
!dangerOrCoercionSignal && !dangerOrCoercionSignal &&
!groundedValueFlowFollowupContextDetected && !effectiveGroundedValueFlowFollowupContextDetected &&
!organizationClarificationContinuationDetected); !organizationClarificationContinuationDetected);
if (unsupportedCurrentTurnMeaningBoundary) { if (unsupportedCurrentTurnMeaningBoundary) {
return { return {

View File

@ -2123,6 +2123,9 @@ function isAddressLaneDebugPayload(debug) {
if (typeof debug.mcp_call_status === "string" && debug.mcp_call_status.trim().length > 0) { if (typeof debug.mcp_call_status === "string" && debug.mcp_call_status.trim().length > 0) {
return true; return true;
} }
if (debug.mcp_discovery_response_applied === true && debug.assistant_mcp_discovery_entry_point_v1) {
return true;
}
if (typeof debug.anchor_type === "string" && debug.anchor_type.trim().length > 0) { if (typeof debug.anchor_type === "string" && debug.anchor_type.trim().length > 0) {
return true; return true;
} }

View File

@ -120,6 +120,22 @@ function createAssistantTransitionPolicy(deps) {
const samples = [userMessage, alternateMessage].map((item) => normalizeFollowupText(item)).filter(Boolean); const samples = [userMessage, alternateMessage].map((item) => normalizeFollowupText(item)).filter(Boolean);
return samples.some((sample) => /(?:итог|summary|резюм|вывод|что\s+(?:мы\s+)?подтверд|что\s+понят|что\s+можно|что\s+нельзя|собери\s+коротк)/iu.test(sample) && /(?:контрагент|группа\s+свк|свк|отдельн)/iu.test(sample)); return samples.some((sample) => /(?:итог|summary|резюм|вывод|что\s+(?:мы\s+)?подтверд|что\s+понят|что\s+можно|что\s+нельзя|собери\s+коротк)/iu.test(sample) && /(?:контрагент|группа\s+свк|свк|отдельн)/iu.test(sample));
} }
function hasBusinessOverviewBoundaryFollowupCue(text) {
const normalized = normalizeFollowupText(text);
if (!normalized) {
return false;
}
const hasBoundaryCue = /(?:\u043f\u0440\u043e\u0441\u0440\u043e\u0447|\u0441\u0440\u043e\u043a\w*\s+\u043e\u043f\u043b\u0430\u0442|\u0434\u043e\u043a\u0430\u0437|\u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0434|\u043f\u0440\u0438\u0431\u044b\u043b|\u0443\u0431\u044b\u0442|\u043c\u0430\u0440\u0436|\u0440\u0435\u0437\u0435\u0440\u0432|\u043d\u0435\u043b\u0438\u043a\u0432\u0438\u0434|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u0432\u0435\u043d\u0434\u043e\u0440|due[-\s]?date|overdue|aging|profit|loss|margin|vendor|risk)/iu.test(normalized);
const hasFollowupShape = /(?:\u0442\u043e\s+\u0435\u0441\u0442\u044c|\u043f\u043e\u0447\u0435\u043c\u0443|\u043a\u043e\u0440\u043e\u0442\u043a|\u043d\u0435\u043b\u044c\u0437\u044f|\u043c\u043e\u0436\u043d\u043e\s+\u043b\u0438|\u0437\u043d\u0430\u0447\u0438\u0442|\u0432\u044b\u0445\u043e\u0434\u0438\u0442|\u0438\u0442\u043e\u0433|why|short|brief|so)/iu.test(normalized);
return hasBoundaryCue && hasFollowupShape;
}
function hasOrganizationClarificationTextCue(text) {
const normalized = deps.compactWhitespace(deps.repairAddressMojibake(String(text ?? "")).toLowerCase());
if (!normalized) {
return false;
}
return /(?<!\p{L})(?:\u043e\u043e\u043e|\u0438\u043f|\u0430\u043e|\u043f\u0430\u043e|\u0437\u0430\u043e)(?!\p{L})|(?:\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446|\u043a\u043e\u043c\u043f\u0430\u043d|llc|company|organization)/iu.test(normalized);
}
function parseDmyDateToIso(value) { function parseDmyDateToIso(value) {
const match = String(value ?? "").trim().match(/^(\d{2})\.(\d{2})\.(\d{4})$/); const match = String(value ?? "").trim().match(/^(\d{2})\.(\d{2})\.(\d{4})$/);
if (!match) { if (!match) {
@ -433,7 +449,11 @@ function createAssistantTransitionPolicy(deps) {
(deps.toNonEmptyString(alternateMessage) (deps.toNonEmptyString(alternateMessage)
? deps.hasDataRetrievalRequestSignal(String(alternateMessage ?? "")) ? deps.hasDataRetrievalRequestSignal(String(alternateMessage ?? ""))
: false); : false);
if (rawCapabilityMetaQuery && !rawDataRetrievalSignal) { const rawBusinessOverviewBoundaryFollowupCue = hasBusinessOverviewBoundaryFollowupCue(userMessage) ||
(deps.toNonEmptyString(alternateMessage)
? hasBusinessOverviewBoundaryFollowupCue(String(alternateMessage ?? ""))
: false);
if (rawCapabilityMetaQuery && !rawDataRetrievalSignal && !rawBusinessOverviewBoundaryFollowupCue) {
return null; return null;
} }
const assistantTurnMeaning = typeof deps.resolveAssistantTurnMeaning === "function" const assistantTurnMeaning = typeof deps.resolveAssistantTurnMeaning === "function"
@ -492,10 +512,25 @@ function createAssistantTransitionPolicy(deps) {
: false)); : false));
const sourceIntentHint = (0, assistantContinuityPolicy_1.readAddressDebugIntent)(carryoverSourceDebug, deps.toNonEmptyString); const sourceIntentHint = (0, assistantContinuityPolicy_1.readAddressDebugIntent)(carryoverSourceDebug, deps.toNonEmptyString);
const sourceDiscoveryPilotScopeHint = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryPilotScope)(carryoverSourceDebug, deps.toNonEmptyString); const sourceDiscoveryPilotScopeHint = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryPilotScope)(carryoverSourceDebug, deps.toNonEmptyString);
const sourceDiscoveryLoopStatusHint = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopStatus)(carryoverSourceDebug, deps.toNonEmptyString);
const sourceDiscoveryLoopSelectedChainIdHint = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopSelectedChainId)(carryoverSourceDebug, deps.toNonEmptyString);
const sourceDiscoveryLoopPendingAxesHint = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopPendingAxes)(carryoverSourceDebug, deps.toNonEmptyString);
const sourceDiscoveryLoopProvidedAxesHint = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopProvidedAxes)(carryoverSourceDebug, deps.toNonEmptyString);
const currentTurnPredecomposeOrganization = deps.toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.entities?.organization) ??
(deps.toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.semantics?.anchor_kind) === "organization"
? deps.toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.semantics?.anchor_value)
: null);
const mcpDiscoveryOrganizationClarificationContinuation = Boolean(sourceDiscoveryLoopStatusHint === "awaiting_clarification" &&
sourceDiscoveryLoopSelectedChainIdHint &&
sourceDiscoveryLoopPendingAxesHint.includes("organization") &&
(currentTurnPredecomposeOrganization ||
explicitOrganizationClarificationSelection ||
[userMessage, alternateMessage].some((message) => hasOrganizationClarificationTextCue(message))));
const hasValueFlowCarryoverSourceHint = sourceIntentHint === "customer_revenue_and_payments" || const hasValueFlowCarryoverSourceHint = sourceIntentHint === "customer_revenue_and_payments" ||
sourceDiscoveryPilotScopeHint === "counterparty_value_flow_query_movements_v1" || sourceDiscoveryPilotScopeHint === "counterparty_value_flow_query_movements_v1" ||
sourceDiscoveryPilotScopeHint === "counterparty_supplier_payout_query_movements_v1" || sourceDiscoveryPilotScopeHint === "counterparty_supplier_payout_query_movements_v1" ||
sourceDiscoveryPilotScopeHint === "counterparty_bidirectional_value_flow_query_movements_v1"; sourceDiscoveryPilotScopeHint === "counterparty_bidirectional_value_flow_query_movements_v1";
const hasBusinessOverviewCarryoverSourceHint = sourceDiscoveryPilotScopeHint === "business_overview_route_template_v1";
const navigationSessionState = (0, assistantContinuityPolicy_1.resolveNavigationSessionContextState)(addressNavigationState, deps.toNonEmptyString, deps.normalizeOrganizationScopeValue); const navigationSessionState = (0, assistantContinuityPolicy_1.resolveNavigationSessionContextState)(addressNavigationState, deps.toNonEmptyString, deps.normalizeOrganizationScopeValue);
const navigationFocusObjectHint = navigationSessionState.focusObject; const navigationFocusObjectHint = navigationSessionState.focusObject;
const hasNavigationInventoryItemFocusHint = Boolean(deps.toNonEmptyString(navigationFocusObjectHint?.label) && const hasNavigationInventoryItemFocusHint = Boolean(deps.toNonEmptyString(navigationFocusObjectHint?.label) &&
@ -521,20 +556,28 @@ function createAssistantTransitionPolicy(deps) {
const shortValueFlowRetargetAlternate = hasValueFlowCarryoverSourceHint && deps.toNonEmptyString(alternateMessage) const shortValueFlowRetargetAlternate = hasValueFlowCarryoverSourceHint && deps.toNonEmptyString(alternateMessage)
? hasShortValueFlowRetargetCue(String(alternateMessage ?? "")) ? hasShortValueFlowRetargetCue(String(alternateMessage ?? ""))
: false; : false;
const businessOverviewBoundaryFollowupPrimary = hasBusinessOverviewCarryoverSourceHint && hasBusinessOverviewBoundaryFollowupCue(userMessage);
const businessOverviewBoundaryFollowupAlternate = hasBusinessOverviewCarryoverSourceHint && deps.toNonEmptyString(alternateMessage)
? hasBusinessOverviewBoundaryFollowupCue(String(alternateMessage ?? ""))
: false;
const explicitSummaryBundleReuseSignal = hasExplicitSummaryBundleReuseSignal(userMessage, alternateMessage); const explicitSummaryBundleReuseSignal = hasExplicitSummaryBundleReuseSignal(userMessage, alternateMessage);
let hasPrimaryFollowupSignal = deps.hasAddressFollowupContextSignal(userMessage) || let hasPrimaryFollowupSignal = deps.hasAddressFollowupContextSignal(userMessage) ||
Boolean(debtRoleSwapPrimary) || Boolean(debtRoleSwapPrimary) ||
shortValueFlowRetargetPrimary || shortValueFlowRetargetPrimary ||
businessOverviewBoundaryFollowupPrimary ||
inventoryShortFollowupPrimary || inventoryShortFollowupPrimary ||
inventoryPurchaseDateVatBridge || inventoryPurchaseDateVatBridge ||
explicitSummaryBundleReuseSignal; explicitSummaryBundleReuseSignal ||
mcpDiscoveryOrganizationClarificationContinuation;
let hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage) let hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage)
? deps.hasAddressFollowupContextSignal(alternateMessage) || ? deps.hasAddressFollowupContextSignal(alternateMessage) ||
Boolean(debtRoleSwapAlternate) || Boolean(debtRoleSwapAlternate) ||
shortValueFlowRetargetAlternate || shortValueFlowRetargetAlternate ||
businessOverviewBoundaryFollowupAlternate ||
inventoryShortFollowupAlternate || inventoryShortFollowupAlternate ||
inventoryPurchaseDateVatBridge || inventoryPurchaseDateVatBridge ||
explicitSummaryBundleReuseSignal explicitSummaryBundleReuseSignal ||
mcpDiscoveryOrganizationClarificationContinuation
: false; : false;
const hasPrimaryIndexReferenceSignal = deps.extractDisplayedEntityIndexMention(userMessage) !== null; const hasPrimaryIndexReferenceSignal = deps.extractDisplayedEntityIndexMention(userMessage) !== null;
const hasAlternateIndexReferenceSignal = deps.toNonEmptyString(alternateMessage) const hasAlternateIndexReferenceSignal = deps.toNonEmptyString(alternateMessage)
@ -557,6 +600,7 @@ function createAssistantTransitionPolicy(deps) {
let hasStrongFollowupReference = hasPrimaryIndexReferenceSignal || let hasStrongFollowupReference = hasPrimaryIndexReferenceSignal ||
hasAlternateIndexReferenceSignal || hasAlternateIndexReferenceSignal ||
hasOrganizationClarificationContinuation || hasOrganizationClarificationContinuation ||
mcpDiscoveryOrganizationClarificationContinuation ||
hasImplicitContinuationSignal || hasImplicitContinuationSignal ||
hasSuggestedIntentPivotSignal || hasSuggestedIntentPivotSignal ||
inventoryShortFollowupPrimary || inventoryShortFollowupPrimary ||
@ -570,6 +614,8 @@ function createAssistantTransitionPolicy(deps) {
Boolean(debtRoleSwapIntent) || Boolean(debtRoleSwapIntent) ||
shortValueFlowRetargetPrimary || shortValueFlowRetargetPrimary ||
shortValueFlowRetargetAlternate || shortValueFlowRetargetAlternate ||
businessOverviewBoundaryFollowupPrimary ||
businessOverviewBoundaryFollowupAlternate ||
deps.hasFollowupMarker(userMessage) || deps.hasFollowupMarker(userMessage) ||
deps.hasReferentialPointer(userMessage) || deps.hasReferentialPointer(userMessage) ||
(deps.toNonEmptyString(alternateMessage) (deps.toNonEmptyString(alternateMessage)
@ -579,6 +625,7 @@ function createAssistantTransitionPolicy(deps) {
const hasConcreteFollowupReference = hasPrimaryIndexReferenceSignal || const hasConcreteFollowupReference = hasPrimaryIndexReferenceSignal ||
hasAlternateIndexReferenceSignal || hasAlternateIndexReferenceSignal ||
hasOrganizationClarificationContinuation || hasOrganizationClarificationContinuation ||
mcpDiscoveryOrganizationClarificationContinuation ||
inventoryShortFollowupPrimary || inventoryShortFollowupPrimary ||
inventoryShortFollowupAlternate || inventoryShortFollowupAlternate ||
hasInventoryRootTemporalFollowupPrimary || hasInventoryRootTemporalFollowupPrimary ||
@ -590,6 +637,8 @@ function createAssistantTransitionPolicy(deps) {
Boolean(debtRoleSwapIntent) || Boolean(debtRoleSwapIntent) ||
shortValueFlowRetargetPrimary || shortValueFlowRetargetPrimary ||
shortValueFlowRetargetAlternate || shortValueFlowRetargetAlternate ||
businessOverviewBoundaryFollowupPrimary ||
businessOverviewBoundaryFollowupAlternate ||
deps.hasFollowupMarker(userMessage) || deps.hasFollowupMarker(userMessage) ||
deps.hasReferentialPointer(userMessage) || deps.hasReferentialPointer(userMessage) ||
(deps.toNonEmptyString(alternateMessage) (deps.toNonEmptyString(alternateMessage)
@ -617,6 +666,7 @@ function createAssistantTransitionPolicy(deps) {
!hasImplicitContinuationSignal && !hasImplicitContinuationSignal &&
!hasSuggestedIntentPivotSignal && !hasSuggestedIntentPivotSignal &&
!hasOrganizationClarificationContinuation && !hasOrganizationClarificationContinuation &&
!mcpDiscoveryOrganizationClarificationContinuation &&
!hasIndexReferenceSignal && !hasIndexReferenceSignal &&
!explicitSummaryBundleReuseSignal) { !explicitSummaryBundleReuseSignal) {
return null; return null;
@ -632,6 +682,7 @@ function createAssistantTransitionPolicy(deps) {
!hasImplicitContinuationSignal && !hasImplicitContinuationSignal &&
!hasSuggestedIntentPivotSignal && !hasSuggestedIntentPivotSignal &&
!hasOrganizationClarificationContinuation && !hasOrganizationClarificationContinuation &&
!mcpDiscoveryOrganizationClarificationContinuation &&
!hasIndexReferenceSignal && !hasIndexReferenceSignal &&
!explicitSummaryBundleReuseSignal) { !explicitSummaryBundleReuseSignal) {
return null; return null;
@ -650,10 +701,10 @@ function createAssistantTransitionPolicy(deps) {
const sourceDiscoveryMetadataAmbiguityEntitySets = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryMetadataAmbiguityEntitySets)(carryoverSourceDebug, deps.toNonEmptyString); const sourceDiscoveryMetadataAmbiguityEntitySets = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryMetadataAmbiguityEntitySets)(carryoverSourceDebug, deps.toNonEmptyString);
const sourceDiscoveryEntityResolutionStatus = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryEntityResolutionStatus)(carryoverSourceDebug, deps.toNonEmptyString); const sourceDiscoveryEntityResolutionStatus = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryEntityResolutionStatus)(carryoverSourceDebug, deps.toNonEmptyString);
const sourceDiscoveryEntityCandidates = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryEntityCandidates)(carryoverSourceDebug, deps.toNonEmptyString); const sourceDiscoveryEntityCandidates = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryEntityCandidates)(carryoverSourceDebug, deps.toNonEmptyString);
const sourceDiscoveryLoopStatus = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopStatus)(carryoverSourceDebug, deps.toNonEmptyString); const sourceDiscoveryLoopStatus = sourceDiscoveryLoopStatusHint;
const sourceDiscoveryLoopSelectedChainId = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopSelectedChainId)(carryoverSourceDebug, deps.toNonEmptyString); const sourceDiscoveryLoopSelectedChainId = sourceDiscoveryLoopSelectedChainIdHint;
const sourceDiscoveryLoopPendingAxes = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopPendingAxes)(carryoverSourceDebug, deps.toNonEmptyString); const sourceDiscoveryLoopPendingAxes = sourceDiscoveryLoopPendingAxesHint;
const sourceDiscoveryLoopProvidedAxes = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopProvidedAxes)(carryoverSourceDebug, deps.toNonEmptyString); const sourceDiscoveryLoopProvidedAxes = sourceDiscoveryLoopProvidedAxesHint;
const sourceDiscoveryLoopAskedDomainFamily = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopAskedDomainFamily)(carryoverSourceDebug, deps.toNonEmptyString); const sourceDiscoveryLoopAskedDomainFamily = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopAskedDomainFamily)(carryoverSourceDebug, deps.toNonEmptyString);
const sourceDiscoveryLoopAskedActionFamily = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopAskedActionFamily)(carryoverSourceDebug, deps.toNonEmptyString); const sourceDiscoveryLoopAskedActionFamily = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopAskedActionFamily)(carryoverSourceDebug, deps.toNonEmptyString);
const sourceDiscoveryLoopUnsupportedFamily = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopUnsupportedFamily)(carryoverSourceDebug, deps.toNonEmptyString); const sourceDiscoveryLoopUnsupportedFamily = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopUnsupportedFamily)(carryoverSourceDebug, deps.toNonEmptyString);
@ -689,6 +740,7 @@ function createAssistantTransitionPolicy(deps) {
explicitIntentFamily && explicitIntentFamily &&
sourceIntentFamily !== explicitIntentFamily && sourceIntentFamily !== explicitIntentFamily &&
!hasOrganizationClarificationContinuation && !hasOrganizationClarificationContinuation &&
!mcpDiscoveryOrganizationClarificationContinuation &&
!hasImplicitContinuationSignal && !hasImplicitContinuationSignal &&
!hasIndexReferenceSignal && !hasIndexReferenceSignal &&
!hasInventoryRootTemporalFollowupPrimary && !hasInventoryRootTemporalFollowupPrimary &&
@ -697,6 +749,8 @@ function createAssistantTransitionPolicy(deps) {
!hasInventoryRootRestatementAlternate && !hasInventoryRootRestatementAlternate &&
!inventoryShortFollowupPrimary && !inventoryShortFollowupPrimary &&
!inventoryShortFollowupAlternate && !inventoryShortFollowupAlternate &&
!businessOverviewBoundaryFollowupPrimary &&
!businessOverviewBoundaryFollowupAlternate &&
!foreignAccountingPivotOverInventory && !foreignAccountingPivotOverInventory &&
!deps.hasFollowupMarker(userMessage) && !deps.hasFollowupMarker(userMessage) &&
!deps.hasReferentialPointer(userMessage) && !deps.hasReferentialPointer(userMessage) &&
@ -753,24 +807,29 @@ function createAssistantTransitionPolicy(deps) {
hasSuggestedIntentPivotSignal || hasSuggestedIntentPivotSignal ||
Boolean(debtRoleSwapPrimary) || Boolean(debtRoleSwapPrimary) ||
shortValueFlowRetargetPrimary || shortValueFlowRetargetPrimary ||
businessOverviewBoundaryFollowupPrimary ||
inventoryShortFollowupPrimary || inventoryShortFollowupPrimary ||
inventoryPurchaseDateVatBridge || inventoryPurchaseDateVatBridge ||
explicitSummaryBundleReuseSignal || explicitSummaryBundleReuseSignal ||
hasInventoryRootTemporalFollowupPrimary; hasInventoryRootTemporalFollowupPrimary ||
mcpDiscoveryOrganizationClarificationContinuation;
hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage) hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage)
? deps.hasAddressFollowupContextSignal(alternateMessage) || ? deps.hasAddressFollowupContextSignal(alternateMessage) ||
hasSuggestedIntentPivotSignal || hasSuggestedIntentPivotSignal ||
Boolean(debtRoleSwapAlternate) || Boolean(debtRoleSwapAlternate) ||
shortValueFlowRetargetAlternate || shortValueFlowRetargetAlternate ||
businessOverviewBoundaryFollowupAlternate ||
inventoryShortFollowupAlternate || inventoryShortFollowupAlternate ||
inventoryPurchaseDateVatBridge || inventoryPurchaseDateVatBridge ||
explicitSummaryBundleReuseSignal || explicitSummaryBundleReuseSignal ||
hasInventoryRootTemporalFollowupAlternate hasInventoryRootTemporalFollowupAlternate ||
mcpDiscoveryOrganizationClarificationContinuation
: false; : false;
hasStrongFollowupReference = hasStrongFollowupReference =
hasPrimaryIndexReferenceSignal || hasPrimaryIndexReferenceSignal ||
hasAlternateIndexReferenceSignal || hasAlternateIndexReferenceSignal ||
hasOrganizationClarificationContinuation || hasOrganizationClarificationContinuation ||
mcpDiscoveryOrganizationClarificationContinuation ||
hasSuggestedIntentPivotSignal || hasSuggestedIntentPivotSignal ||
hasImplicitContinuationSignal || hasImplicitContinuationSignal ||
inventoryShortFollowupPrimary || inventoryShortFollowupPrimary ||
@ -782,6 +841,8 @@ function createAssistantTransitionPolicy(deps) {
Boolean(debtRoleSwapIntent) || Boolean(debtRoleSwapIntent) ||
shortValueFlowRetargetPrimary || shortValueFlowRetargetPrimary ||
shortValueFlowRetargetAlternate || shortValueFlowRetargetAlternate ||
businessOverviewBoundaryFollowupPrimary ||
businessOverviewBoundaryFollowupAlternate ||
deps.hasFollowupMarker(userMessage) || deps.hasFollowupMarker(userMessage) ||
deps.hasReferentialPointer(userMessage) || deps.hasReferentialPointer(userMessage) ||
(deps.toNonEmptyString(alternateMessage) (deps.toNonEmptyString(alternateMessage)

View File

@ -0,0 +1,46 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.isLikelyFinancialInstitutionCounterparty = isLikelyFinancialInstitutionCounterparty;
exports.counterpartyRoleHintForName = counterpartyRoleHintForName;
const FINANCIAL_INSTITUTION_PATTERNS = [
/(?:^|[\s"«(,-])банк(?:$|[\s"»),.-])/u,
/сбербанк/u,
/(?:^|[\s"«(,-])сбер(?:$|[\s"»),.-])/u,
/(?:^|[\s"«(,-])втб(?:$|[\s"»),.-])/u,
/альфа[\s-]*банк/u,
/тинькофф/u,
/(?:^|[\s"«(,-])т[\s-]*банк(?:$|[\s"»),.-])/u,
/газпромбанк/u,
/росбанк/u,
/райффайзен/u,
/совкомбанк/u,
/промсвязьбанк/u,
/(?:^|[\s"«(,-])псб(?:$|[\s"»),.-])/u,
/(?:^|[\s"«(,-])мкб(?:$|[\s"»),.-])/u,
/ак[\s-]*барс/u,
/уралсиб/u,
/юникредит/u,
/почта[\s-]*банк/u,
/(?:^|[\s"«(,-])открытие(?:$|[\s"»),.-])/u,
/кредитн(?:ая|ый|ое|ые)\s+организац/u
];
function normalizeCounterpartyRoleText(value) {
return String(value ?? "")
.toLowerCase()
.replace(/ё/g, "е")
.replace(/[._]+/g, " ")
.replace(/\s+/g, " ")
.trim();
}
function isLikelyFinancialInstitutionCounterparty(value) {
const normalized = normalizeCounterpartyRoleText(value);
if (!normalized) {
return false;
}
return FINANCIAL_INSTITUTION_PATTERNS.some((pattern) => pattern.test(normalized));
}
function counterpartyRoleHintForName(value) {
return isLikelyFinancialInstitutionCounterparty(value)
? "bank_or_financial_institution"
: "ordinary_counterparty";
}

View File

@ -174,7 +174,7 @@ export const ASSISTANT_MCP_PROXY_URL = (process.env.ASSISTANT_MCP_PROXY_URL ?? "
); );
export const ASSISTANT_MCP_CHANNEL = process.env.ASSISTANT_MCP_CHANNEL ?? "default"; export const ASSISTANT_MCP_CHANNEL = process.env.ASSISTANT_MCP_CHANNEL ?? "default";
export const ASSISTANT_MCP_TIMEOUT_MS = toNumberFlag(process.env.ASSISTANT_MCP_TIMEOUT_MS, 6000); export const ASSISTANT_MCP_TIMEOUT_MS = toNumberFlag(process.env.ASSISTANT_MCP_TIMEOUT_MS, 6000);
export const ASSISTANT_MCP_LIVE_LIMIT = Math.max(1, Math.trunc(toNumberFlag(process.env.ASSISTANT_MCP_LIVE_LIMIT, 24))); export const ASSISTANT_MCP_LIVE_LIMIT = Math.max(1, Math.trunc(toNumberFlag(process.env.ASSISTANT_MCP_LIVE_LIMIT, 128)));
export const VAT_PAYABLE_68_PREFIXES = toStringListFlag(process.env.VAT_PAYABLE_68_PREFIXES, ["68.02"]); export const VAT_PAYABLE_68_PREFIXES = toStringListFlag(process.env.VAT_PAYABLE_68_PREFIXES, ["68.02"]);
export const VAT_PAYABLE_19_PREFIXES = toStringListFlag(process.env.VAT_PAYABLE_19_PREFIXES, ["19"]); export const VAT_PAYABLE_19_PREFIXES = toStringListFlag(process.env.VAT_PAYABLE_19_PREFIXES, ["19"]);

View File

@ -2014,7 +2014,6 @@ export function extractAddressFilters(userMessage: string, intent: AddressIntent
warnings.includes("period_derived_from_year_range_phrase") || warnings.includes("period_derived_from_year_range_phrase") ||
warnings.includes("period_derived_from_year_phrase"); warnings.includes("period_derived_from_year_phrase");
const preserveDerivedPeriodWindow = const preserveDerivedPeriodWindow =
usesAsOfPrimaryWindow(intent) ||
intent === "inventory_on_hand_as_of_date" || intent === "inventory_on_hand_as_of_date" ||
intent === "inventory_supplier_stock_overlap_as_of_date"; intent === "inventory_supplier_stock_overlap_as_of_date";
if (periodWasDerivedHeuristically && !warnings.includes("exact_historical_period_window_requested")) { if (periodWasDerivedHeuristically && !warnings.includes("exact_historical_period_window_requested")) {

View File

@ -2166,7 +2166,11 @@ function hasVatPeriodInspectionBridgeSignal(text: string): boolean {
normalized normalized
); );
const forecastOnlyCue = /(?:прогноз|план|примерн|ориентировочн)/iu.test(normalized) && !hasInspectionCue; const forecastOnlyCue = /(?:прогноз|план|примерн|ориентировочн)/iu.test(normalized) && !hasInspectionCue;
return hasPeriodCue && hasInspectionCue && !forecastOnlyCue; const hasVatMovementInspectionCue =
/(?:покаж|движен|операц|по\s+сч(?:е|ё)т|РїРѕРєР°Р|РґРІРёРен|операС|РїРѕ\s+СЃС(?:Рµ|С)С|show|movement|movements|operation|operations|account)/iu.test(
normalized
);
return hasPeriodCue && (hasInspectionCue || hasVatMovementInspectionCue) && !forecastOnlyCue;
} }
function resolveUnicodeAddressIntentBridge(text: string): AddressIntentResolution | null { function resolveUnicodeAddressIntentBridge(text: string): AddressIntentResolution | null {
@ -3034,6 +3038,22 @@ export function resolveAddressIntent(userMessage: string): AddressIntentResoluti
}; };
} }
const hasExplicitVatLiabilityPeriodBridge =
/(?:\u043d\u0434\u0441|vat)/iu.test(text) &&
/(?:\b(?:19|20)\d{2}\b|\u0437\u0430\s+(?:\d{4}|\u0433\u043e\u0434|\u043f\u0435\u0440\u0438\u043e\u0434|\u043a\u0432\u0430\u0440\u0442\u0430\u043b|\u043c\u0435\u0441\u044f\u0446))/iu.test(
text
) &&
/(?:\u043a\u0430\u043a\u043e\u0439|\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u043d\u0430\u0447\u0438\u0441\u043b|\u0443\u043f\u043b\u0430\u0447|\u0443\u043f\u043b\u0430\u0442|\u043f\u0440\u043e\u0434\u0430\u0436|\u043f\u043e\u043a\u0443\u043f|\u0432\u044b\u0447\u0435\u0442|\u043a\s+\u0443\u043f\u043b\u0430\u0442|\u043a\s+\u0432\u043e\u0437\u043c\u0435\u0449|\u043f\u043e\u0437\u0438\u0446|liability|payable|charged|paid|sales|purchase|deduction|position)/iu.test(
text
);
if (hasExplicitVatLiabilityPeriodBridge) {
return {
intent: "vat_liability_confirmed_for_tax_period",
confidence: "high",
reasons: ["vat_liability_explicit_period_bridge_signal_detected"]
};
}
const hasLooseVatPayableBridge = const hasLooseVatPayableBridge =
/(?:\u043d\u0434\u0441|vat)/iu.test(text) && /(?:\u043d\u0434\u0441|vat)/iu.test(text) &&
/(?:\u043a\u0430\u043a\u043e\u0439\s+\u043d\u0434\u0441\s+(?:(?:\u043d\u0430\u043c|(?:\u043c\u044b\s+)?\u0434\u043e\u043b\u0436\u043d\u044b)\s+)?(?:\u043d\u0430\u0434\u043e|\u043d\u0443\u0436\u043d\u043e|\u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e)|(?:\u043d\u0430\u043c|\u043c\u044b\s+)?\u043d\u0430\u0434\u043e\s+(?:\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u0442\u044c|\u0441\u0433\u0440\u0443\u0437\u0438\u0442\u044c)|(?:\u043d\u0430\u043c|\u043c\u044b\s+)?\u043d\u0443\u0436\u043d\u043e\s+(?:\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u0442\u044c|\u0441\u0433\u0440\u0443\u0437\u0438\u0442\u044c)|\u043c\u044b\s+\u0434\u043e\u043b\u0436\u043d\u044b\s+(?:\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u0442\u044c|\u0441\u0433\u0440\u0443\u0437\u0438\u0442\u044c)|\u043d\u0434\u0441\s+\u043a\s+\u0443\u043f\u043b\u0430\u0442\u0435)/iu.test( /(?:\u043a\u0430\u043a\u043e\u0439\s+\u043d\u0434\u0441\s+(?:(?:\u043d\u0430\u043c|(?:\u043c\u044b\s+)?\u0434\u043e\u043b\u0436\u043d\u044b)\s+)?(?:\u043d\u0430\u0434\u043e|\u043d\u0443\u0436\u043d\u043e|\u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e)|(?:\u043d\u0430\u043c|\u043c\u044b\s+)?\u043d\u0430\u0434\u043e\s+(?:\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u0442\u044c|\u0441\u0433\u0440\u0443\u0437\u0438\u0442\u044c)|(?:\u043d\u0430\u043c|\u043c\u044b\s+)?\u043d\u0443\u0436\u043d\u043e\s+(?:\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u0442\u044c|\u0441\u0433\u0440\u0443\u0437\u0438\u0442\u044c)|\u043c\u044b\s+\u0434\u043e\u043b\u0436\u043d\u044b\s+(?:\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u0442\u044c|\u0441\u0433\u0440\u0443\u0437\u0438\u0442\u044c)|\u043d\u0434\u0441\s+\u043a\s+\u0443\u043f\u043b\u0430\u0442\u0435)/iu.test(

View File

@ -207,6 +207,52 @@ const OPEN_CONTRACTS_CONFIRMED_AS_OF_QUERY_TEMPLATE = `
Сумма __ORDER_DIRECTION__ Сумма __ORDER_DIRECTION__
`; `;
const DEBT_DUE_DATE_AGING_QUERY_TEMPLATE = `
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
__AS_OF_EXPR__ КАК Период,
"DUE_DATE_OPEN_BALANCE" КАК Регистратор,
ПРЕДСТАВЛЕНИЕ(Остатки.Счет) КАК СчетДт,
"" КАК СчетКт,
Остатки.СуммаРазвернутыйОстатокДт КАК Сумма,
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто1) КАК СубконтоДт1,
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто2) КАК СубконтоДт2,
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто3) КАК СубконтоДт3,
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто1) КАК Контрагент,
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто2) КАК Договор,
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто3) КАК ДокументРасчетов,
ПРЕДСТАВЛЕНИЕ(Остатки.Организация) КАК Организация,
ВЫРАЗИТЬ(Остатки.Субконто2 КАК Справочник.ДоговорыКонтрагентов).Дата КАК ДатаДоговора,
ВЫРАЗИТЬ(Остатки.Субконто2 КАК Справочник.ДоговорыКонтрагентов).УстановленСрокОплаты КАК УстановленСрокОплаты,
ВЫРАЗИТЬ(Остатки.Субконто2 КАК Справочник.ДоговорыКонтрагентов).СрокОплаты КАК СрокОплаты,
"debit_open_balance" КАК НаправлениеОстатка
ИЗ
РегистрБухгалтерии.Хозрасчетный.Остатки(__AS_OF_EXPR__, , , ) КАК Остатки
__WHERE_DT__
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
__AS_OF_EXPR__ КАК Период,
"DUE_DATE_OPEN_BALANCE" КАК Регистратор,
"" КАК СчетДт,
ПРЕДСТАВЛЕНИЕ(Остатки.Счет) КАК СчетКт,
Остатки.СуммаРазвернутыйОстатокКт КАК Сумма,
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто1) КАК СубконтоДт1,
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто2) КАК СубконтоДт2,
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто3) КАК СубконтоДт3,
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто1) КАК Контрагент,
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто2) КАК Договор,
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто3) КАК ДокументРасчетов,
ПРЕДСТАВЛЕНИЕ(Остатки.Организация) КАК Организация,
ВЫРАЗИТЬ(Остатки.Субконто2 КАК Справочник.ДоговорыКонтрагентов).Дата КАК ДатаДоговора,
ВЫРАЗИТЬ(Остатки.Субконто2 КАК Справочник.ДоговорыКонтрагентов).УстановленСрокОплаты КАК УстановленСрокОплаты,
ВЫРАЗИТЬ(Остатки.Субконто2 КАК Справочник.ДоговорыКонтрагентов).СрокОплаты КАК СрокОплаты,
"credit_open_balance" КАК НаправлениеОстатка
ИЗ
РегистрБухгалтерии.Хозрасчетный.Остатки(__AS_OF_EXPR__, , , ) КАК Остатки
__WHERE_KT__
УПОРЯДОЧИТЬ ПО
Сумма __ORDER_DIRECTION__
`;
const VAT_PAYABLE_CONFIRMED_AS_OF_QUERY_TEMPLATE = ` const VAT_PAYABLE_CONFIRMED_AS_OF_QUERY_TEMPLATE = `
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__ ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
__AS_OF_EXPR__ КАК Период, __AS_OF_EXPR__ КАК Период,
@ -747,7 +793,7 @@ const BASE_RECIPES: AddressRecipeDefinition[] = [
purpose: "Build customer value ranking and incoming deal profile from bank inflow docs", purpose: "Build customer value ranking and incoming deal profile from bank inflow docs",
required_filters: [], required_filters: [],
optional_filters: ["period_from", "period_to", "organization", "counterparty", "contract", "limit", "sort"], optional_filters: ["period_from", "period_to", "organization", "counterparty", "contract", "limit", "sort"],
default_limit: 20, default_limit: 200,
account_scope_mode: "preferred", account_scope_mode: "preferred",
query_template: "customer_revenue_profile" query_template: "customer_revenue_profile"
}, },
@ -757,7 +803,7 @@ const BASE_RECIPES: AddressRecipeDefinition[] = [
purpose: "Build supplier payout ranking and outgoing deal profile from bank outflow docs", purpose: "Build supplier payout ranking and outgoing deal profile from bank outflow docs",
required_filters: [], required_filters: [],
optional_filters: ["period_from", "period_to", "organization", "counterparty", "contract", "limit", "sort"], optional_filters: ["period_from", "period_to", "organization", "counterparty", "contract", "limit", "sort"],
default_limit: 20, default_limit: 200,
account_scope_mode: "preferred", account_scope_mode: "preferred",
query_template: "supplier_payout_profile" query_template: "supplier_payout_profile"
}, },
@ -802,6 +848,17 @@ const BASE_RECIPES: AddressRecipeDefinition[] = [
account_scope_mode: "preferred", account_scope_mode: "preferred",
query_template: "vat_liability_confirmed_tax_period_profile" query_template: "vat_liability_confirmed_tax_period_profile"
}, },
{
recipe_id: "address_accounting_financial_result_for_organization_v1",
intent: "accounting_financial_result_for_organization",
purpose: "Build reviewed accounting financial-result aggregate from 90/91/99 period-close movements",
required_filters: ["period_from", "period_to"],
optional_filters: ["organization", "limit", "sort"],
default_limit: 32,
account_scope: ["90", "91", "99"],
account_scope_mode: "strict",
query_template: "accounting_financial_result_profile"
},
{ {
recipe_id: "address_inventory_on_hand_as_of_date_v1", recipe_id: "address_inventory_on_hand_as_of_date_v1",
intent: "inventory_on_hand_as_of_date", intent: "inventory_on_hand_as_of_date",
@ -912,6 +969,17 @@ const BASE_RECIPES: AddressRecipeDefinition[] = [
account_scope_mode: "strict", account_scope_mode: "strict",
query_template: "open_contracts_confirmed_as_of_balance_profile" query_template: "open_contracts_confirmed_as_of_balance_profile"
}, },
{
recipe_id: "address_debt_due_date_aging_for_organization_v1",
intent: "debt_due_date_aging_for_organization",
purpose: "Check open 60/62/76 settlements against contract payment-term fields and settlement document dates before claiming overdue debt",
required_filters: ["as_of_date"],
optional_filters: ["period_from", "period_to", "organization", "counterparty", "contract", "limit", "sort"],
default_limit: 400,
account_scope: ["60", "62", "76"],
account_scope_mode: "strict",
query_template: "debt_due_date_aging_profile"
},
{ {
recipe_id: "address_contracts_by_counterparty_v1", recipe_id: "address_contracts_by_counterparty_v1",
intent: "list_contracts_by_counterparty", intent: "list_contracts_by_counterparty",
@ -1146,6 +1214,37 @@ function buildContractValueWhereClause(filters: AddressFilterSet, fieldPath: str
]); ]);
} }
function buildContractReferenceCondition(filters: AddressFilterSet, fieldPaths: string[]): string | null {
const contract = typeof filters.contract === "string" ? filters.contract.trim() : "";
if (!contract) {
return null;
}
const contractTokens = Array.from(
new Set(
contract
.split(/[^A-Za-zА-Яа-яЁё0-9]+/u)
.map((token) => token.trim())
.filter((token) => token.length >= 3)
.filter((token) => !["договор", "дог"].includes(token.toLowerCase()))
)
);
const tokens = contractTokens.length > 0 ? contractTokens : [contract];
const clauses = fieldPaths
.map((fieldPath) => String(fieldPath ?? "").trim())
.filter((fieldPath) => fieldPath.length > 0)
.map((fieldPath) => {
const tokenConditions = tokens.map((token) => {
const escapedToken = toQueryStringLiteral(token);
return `${fieldPath}.Наименование ПОДОБНО "%${escapedToken}%"`;
});
return tokenConditions.length === 1 ? tokenConditions[0] : `(${tokenConditions.join(" И ")})`;
});
if (clauses.length === 0) {
return null;
}
return clauses.length === 1 ? clauses[0] : `(${clauses.join(" ИЛИ ")})`;
}
function normalizeAccountTokenForQuery(value: string): string { function normalizeAccountTokenForQuery(value: string): string {
const source = String(value ?? "").trim().replace(",", "."); const source = String(value ?? "").trim().replace(",", ".");
const match = source.match(/^(\d{2})(?:\.(\d{1,2}))?/); const match = source.match(/^(\d{2})(?:\.(\d{1,2}))?/);
@ -1251,6 +1350,48 @@ function buildAccountPrefixPredicate(fieldPath: string, prefixes: string[]): str
return clauses.length === 1 ? clauses[0] : `(${clauses.join(" ИЛИ ")})`; return clauses.length === 1 ? clauses[0] : `(${clauses.join(" ИЛИ ")})`;
} }
function buildDebtDueDateAgingWhereClause(
filters: AddressFilterSet,
amountFieldPath: string,
accountPredicate: string
): string {
const conditions = [
`${amountFieldPath} > 0`,
`(${accountPredicate})`,
buildOrganizationReferenceCondition(filters, ["Остатки.Организация"]),
buildCounterpartyReferenceCondition(filters, ["Остатки.Субконто1"]),
buildContractReferenceCondition(filters, ["Остатки.Субконто2"])
].filter((item): item is string => Boolean(item));
return `ГДЕ\n ${conditions.join("\n И ")}`;
}
function buildDebtDueDateAgingQuery(filters: AddressFilterSet, resolvedLimit: number): string {
const asOfExpr =
(typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
? toDateTimeExpr(filters.as_of_date, true)
: null) ??
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0
? toDateTimeExpr(filters.period_to, true)
: null) ??
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0
? toDateTimeExpr(filters.period_from, true)
: null) ??
"ТЕКУЩАЯДАТА()";
const accountPredicate = buildAccountPrefixPredicate("Остатки.Счет", ["60", "62", "76"]);
return DEBT_DUE_DATE_AGING_QUERY_TEMPLATE
.replaceAll("__LIMIT__", String(resolvedLimit))
.replaceAll("__AS_OF_EXPR__", asOfExpr)
.replaceAll(
"__WHERE_DT__",
buildDebtDueDateAgingWhereClause(filters, "Остатки.СуммаРазвернутыйОстатокДт", accountPredicate)
)
.replaceAll(
"__WHERE_KT__",
buildDebtDueDateAgingWhereClause(filters, "Остатки.СуммаРазвернутыйОстатокКт", accountPredicate)
)
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
}
function buildInventoryMovementQuery( function buildInventoryMovementQuery(
filters: AddressFilterSet, filters: AddressFilterSet,
resolvedLimit: number, resolvedLimit: number,
@ -1340,6 +1481,179 @@ function buildCounterpartyReferenceCondition(filters: AddressFilterSet, fieldPat
return clauses.length === 1 ? clauses[0] : `(${clauses.join(" ИЛИ ")})`; return clauses.length === 1 ? clauses[0] : `(${clauses.join(" ИЛИ ")})`;
} }
const ORGANIZATION_REFERENCE_STOP_WORDS = new Set([
"ооо",
"зао",
"оао",
"ао",
"пао",
"ип",
"на",
"за",
"по",
"конец",
"начало",
"год",
"года",
"период",
"можно",
"точно",
"понять",
"какая",
"какой",
"какие",
"какую",
"компания",
"компании",
"организация",
"организации",
"дебиторка",
"дебиторки",
"кредиторка",
"кредиторки",
"просрочена",
"просроченные",
"просрочка",
"срок",
"оплаты",
"прибыль",
"маржа",
"ндс"
]);
const ORGANIZATION_REFERENCE_BOUNDARY_WORDS = new Set([
"на",
"за",
"конец",
"начало",
"можно",
"точно",
"понять",
"какая",
"какой",
"какие",
"какую",
"дебиторка",
"дебиторки",
"кредиторка",
"кредиторки",
"просрочена",
"просроченные",
"просрочка",
"прибыль",
"маржа",
"ндс"
]);
function organizationReferenceTokens(organization: string): string[] {
const rawTokens = organization
.split(/[^A-Za-zА-Яа-яЁё0-9]+/u)
.map((token) => token.trim())
.filter((token) => token.length > 0);
const boundaryIndex = rawTokens.findIndex((token) => {
const lower = token.toLowerCase();
return /^\d+$/.test(token) || ORGANIZATION_REFERENCE_BOUNDARY_WORDS.has(lower);
});
const scopedTokens = boundaryIndex > 0 ? rawTokens.slice(0, boundaryIndex) : rawTokens;
return Array.from(
new Set(
scopedTokens
.filter((token) => token.length >= 3)
.filter((token) => !/^\d+$/.test(token))
.filter((token) => !ORGANIZATION_REFERENCE_STOP_WORDS.has(token.toLowerCase()))
)
).slice(0, 4);
}
function buildOrganizationReferenceCondition(filters: AddressFilterSet, fieldPaths: string[]): string | null {
const organization = typeof filters.organization === "string" ? filters.organization.trim() : "";
if (!organization) {
return null;
}
const organizationTokens = organizationReferenceTokens(organization);
const tokens = organizationTokens.length > 0 ? organizationTokens : [organization];
const clauses = fieldPaths
.map((fieldPath) => String(fieldPath ?? "").trim())
.filter((fieldPath) => fieldPath.length > 0)
.map((fieldPath) => {
const tokenConditions = tokens.map((token) => {
const escapedToken = toQueryStringLiteral(token);
return `(Организации.Наименование ПОДОБНО "%${escapedToken}%" ИЛИ Организации.НаименованиеПолное ПОДОБНО "%${escapedToken}%")`;
});
const referenceSubquery =
`(ВЫБРАТЬ Организации.Ссылка ИЗ Справочник.Организации КАК Организации ` +
`ГДЕ ${tokenConditions.length === 1 ? tokenConditions[0] : tokenConditions.join(" И ")})`;
return `${fieldPath} В ${referenceSubquery}`;
});
if (clauses.length === 0) {
return null;
}
return clauses.length === 1 ? clauses[0] : `(${clauses.join(" ИЛИ ")})`;
}
function buildAccountingFinancialResultAggregateSelect(
filters: AddressFilterSet,
marker: string,
debitLabel: string,
creditLabel: string,
debitPrefixes: string[],
creditPrefixes: string[]
): string {
const whereClause = buildWhereClause(
filters,
"Движения.Период",
[
debitPrefixes.length > 0 ? buildAccountPrefixPredicate("Движения.СчетДт", debitPrefixes) : null,
creditPrefixes.length > 0 ? buildAccountPrefixPredicate("Движения.СчетКт", creditPrefixes) : null,
buildOrganizationReferenceCondition(filters, ["Движения.Организация"])
].filter((item): item is string => Boolean(item))
);
return `
ВЫБРАТЬ
ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0) КАК Период,
"${marker}" КАК Регистратор,
"${debitLabel}" КАК СчетДт,
"${creditLabel}" КАК СчетКт,
ЕСТЬNULL(СУММА(Движения.Сумма), 0) КАК Сумма,
"" КАК СубконтоДт1,
"" КАК СубконтоДт2,
"" КАК СубконтоДт3,
"" КАК СубконтоКт1,
"" КАК СубконтоКт2,
"" КАК СубконтоКт3,
"" КАК Организация
ИЗ
РегистрБухгалтерии.Хозрасчетный.ДвиженияССубконто КАК Движения
${whereClause}`;
}
function buildAccountingFinancialResultQuery(filters: AddressFilterSet): string {
const rows = [
["ACC90_REVENUE_KT", "ANY", "90.01", [], ["90.01"]],
["ACC90_COST_DT", "90.02", "ANY", ["90.02"], []],
["ACC90_SELLING_DT", "90.07", "ANY", ["90.07"], []],
["ACC90_ADMIN_DT", "90.08", "ANY", ["90.08"], []],
["ACC90_RESULT_TO_99_PROFIT", "90.09", "99", ["90.09"], ["99"]],
["ACC90_RESULT_FROM_99_LOSS", "99", "90.09", ["99"], ["90.09"]],
["ACC91_RESULT_TO_99_PROFIT", "91.09", "99", ["91.09"], ["99"]],
["ACC91_RESULT_FROM_99_LOSS", "99", "91.09", ["99"], ["91.09"]],
["ACC99_TO84_PROFIT_TRANSFER", "99", "84", ["99"], ["84"]],
["ACC84_TO99_LOSS_TRANSFER", "84", "99", ["84"], ["99"]]
] as const;
return rows
.map(([marker, debitLabel, creditLabel, debitPrefixes, creditPrefixes]) =>
buildAccountingFinancialResultAggregateSelect(
filters,
marker,
debitLabel,
creditLabel,
[...debitPrefixes],
[...creditPrefixes]
).trim()
)
.join("\nОБЪЕДИНИТЬ ВСЕ\n");
}
function buildInventorySaleDocumentQuery(filters: AddressFilterSet, resolvedLimit: number): string { function buildInventorySaleDocumentQuery(filters: AddressFilterSet, resolvedLimit: number): string {
const itemCondition = buildInventoryItemReferenceCondition(filters, ["Товары.Номенклатура"]); const itemCondition = buildInventoryItemReferenceCondition(filters, ["Товары.Номенклатура"]);
return INVENTORY_SALE_DOCUMENTS_QUERY_TEMPLATE return INVENTORY_SALE_DOCUMENTS_QUERY_TEMPLATE
@ -1458,6 +1772,8 @@ function maxLimitForIntent(intent: AddressIntent): number {
intent === "contract_usage_and_value" || intent === "contract_usage_and_value" ||
intent === "vat_payable_forecast" || intent === "vat_payable_forecast" ||
intent === "vat_liability_confirmed_for_tax_period" || intent === "vat_liability_confirmed_for_tax_period" ||
intent === "accounting_financial_result_for_organization" ||
intent === "debt_due_date_aging_for_organization" ||
intent === "inventory_on_hand_as_of_date" || intent === "inventory_on_hand_as_of_date" ||
intent === "inventory_purchase_provenance_for_item" || intent === "inventory_purchase_provenance_for_item" ||
intent === "inventory_purchase_documents_for_item" || intent === "inventory_purchase_documents_for_item" ||
@ -1518,7 +1834,8 @@ export function buildAddressRecipePlan(
recipe.query_template === "counterparty_roles_profile" || recipe.query_template === "counterparty_roles_profile" ||
recipe.query_template === "contract_usage_profile" || recipe.query_template === "contract_usage_profile" ||
recipe.query_template === "vat_payable_forecast_profile" || recipe.query_template === "vat_payable_forecast_profile" ||
recipe.query_template === "vat_liability_confirmed_tax_period_profile"; recipe.query_template === "vat_liability_confirmed_tax_period_profile" ||
recipe.query_template === "accounting_financial_result_profile";
const baseLimit = const baseLimit =
typeof filters.limit === "number" && Number.isFinite(filters.limit) typeof filters.limit === "number" && Number.isFinite(filters.limit)
? Math.max(1, Math.min(maxLimit, Math.trunc(filters.limit))) ? Math.max(1, Math.min(maxLimit, Math.trunc(filters.limit)))
@ -1659,6 +1976,10 @@ export function buildAddressRecipePlan(
.replaceAll("__WHERE_CLAUSE__", buildManagementWhereClause(filters, "Движения.Период")) .replaceAll("__WHERE_CLAUSE__", buildManagementWhereClause(filters, "Движения.Период"))
.replaceAll("__PERIOD_TO_EXPR__", periodToExpr); .replaceAll("__PERIOD_TO_EXPR__", periodToExpr);
})() })()
: recipe.query_template === "accounting_financial_result_profile"
? buildAccountingFinancialResultQuery(filters)
: recipe.query_template === "debt_due_date_aging_profile"
? buildDebtDueDateAgingQuery(filters, resolvedLimit)
: recipe.query_template === "vat_payable_confirmed_as_of_balance_profile" : recipe.query_template === "vat_payable_confirmed_as_of_balance_profile"
? (() => { ? (() => {
const asOfExpr = const asOfExpr =

View File

@ -705,11 +705,19 @@ function detectValueRankingFocus(userMessage: string | null | undefined): ValueR
if (asksTotalMoneyEarned) { if (asksTotalMoneyEarned) {
return "total_flow"; return "total_flow";
} }
const hasCounterpartyRankingSubject =
/(?:клиенС|заказСРёРє|РїРѕРєСѓРїР°Сел|РєРѕРЅСрагенС|customer|client|counterpart|\u043a\u043b\u0438\u0435\u043d\u0442|\u0437\u0430\u043a\u0430\u0437\u0447\u0438\u043a|\u043f\u043e\u043a\u0443\u043f\u0430\u0442\u0435\u043b|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442)/iu.test(
text
);
const asksExplicitYearBreakdown =
/(?:РїРѕ\s+годам|Р·Р°\s+какие\s+РіРѕРґС|динамик\w*\s+РїРѕ\s+РіРѕРґ|yearly\s+breakdown|by\s+year|\u043f\u043e\s+\u0433\u043e\u0434\u0430\u043c|\u0437\u0430\s+\u043a\u0430\u043a\u0438\u0435\s+\u0433\u043e\u0434\u044b|\u0434\u0438\u043d\u0430\u043c\u0438\u043a\w*\s+\u043f\u043e\s+\u0433\u043e\u0434)/iu.test(
text
);
const asksYearlyRevenueRanking = const asksYearlyRevenueRanking =
/(?:доходн|выручк|оборот|прибыл|деньг|денег|revenue|turnover|income)/iu.test(text) && /(?:доходн|выручк|оборот|прибыл|деньг|денег|revenue|turnover|income)/iu.test(text) &&
/(?:год|года|годы|year|years|по\s+годам)/iu.test(text) && /(?:год|года|годы|year|years|по\s+годам)/iu.test(text) &&
/(?:сам(?:ый|ая|ое|ые)|топ|луч|best|max|наибольш|больше)/iu.test(text); /(?:сам(?:ый|ая|ое|ые)|топ|луч|best|max|наибольш|больше)/iu.test(text);
if (asksYearlyRevenueRanking) { if (asksYearlyRevenueRanking && (!hasCounterpartyRankingSubject || asksExplicitYearBreakdown)) {
return "top_years_by_total"; return "top_years_by_total";
} }
if (/(?:сам(?:ый|ая|ое|ые)\s+высок[а-яё]*|highest|largest)\s+чек|(?:max\s+check|чек\s+макс)/iu.test(text)) { if (/(?:сам(?:ый|ая|ое|ые)\s+высок[а-яё]*|highest|largest)\s+чек|(?:max\s+check|чек\s+макс)/iu.test(text)) {
@ -3214,7 +3222,7 @@ function composeFactualReplyBody(
const erroredSources = vatProbe.probedSources.filter((item) => item.status === "error").length; const erroredSources = vatProbe.probedSources.filter((item) => item.status === "error").length;
lines.push( lines.push(
"", "",
"Покрытие VAT-источников через MCP:", "Покрытие VAT-источников в 1С:",
`- Найдено VAT-объектов: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`, `- Найдено VAT-объектов: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`,
`- Прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`, `- Прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`,
`- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.`, `- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.`,
@ -3241,7 +3249,7 @@ function composeFactualReplyBody(
} }
lines.push("- Сумма прогноза выше рассчитана строго по оборотам 68.02*/19*; прямые VAT-источники показаны для проверки покрытия."); lines.push("- Сумма прогноза выше рассчитана строго по оборотам 68.02*/19*; прямые VAT-источники показаны для проверки покрытия.");
} else if (vatProbe && vatProbe.status === "error") { } else if (vatProbe && vatProbe.status === "error") {
lines.push("", "Покрытие VAT-источников через MCP: probe завершился ошибкой, поэтому использован только базовый контур 68.02*/19*."); lines.push("", "Покрытие VAT-источников в 1С: дополнительная проверка завершилась ошибкой, поэтому использован только базовый контур 68.02*/19*.");
} }
if (!vatActivityDetected) { if (!vatActivityDetected) {
@ -3328,13 +3336,16 @@ function composeFactualReplyBody(
options.periodFrom && options.periodTo ? `${formatDateRu(options.periodFrom)}..${formatDateRu(options.periodTo)}` : null; options.periodFrom && options.periodTo ? `${formatDateRu(options.periodFrom)}..${formatDateRu(options.periodTo)}` : null;
const formatConfirmedMoney = (value: number): string => (options.useRubCurrency ? formatMoneyRub(value) : formatMoney(value)); const formatConfirmedMoney = (value: number): string => (options.useRubCurrency ? formatMoneyRub(value) : formatMoney(value));
const vatProbe = options.vatDirectSourceProbe ?? null; const vatProbe = options.vatDirectSourceProbe ?? null;
const organizationLabel = normalizeOrganizationScopeValue(options.organizationHint);
const organizationScopeLabel = organizationLabel ? ` по организации ${organizationLabel}` : "";
const lines = [ const lines = [
`Коротко: подтвержденный НДС к уплате за налоговый период${formatConfirmedMoney(vatToPay)}.`, `Коротко: подтвержденный НДС к уплате за налоговый период${organizationScopeLabel}${formatConfirmedMoney(vatToPay)}.`,
`Если смотреть на возможный перенос или переплату, получается ${formatConfirmedMoney(carryoverOrOverpayment)}.`, `Если смотреть на возможный перенос или переплату, получается ${formatConfirmedMoney(carryoverOrOverpayment)}.`,
"Это подтвержденный расчет по регистрам книг продаж и покупок, без surrogate-формулы 68/19.", "Это подтвержденный расчет по регистрам книг продаж и покупок, без surrogate-формулы 68/19.",
"", "",
"Что вошло в расчет:", "Что вошло в расчет:",
...(organizationLabel ? [`- Организация: ${organizationLabel}.`] : []),
`- Налоговый период расчета: ${periodWindowLabel ?? "не задан (нужен явный период)"}.`, `- Налоговый период расчета: ${periodWindowLabel ?? "не задан (нужен явный период)"}.`,
`- НДС по книге продаж: ${formatConfirmedMoney(salesVat)}.`, `- НДС по книге продаж: ${formatConfirmedMoney(salesVat)}.`,
`- НДС по книге покупок (вычеты): ${formatConfirmedMoney(purchaseVat)}.`, `- НДС по книге покупок (вычеты): ${formatConfirmedMoney(purchaseVat)}.`,
@ -3346,7 +3357,7 @@ function composeFactualReplyBody(
const erroredSources = vatProbe.probedSources.filter((item) => item.status === "error").length; const erroredSources = vatProbe.probedSources.filter((item) => item.status === "error").length;
lines.push( lines.push(
"", "",
"Покрытие VAT-источников через MCP:", "Покрытие VAT-источников в 1С:",
`- Найдено VAT-объектов: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`, `- Найдено VAT-объектов: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`,
`- Прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`, `- Прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`,
`- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.`, `- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.`,
@ -3355,11 +3366,11 @@ function composeFactualReplyBody(
if (vatProbe.errors.length > 0) { if (vatProbe.errors.length > 0) {
lines.push(`- Ограничения probe: ${vatProbe.errors.slice(0, 2).join("; ")}.`); lines.push(`- Ограничения probe: ${vatProbe.errors.slice(0, 2).join("; ")}.`);
} }
lines.push("- Сумма расчета выше получена по книгам продаж/покупок; probe использован для контроля полноты VAT-источников."); lines.push("- Сумма расчета выше получена по книгам продаж/покупок; дополнительная проверка использована для контроля полноты VAT-источников.");
} else if (vatProbe && vatProbe.status === "error") { } else if (vatProbe && vatProbe.status === "error") {
lines.push( lines.push(
"", "",
"Покрытие VAT-источников через MCP: дополнительный probe недоступен (например, timeout metadata).", "Покрытие VAT-источников в 1С: дополнительная проверка недоступна, поэтому использован основной бухгалтерский срез.",
"Итоговая сумма НДС выше рассчитана по основному маршруту книг продаж/покупок; probe влияет только на диагностику покрытия." "Итоговая сумма НДС выше рассчитана по основному маршруту книг продаж/покупок; probe влияет только на диагностику покрытия."
); );
if (vatProbe.errors.length > 0) { if (vatProbe.errors.length > 0) {
@ -3453,7 +3464,7 @@ function composeFactualReplyBody(
const nonEmptySources = vatProbe.probedSources.filter((item) => item.status === "ok").length; const nonEmptySources = vatProbe.probedSources.filter((item) => item.status === "ok").length;
lines.push( lines.push(
"", "",
"Блок 2.1. MCP-проверка VAT-источников", "Блок 2.1. Проверка VAT-источников в 1С",
`- VAT-объектов в метаданных 1С: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`, `- VAT-объектов в метаданных 1С: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`,
`- Пробных прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`, `- Пробных прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`,
`- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.` `- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.`
@ -3478,8 +3489,8 @@ function composeFactualReplyBody(
} else if (vatProbe && vatProbe.status === "error") { } else if (vatProbe && vatProbe.status === "error") {
lines.push( lines.push(
"", "",
"Блок 2.1. MCP-проверка VAT-источников", "Блок 2.1. Проверка VAT-источников в 1С",
"- Probe VAT-источников завершился ошибкой, поэтому срез подтвержден по доступному бухгалтерскому источнику (68*)." "- Дополнительная проверка VAT-источников завершилась ошибкой, поэтому срез подтвержден по доступному бухгалтерскому источнику (68*)."
); );
} }

View File

@ -148,9 +148,9 @@ export function composeCounterpartyAnalyticsReply(
const includeRoles = focus === "full_profile" || focus === "roles_only"; const includeRoles = focus === "full_profile" || focus === "roles_only";
const directLead = const directLead =
focus === "suppliers_only" focus === "suppliers_only"
? `Поставщиков (только supplier-роль): ${supplierOnly}.` ? `Поставщиков с ролью поставщика: ${supplierOnly}.`
: focus === "customers_only" : focus === "customers_only"
? `Заказчиков (только customer-роль): ${customerOnly}.` ? `Заказчиков с ролью покупателя: ${customerOnly}.`
: focus === "mixed_only" : focus === "mixed_only"
? `Контрагентов со смешанной ролью: ${mixedActive}.` ? `Контрагентов со смешанной ролью: ${mixedActive}.`
: includeTotal && totalCounterparties > 0 : includeTotal && totalCounterparties > 0
@ -175,9 +175,9 @@ export function composeCounterpartyAnalyticsReply(
if (includeRoles) { if (includeRoles) {
if (resolvedActive > 0 || activeCounterparties > 0) { if (resolvedActive > 0 || activeCounterparties > 0) {
lines.push("Роли контрагентов по активности:"); lines.push("Распределение ролей по активности:");
lines.push(`Заказчики (только customer-роль): ${customerOnly}.`); lines.push(`Заказчики с ролью покупателя: ${customerOnly}.`);
lines.push(`Поставщики (только supplier-роль): ${supplierOnly}.`); lines.push(`Поставщики с ролью поставщика: ${supplierOnly}.`);
lines.push(`Смешанные (и покупатель, и поставщик): ${mixedActive}.`); lines.push(`Смешанные (и покупатель, и поставщик): ${mixedActive}.`);
lines.push(`4. Всего активных контрагентов: ${activeCounterparties}.`); lines.push(`4. Всего активных контрагентов: ${activeCounterparties}.`);
if (otherCounterparties !== null) { if (otherCounterparties !== null) {
@ -189,10 +189,10 @@ export function composeCounterpartyAnalyticsReply(
} }
if (focus === "suppliers_only") { if (focus === "suppliers_only") {
lines.push(`Поставщиков (только supplier-роль): ${supplierOnly}.`); lines.push(`Поставщиков с ролью поставщика: ${supplierOnly}.`);
} }
if (focus === "customers_only") { if (focus === "customers_only") {
lines.push(`Заказчиков (только customer-роль): ${customerOnly}.`); lines.push(`Заказчиков с ролью покупателя: ${customerOnly}.`);
} }
if (focus === "mixed_only") { if (focus === "mixed_only") {
lines.push(`Контрагентов со смешанной ролью: ${mixedActive}.`); lines.push(`Контрагентов со смешанной ролью: ${mixedActive}.`);
@ -525,6 +525,16 @@ export function composeCounterpartyAnalyticsReply(
const limit = deps.detectRankingLimit(options.userMessage, 20); const limit = deps.detectRankingLimit(options.userMessage, 20);
const minOpsForAvgCheck = deps.detectMinOpsForAvgCheck(options.userMessage); const minOpsForAvgCheck = deps.detectMinOpsForAvgCheck(options.userMessage);
const normalizedQuestion = deps.normalizeQuestionText(options.userMessage); const normalizedQuestion = deps.normalizeQuestionText(options.userMessage);
const asksSingleBestCounterparty =
focus === "top_by_total" &&
/(?:какой|кто|which|who|какоР|РєСРѕ)/iu.test(normalizedQuestion) &&
/(?:больше\s+всего|сам(?:ый|ая|ое|ые)|наибольш|прин[её]с|highest|most|больСРµ\s+всего|сам(?:СР|ая|РѕРµ|СРµ)|наибол|РїСЂРёРЅ[РµС]СЃ)/iu.test(
normalizedQuestion
) &&
!/(?:\btop\b|топ|рейтинг|список|первые|покажи\s+топ|дай\s+топ|РїРѕРєР°Р\w*\s+СРѕРї|РґР°Р\s+СРѕРї)/iu.test(
normalizedQuestion
);
const effectiveLimit = asksSingleBestCounterparty ? 1 : limit;
const byCounterparty = new Map<string, CounterpartyValuePoint>(); const byCounterparty = new Map<string, CounterpartyValuePoint>();
const byYear = new Map<number, CounterpartyYearPoint>(); const byYear = new Map<number, CounterpartyYearPoint>();
@ -728,7 +738,7 @@ export function composeCounterpartyAnalyticsReply(
lines.push( lines.push(
...visible.map( ...visible.map(
(item, index) => (item, index) =>
`${index + 1}. ${item.name} | max single: ${item.maxSingle} | максимальная разовая сумма: ${deps.formatMoneyRub(item.maxSingle)} | сумма: ${deps.formatMoneyRub(item.total)} | операций: ${item.ops}` `${index + 1}. ${item.name} | максимальная разовая сумма: ${deps.formatMoneyRub(item.maxSingle)} | сумма: ${deps.formatMoneyRub(item.total)} | операций: ${item.ops}`
) )
); );
return buildFactualListReply(lines); return buildFactualListReply(lines);
@ -786,8 +796,12 @@ export function composeCounterpartyAnalyticsReply(
return buildFactualListReply(lines); return buildFactualListReply(lines);
} }
const visible = rankedByTotal.slice(0, limit); const visible = rankedByTotal.slice(0, effectiveLimit);
const singleCandidateOnly = rankedByTotal.length === 1; const singleCandidateOnly = rankedByTotal.length === 1;
const rankingPeriodLabel =
options.periodFrom && options.periodTo
? `за период ${deps.formatDateRu(options.periodFrom)}..${deps.formatDateRu(options.periodTo)}`
: "за доступное время";
const heading = singleCandidateOnly const heading = singleCandidateOnly
? isSupplier ? isSupplier
? "Найденный поставщик по сумме выплат:" ? "Найденный поставщик по сумме выплат:"
@ -797,14 +811,17 @@ export function composeCounterpartyAnalyticsReply(
: `Топ-${visible.length} заказчиков по сумме поступлений:`; : `Топ-${visible.length} заказчиков по сумме поступлений:`;
const leadingCounterparty = visible[0] ?? null; const leadingCounterparty = visible[0] ?? null;
lines.unshift(heading); lines.unshift(heading);
if (options.periodFrom && options.periodTo) {
lines.push(`Период рейтинга: ${rankingPeriodLabel}.`);
}
if (leadingCounterparty) { if (leadingCounterparty) {
const directAnswerLine = singleCandidateOnly const directAnswerLine = singleCandidateOnly
? isSupplier ? isSupplier
? `В выбранном срезе найден один поставщик: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг.` ? `В выбранном срезе найден один поставщик: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг.`
: `В выбранном срезе найден один клиент: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг; сумма является денежным потоком, а не чистой прибылью.` : `В выбранном срезе найден один клиент: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг; сумма является денежным потоком, а не чистой прибылью.`
: isSupplier : isSupplier
? `Крупнейший поставщик по подтвержденным выплатам за доступное время: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).` ? `Крупнейший поставщик по подтвержденным выплатам ${rankingPeriodLabel}: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).`
: `Самый доходный клиент за доступное время по подтвержденным поступлениям: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это денежный поток, а не чистая прибыль.`; : `Самый доходный клиент ${rankingPeriodLabel} по подтвержденным поступлениям: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это денежный поток, а не чистая прибыль.`;
lines.unshift(directAnswerLine); lines.unshift(directAnswerLine);
} }
lines.push( lines.push(

View File

@ -1,4 +1,5 @@
import type { AssistantMcpDiscoveryPilotExecutionContract } from "./assistantMcpDiscoveryPilotExecutor"; import type { AssistantMcpDiscoveryPilotExecutionContract } from "./assistantMcpDiscoveryPilotExecutor";
import { isLikelyFinancialInstitutionCounterparty } from "./counterpartyRoleHeuristics";
export const ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION = export const ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION =
"assistant_mcp_discovery_answer_draft_v1" as const; "assistant_mcp_discovery_answer_draft_v1" as const;
@ -26,6 +27,7 @@ export interface AssistantMcpDiscoveryAnswerDraftContract {
} }
type BusinessOverview = NonNullable<AssistantMcpDiscoveryPilotExecutionContract["derived_business_overview"]>; type BusinessOverview = NonNullable<AssistantMcpDiscoveryPilotExecutionContract["derived_business_overview"]>;
type BusinessOverviewRankedBucket = BusinessOverview["top_customers"][number];
function normalizeReasonCode(value: string): string | null { function normalizeReasonCode(value: string): string | null {
const normalized = value const normalized = value
@ -483,6 +485,30 @@ function metadataRouteFamilyLabelRu(
return null; return null;
} }
function isInventoryReserveBoundaryTurn(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
const action = pilot.evidence.query_plan.turn_meaning_ref?.asked_action_family;
const unsupported = pilot.evidence.query_plan.turn_meaning_ref?.unsupported_but_understood_family;
return action === "inventory_reserve_boundary" || unsupported === "inventory_reserve_liquidation_boundary";
}
function isProfitMarginBoundaryTurn(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
const action = pilot.evidence.query_plan.turn_meaning_ref?.asked_action_family;
const unsupported = pilot.evidence.query_plan.turn_meaning_ref?.unsupported_but_understood_family;
return action === "profit_margin_boundary" || unsupported === "profit_margin_boundary";
}
function isDebtDueDateBoundaryTurn(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
const action = pilot.evidence.query_plan.turn_meaning_ref?.asked_action_family;
const unsupported = pilot.evidence.query_plan.turn_meaning_ref?.unsupported_but_understood_family;
return action === "debt_due_date_boundary" || unsupported === "debt_due_date_boundary";
}
function isVendorRiskBoundaryTurn(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
const action = pilot.evidence.query_plan.turn_meaning_ref?.asked_action_family;
const unsupported = pilot.evidence.query_plan.turn_meaning_ref?.unsupported_but_understood_family;
return action === "vendor_risk_procurement_boundary" || unsupported === "vendor_risk_procurement_boundary";
}
function businessOverviewInventoryUnknownLabel(overview: BusinessOverview): string { function businessOverviewInventoryUnknownLabel(overview: BusinessOverview): string {
if (overview.inventory_staleness_risk_proxy) { if (overview.inventory_staleness_risk_proxy) {
return "резервы/списания/ликвидационная стоимость склада"; return "резервы/списания/ликвидационная стоимость склада";
@ -543,6 +569,81 @@ function inlineBusinessOverviewAmount(value: string): string {
.replace(/[\s.]+$/u, ""); .replace(/[\s.]+$/u, "");
} }
function isFinancialInstitutionBucket(bucket: BusinessOverviewRankedBucket | null | undefined): boolean {
if (!bucket) {
return false;
}
return (
bucket.counterparty_role_hint === "bank_or_financial_institution" ||
isLikelyFinancialInstitutionCounterparty(bucket.axis_value)
);
}
function firstNonFinancialInstitutionBucket(
buckets: BusinessOverviewRankedBucket[] | null | undefined
): BusinessOverviewRankedBucket | null {
return (buckets ?? []).find((bucket) => !isFinancialInstitutionBucket(bucket)) ?? null;
}
function rankedBucketAmountLabel(bucket: BusinessOverviewRankedBucket): string {
return `${bucket.axis_value}${bucket.total_amount_human_ru}`;
}
function businessOverviewIncomingLeaderLine(overview: BusinessOverview): string | null {
const leader = overview.top_customers[0];
if (!leader) {
return null;
}
if (!isFinancialInstitutionBucket(leader)) {
return `Самый крупный подтвержденный клиент в проверенном срезе: ${rankedBucketAmountLabel(leader)}.`;
}
const nonFinancial = firstNonFinancialInstitutionBucket(overview.top_customers.slice(1));
const nonFinancialText = nonFinancial
? ` Крупнейший небанковский входящий контрагент в этом же срезе: ${rankedBucketAmountLabel(nonFinancial)}.`
: "";
return (
`Крупнейший входящий денежный источник в проверенном срезе: ${rankedBucketAmountLabel(leader)}. ` +
"По названию это банк/финансовая организация, поэтому без проверки назначения платежа не называю это клиентской выручкой или бизнес-заказчиком." +
nonFinancialText
);
}
function businessOverviewOutgoingLeaderLine(overview: BusinessOverview): string | null {
const leader = overview.top_suppliers?.[0];
if (!leader) {
return null;
}
if (!isFinancialInstitutionBucket(leader)) {
return `Самый крупный подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${rankedBucketAmountLabel(leader)}.`;
}
const nonFinancial = firstNonFinancialInstitutionBucket(overview.top_suppliers.slice(1));
const nonFinancialText = nonFinancial
? ` Крупнейший небанковский получатель исходящих денег в этом же срезе: ${rankedBucketAmountLabel(nonFinancial)}.`
: "";
return (
`Крупнейший получатель исходящих денег в проверенном срезе: ${rankedBucketAmountLabel(leader)}. ` +
"По названию это банк/финансовая организация, поэтому без назначения платежа/договора не считаю это обычным поставщиком." +
nonFinancialText
);
}
function businessOverviewSupplierBoundaryBasis(overview: BusinessOverview): string {
const leader = overview.top_suppliers?.[0] ?? null;
if (!leader) {
return "есть только общий срез исходящих платежей без надежного vendor-risk профиля";
}
const share = percentText(leader.total_amount, overview.outgoing_supplier_payout.total_amount);
if (isFinancialInstitutionBucket(leader)) {
const base = share
? `крупнейший получатель исходящих денег ${leader.axis_value} держит около ${share} проверенного исходящего потока (${leader.total_amount_human_ru})`
: `крупнейший получатель исходящих денег: ${rankedBucketAmountLabel(leader)}`;
return `${base}; по названию это банк/финансовая организация, поэтому этот факт нельзя считать доказанной зависимостью от одного обычного поставщика`;
}
return share
? `крупнейший подтвержденный поставщик/получатель исходящих платежей ${leader.axis_value} держит около ${share} проверенного исходящего потока (${leader.total_amount_human_ru})`
: `крупнейший подтвержденный поставщик/получатель исходящих платежей: ${rankedBucketAmountLabel(leader)}`;
}
function businessOverviewHeadlineMetricsLine(overview: BusinessOverview): string | null { function businessOverviewHeadlineMetricsLine(overview: BusinessOverview): string | null {
const parts: string[] = []; const parts: string[] = [];
if (overview.incoming_customer_revenue.rows_with_amount > 0) { if (overview.incoming_customer_revenue.rows_with_amount > 0) {
@ -554,6 +655,24 @@ function businessOverviewHeadlineMetricsLine(overview: BusinessOverview): string
if (overview.incoming_customer_revenue.rows_with_amount > 0 || overview.outgoing_supplier_payout.rows_with_amount > 0) { if (overview.incoming_customer_revenue.rows_with_amount > 0 || overview.outgoing_supplier_payout.rows_with_amount > 0) {
parts.push(`расчетное операционное нетто ${inlineBusinessOverviewAmount(overview.net_amount_human_ru)}`); parts.push(`расчетное операционное нетто ${inlineBusinessOverviewAmount(overview.net_amount_human_ru)}`);
} }
if (overview.accounting_financial_result) {
const result = overview.accounting_financial_result;
const direction =
result.final_result_direction === "profit"
? "учетная прибыль"
: result.final_result_direction === "loss"
? "учетный убыток"
: "нулевой учетный финрезультат";
const amount =
result.final_result_direction === "loss"
? `минус ${inlineBusinessOverviewAmount(result.final_result_amount_human_ru)}`
: inlineBusinessOverviewAmount(result.final_result_amount_human_ru);
const margin =
result.net_margin_to_revenue_pct === null
? "маржа к выручке 90.01 не рассчитана"
: `маржа к выручке 90.01 ${result.net_margin_to_revenue_pct}%`;
parts.push(`${direction} 90/91/99 ${amount}; ${margin}`);
}
const strongestIncomingYear = businessOverviewStrongestIncomingYear(overview); const strongestIncomingYear = businessOverviewStrongestIncomingYear(overview);
if (strongestIncomingYear) { if (strongestIncomingYear) {
parts.push( parts.push(
@ -561,10 +680,59 @@ function businessOverviewHeadlineMetricsLine(overview: BusinessOverview): string
); );
} }
return parts.length > 0 return parts.length > 0
? `${parts.join("; ")}. Это operating-flow proxy по найденным строкам, не бухгалтерская прибыль и не финрезультат` ? overview.accounting_financial_result
? `${parts.join("; ")}. Финрезультат ограничен найденными строками 1С и не является внешним аудитом или юридически подтвержденной отчетностью`
: `${parts.join("; ")}. Это operating-flow proxy по найденным строкам, не бухгалтерская прибыль и не финрезультат`
: null; : null;
} }
function businessOverviewAccountingFinancialResultText(overview: BusinessOverview): string | null {
const result = overview.accounting_financial_result;
if (!result) {
return null;
}
const direction =
result.final_result_direction === "profit"
? "учетная прибыль"
: result.final_result_direction === "loss"
? "учетный убыток"
: "нулевой учетный финрезультат";
const signedAmount =
result.final_result_direction === "loss"
? `минус ${result.final_result_amount_human_ru}`
: result.final_result_amount_human_ru;
const marginText =
result.net_margin_to_revenue_pct === null
? "маржа к выручке 90.01 не рассчитана"
: `маржа к выручке 90.01 ${result.net_margin_to_revenue_pct}%`;
const basis =
result.final_transfer_basis === "account_99_to_84_period_close"
? "по закрытию 99 на 84"
: "по закрытию 90/91 на 99";
return `По бухгалтерскому маршруту 90/91/99 за ${result.period_scope} подтвержден ${direction}: ${signedAmount}; ${marginText}. Основа: ${basis}, ${result.period_close_rows_with_amount} строк(и) закрытия периода с суммой. Это учетный финрезультат по найденным строкам 1С, не внешний аудит и не юридически подтвержденная отчетность.`;
}
function businessOverviewDebtDueDateAgingText(overview: BusinessOverview): string | null {
const aging = overview.debt_due_date_aging;
if (!aging) {
return null;
}
if (aging.evidence_status === "confirmed_overdue") {
const top = aging.top_overdue_items?.[0] ?? null;
const topText = top
? ` Самая старая строка: due date ${top.due_date}, просрочка ${top.overdue_days} дн., ${top.amount_human_ru}${top.contract ? ` по договору ${top.contract}` : ""}.`
: "";
return `Due-date aging на ${aging.as_of_date} проверен по срокам оплаты договоров и датам расчетных документов: подтверждено просроченных строк ${aging.overdue_rows}, сумма ${aging.overdue_amount_human_ru}.${topText}`;
}
if (aging.evidence_status === "no_payment_terms_configured") {
return `Due-date aging на ${aging.as_of_date} проверен по открытым расчетам: брутто ${aging.gross_open_amount_human_ru}, строк с суммой ${aging.rows_with_amount}, но в проверенных договорах срок оплаты не установлен. Подтвержденной просрочки по договорным срокам оплаты нет.`;
}
if (aging.evidence_status === "insufficient_due_date_basis") {
return `Due-date aging на ${aging.as_of_date} запускался, но по строкам с установленным сроком оплаты не хватило даты расчетного документа для честного расчета due date. Просрочка не подтверждена.`;
}
return `Due-date aging на ${aging.as_of_date} проверен: строк с установленным сроком оплаты ${aging.rows_with_payment_terms}, подтвержденной просрочки не найдено; не просрочено по расчету ${aging.not_yet_due_amount_human_ru}.`;
}
function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpDiscoveryPilotExecutionContract): string { function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpDiscoveryPilotExecutionContract): string {
const askedMonthlyBreakdown = const askedMonthlyBreakdown =
pilot.derived_bidirectional_value_flow?.aggregation_axis === "month" || pilot.derived_bidirectional_value_flow?.aggregation_axis === "month" ||
@ -583,6 +751,35 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
} }
if (isBusinessOverviewPilot(pilot) && pilot.derived_business_overview && mode === "confirmed_with_bounded_inference") { if (isBusinessOverviewPilot(pilot) && pilot.derived_business_overview && mode === "confirmed_with_bounded_inference") {
const overview = pilot.derived_business_overview; const overview = pilot.derived_business_overview;
if (isProfitMarginBoundaryTurn(pilot)) {
const accountingFinancialResultText = businessOverviewAccountingFinancialResultText(overview);
if (accountingFinancialResultText) {
return accountingFinancialResultText;
}
return "Нельзя точно подтвердить чистую прибыль и маржу по текущему срезу 1С; есть только bounded operating-flow/trading-margin proxy, не P&L и не бухгалтерский финрезультат.";
}
if (isDebtDueDateBoundaryTurn(pilot)) {
const dueDateText = businessOverviewDebtDueDateAgingText(overview);
if (dueDateText) {
return dueDateText;
}
return "Нельзя точно определить, какая дебиторка просрочена, по текущему срезу 1С; есть только debt-quality proxy, но нет проверенного due-date маршрута по договорам, срокам оплаты и погашению расчетов.";
}
if (isInventoryReserveBoundaryTurn(pilot)) {
const inventoryBasis = overview.inventory_staleness_risk_proxy
? "есть только складской staleness-risk proxy по найденным строкам"
: overview.inventory_position || overview.inventory_turnover_proxy
? "есть только ограниченные складские proxy-сигналы по найденным строкам"
: "нет отдельного складского среза на дату и проверки учетной политики резервов";
return `Коротко: точно подтвердить резерв под неликвиды по текущим данным нельзя; ${inventoryBasis}. Можно честно говорить только о необходимости отдельной проверки склада, списаний/резервов и ликвидационной стоимости, не превращая proxy в доказанный факт резерва.`;
}
if (isVendorRiskBoundaryTurn(pilot)) {
const supplierLeader = overview.top_suppliers?.[0] ?? null;
const proxyLabel = isFinancialInstitutionBucket(supplierLeader)
? "outgoing cash concentration proxy"
: "procurement concentration proxy";
return `Коротко: точный риск зависимости от одного поставщика по текущим данным не подтвержден; есть только ${proxyLabel}: ${businessOverviewSupplierBoundaryBasis(overview)}. Это сигнал концентрации исходящих платежей, а не полный аудит надежности поставщиков, условий, качества и структуры всех расходов.`;
}
const families: string[] = []; const families: string[] = [];
if ( if (
overview.incoming_customer_revenue.rows_with_amount > 0 || overview.incoming_customer_revenue.rows_with_amount > 0 ||
@ -608,6 +805,9 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
if (overview.tax_position) { if (overview.tax_position) {
families.push("НДС-позиция"); families.push("НДС-позиция");
} }
if (overview.accounting_financial_result) {
families.push("учетный финрезультат 90/91/99");
}
if (overview.trading_margin_proxy) { if (overview.trading_margin_proxy) {
families.push("торговый margin proxy"); families.push("торговый margin proxy");
} }
@ -623,6 +823,9 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
if (overview.debt_staleness_risk_proxy) { if (overview.debt_staleness_risk_proxy) {
families.push("staleness risk proxy открытых расчетов"); families.push("staleness risk proxy открытых расчетов");
} }
if (overview.debt_due_date_aging) {
families.push("due-date aging открытых расчетов");
}
if (overview.inventory_position) { if (overview.inventory_position) {
families.push("складской срез на дату"); families.push("складской срез на дату");
} }
@ -632,20 +835,24 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
if (overview.inventory_staleness_risk_proxy) { if (overview.inventory_staleness_risk_proxy) {
families.push("staleness risk proxy склада"); families.push("staleness risk proxy склада");
} }
const unknownFamilies = [overview.trading_margin_proxy ? "чистая прибыль/точная маржа" : "прибыль/маржа"]; const unknownFamilies = overview.accounting_financial_result
? ["аудированная/юридически подтвержденная прибыль"]
: [overview.trading_margin_proxy ? "чистая прибыль/точная маржа" : "прибыль/маржа"];
if (!overview.tax_position) { if (!overview.tax_position) {
unknownFamilies.push("НДС"); unknownFamilies.push("НДС");
} }
if (!overview.debt_position) { if (!overview.debt_position) {
unknownFamilies.push("долговой срез"); unknownFamilies.push("долговой срез");
} }
unknownFamilies.push( if (!overview.debt_due_date_aging) {
overview.debt_staleness_risk_proxy unknownFamilies.push(
overview.debt_staleness_risk_proxy
? "договорные сроки оплаты/due-date просрочка" ? "договорные сроки оплаты/due-date просрочка"
: overview.debt_open_settlement_quality : overview.debt_open_settlement_quality
? "due-date просрочка" ? "due-date просрочка"
: "качество открытых расчетов" : "качество открытых расчетов"
); );
}
unknownFamilies.push(businessOverviewInventoryUnknownLabel(overview)); unknownFamilies.push(businessOverviewInventoryUnknownLabel(overview));
const metricLead = businessOverviewHeadlineMetricsLine(overview); const metricLead = businessOverviewHeadlineMetricsLine(overview);
if (metricLead) { if (metricLead) {
@ -851,9 +1058,16 @@ function buildMustNotClaim(pilot: AssistantMcpDiscoveryPilotExecutionContract):
claims.push("Do not present a debt-position snapshot as debt aging, overdue debt, or credit-quality analysis."); claims.push("Do not present a debt-position snapshot as debt aging, overdue debt, or credit-quality analysis.");
claims.push("Do not present open-settlement concentration as contractual due-date aging or confirmed overdue debt."); claims.push("Do not present open-settlement concentration as contractual due-date aging or confirmed overdue debt.");
claims.push("Do not present business overview debt staleness risk proxy as confirmed overdue debt, contractual delinquency, credit risk, or due-date aging."); claims.push("Do not present business overview debt staleness risk proxy as confirmed overdue debt, contractual delinquency, credit risk, or due-date aging.");
claims.push("Do not claim contractual overdue debt unless the due-date aging route found configured payment terms and enough settlement-date evidence.");
claims.push("Do not present an inventory snapshot or purchase-date aging signal as turnover, obsolescence, liquidation value, or full inventory health."); claims.push("Do not present an inventory snapshot or purchase-date aging signal as turnover, obsolescence, liquidation value, or full inventory health.");
claims.push("Do not present business overview inventory turnover proxy as full inventory liquidity, FIFO turnover, obsolescence analysis, or liquidation value."); claims.push("Do not present business overview inventory turnover proxy as full inventory liquidity, FIFO turnover, obsolescence analysis, or liquidation value.");
claims.push("Do not present business overview inventory staleness risk proxy as confirmed obsolete stock, reserve, write-off, or liquidation value."); claims.push("Do not present business overview inventory staleness risk proxy as confirmed obsolete stock, reserve, write-off, or liquidation value.");
if (
pilot.derived_business_overview?.top_customers?.some(isFinancialInstitutionBucket) ||
pilot.derived_business_overview?.top_suppliers?.some(isFinancialInstitutionBucket)
) {
claims.push("Do not present bank-like counterparties as ordinary customers, suppliers, revenue, procurement dependency, or business quality evidence without payment-purpose/contract proof.");
}
if (pilot.derived_business_overview?.missing_proof_families?.length) { if (pilot.derived_business_overview?.missing_proof_families?.length) {
claims.push("Do not present business overview missing proof families as checked, executed, or confirmed routes."); claims.push("Do not present business overview missing proof families as checked, executed, or confirmed routes.");
} }
@ -862,6 +1076,9 @@ function buildMustNotClaim(pilot: AssistantMcpDiscoveryPilotExecutionContract):
if (pilot.derived_ranked_value_flow) { if (pilot.derived_ranked_value_flow) {
claims.push("Do not present a bounded ranking as a complete all-time ranking outside the checked period and organization."); claims.push("Do not present a bounded ranking as a complete all-time ranking outside the checked period and organization.");
claims.push("Do not imply the top-ranked counterparty is globally final when probe-limit or scope boundaries still exist."); claims.push("Do not imply the top-ranked counterparty is globally final when probe-limit or scope boundaries still exist.");
if (pilot.derived_ranked_value_flow.ranked_values.some(isFinancialInstitutionBucket)) {
claims.push("Do not present bank-like counterparties as ordinary customers, suppliers, revenue, procurement dependency, or business quality evidence without payment-purpose/contract proof.");
}
} }
if (isDocumentPilot(pilot)) { if (isDocumentPilot(pilot)) {
claims.push("Do not claim full document history outside the checked period."); claims.push("Do not claim full document history outside the checked period.");
@ -1028,26 +1245,40 @@ function derivedRankedValueFlowConfirmedLine(pilot: AssistantMcpDiscoveryPilotEx
return null; return null;
} }
const leader = ranking.ranked_values[0]; const leader = ranking.ranked_values[0];
const leaderLooksFinancial = isFinancialInstitutionBucket(leader);
const organization = ranking.organization_scope ? ` по организации ${ranking.organization_scope}` : ""; const organization = ranking.organization_scope ? ` по организации ${ranking.organization_scope}` : "";
const period = ranking.period_scope ? ` за период ${ranking.period_scope}` : " в проверенном окне"; const period = ranking.period_scope ? ` за период ${ranking.period_scope}` : " в проверенном окне";
const roleCaveat = leaderLooksFinancial
? ranking.value_flow_direction === "outgoing_supplier_payout"
? " По названию это банк/финансовая организация, поэтому без назначения платежа/договора не называю это обычным поставщиком."
: " По названию это банк/финансовая организация, поэтому без назначения платежа не называю это клиентской выручкой или бизнес-заказчиком."
: "";
if (ranking.ranked_values.length === 1) { if (ranking.ranked_values.length === 1) {
const singleLead = const singleLead =
ranking.value_flow_direction === "outgoing_supplier_payout" leaderLooksFinancial
? "В проверенных исходящих платежах найден один контрагент" ? ranking.value_flow_direction === "outgoing_supplier_payout"
: "В проверенных входящих поступлениях найден один контрагент"; ? "В проверенных исходящих платежах найден один банковский/финансовый получатель"
: "В проверенных входящих поступлениях найден один банковский/финансовый источник"
: ranking.value_flow_direction === "outgoing_supplier_payout"
? "В проверенных исходящих платежах найден один контрагент"
: "В проверенных входящих поступлениях найден один контрагент";
const limitCaveat = ranking.coverage_limited_by_probe_limit const limitCaveat = ranking.coverage_limited_by_probe_limit
? " Лимит строк проверки достигнут; сравнение с другими контрагентами может быть неполным." ? " Лимит строк проверки достигнут; сравнение с другими контрагентами может быть неполным."
: " Других контрагентов в этом проверенном срезе не найдено, поэтому это не полноценный сравнительный рейтинг."; : " Других контрагентов в этом проверенном срезе не найдено, поэтому это не полноценный сравнительный рейтинг.";
return `${singleLead} ${leader.axis_value}${organization}${period}: ${leader.total_amount_human_ru} по ${leader.rows_with_amount} строкам с суммой.${limitCaveat}`; return `${singleLead} ${leader.axis_value}${organization}${period}: ${leader.total_amount_human_ru} по ${leader.rows_with_amount} строкам с суммой.${roleCaveat}${limitCaveat}`;
} }
const directionLead = const directionLead =
ranking.ranking_need === "bottom_asc" leaderLooksFinancial
? ranking.value_flow_direction === "outgoing_supplier_payout" ? ranking.value_flow_direction === "outgoing_supplier_payout"
? "Меньше всего заплатили контрагенту" ? "Крупнейший получатель исходящих денег"
: "Меньше всего денег принёс контрагент" : "Крупнейший входящий денежный источник"
: ranking.value_flow_direction === "outgoing_supplier_payout" : ranking.ranking_need === "bottom_asc"
? "Больше всего заплатили контрагенту" ? ranking.value_flow_direction === "outgoing_supplier_payout"
: "Больше всего денег принёс контрагент"; ? "Меньше всего заплатили контрагенту"
: "Меньше всего денег принёс контрагент"
: ranking.value_flow_direction === "outgoing_supplier_payout"
? "Больше всего заплатили контрагенту"
: "Больше всего денег принёс контрагент";
const tail = ranking.ranked_values const tail = ranking.ranked_values
.slice(1, 3) .slice(1, 3)
.map((bucket) => `${bucket.axis_value}${bucket.total_amount_human_ru}`) .map((bucket) => `${bucket.axis_value}${bucket.total_amount_human_ru}`)
@ -1056,7 +1287,7 @@ function derivedRankedValueFlowConfirmedLine(pilot: AssistantMcpDiscoveryPilotEx
const limitCaveat = ranking.coverage_limited_by_probe_limit const limitCaveat = ranking.coverage_limited_by_probe_limit
? " Лимит строк проверки достигнут; рейтинг может быть неполным." ? " Лимит строк проверки достигнут; рейтинг может быть неполным."
: ""; : "";
return `${directionLead} ${leader.axis_value}${organization}${period}: ${leader.total_amount_human_ru} по ${leader.rows_with_amount} строкам с суммой.${trail}${limitCaveat}`; return `${directionLead} ${leader.axis_value}${organization}${period}: ${leader.total_amount_human_ru} по ${leader.rows_with_amount} строкам с суммой.${roleCaveat}${trail}${limitCaveat}`;
} }
function derivedValueFlowConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null { function derivedValueFlowConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
@ -1247,15 +1478,13 @@ function derivedBusinessOverviewConfirmedLines(pilot: AssistantMcpDiscoveryPilot
`Самый сильный год по подтвержденным входящим поступлениям: ${strongestIncomingYear.year_bucket}${strongestIncomingYear.incoming_total_amount_human_ru} по ${strongestIncomingYear.incoming_rows_with_amount} строкам с суммой. Это не бухгалтерская прибыль.` `Самый сильный год по подтвержденным входящим поступлениям: ${strongestIncomingYear.year_bucket}${strongestIncomingYear.incoming_total_amount_human_ru} по ${strongestIncomingYear.incoming_rows_with_amount} строкам с суммой. Это не бухгалтерская прибыль.`
); );
} }
const leader = overview.top_customers[0]; const incomingLeaderLine = businessOverviewIncomingLeaderLine(overview);
if (leader) { if (incomingLeaderLine) {
lines.push(`Самый крупный подтвержденный клиент в проверенном срезе: ${leader.axis_value}${leader.total_amount_human_ru}.`); lines.push(incomingLeaderLine);
} }
const supplierLeader = overview.top_suppliers?.[0]; const outgoingLeaderLine = businessOverviewOutgoingLeaderLine(overview);
if (supplierLeader) { if (outgoingLeaderLine) {
lines.push( lines.push(outgoingLeaderLine);
`Самый крупный подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${supplierLeader.axis_value}${supplierLeader.total_amount_human_ru}.`
);
} }
if (overview.yearly_breakdown?.length) { if (overview.yearly_breakdown?.length) {
lines.push( lines.push(
@ -1316,6 +1545,12 @@ function derivedBusinessOverviewConfirmedLines(pilot: AssistantMcpDiscoveryPilot
`НДС-позиция за ${overview.tax_position.period_scope}: книга продаж ${overview.tax_position.sales_vat_amount_human_ru}, книга покупок/вычеты ${overview.tax_position.purchase_vat_amount_human_ru}, нетто ${taxDirection} ${overview.tax_position.net_vat_amount_human_ru}.` `НДС-позиция за ${overview.tax_position.period_scope}: книга продаж ${overview.tax_position.sales_vat_amount_human_ru}, книга покупок/вычеты ${overview.tax_position.purchase_vat_amount_human_ru}, нетто ${taxDirection} ${overview.tax_position.net_vat_amount_human_ru}.`
); );
} }
if (overview.accounting_financial_result) {
const accountingFinancialResultText = businessOverviewAccountingFinancialResultText(overview);
if (accountingFinancialResultText) {
lines.push(accountingFinancialResultText);
}
}
if (overview.trading_margin_proxy) { if (overview.trading_margin_proxy) {
const proxy = overview.trading_margin_proxy; const proxy = overview.trading_margin_proxy;
const marginText = proxy.margin_to_revenue_pct === null ? "не рассчитана" : `${proxy.margin_to_revenue_pct}%`; const marginText = proxy.margin_to_revenue_pct === null ? "не рассчитана" : `${proxy.margin_to_revenue_pct}%`;
@ -1359,6 +1594,10 @@ function derivedBusinessOverviewConfirmedLines(pilot: AssistantMcpDiscoveryPilot
`Staleness risk proxy открытых расчетов на ${proxy.as_of_date}: самый старый договорный сигнал ${proxy.oldest_contract_start_date}, возраст ${proxy.max_contract_age_days} дн.; старейший крупный договор ${proxy.top_contract}${counterpartyText} держит ${proxy.top_contract_amount_human_ru} (${proxy.top_contract_share_pct}% брутто открытых остатков); оценка ${debtStalenessRiskBandRu(proxy.risk_band)}. Это не подтвержденная просрочка, не кредитный риск и не due-date aging.` `Staleness risk proxy открытых расчетов на ${proxy.as_of_date}: самый старый договорный сигнал ${proxy.oldest_contract_start_date}, возраст ${proxy.max_contract_age_days} дн.; старейший крупный договор ${proxy.top_contract}${counterpartyText} держит ${proxy.top_contract_amount_human_ru} (${proxy.top_contract_share_pct}% брутто открытых остатков); оценка ${debtStalenessRiskBandRu(proxy.risk_band)}. Это не подтвержденная просрочка, не кредитный риск и не due-date aging.`
); );
} }
const dueDateText = businessOverviewDebtDueDateAgingText(overview);
if (dueDateText) {
lines.push(dueDateText);
}
if (overview.inventory_position) { if (overview.inventory_position) {
const leader = overview.inventory_position.top_items[0]; const leader = overview.inventory_position.top_items[0];
const leaderText = leader const leaderText = leader
@ -1416,6 +1655,13 @@ function businessOverviewCustomerConcentrationLine(overview: BusinessOverview):
return null; return null;
} }
const share = percentText(leader.total_amount, overview.incoming_customer_revenue.total_amount); const share = percentText(leader.total_amount, overview.incoming_customer_revenue.total_amount);
if (isFinancialInstitutionBucket(leader)) {
const base = share
? `Крупнейший входящий денежный источник ${leader.axis_value} дает около ${share} проверенных входящих поступлений (${leader.total_amount_human_ru})`
: `Крупнейший входящий денежный источник в проверенном срезе: ${rankedBucketAmountLabel(leader)}`;
const nonFinancial = firstNonFinancialInstitutionBucket(overview.top_customers.slice(1));
return `${base}. По названию это банк/финансовая организация, поэтому это не доказывает клиентскую выручку или зависимость от клиента.${nonFinancial ? ` Крупнейший небанковский входящий контрагент: ${rankedBucketAmountLabel(nonFinancial)}.` : ""}`;
}
return share return share
? `Концентрация входящего потока: крупнейший подтвержденный клиент ${leader.axis_value} дает около ${share} проверенных входящих поступлений (${leader.total_amount_human_ru}). Это сигнал зависимости от клиента, а не полный customer-risk аудит.` ? `Концентрация входящего потока: крупнейший подтвержденный клиент ${leader.axis_value} дает около ${share} проверенных входящих поступлений (${leader.total_amount_human_ru}). Это сигнал зависимости от клиента, а не полный customer-risk аудит.`
: `Крупнейший подтвержденный клиент в проверенном срезе: ${leader.axis_value}${leader.total_amount_human_ru}.`; : `Крупнейший подтвержденный клиент в проверенном срезе: ${leader.axis_value}${leader.total_amount_human_ru}.`;
@ -1427,6 +1673,13 @@ function businessOverviewSupplierConcentrationLine(overview: BusinessOverview):
return null; return null;
} }
const share = percentText(leader.total_amount, overview.outgoing_supplier_payout.total_amount); const share = percentText(leader.total_amount, overview.outgoing_supplier_payout.total_amount);
if (isFinancialInstitutionBucket(leader)) {
const base = share
? `Концентрация исходящего потока: крупнейший получатель исходящих денег ${leader.axis_value} держит около ${share} проверенных исходящих платежей (${leader.total_amount_human_ru})`
: `Крупнейший получатель исходящих денег в проверенном срезе: ${rankedBucketAmountLabel(leader)}`;
const nonFinancial = firstNonFinancialInstitutionBucket(overview.top_suppliers.slice(1));
return `${base}. По названию это банк/финансовая организация, поэтому это не доказательство зависимости от обычного поставщика без проверки назначения платежа/договора.${nonFinancial ? ` Крупнейший небанковский получатель исходящих денег: ${rankedBucketAmountLabel(nonFinancial)}.` : ""}`;
}
return share return share
? `Концентрация исходящего потока: крупнейший подтвержденный поставщик/получатель исходящих платежей ${leader.axis_value} держит около ${share} проверенных исходящих платежей (${leader.total_amount_human_ru}). Это сигнал procurement concentration по найденным строкам, а не полный vendor-risk аудит или структура всех расходов.` ? `Концентрация исходящего потока: крупнейший подтвержденный поставщик/получатель исходящих платежей ${leader.axis_value} держит около ${share} проверенных исходящих платежей (${leader.total_amount_human_ru}). Это сигнал procurement concentration по найденным строкам, а не полный vendor-risk аудит или структура всех расходов.`
: `Крупнейший подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${leader.axis_value}${leader.total_amount_human_ru}.`; : `Крупнейший подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${leader.axis_value}${leader.total_amount_human_ru}.`;
@ -1474,6 +1727,20 @@ function businessOverviewRiskSynthesisLine(overview: BusinessOverview): string |
: `маржинальность proxy ${overview.trading_margin_proxy.margin_to_revenue_pct}%`; : `маржинальность proxy ${overview.trading_margin_proxy.margin_to_revenue_pct}%`;
signals.push(`торговый спред proxy ${overview.trading_margin_proxy.gross_spread_proxy_human_ru}, ${marginText}`); signals.push(`торговый спред proxy ${overview.trading_margin_proxy.gross_spread_proxy_human_ru}, ${marginText}`);
} }
if (overview.accounting_financial_result) {
const result = overview.accounting_financial_result;
const direction =
result.final_result_direction === "profit"
? "учетная прибыль"
: result.final_result_direction === "loss"
? "учетный убыток"
: "нулевой учетный финрезультат";
const marginText =
result.net_margin_to_revenue_pct === null
? "маржа к выручке 90.01 не рассчитана"
: `маржа к выручке 90.01 ${result.net_margin_to_revenue_pct}%`;
signals.push(`${direction} 90/91/99 ${result.final_result_amount_human_ru}, ${marginText}`);
}
if (overview.debt_position) { if (overview.debt_position) {
const debtDirection = const debtDirection =
overview.debt_position.net_debt_position_direction === "net_receivable" overview.debt_position.net_debt_position_direction === "net_receivable"
@ -1495,6 +1762,18 @@ function businessOverviewRiskSynthesisLine(overview: BusinessOverview): string |
`staleness risk proxy открытых расчетов: ${debtStalenessRiskBandRu(overview.debt_staleness_risk_proxy.risk_band)}, возраст ${overview.debt_staleness_risk_proxy.max_contract_age_days} дн., концентрация старейшего крупного договора ${overview.debt_staleness_risk_proxy.top_contract_share_pct}%` `staleness risk proxy открытых расчетов: ${debtStalenessRiskBandRu(overview.debt_staleness_risk_proxy.risk_band)}, возраст ${overview.debt_staleness_risk_proxy.max_contract_age_days} дн., концентрация старейшего крупного договора ${overview.debt_staleness_risk_proxy.top_contract_share_pct}%`
); );
} }
if (overview.debt_due_date_aging) {
const aging = overview.debt_due_date_aging;
signals.push(
aging.evidence_status === "confirmed_overdue"
? `due-date aging: подтвержденная просрочка ${aging.overdue_amount_human_ru}, строк ${aging.overdue_rows}`
: aging.evidence_status === "no_payment_terms_configured"
? "due-date aging: проверено, но сроки оплаты в договорах не установлены; подтвержденной просрочки нет"
: aging.evidence_status === "insufficient_due_date_basis"
? "due-date aging: не хватило даты расчетного документа для честного расчета просрочки"
: `due-date aging: проверено, подтвержденной просрочки не найдено`
);
}
if (overview.document_activity_profile) { if (overview.document_activity_profile) {
const topDocument = overview.document_activity_profile.top_document_types[0]; const topDocument = overview.document_activity_profile.top_document_types[0];
const topSection = overview.document_activity_profile.top_account_sections[0]; const topSection = overview.document_activity_profile.top_account_sections[0];
@ -1648,6 +1927,9 @@ export function buildAssistantMcpDiscoveryAnswerDraft(
if (pilot.derived_business_overview?.tax_position) { if (pilot.derived_business_overview?.tax_position) {
pushReason(reasonCodes, "answer_contains_business_overview_tax_position"); pushReason(reasonCodes, "answer_contains_business_overview_tax_position");
} }
if (pilot.derived_business_overview?.accounting_financial_result) {
pushReason(reasonCodes, "answer_contains_business_overview_accounting_financial_result");
}
if (pilot.derived_business_overview?.trading_margin_proxy) { if (pilot.derived_business_overview?.trading_margin_proxy) {
pushReason(reasonCodes, "answer_contains_business_overview_trading_margin_proxy"); pushReason(reasonCodes, "answer_contains_business_overview_trading_margin_proxy");
} }
@ -1678,6 +1960,10 @@ export function buildAssistantMcpDiscoveryAnswerDraft(
if (pilot.derived_business_overview?.debt_staleness_risk_proxy) { if (pilot.derived_business_overview?.debt_staleness_risk_proxy) {
pushReason(reasonCodes, "answer_contains_business_overview_debt_staleness_risk_proxy"); pushReason(reasonCodes, "answer_contains_business_overview_debt_staleness_risk_proxy");
} }
if (pilot.derived_business_overview?.debt_due_date_aging) {
pushReason(reasonCodes, "answer_contains_business_overview_debt_due_date_aging");
pushReason(reasonCodes, `answer_contains_business_overview_debt_due_date_aging_${pilot.derived_business_overview.debt_due_date_aging.evidence_status}`);
}
if (pilot.derived_business_overview?.inventory_position) { if (pilot.derived_business_overview?.inventory_position) {
pushReason(reasonCodes, "answer_contains_business_overview_inventory_position"); pushReason(reasonCodes, "answer_contains_business_overview_inventory_position");
} }

View File

@ -1,4 +1,5 @@
import type { AssistantMcpDiscoveryRuntimeEntryPointContract } from "./assistantMcpDiscoveryRuntimeEntryPoint"; import type { AssistantMcpDiscoveryRuntimeEntryPointContract } from "./assistantMcpDiscoveryRuntimeEntryPoint";
import type { AssistantMcpRouteCandidateContract } from "./assistantMcpDiscoveryRuntimeBridge";
export interface AssistantMcpDiscoveryDebugAttachmentFields { export interface AssistantMcpDiscoveryDebugAttachmentFields {
assistant_mcp_discovery_entry_point_v1: AssistantMcpDiscoveryRuntimeEntryPointContract | null; assistant_mcp_discovery_entry_point_v1: AssistantMcpDiscoveryRuntimeEntryPointContract | null;
@ -11,6 +12,16 @@ export interface AssistantMcpDiscoveryDebugAttachmentFields {
mcp_discovery_catalog_chain_alignment_status: string | null; mcp_discovery_catalog_chain_alignment_status: string | null;
mcp_discovery_catalog_chain_top_match: string | null; mcp_discovery_catalog_chain_top_match: string | null;
mcp_discovery_catalog_chain_selected_matches_top: boolean; mcp_discovery_catalog_chain_selected_matches_top: boolean;
mcp_discovery_route_candidate_v1: AssistantMcpRouteCandidateContract | null;
mcp_discovery_route_candidate_status: string | null;
mcp_discovery_route_candidate_fact_family: string | null;
mcp_discovery_route_candidate_action_family: string | null;
mcp_discovery_route_candidate_proof_expectation: string | null;
mcp_discovery_route_candidate_missing_axes: string[];
mcp_discovery_route_candidate_provided_axes: string[];
mcp_discovery_route_candidate_executable_now: boolean;
mcp_discovery_route_candidate_enablement_reason: string | null;
mcp_discovery_route_candidate_next_action: string | null;
mcp_discovery_answer_mode: string | null; mcp_discovery_answer_mode: string | null;
mcp_discovery_business_fact_answer_allowed: boolean; mcp_discovery_business_fact_answer_allowed: boolean;
mcp_discovery_user_facing_response_allowed: boolean; mcp_discovery_user_facing_response_allowed: boolean;
@ -59,6 +70,14 @@ function isMcpDiscoveryEntryPointContract(value: unknown): value is AssistantMcp
); );
} }
function isRouteCandidateContract(value: unknown): value is AssistantMcpRouteCandidateContract {
const record = toRecordObject(value);
return (
record?.schema_version === "assistant_mcp_route_candidate_v1" &&
record?.policy_owner === "assistantMcpDiscoveryRuntimeBridge"
);
}
function resolveEntryPoint(input: AttachAssistantMcpDiscoveryDebugInput): AssistantMcpDiscoveryRuntimeEntryPointContract | null { function resolveEntryPoint(input: AttachAssistantMcpDiscoveryDebugInput): AssistantMcpDiscoveryRuntimeEntryPointContract | null {
if (isMcpDiscoveryEntryPointContract(input.entryPoint)) { if (isMcpDiscoveryEntryPointContract(input.entryPoint)) {
return input.entryPoint; return input.entryPoint;
@ -77,6 +96,7 @@ export function buildAssistantMcpDiscoveryDebugAttachmentFields(
const bridge = toRecordObject(entryPoint?.bridge); const bridge = toRecordObject(entryPoint?.bridge);
const planner = toRecordObject(bridge?.planner); const planner = toRecordObject(bridge?.planner);
const chainAlignment = toRecordObject(planner?.catalog_chain_template_alignment); const chainAlignment = toRecordObject(planner?.catalog_chain_template_alignment);
const routeCandidate = isRouteCandidateContract(bridge?.route_candidate) ? bridge.route_candidate : null;
const answerDraft = toRecordObject(bridge?.answer_draft); const answerDraft = toRecordObject(bridge?.answer_draft);
return { return {
@ -90,6 +110,16 @@ export function buildAssistantMcpDiscoveryDebugAttachmentFields(
mcp_discovery_catalog_chain_alignment_status: toNonEmptyString(chainAlignment?.alignment_status), mcp_discovery_catalog_chain_alignment_status: toNonEmptyString(chainAlignment?.alignment_status),
mcp_discovery_catalog_chain_top_match: toNonEmptyString(chainAlignment?.top_chain_template_match), mcp_discovery_catalog_chain_top_match: toNonEmptyString(chainAlignment?.top_chain_template_match),
mcp_discovery_catalog_chain_selected_matches_top: chainAlignment?.selected_chain_matches_top === true, mcp_discovery_catalog_chain_selected_matches_top: chainAlignment?.selected_chain_matches_top === true,
mcp_discovery_route_candidate_v1: routeCandidate,
mcp_discovery_route_candidate_status: toNonEmptyString(routeCandidate?.candidate_status),
mcp_discovery_route_candidate_fact_family: toNonEmptyString(routeCandidate?.business_fact_family),
mcp_discovery_route_candidate_action_family: toNonEmptyString(routeCandidate?.action_family),
mcp_discovery_route_candidate_proof_expectation: toNonEmptyString(routeCandidate?.proof_expectation),
mcp_discovery_route_candidate_missing_axes: toStringArray(routeCandidate?.missing_axes),
mcp_discovery_route_candidate_provided_axes: toStringArray(routeCandidate?.provided_axes),
mcp_discovery_route_candidate_executable_now: routeCandidate?.executable_now === true,
mcp_discovery_route_candidate_enablement_reason: toNonEmptyString(routeCandidate?.enablement_reason),
mcp_discovery_route_candidate_next_action: toNonEmptyString(routeCandidate?.recommended_next_action),
mcp_discovery_answer_mode: toNonEmptyString(answerDraft?.answer_mode), mcp_discovery_answer_mode: toNonEmptyString(answerDraft?.answer_mode),
mcp_discovery_business_fact_answer_allowed: bridge?.business_fact_answer_allowed === true, mcp_discovery_business_fact_answer_allowed: bridge?.business_fact_answer_allowed === true,
mcp_discovery_user_facing_response_allowed: bridge?.user_facing_response_allowed === true, mcp_discovery_user_facing_response_allowed: bridge?.user_facing_response_allowed === true,

View File

@ -18,6 +18,11 @@ import {
type AssistantMcpDiscoveryProbeResult type AssistantMcpDiscoveryProbeResult
} from "./assistantMcpDiscoveryPolicy"; } from "./assistantMcpDiscoveryPolicy";
import { buildAddressRecipePlan, selectAddressRecipe } from "./addressRecipeCatalog"; import { buildAddressRecipePlan, selectAddressRecipe } from "./addressRecipeCatalog";
import {
counterpartyRoleHintForName,
isLikelyFinancialInstitutionCounterparty,
type CounterpartyRoleHint
} from "./counterpartyRoleHeuristics";
import type { AddressFilterSet, AddressIntent } from "../types/addressQuery"; import type { AddressFilterSet, AddressIntent } from "../types/addressQuery";
export const ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION = export const ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION =
@ -83,6 +88,7 @@ export interface AssistantMcpDiscoveryRankedValueFlowBucket {
rows_with_amount: number; rows_with_amount: number;
total_amount: number; total_amount: number;
total_amount_human_ru: string; total_amount_human_ru: string;
counterparty_role_hint?: CounterpartyRoleHint;
} }
export interface AssistantMcpDiscoveryDerivedRankedValueFlow { export interface AssistantMcpDiscoveryDerivedRankedValueFlow {
@ -228,10 +234,12 @@ export interface AssistantMcpDiscoveryDerivedBusinessOverview {
yearly_breakdown: AssistantMcpDiscoveryBusinessOverviewYearBucket[]; yearly_breakdown: AssistantMcpDiscoveryBusinessOverviewYearBucket[];
activity_period: AssistantMcpDiscoveryDerivedActivityPeriod | null; activity_period: AssistantMcpDiscoveryDerivedActivityPeriod | null;
tax_position: AssistantMcpDiscoveryDerivedBusinessOverviewTaxPosition | null; tax_position: AssistantMcpDiscoveryDerivedBusinessOverviewTaxPosition | null;
accounting_financial_result: AssistantMcpDiscoveryDerivedBusinessOverviewAccountingFinancialResult | null;
trading_margin_proxy: AssistantMcpDiscoveryDerivedBusinessOverviewTradingMarginProxy | null; trading_margin_proxy: AssistantMcpDiscoveryDerivedBusinessOverviewTradingMarginProxy | null;
debt_position: AssistantMcpDiscoveryDerivedBusinessOverviewDebtPosition | null; debt_position: AssistantMcpDiscoveryDerivedBusinessOverviewDebtPosition | null;
debt_open_settlement_quality: AssistantMcpDiscoveryDerivedBusinessOverviewDebtOpenSettlementQuality | null; debt_open_settlement_quality: AssistantMcpDiscoveryDerivedBusinessOverviewDebtOpenSettlementQuality | null;
debt_staleness_risk_proxy: AssistantMcpDiscoveryDerivedBusinessOverviewDebtStalenessRiskProxy | null; debt_staleness_risk_proxy: AssistantMcpDiscoveryDerivedBusinessOverviewDebtStalenessRiskProxy | null;
debt_due_date_aging: AssistantMcpDiscoveryDerivedBusinessOverviewDebtDueDateAging | null;
inventory_position: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryPosition | null; inventory_position: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryPosition | null;
inventory_turnover_proxy: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryTurnoverProxy | null; inventory_turnover_proxy: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryTurnoverProxy | null;
inventory_staleness_risk_proxy: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryStalenessRiskProxy | null; inventory_staleness_risk_proxy: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryStalenessRiskProxy | null;
@ -264,6 +272,31 @@ export interface AssistantMcpDiscoveryDerivedBusinessOverviewTaxPosition {
inference_basis: "sales_book_minus_purchase_book_confirmed_1c_vat_rows"; inference_basis: "sales_book_minus_purchase_book_confirmed_1c_vat_rows";
} }
export interface AssistantMcpDiscoveryDerivedBusinessOverviewAccountingFinancialResult {
period_scope: string;
rows_matched: number;
rows_with_amount: number;
sales_revenue_accounting: number;
sales_revenue_accounting_human_ru: string;
cost_of_sales_accounting: number;
cost_of_sales_accounting_human_ru: string;
selling_expenses_accounting: number;
selling_expenses_accounting_human_ru: string;
admin_expenses_accounting: number;
admin_expenses_accounting_human_ru: string;
sales_result_amount: number;
sales_result_amount_human_ru: string;
other_result_amount: number;
other_result_amount_human_ru: string;
final_result_amount: number;
final_result_amount_human_ru: string;
final_result_direction: "profit" | "loss" | "balanced";
net_margin_to_revenue_pct: number | null;
final_transfer_basis: "account_99_to_84_period_close" | "account_90_91_to_99_period_close";
period_close_rows_with_amount: number;
inference_basis: "account_90_91_99_period_close_aggregate_confirmed_1c_rows";
}
export interface AssistantMcpDiscoveryBusinessOverviewTradingItemBucket { export interface AssistantMcpDiscoveryBusinessOverviewTradingItemBucket {
item: string; item: string;
sales_revenue: number; sales_revenue: number;
@ -373,6 +406,45 @@ export interface AssistantMcpDiscoveryDerivedBusinessOverviewDebtStalenessRiskPr
inference_basis: "contract_date_age_and_open_balance_concentration_confirmed_1c_rows"; inference_basis: "contract_date_age_and_open_balance_concentration_confirmed_1c_rows";
} }
export interface AssistantMcpDiscoveryBusinessOverviewDebtDueDateAgingBucket {
counterparty: string | null;
contract: string | null;
settlement_document: string | null;
document_date: string;
due_date: string;
payment_term_days: number;
overdue_days: number;
amount: number;
amount_human_ru: string;
share_of_overdue_amount_pct: number | null;
}
export interface AssistantMcpDiscoveryDerivedBusinessOverviewDebtDueDateAging {
as_of_date: string;
rows_matched: number;
rows_with_amount: number;
gross_open_amount: number;
gross_open_amount_human_ru: string;
rows_with_payment_terms: number;
rows_without_payment_terms: number;
rows_without_document_date: number;
overdue_rows: number;
overdue_amount: number;
overdue_amount_human_ru: string;
not_yet_due_rows: number;
not_yet_due_amount: number;
not_yet_due_amount_human_ru: string;
oldest_due_date: string | null;
max_overdue_days: number | null;
top_overdue_items: AssistantMcpDiscoveryBusinessOverviewDebtDueDateAgingBucket[];
evidence_status:
| "confirmed_overdue"
| "no_overdue_found"
| "no_payment_terms_configured"
| "insufficient_due_date_basis";
inference_basis: "contract_payment_terms_and_settlement_document_dates_from_open_balance_rows";
}
export interface AssistantMcpDiscoveryBusinessOverviewInventoryItemBucket { export interface AssistantMcpDiscoveryBusinessOverviewInventoryItemBucket {
item: string; item: string;
rows_with_amount: number; rows_with_amount: number;
@ -723,6 +795,17 @@ function buildBusinessOverviewDebtFilters(planner: AssistantMcpDiscoveryPlannerC
}; };
} }
function shouldRunDebtDueDateAgingProbe(planner: AssistantMcpDiscoveryPlannerContract): boolean {
const actionFamily = toNonEmptyString(planner.data_need_graph?.action_family);
const turnActionFamily = toNonEmptyString(planner.discovery_plan.turn_meaning_ref?.asked_action_family);
const unsupportedFamily = toNonEmptyString(planner.discovery_plan.turn_meaning_ref?.unsupported_but_understood_family);
const proofExpectation = toNonEmptyString(planner.data_need_graph?.proof_expectation);
const combined = [actionFamily, turnActionFamily, unsupportedFamily, proofExpectation]
.filter((item): item is string => Boolean(item))
.join(" ");
return /(?:debt_due_date_boundary|due[-_ ]?date|overdue|aging|просроч|срок\s+оплат|дебиторк|кредиторск)/iu.test(combined);
}
function buildBusinessOverviewInventoryFilters(planner: AssistantMcpDiscoveryPlannerContract): AddressFilterSet | null { function buildBusinessOverviewInventoryFilters(planner: AssistantMcpDiscoveryPlannerContract): AddressFilterSet | null {
const meaning = planner.discovery_plan.turn_meaning_ref; const meaning = planner.discovery_plan.turn_meaning_ref;
const organization = toNonEmptyString(meaning?.explicit_organization_scope); const organization = toNonEmptyString(meaning?.explicit_organization_scope);
@ -756,6 +839,20 @@ function buildBusinessOverviewTradingMarginFilters(planner: AssistantMcpDiscover
}; };
} }
function buildBusinessOverviewAccountingFinancialResultFilters(
planner: AssistantMcpDiscoveryPlannerContract
): AddressFilterSet | null {
const filters = buildBusinessOverviewTradingMarginFilters(planner);
if (!filters) {
return null;
}
return {
...filters,
limit: Math.max(32, planner.discovery_plan.execution_budget.max_rows_per_probe),
sort: "period_asc"
};
}
function buildInventoryExactFilters(planner: AssistantMcpDiscoveryPlannerContract): AddressFilterSet { function buildInventoryExactFilters(planner: AssistantMcpDiscoveryPlannerContract): AddressFilterSet {
const meaning = planner.discovery_plan.turn_meaning_ref; const meaning = planner.discovery_plan.turn_meaning_ref;
const subject = firstEntityCandidate(planner); const subject = firstEntityCandidate(planner);
@ -1359,7 +1456,8 @@ async function executeCoverageAwareValueFlowQuery(input: {
}); });
executedProbeCount += 1; executedProbeCount += 1;
probeResults.push(queryResultToProbeResult(input.primitiveId, broadResult)); probeResults.push(queryResultToProbeResult(input.primitiveId, broadResult));
const broadCoverageLimited = !broadResult.error && broadResult.matched_rows >= input.maxRowsPerProbe; const broadLimitThreshold = Math.max(1, Math.min(input.maxRowsPerProbe, broadRecipePlan.limit));
const broadCoverageLimited = !broadResult.error && broadResult.matched_rows >= broadLimitThreshold;
if (broadResult.error) { if (broadResult.error) {
pushUnique(queryLimitations, broadResult.error); pushUnique(queryLimitations, broadResult.error);
@ -1424,7 +1522,8 @@ async function executeCoverageAwareValueFlowQuery(input: {
pushUnique(queryLimitations, chunkResult.error); pushUnique(queryLimitations, chunkResult.error);
continue; continue;
} }
if (chunkResult.matched_rows >= input.maxRowsPerProbe) { const chunkLimitThreshold = Math.max(1, Math.min(input.maxRowsPerProbe, chunkPlan.limit));
if (chunkResult.matched_rows >= chunkLimitThreshold) {
anyChunkLimited = true; anyChunkLimited = true;
} }
chunkResults.push(chunkResult); chunkResults.push(chunkResult);
@ -2402,6 +2501,14 @@ function extractContractDateFromText(value: string | null): string | null {
if (!text || !/(?:договор|contract|дог\.)/iu.test(text)) { if (!text || !/(?:договор|contract|дог\.)/iu.test(text)) {
return null; return null;
} }
return extractAnyDateFromText(text);
}
function extractAnyDateFromText(value: string | null): string | null {
const text = toNonEmptyString(value);
if (!text) {
return null;
}
const isoLikeMatch = text.match(/(\d{4})[-./](\d{1,2})[-./](\d{1,2})/); const isoLikeMatch = text.match(/(\d{4})[-./](\d{1,2})[-./](\d{1,2})/);
if (isoLikeMatch) { if (isoLikeMatch) {
return normalizeDateParts(isoLikeMatch[1], isoLikeMatch[2], isoLikeMatch[3]); return normalizeDateParts(isoLikeMatch[1], isoLikeMatch[2], isoLikeMatch[3]);
@ -2413,6 +2520,64 @@ function extractContractDateFromText(value: string | null): string | null {
return null; return null;
} }
function rowContractDateValue(row: Record<string, unknown>): string | null {
const explicit = rowTextValue(row, ["ДатаДоговора", "ContractDate", "contract_date"]);
return extractAnyDateFromText(explicit) ?? rowOpenSettlementContractStartDateValue(row);
}
function rowSettlementDocumentDateValue(row: Record<string, unknown>): string | null {
const explicit = rowTextValue(row, [
"ДатаДокументаРасчетов",
"SettlementDocumentDate",
"settlement_document_date"
]);
const settlementDocument = rowTextValue(row, [
"ДокументРасчетов",
"SettlementDocument",
"settlement_document"
]);
return extractAnyDateFromText(explicit) ?? extractAnyDateFromText(settlementDocument) ?? extractAnyDateFromText(rowDocumentValue(row));
}
function rowPaymentTermIsSetValue(row: Record<string, unknown>): boolean {
const candidate = rowTextValue(row, ["УстановленСрокОплаты", "PaymentTermIsSet", "payment_term_is_set"]);
if (typeof row["УстановленСрокОплаты"] === "boolean") {
return row["УстановленСрокОплаты"] === true;
}
if (typeof row["PaymentTermIsSet"] === "boolean") {
return row["PaymentTermIsSet"] === true;
}
if (!candidate) {
return false;
}
return /^(?:true|истина|да|yes|1)$/iu.test(candidate.trim());
}
function rowPaymentTermDaysValue(row: Record<string, unknown>): number | null {
const value = rowNumberValue(row, ["СрокОплаты", "PaymentTermDays", "payment_term_days"]);
if (value === null || !Number.isFinite(value) || value <= 0) {
return null;
}
return Math.trunc(value);
}
function addDaysToIsoDate(isoDate: string, days: number): string | null {
const match = isoDate.match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (!match || !Number.isFinite(days)) {
return null;
}
const date = new Date(Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3])));
date.setUTCDate(date.getUTCDate() + Math.trunc(days));
if (Number.isNaN(date.getTime())) {
return null;
}
return [
String(date.getUTCFullYear()).padStart(4, "0"),
String(date.getUTCMonth() + 1).padStart(2, "0"),
String(date.getUTCDate()).padStart(2, "0")
].join("-");
}
function earlierIsoDate(left: string | null, right: string | null): string | null { function earlierIsoDate(left: string | null, right: string | null): string | null {
if (!left) { if (!left) {
return right; return right;
@ -2908,7 +3073,8 @@ function deriveRankedValueFlow(
axis_value: axisValue, axis_value: axisValue,
rows_with_amount: bucket.rows_with_amount, rows_with_amount: bucket.rows_with_amount,
total_amount: bucket.total_amount, total_amount: bucket.total_amount,
total_amount_human_ru: formatAmountHumanRu(bucket.total_amount) total_amount_human_ru: formatAmountHumanRu(bucket.total_amount),
counterparty_role_hint: counterpartyRoleHintForName(axisValue)
})) }))
.sort((left, right) => { .sort((left, right) => {
const amountDelta = right.total_amount - left.total_amount; const amountDelta = right.total_amount - left.total_amount;
@ -3097,6 +3263,116 @@ function deriveBusinessOverviewTaxPosition(
}; };
} }
function accountingFinancialResultMarkerAmount(
result: AddressMcpQueryExecutorResult,
marker: string
): number {
let total = 0;
for (const row of result.rows) {
if (String(rowDocumentValue(row) ?? "") !== marker) {
continue;
}
const amount = rowAmountValue(row);
if (amount !== null && Number.isFinite(amount)) {
total += amount;
}
}
return total;
}
function accountingFinancialResultNonZeroCount(
values: number[]
): number {
return values.filter((value) => Math.abs(value) > 0).length;
}
function deriveBusinessOverviewAccountingFinancialResult(
result: AddressMcpQueryExecutorResult | null,
periodScope: string | null
): AssistantMcpDiscoveryDerivedBusinessOverviewAccountingFinancialResult | null {
if (!result || result.error || result.matched_rows <= 0 || !periodScope) {
return null;
}
const salesRevenueAccounting = accountingFinancialResultMarkerAmount(result, "ACC90_REVENUE_KT");
const costOfSalesAccounting = accountingFinancialResultMarkerAmount(result, "ACC90_COST_DT");
const sellingExpensesAccounting = accountingFinancialResultMarkerAmount(result, "ACC90_SELLING_DT");
const adminExpensesAccounting = accountingFinancialResultMarkerAmount(result, "ACC90_ADMIN_DT");
const salesProfitTo99 = accountingFinancialResultMarkerAmount(result, "ACC90_RESULT_TO_99_PROFIT");
const salesLossFrom99 = accountingFinancialResultMarkerAmount(result, "ACC90_RESULT_FROM_99_LOSS");
const otherProfitTo99 = accountingFinancialResultMarkerAmount(result, "ACC91_RESULT_TO_99_PROFIT");
const otherLossFrom99 = accountingFinancialResultMarkerAmount(result, "ACC91_RESULT_FROM_99_LOSS");
const profitTransferTo84 = accountingFinancialResultMarkerAmount(result, "ACC99_TO84_PROFIT_TRANSFER");
const lossTransferFrom84 = accountingFinancialResultMarkerAmount(result, "ACC84_TO99_LOSS_TRANSFER");
const amountSignals = [
salesRevenueAccounting,
costOfSalesAccounting,
sellingExpensesAccounting,
adminExpensesAccounting,
salesProfitTo99,
salesLossFrom99,
otherProfitTo99,
otherLossFrom99,
profitTransferTo84,
lossTransferFrom84
];
const rowsWithAmount = accountingFinancialResultNonZeroCount(amountSignals);
if (rowsWithAmount <= 0) {
return null;
}
const salesResultAmount = salesProfitTo99 - salesLossFrom99;
const otherResultAmount = otherProfitTo99 - otherLossFrom99;
const hasFinalTransfer = profitTransferTo84 > 0 || lossTransferFrom84 > 0;
const finalResultAmount = hasFinalTransfer
? profitTransferTo84 - lossTransferFrom84
: salesResultAmount + otherResultAmount;
const finalResultDirection =
finalResultAmount > 0
? "profit"
: finalResultAmount < 0
? "loss"
: "balanced";
const netMarginToRevenuePct =
salesRevenueAccounting > 0 ? percentageOfTotal(finalResultAmount, salesRevenueAccounting) : null;
const periodCloseRowsWithAmount = accountingFinancialResultNonZeroCount([
salesProfitTo99,
salesLossFrom99,
otherProfitTo99,
otherLossFrom99,
profitTransferTo84,
lossTransferFrom84
]);
return {
period_scope: periodScope,
rows_matched: result.matched_rows,
rows_with_amount: rowsWithAmount,
sales_revenue_accounting: salesRevenueAccounting,
sales_revenue_accounting_human_ru: formatAmountHumanRu(salesRevenueAccounting),
cost_of_sales_accounting: costOfSalesAccounting,
cost_of_sales_accounting_human_ru: formatAmountHumanRu(costOfSalesAccounting),
selling_expenses_accounting: sellingExpensesAccounting,
selling_expenses_accounting_human_ru: formatAmountHumanRu(sellingExpensesAccounting),
admin_expenses_accounting: adminExpensesAccounting,
admin_expenses_accounting_human_ru: formatAmountHumanRu(adminExpensesAccounting),
sales_result_amount: salesResultAmount,
sales_result_amount_human_ru: formatAmountHumanRu(Math.abs(salesResultAmount)),
other_result_amount: otherResultAmount,
other_result_amount_human_ru: formatAmountHumanRu(Math.abs(otherResultAmount)),
final_result_amount: finalResultAmount,
final_result_amount_human_ru: formatAmountHumanRu(Math.abs(finalResultAmount)),
final_result_direction: finalResultDirection,
net_margin_to_revenue_pct: netMarginToRevenuePct,
final_transfer_basis: hasFinalTransfer
? "account_99_to_84_period_close"
: "account_90_91_to_99_period_close",
period_close_rows_with_amount: periodCloseRowsWithAmount,
inference_basis: "account_90_91_99_period_close_aggregate_confirmed_1c_rows"
};
}
function deriveBusinessOverviewTradingMarginProxy( function deriveBusinessOverviewTradingMarginProxy(
result: AddressMcpQueryExecutorResult | null, result: AddressMcpQueryExecutorResult | null,
periodScope: string | null periodScope: string | null
@ -3505,6 +3781,135 @@ function deriveBusinessOverviewDebtStalenessRiskProxy(
}; };
} }
function deriveBusinessOverviewDebtDueDateAging(input: {
dueDateResult: AddressMcpQueryExecutorResult | null;
debtAsOfDate: string | null;
}): AssistantMcpDiscoveryDerivedBusinessOverviewDebtDueDateAging | null {
if (!input.debtAsOfDate || !input.dueDateResult || input.dueDateResult.error || input.dueDateResult.matched_rows <= 0) {
return null;
}
const overdueItems: Array<Omit<AssistantMcpDiscoveryBusinessOverviewDebtDueDateAgingBucket, "share_of_overdue_amount_pct">> = [];
let rowsWithAmount = 0;
let grossOpenAmount = 0;
let rowsWithPaymentTerms = 0;
let rowsWithoutPaymentTerms = 0;
let rowsWithoutDocumentDate = 0;
let overdueAmount = 0;
let notYetDueAmount = 0;
let notYetDueRows = 0;
for (const row of input.dueDateResult.rows) {
const amount = rowAmountValue(row);
if (amount === null) {
continue;
}
const absAmount = Math.abs(amount);
if (absAmount <= 0) {
continue;
}
rowsWithAmount += 1;
grossOpenAmount += absAmount;
const paymentTermIsSet = rowPaymentTermIsSetValue(row);
const paymentTermDays = rowPaymentTermDaysValue(row);
if (!paymentTermIsSet || paymentTermDays === null) {
rowsWithoutPaymentTerms += 1;
continue;
}
rowsWithPaymentTerms += 1;
const documentDate = rowSettlementDocumentDateValue(row) ?? rowContractDateValue(row);
if (!documentDate) {
rowsWithoutDocumentDate += 1;
continue;
}
const dueDate = addDaysToIsoDate(documentDate, paymentTermDays);
if (!dueDate) {
rowsWithoutDocumentDate += 1;
continue;
}
const overdueDays = dueDate < input.debtAsOfDate ? daysBetweenIsoDates(dueDate, input.debtAsOfDate) : null;
if (overdueDays !== null && overdueDays > 0) {
overdueAmount += absAmount;
overdueItems.push({
counterparty: rowCounterpartyValue(row),
contract: rowContractValue(row),
settlement_document: rowTextValue(row, [
"ДокументРасчетов",
"SettlementDocument",
"settlement_document"
]) ?? rowDocumentValue(row),
document_date: documentDate,
due_date: dueDate,
payment_term_days: paymentTermDays,
overdue_days: overdueDays,
amount: absAmount,
amount_human_ru: formatAmountHumanRu(absAmount)
});
} else {
notYetDueRows += 1;
notYetDueAmount += absAmount;
}
}
if (rowsWithAmount <= 0) {
return null;
}
const topOverdueItems = overdueItems
.sort((left, right) => {
const daysDelta = right.overdue_days - left.overdue_days;
if (daysDelta !== 0) {
return daysDelta;
}
const amountDelta = right.amount - left.amount;
return amountDelta !== 0 ? amountDelta : String(left.contract ?? "").localeCompare(String(right.contract ?? ""), "ru");
})
.slice(0, 5)
.map((item) => ({
...item,
share_of_overdue_amount_pct: percentageOfTotal(item.amount, overdueAmount)
}));
const oldestDueDate = overdueItems
.map((item) => item.due_date)
.sort()[0] ?? null;
const maxOverdueDays = overdueItems.reduce<number | null>(
(max, item) => max === null ? item.overdue_days : Math.max(max, item.overdue_days),
null
);
const evidenceStatus: AssistantMcpDiscoveryDerivedBusinessOverviewDebtDueDateAging["evidence_status"] =
overdueItems.length > 0
? "confirmed_overdue"
: rowsWithPaymentTerms <= 0
? "no_payment_terms_configured"
: rowsWithoutDocumentDate >= rowsWithPaymentTerms
? "insufficient_due_date_basis"
: "no_overdue_found";
return {
as_of_date: input.debtAsOfDate,
rows_matched: input.dueDateResult.matched_rows,
rows_with_amount: rowsWithAmount,
gross_open_amount: grossOpenAmount,
gross_open_amount_human_ru: formatAmountHumanRu(grossOpenAmount),
rows_with_payment_terms: rowsWithPaymentTerms,
rows_without_payment_terms: rowsWithoutPaymentTerms,
rows_without_document_date: rowsWithoutDocumentDate,
overdue_rows: overdueItems.length,
overdue_amount: overdueAmount,
overdue_amount_human_ru: formatAmountHumanRu(overdueAmount),
not_yet_due_rows: notYetDueRows,
not_yet_due_amount: notYetDueAmount,
not_yet_due_amount_human_ru: formatAmountHumanRu(notYetDueAmount),
oldest_due_date: oldestDueDate,
max_overdue_days: maxOverdueDays,
top_overdue_items: topOverdueItems,
evidence_status: evidenceStatus,
inference_basis: "contract_payment_terms_and_settlement_document_dates_from_open_balance_rows"
};
}
function debtStalenessRiskBandRu( function debtStalenessRiskBandRu(
riskBand: AssistantMcpDiscoveryDerivedBusinessOverviewDebtStalenessRiskProxy["risk_band"] riskBand: AssistantMcpDiscoveryDerivedBusinessOverviewDebtStalenessRiskProxy["risk_band"]
): string { ): string {
@ -3737,9 +4142,11 @@ function inventoryStalenessRiskBandRu(
function buildBusinessOverviewMissingProofFamilies(input: { function buildBusinessOverviewMissingProofFamilies(input: {
missingSignalFamilies: string[]; missingSignalFamilies: string[];
accountingFinancialResult: AssistantMcpDiscoveryDerivedBusinessOverviewAccountingFinancialResult | null;
tradingMarginProxy: AssistantMcpDiscoveryDerivedBusinessOverviewTradingMarginProxy | null; tradingMarginProxy: AssistantMcpDiscoveryDerivedBusinessOverviewTradingMarginProxy | null;
debtOpenSettlementQuality: AssistantMcpDiscoveryDerivedBusinessOverviewDebtOpenSettlementQuality | null; debtOpenSettlementQuality: AssistantMcpDiscoveryDerivedBusinessOverviewDebtOpenSettlementQuality | null;
debtStalenessRiskProxy: AssistantMcpDiscoveryDerivedBusinessOverviewDebtStalenessRiskProxy | null; debtStalenessRiskProxy: AssistantMcpDiscoveryDerivedBusinessOverviewDebtStalenessRiskProxy | null;
debtDueDateAging: AssistantMcpDiscoveryDerivedBusinessOverviewDebtDueDateAging | null;
inventoryPosition: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryPosition | null; inventoryPosition: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryPosition | null;
inventoryTurnoverProxy: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryTurnoverProxy | null; inventoryTurnoverProxy: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryTurnoverProxy | null;
inventoryStalenessRiskProxy: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryStalenessRiskProxy | null; inventoryStalenessRiskProxy: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryStalenessRiskProxy | null;
@ -3753,7 +4160,7 @@ function buildBusinessOverviewMissingProofFamilies(input: {
} }
}; };
if (missing.has("profit_margin") || missing.has("accounting_profit_margin")) { if ((missing.has("profit_margin") || missing.has("accounting_profit_margin")) && !input.accountingFinancialResult) {
pushUnique({ pushUnique({
family: "accounting_profit_margin", family: "accounting_profit_margin",
current_status: input.tradingMarginProxy ? "proxy_only_currently" : "reviewed_route_not_wired", current_status: input.tradingMarginProxy ? "proxy_only_currently" : "reviewed_route_not_wired",
@ -3765,7 +4172,7 @@ function buildBusinessOverviewMissingProofFamilies(input: {
}); });
} }
if (missing.has("debt_due_date_aging_quality") || missing.has("debt_open_settlement_quality")) { if ((missing.has("debt_due_date_aging_quality") || missing.has("debt_open_settlement_quality")) && !input.debtDueDateAging) {
pushUnique({ pushUnique({
family: "debt_due_date_aging_quality", family: "debt_due_date_aging_quality",
current_status: input.debtStalenessRiskProxy current_status: input.debtStalenessRiskProxy
@ -3826,10 +4233,12 @@ function deriveBusinessOverview(input: {
outgoingResult: AssistantMcpDiscoveryCoverageAwareQueryResult | null; outgoingResult: AssistantMcpDiscoveryCoverageAwareQueryResult | null;
lifecycleResult: AddressMcpQueryExecutorResult | null; lifecycleResult: AddressMcpQueryExecutorResult | null;
taxResult: AddressMcpQueryExecutorResult | null; taxResult: AddressMcpQueryExecutorResult | null;
accountingFinancialResultResult: AddressMcpQueryExecutorResult | null;
tradingMarginResult: AddressMcpQueryExecutorResult | null; tradingMarginResult: AddressMcpQueryExecutorResult | null;
receivablesResult: AddressMcpQueryExecutorResult | null; receivablesResult: AddressMcpQueryExecutorResult | null;
payablesResult: AddressMcpQueryExecutorResult | null; payablesResult: AddressMcpQueryExecutorResult | null;
openContractsResult: AddressMcpQueryExecutorResult | null; openContractsResult: AddressMcpQueryExecutorResult | null;
dueDateAgingResult: AddressMcpQueryExecutorResult | null;
documentActivityProfileResult: AddressMcpQueryExecutorResult | null; documentActivityProfileResult: AddressMcpQueryExecutorResult | null;
counterpartyProfileResult: AddressMcpQueryExecutorResult | null; counterpartyProfileResult: AddressMcpQueryExecutorResult | null;
contractUsageProfileResult: AddressMcpQueryExecutorResult | null; contractUsageProfileResult: AddressMcpQueryExecutorResult | null;
@ -3860,6 +4269,10 @@ function deriveBusinessOverview(input: {
}); });
const activityPeriod = deriveActivityPeriod(input.lifecycleResult); const activityPeriod = deriveActivityPeriod(input.lifecycleResult);
const taxPosition = deriveBusinessOverviewTaxPosition(input.taxResult, input.periodScope); const taxPosition = deriveBusinessOverviewTaxPosition(input.taxResult, input.periodScope);
const accountingFinancialResult = deriveBusinessOverviewAccountingFinancialResult(
input.accountingFinancialResultResult,
input.periodScope
);
const tradingMarginProxy = deriveBusinessOverviewTradingMarginProxy(input.tradingMarginResult, input.periodScope); const tradingMarginProxy = deriveBusinessOverviewTradingMarginProxy(input.tradingMarginResult, input.periodScope);
const debtPosition = deriveBusinessOverviewDebtPosition({ const debtPosition = deriveBusinessOverviewDebtPosition({
receivablesResult: input.receivablesResult, receivablesResult: input.receivablesResult,
@ -3883,6 +4296,10 @@ function deriveBusinessOverview(input: {
input.periodScope input.periodScope
); );
const debtStalenessRiskProxy = deriveBusinessOverviewDebtStalenessRiskProxy(debtOpenSettlementQuality); const debtStalenessRiskProxy = deriveBusinessOverviewDebtStalenessRiskProxy(debtOpenSettlementQuality);
const debtDueDateAging = deriveBusinessOverviewDebtDueDateAging({
dueDateResult: input.dueDateAgingResult,
debtAsOfDate: input.debtAsOfDate
});
const inventoryPosition = deriveBusinessOverviewInventoryPosition({ const inventoryPosition = deriveBusinessOverviewInventoryPosition({
inventoryOnHandResult: input.inventoryOnHandResult, inventoryOnHandResult: input.inventoryOnHandResult,
inventoryAgingResult: input.inventoryAgingResult, inventoryAgingResult: input.inventoryAgingResult,
@ -3901,10 +4318,12 @@ function deriveBusinessOverview(input: {
outgoing.rows_with_amount > 0, outgoing.rows_with_amount > 0,
Boolean(activityPeriod), Boolean(activityPeriod),
Boolean(taxPosition), Boolean(taxPosition),
Boolean(accountingFinancialResult),
Boolean(tradingMarginProxy), Boolean(tradingMarginProxy),
Boolean(debtPosition), Boolean(debtPosition),
Boolean(debtOpenSettlementQuality), Boolean(debtOpenSettlementQuality),
Boolean(debtStalenessRiskProxy), Boolean(debtStalenessRiskProxy),
Boolean(debtDueDateAging),
Boolean(documentActivityProfile), Boolean(documentActivityProfile),
Boolean(counterpartyProfile), Boolean(counterpartyProfile),
Boolean(contractUsageProfile), Boolean(contractUsageProfile),
@ -3921,9 +4340,9 @@ function deriveBusinessOverview(input: {
documentActivityProfile || counterpartyProfile || contractUsageProfile documentActivityProfile || counterpartyProfile || contractUsageProfile
); );
const missingSignalFamilies = [ const missingSignalFamilies = [
tradingMarginProxy ? "accounting_profit_margin" : "profit_margin", accountingFinancialResult ? null : tradingMarginProxy ? "accounting_profit_margin" : "profit_margin",
debtPosition ? null : "debt_position", debtPosition ? null : "debt_position",
debtOpenSettlementQuality ? "debt_due_date_aging_quality" : "debt_open_settlement_quality", debtDueDateAging ? null : debtOpenSettlementQuality ? "debt_due_date_aging_quality" : "debt_open_settlement_quality",
taxPosition ? null : "tax_position", taxPosition ? null : "tax_position",
inventoryPosition inventoryPosition
? inventoryStalenessRiskProxy ? inventoryStalenessRiskProxy
@ -3936,9 +4355,11 @@ function deriveBusinessOverview(input: {
].filter((item): item is string => Boolean(item)); ].filter((item): item is string => Boolean(item));
const missingProofFamilies = buildBusinessOverviewMissingProofFamilies({ const missingProofFamilies = buildBusinessOverviewMissingProofFamilies({
missingSignalFamilies, missingSignalFamilies,
accountingFinancialResult,
tradingMarginProxy, tradingMarginProxy,
debtOpenSettlementQuality, debtOpenSettlementQuality,
debtStalenessRiskProxy, debtStalenessRiskProxy,
debtDueDateAging,
inventoryPosition, inventoryPosition,
inventoryTurnoverProxy, inventoryTurnoverProxy,
inventoryStalenessRiskProxy, inventoryStalenessRiskProxy,
@ -3957,10 +4378,12 @@ function deriveBusinessOverview(input: {
yearly_breakdown: yearlyBreakdown, yearly_breakdown: yearlyBreakdown,
activity_period: activityPeriod, activity_period: activityPeriod,
tax_position: taxPosition, tax_position: taxPosition,
accounting_financial_result: accountingFinancialResult,
trading_margin_proxy: tradingMarginProxy, trading_margin_proxy: tradingMarginProxy,
debt_position: debtPosition, debt_position: debtPosition,
debt_open_settlement_quality: debtOpenSettlementQuality, debt_open_settlement_quality: debtOpenSettlementQuality,
debt_staleness_risk_proxy: debtStalenessRiskProxy, debt_staleness_risk_proxy: debtStalenessRiskProxy,
debt_due_date_aging: debtDueDateAging,
inventory_position: inventoryPosition, inventory_position: inventoryPosition,
inventory_turnover_proxy: inventoryTurnoverProxy, inventory_turnover_proxy: inventoryTurnoverProxy,
inventory_staleness_risk_proxy: inventoryStalenessRiskProxy, inventory_staleness_risk_proxy: inventoryStalenessRiskProxy,
@ -3973,9 +4396,9 @@ function deriveBusinessOverview(input: {
missing_signal_families: missingSignalFamilies, missing_signal_families: missingSignalFamilies,
missing_proof_families: missingProofFamilies, missing_proof_families: missingProofFamilies,
inference_basis: inference_basis:
hasBusinessOverviewProfileSignal || inventoryPosition hasBusinessOverviewProfileSignal || inventoryPosition || accountingFinancialResult
? "business_overview_from_confirmed_1c_multi_family_rows" ? "business_overview_from_confirmed_1c_multi_family_rows"
: debtOpenSettlementQuality : debtOpenSettlementQuality || debtDueDateAging
? "business_overview_from_confirmed_1c_multi_family_rows" ? "business_overview_from_confirmed_1c_multi_family_rows"
: taxPosition && debtPosition : taxPosition && debtPosition
? "business_overview_from_confirmed_1c_money_activity_tax_and_debt_rows" ? "business_overview_from_confirmed_1c_money_activity_tax_and_debt_rows"
@ -3992,10 +4415,12 @@ function summarizeBusinessOverviewRows(input: {
outgoingResult: AssistantMcpDiscoveryCoverageAwareQueryResult | null; outgoingResult: AssistantMcpDiscoveryCoverageAwareQueryResult | null;
lifecycleResult: AddressMcpQueryExecutorResult | null; lifecycleResult: AddressMcpQueryExecutorResult | null;
taxResult: AddressMcpQueryExecutorResult | null; taxResult: AddressMcpQueryExecutorResult | null;
accountingFinancialResultResult: AddressMcpQueryExecutorResult | null;
tradingMarginResult: AddressMcpQueryExecutorResult | null; tradingMarginResult: AddressMcpQueryExecutorResult | null;
receivablesResult: AddressMcpQueryExecutorResult | null; receivablesResult: AddressMcpQueryExecutorResult | null;
payablesResult: AddressMcpQueryExecutorResult | null; payablesResult: AddressMcpQueryExecutorResult | null;
openContractsResult: AddressMcpQueryExecutorResult | null; openContractsResult: AddressMcpQueryExecutorResult | null;
dueDateAgingResult: AddressMcpQueryExecutorResult | null;
documentActivityProfileResult: AddressMcpQueryExecutorResult | null; documentActivityProfileResult: AddressMcpQueryExecutorResult | null;
counterpartyProfileResult: AddressMcpQueryExecutorResult | null; counterpartyProfileResult: AddressMcpQueryExecutorResult | null;
contractUsageProfileResult: AddressMcpQueryExecutorResult | null; contractUsageProfileResult: AddressMcpQueryExecutorResult | null;
@ -4015,6 +4440,9 @@ function summarizeBusinessOverviewRows(input: {
if (input.taxResult && !input.taxResult.error) { if (input.taxResult && !input.taxResult.error) {
parts.push(`${input.taxResult.fetched_rows} VAT/tax rows fetched, ${input.taxResult.matched_rows} matched`); parts.push(`${input.taxResult.fetched_rows} VAT/tax rows fetched, ${input.taxResult.matched_rows} matched`);
} }
if (input.accountingFinancialResultResult && !input.accountingFinancialResultResult.error) {
parts.push(`${input.accountingFinancialResultResult.fetched_rows} accounting financial-result aggregate rows fetched, ${input.accountingFinancialResultResult.matched_rows} matched`);
}
if (input.tradingMarginResult && !input.tradingMarginResult.error) { if (input.tradingMarginResult && !input.tradingMarginResult.error) {
parts.push(`${input.tradingMarginResult.fetched_rows} trading-margin document rows fetched, ${input.tradingMarginResult.matched_rows} matched`); parts.push(`${input.tradingMarginResult.fetched_rows} trading-margin document rows fetched, ${input.tradingMarginResult.matched_rows} matched`);
} }
@ -4027,6 +4455,9 @@ function summarizeBusinessOverviewRows(input: {
if (input.openContractsResult && !input.openContractsResult.error) { if (input.openContractsResult && !input.openContractsResult.error) {
parts.push(`${input.openContractsResult.fetched_rows} open-contract rows fetched, ${input.openContractsResult.matched_rows} matched`); parts.push(`${input.openContractsResult.fetched_rows} open-contract rows fetched, ${input.openContractsResult.matched_rows} matched`);
} }
if (input.dueDateAgingResult && !input.dueDateAgingResult.error) {
parts.push(`${input.dueDateAgingResult.fetched_rows} due-date aging rows fetched, ${input.dueDateAgingResult.matched_rows} matched`);
}
if (input.documentActivityProfileResult && !input.documentActivityProfileResult.error) { if (input.documentActivityProfileResult && !input.documentActivityProfileResult.error) {
parts.push(`${input.documentActivityProfileResult.fetched_rows} document/account-section profile rows fetched, ${input.documentActivityProfileResult.matched_rows} matched`); parts.push(`${input.documentActivityProfileResult.fetched_rows} document/account-section profile rows fetched, ${input.documentActivityProfileResult.matched_rows} matched`);
} }
@ -4064,15 +4495,39 @@ function buildBusinessOverviewConfirmedFacts(derived: AssistantMcpDiscoveryDeriv
} }
if (derived.top_customers.length > 0) { if (derived.top_customers.length > 0) {
const leader = derived.top_customers[0]; const leader = derived.top_customers[0];
facts.push( if (isLikelyFinancialInstitutionCounterparty(leader.axis_value)) {
`Самый крупный подтвержденный клиент в проверенном срезе: ${leader.axis_value}${leader.total_amount_human_ru}.` facts.push(
); `Крупнейший входящий денежный источник в проверенном срезе: ${leader.axis_value}${leader.total_amount_human_ru}. По названию это банк/финансовая организация, поэтому без назначения платежа это не доказанный клиент или бизнес-выручка.`
);
const nonFinancialLeader = derived.top_customers.slice(1).find((item) => !isLikelyFinancialInstitutionCounterparty(item.axis_value));
if (nonFinancialLeader) {
facts.push(
`Крупнейший небанковский входящий контрагент в проверенном срезе: ${nonFinancialLeader.axis_value}${nonFinancialLeader.total_amount_human_ru}.`
);
}
} else {
facts.push(
`Самый крупный подтвержденный клиент в проверенном срезе: ${leader.axis_value}${leader.total_amount_human_ru}.`
);
}
} }
if (derived.top_suppliers.length > 0) { if (derived.top_suppliers.length > 0) {
const leader = derived.top_suppliers[0]; const leader = derived.top_suppliers[0];
facts.push( if (isLikelyFinancialInstitutionCounterparty(leader.axis_value)) {
`Самый крупный подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${leader.axis_value}${leader.total_amount_human_ru}.` facts.push(
); `Крупнейший получатель исходящих денег в проверенном срезе: ${leader.axis_value}${leader.total_amount_human_ru}. По названию это банк/финансовая организация, поэтому без назначения платежа/договора это не доказанный обычный поставщик.`
);
const nonFinancialLeader = derived.top_suppliers.slice(1).find((item) => !isLikelyFinancialInstitutionCounterparty(item.axis_value));
if (nonFinancialLeader) {
facts.push(
`Крупнейший небанковский получатель исходящих денег в проверенном срезе: ${nonFinancialLeader.axis_value}${nonFinancialLeader.total_amount_human_ru}.`
);
}
} else {
facts.push(
`Самый крупный подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${leader.axis_value}${leader.total_amount_human_ru}.`
);
}
} }
if (derived.yearly_breakdown.length > 0) { if (derived.yearly_breakdown.length > 0) {
facts.push( facts.push(
@ -4179,6 +4634,30 @@ function buildBusinessOverviewConfirmedFacts(derived: AssistantMcpDiscoveryDeriv
`Staleness risk proxy открытых расчетов на ${proxy.as_of_date}: самый старый договорный сигнал ${proxy.oldest_contract_start_date}, возраст ${proxy.max_contract_age_days} дн.; старейший крупный договор ${proxy.top_contract}${counterpartyText} держит ${proxy.top_contract_amount_human_ru} (${proxy.top_contract_share_pct}% брутто открытых остатков); оценка ${debtStalenessRiskBandRu(proxy.risk_band)}. Это не подтвержденная просрочка, не кредитный риск и не due-date aging.` `Staleness risk proxy открытых расчетов на ${proxy.as_of_date}: самый старый договорный сигнал ${proxy.oldest_contract_start_date}, возраст ${proxy.max_contract_age_days} дн.; старейший крупный договор ${proxy.top_contract}${counterpartyText} держит ${proxy.top_contract_amount_human_ru} (${proxy.top_contract_share_pct}% брутто открытых остатков); оценка ${debtStalenessRiskBandRu(proxy.risk_band)}. Это не подтвержденная просрочка, не кредитный риск и не due-date aging.`
); );
} }
if (derived.debt_due_date_aging) {
const aging = derived.debt_due_date_aging;
if (aging.evidence_status === "confirmed_overdue") {
const top = aging.top_overdue_items[0];
const topText = top
? ` Самая старая просрочка: ${top.due_date}, ${top.overdue_days} дн., ${top.amount_human_ru}${top.contract ? ` по договору ${top.contract}` : ""}.`
: "";
facts.push(
`Due-date aging открытых расчетов на ${aging.as_of_date} проверен по срокам оплаты договоров и датам расчетных документов: подтверждено просроченных строк ${aging.overdue_rows}, сумма ${aging.overdue_amount_human_ru}.${topText}`
);
} else if (aging.evidence_status === "no_payment_terms_configured") {
facts.push(
`Due-date aging открытых расчетов на ${aging.as_of_date} проверен по ${aging.rows_with_amount} строкам с суммой: брутто открытых остатков ${aging.gross_open_amount_human_ru}, но в проверенных договорах срок оплаты не установлен. Подтвержденной просрочки по договорным срокам оплаты нет.`
);
} else if (aging.evidence_status === "insufficient_due_date_basis") {
facts.push(
`Due-date aging открытых расчетов на ${aging.as_of_date} запускался по ${aging.rows_with_payment_terms} строкам с установленным сроком оплаты, но в найденных строках не хватило даты расчетного документа для честного расчета due date. Просрочка не подтверждена.`
);
} else {
facts.push(
`Due-date aging открытых расчетов на ${aging.as_of_date} проверен: строк с установленным сроком оплаты ${aging.rows_with_payment_terms}, подтвержденной просрочки не найдено; не просрочено по расчету ${aging.not_yet_due_amount_human_ru}.`
);
}
}
if (derived.inventory_position) { if (derived.inventory_position) {
const leader = derived.inventory_position.top_items[0]; const leader = derived.inventory_position.top_items[0];
const leaderText = leader const leaderText = leader
@ -4237,6 +4716,9 @@ function buildBusinessOverviewInferredFacts(derived: AssistantMcpDiscoveryDerive
const supplierSharePct = supplierLeader const supplierSharePct = supplierLeader
? percentageOfTotal(supplierLeader.total_amount, derived.outgoing_supplier_payout.total_amount) ? percentageOfTotal(supplierLeader.total_amount, derived.outgoing_supplier_payout.total_amount)
: null; : null;
const supplierLeaderIsFinancial = supplierLeader
? isLikelyFinancialInstitutionCounterparty(supplierLeader.axis_value)
: false;
const strongestIncomingYear = [...derived.yearly_breakdown] const strongestIncomingYear = [...derived.yearly_breakdown]
.filter((bucket) => bucket.incoming_total_amount > 0) .filter((bucket) => bucket.incoming_total_amount > 0)
.sort((left, right) => right.incoming_total_amount - left.incoming_total_amount || left.year_bucket.localeCompare(right.year_bucket))[0]; .sort((left, right) => right.incoming_total_amount - left.incoming_total_amount || left.year_bucket.localeCompare(right.year_bucket))[0];
@ -4246,9 +4728,13 @@ function buildBusinessOverviewInferredFacts(derived: AssistantMcpDiscoveryDerive
return [ return [
`Расчетное нетто по найденным строкам: ${derived.net_amount_human_ru}; ${direction}.`, `Расчетное нетто по найденным строкам: ${derived.net_amount_human_ru}; ${direction}.`,
supplierLeader supplierLeader
? supplierSharePct !== null ? supplierLeaderIsFinancial
? `Крупнейший подтвержденный поставщик/получатель исходящих платежей ${supplierLeader.axis_value} держит около ${supplierSharePct}% проверенного исходящего потока (${supplierLeader.total_amount_human_ru}). Это procurement concentration proxy по найденным строкам, а не полный vendor-risk аудит.` ? supplierSharePct !== null
: `Крупнейший подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${supplierLeader.axis_value}${supplierLeader.total_amount_human_ru}.` ? `Крупнейший получатель исходящих денег ${supplierLeader.axis_value} держит около ${supplierSharePct}% проверенного исходящего потока (${supplierLeader.total_amount_human_ru}). По названию это банк/финансовая организация, поэтому это outgoing cash concentration proxy, а не доказанный vendor-risk по обычному поставщику.`
: `Крупнейший получатель исходящих денег в проверенном срезе: ${supplierLeader.axis_value}${supplierLeader.total_amount_human_ru}. По названию это банк/финансовая организация, поэтому это не доказанный обычный поставщик.`
: supplierSharePct !== null
? `Крупнейший подтвержденный поставщик/получатель исходящих платежей ${supplierLeader.axis_value} держит около ${supplierSharePct}% проверенного исходящего потока (${supplierLeader.total_amount_human_ru}). Это procurement concentration proxy по найденным строкам, а не полный vendor-risk аудит.`
: `Крупнейший подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${supplierLeader.axis_value}${supplierLeader.total_amount_human_ru}.`
: null, : null,
strongestIncomingYear strongestIncomingYear
? `Самый сильный год по подтвержденным входящим поступлениям: ${strongestIncomingYear.year_bucket} (${strongestIncomingYear.incoming_total_amount_human_ru}).` ? `Самый сильный год по подтвержденным входящим поступлениям: ${strongestIncomingYear.year_bucket} (${strongestIncomingYear.incoming_total_amount_human_ru}).`
@ -5032,10 +5518,12 @@ export async function executeAssistantMcpDiscoveryPilot(
let outgoingResult: AssistantMcpDiscoveryCoverageAwareQueryResult | null = null; let outgoingResult: AssistantMcpDiscoveryCoverageAwareQueryResult | null = null;
let lifecycleResult: AddressMcpQueryExecutorResult | null = null; let lifecycleResult: AddressMcpQueryExecutorResult | null = null;
let taxResult: AddressMcpQueryExecutorResult | null = null; let taxResult: AddressMcpQueryExecutorResult | null = null;
let accountingFinancialResultResult: AddressMcpQueryExecutorResult | null = null;
let tradingMarginResult: AddressMcpQueryExecutorResult | null = null; let tradingMarginResult: AddressMcpQueryExecutorResult | null = null;
let receivablesResult: AddressMcpQueryExecutorResult | null = null; let receivablesResult: AddressMcpQueryExecutorResult | null = null;
let payablesResult: AddressMcpQueryExecutorResult | null = null; let payablesResult: AddressMcpQueryExecutorResult | null = null;
let openContractsResult: AddressMcpQueryExecutorResult | null = null; let openContractsResult: AddressMcpQueryExecutorResult | null = null;
let dueDateAgingResult: AddressMcpQueryExecutorResult | null = null;
let documentActivityProfileResult: AddressMcpQueryExecutorResult | null = null; let documentActivityProfileResult: AddressMcpQueryExecutorResult | null = null;
let counterpartyProfileResult: AddressMcpQueryExecutorResult | null = null; let counterpartyProfileResult: AddressMcpQueryExecutorResult | null = null;
let contractUsageProfileResult: AddressMcpQueryExecutorResult | null = null; let contractUsageProfileResult: AddressMcpQueryExecutorResult | null = null;
@ -5045,8 +5533,10 @@ export async function executeAssistantMcpDiscoveryPilot(
const lifecycleFilters = buildLifecycleFilters(planner); const lifecycleFilters = buildLifecycleFilters(planner);
const profileFilters = buildBusinessOverviewProfileFilters(planner); const profileFilters = buildBusinessOverviewProfileFilters(planner);
const taxFilters = buildBusinessOverviewTaxFilters(planner); const taxFilters = buildBusinessOverviewTaxFilters(planner);
const accountingFinancialResultFilters = buildBusinessOverviewAccountingFinancialResultFilters(planner);
const tradingMarginFilters = buildBusinessOverviewTradingMarginFilters(planner); const tradingMarginFilters = buildBusinessOverviewTradingMarginFilters(planner);
const debtFilters = buildBusinessOverviewDebtFilters(planner); const debtFilters = buildBusinessOverviewDebtFilters(planner);
const debtDueDateAgingProbeEnabled = shouldRunDebtDueDateAgingProbe(planner);
const inventoryFilters = buildBusinessOverviewInventoryFilters(planner); const inventoryFilters = buildBusinessOverviewInventoryFilters(planner);
const debtAsOfDate = toNonEmptyString(debtFilters?.as_of_date); const debtAsOfDate = toNonEmptyString(debtFilters?.as_of_date);
const inventoryAsOfDate = toNonEmptyString(inventoryFilters?.as_of_date); const inventoryAsOfDate = toNonEmptyString(inventoryFilters?.as_of_date);
@ -5059,6 +5549,9 @@ export async function executeAssistantMcpDiscoveryPilot(
const taxSelection = taxFilters const taxSelection = taxFilters
? selectAddressRecipe("vat_liability_confirmed_for_tax_period", taxFilters) ? selectAddressRecipe("vat_liability_confirmed_for_tax_period", taxFilters)
: null; : null;
const accountingFinancialResultSelection = accountingFinancialResultFilters
? selectAddressRecipe("accounting_financial_result_for_organization", accountingFinancialResultFilters)
: null;
const tradingMarginSelection = tradingMarginFilters const tradingMarginSelection = tradingMarginFilters
? selectAddressRecipe("inventory_trading_margin_proxy_for_organization", tradingMarginFilters) ? selectAddressRecipe("inventory_trading_margin_proxy_for_organization", tradingMarginFilters)
: null; : null;
@ -5071,6 +5564,9 @@ export async function executeAssistantMcpDiscoveryPilot(
const openContractsSelection = debtFilters const openContractsSelection = debtFilters
? selectAddressRecipe("open_contracts_confirmed_as_of_date", debtFilters) ? selectAddressRecipe("open_contracts_confirmed_as_of_date", debtFilters)
: null; : null;
const dueDateAgingSelection = debtFilters && debtDueDateAgingProbeEnabled
? selectAddressRecipe("debt_due_date_aging_for_organization", debtFilters)
: null;
const inventoryOnHandSelection = inventoryFilters const inventoryOnHandSelection = inventoryFilters
? selectAddressRecipe("inventory_on_hand_as_of_date", inventoryFilters) ? selectAddressRecipe("inventory_on_hand_as_of_date", inventoryFilters)
: null; : null;
@ -5135,6 +5631,14 @@ export async function executeAssistantMcpDiscoveryPilot(
pushReason(reasonCodes, "pilot_business_overview_tax_recipe_not_available"); pushReason(reasonCodes, "pilot_business_overview_tax_recipe_not_available");
pushUnique(queryLimitations, "Business overview VAT/tax probe requires an executable tax-period recipe"); pushUnique(queryLimitations, "Business overview VAT/tax probe requires an executable tax-period recipe");
} }
if (accountingFinancialResultSelection?.selected_recipe) {
pushReason(reasonCodes, "pilot_business_overview_accounting_financial_result_recipe_selected");
} else if (!accountingFinancialResultFilters) {
pushReason(reasonCodes, "pilot_business_overview_accounting_financial_result_probe_skipped_without_explicit_period");
} else {
pushReason(reasonCodes, "pilot_business_overview_accounting_financial_result_recipe_not_available");
pushUnique(queryLimitations, "Business overview accounting financial-result probe requires an executable 90/91/99 period-close recipe");
}
if (tradingMarginSelection?.selected_recipe) { if (tradingMarginSelection?.selected_recipe) {
pushReason(reasonCodes, "pilot_business_overview_trading_margin_recipe_selected"); pushReason(reasonCodes, "pilot_business_overview_trading_margin_recipe_selected");
} else if (!tradingMarginFilters) { } else if (!tradingMarginFilters) {
@ -5159,6 +5663,16 @@ export async function executeAssistantMcpDiscoveryPilot(
pushReason(reasonCodes, "pilot_business_overview_open_contracts_recipe_not_available"); pushReason(reasonCodes, "pilot_business_overview_open_contracts_recipe_not_available");
pushUnique(queryLimitations, "Business overview open-settlement quality probe requires executable open-contracts as-of-date recipe"); pushUnique(queryLimitations, "Business overview open-settlement quality probe requires executable open-contracts as-of-date recipe");
} }
if (dueDateAgingSelection?.selected_recipe) {
pushReason(reasonCodes, "pilot_business_overview_debt_due_date_aging_recipe_selected");
} else if (!debtFilters) {
pushReason(reasonCodes, "pilot_business_overview_debt_due_date_aging_probe_skipped_without_explicit_as_of_date");
} else if (!debtDueDateAgingProbeEnabled) {
pushReason(reasonCodes, "pilot_business_overview_debt_due_date_aging_probe_skipped_without_boundary_need");
} else {
pushReason(reasonCodes, "pilot_business_overview_debt_due_date_aging_recipe_not_available");
pushUnique(queryLimitations, "Business overview due-date aging probe requires executable contract payment-term/open-balance recipe");
}
if (inventoryOnHandSelection?.selected_recipe) { if (inventoryOnHandSelection?.selected_recipe) {
pushReason(reasonCodes, "pilot_business_overview_inventory_on_hand_recipe_selected"); pushReason(reasonCodes, "pilot_business_overview_inventory_on_hand_recipe_selected");
if (inventoryAgingSelection?.selected_recipe) { if (inventoryAgingSelection?.selected_recipe) {
@ -5200,6 +5714,17 @@ export async function executeAssistantMcpDiscoveryPilot(
account_scope: taxPlan.account_scope account_scope: taxPlan.account_scope
}); });
} }
if (accountingFinancialResultSelection?.selected_recipe) {
const accountingFinancialResultPlan = buildAddressRecipePlan(
accountingFinancialResultSelection.selected_recipe,
accountingFinancialResultFilters!
);
accountingFinancialResultResult = await runtimeDeps.executeAddressMcpQuery({
query: accountingFinancialResultPlan.query,
limit: accountingFinancialResultPlan.limit,
account_scope: accountingFinancialResultPlan.account_scope
});
}
if (receivablesSelection?.selected_recipe && payablesSelection?.selected_recipe) { if (receivablesSelection?.selected_recipe && payablesSelection?.selected_recipe) {
const receivablesPlan = buildAddressRecipePlan(receivablesSelection.selected_recipe, debtFilters!); const receivablesPlan = buildAddressRecipePlan(receivablesSelection.selected_recipe, debtFilters!);
receivablesResult = await runtimeDeps.executeAddressMcpQuery({ receivablesResult = await runtimeDeps.executeAddressMcpQuery({
@ -5222,6 +5747,14 @@ export async function executeAssistantMcpDiscoveryPilot(
account_scope: openContractsPlan.account_scope account_scope: openContractsPlan.account_scope
}); });
} }
if (dueDateAgingSelection?.selected_recipe) {
const dueDateAgingPlan = buildAddressRecipePlan(dueDateAgingSelection.selected_recipe, debtFilters!);
dueDateAgingResult = await runtimeDeps.executeAddressMcpQuery({
query: dueDateAgingPlan.query,
limit: dueDateAgingPlan.limit,
account_scope: dueDateAgingPlan.account_scope
});
}
if (inventoryOnHandSelection?.selected_recipe) { if (inventoryOnHandSelection?.selected_recipe) {
const inventoryOnHandPlan = buildAddressRecipePlan(inventoryOnHandSelection.selected_recipe, inventoryFilters!); const inventoryOnHandPlan = buildAddressRecipePlan(inventoryOnHandSelection.selected_recipe, inventoryFilters!);
inventoryOnHandResult = await runtimeDeps.executeAddressMcpQuery({ inventoryOnHandResult = await runtimeDeps.executeAddressMcpQuery({
@ -5243,6 +5776,9 @@ export async function executeAssistantMcpDiscoveryPilot(
if (taxResult) { if (taxResult) {
probeResults.push(queryResultToProbeResult(step.primitive_id, taxResult)); probeResults.push(queryResultToProbeResult(step.primitive_id, taxResult));
} }
if (accountingFinancialResultResult) {
probeResults.push(queryResultToProbeResult(step.primitive_id, accountingFinancialResultResult));
}
if (receivablesResult) { if (receivablesResult) {
probeResults.push(queryResultToProbeResult(step.primitive_id, receivablesResult)); probeResults.push(queryResultToProbeResult(step.primitive_id, receivablesResult));
} }
@ -5252,6 +5788,9 @@ export async function executeAssistantMcpDiscoveryPilot(
if (openContractsResult) { if (openContractsResult) {
probeResults.push(queryResultToProbeResult(step.primitive_id, openContractsResult)); probeResults.push(queryResultToProbeResult(step.primitive_id, openContractsResult));
} }
if (dueDateAgingResult) {
probeResults.push(queryResultToProbeResult(step.primitive_id, dueDateAgingResult));
}
if (inventoryOnHandResult) { if (inventoryOnHandResult) {
probeResults.push(queryResultToProbeResult(step.primitive_id, inventoryOnHandResult)); probeResults.push(queryResultToProbeResult(step.primitive_id, inventoryOnHandResult));
} }
@ -5276,6 +5815,12 @@ export async function executeAssistantMcpDiscoveryPilot(
} else if (taxResult) { } else if (taxResult) {
pushReason(reasonCodes, "pilot_business_overview_tax_query_mcp_executed"); pushReason(reasonCodes, "pilot_business_overview_tax_query_mcp_executed");
} }
if (accountingFinancialResultResult?.error) {
pushUnique(queryLimitations, accountingFinancialResultResult.error);
pushReason(reasonCodes, "pilot_business_overview_accounting_financial_result_query_mcp_error");
} else if (accountingFinancialResultResult) {
pushReason(reasonCodes, "pilot_business_overview_accounting_financial_result_query_mcp_executed");
}
if (receivablesResult?.error) { if (receivablesResult?.error) {
pushUnique(queryLimitations, receivablesResult.error); pushUnique(queryLimitations, receivablesResult.error);
pushReason(reasonCodes, "pilot_business_overview_receivables_query_mcp_error"); pushReason(reasonCodes, "pilot_business_overview_receivables_query_mcp_error");
@ -5300,6 +5845,12 @@ export async function executeAssistantMcpDiscoveryPilot(
} else if (openContractsResult) { } else if (openContractsResult) {
pushReason(reasonCodes, "pilot_business_overview_open_contracts_query_mcp_executed"); pushReason(reasonCodes, "pilot_business_overview_open_contracts_query_mcp_executed");
} }
if (dueDateAgingResult?.error) {
pushUnique(queryLimitations, dueDateAgingResult.error);
pushReason(reasonCodes, "pilot_business_overview_debt_due_date_aging_query_mcp_error");
} else if (dueDateAgingResult) {
pushReason(reasonCodes, "pilot_business_overview_debt_due_date_aging_query_mcp_executed");
}
if (inventoryOnHandResult?.error) { if (inventoryOnHandResult?.error) {
pushUnique(queryLimitations, inventoryOnHandResult.error); pushUnique(queryLimitations, inventoryOnHandResult.error);
pushReason(reasonCodes, "pilot_business_overview_inventory_on_hand_query_mcp_error"); pushReason(reasonCodes, "pilot_business_overview_inventory_on_hand_query_mcp_error");
@ -5411,10 +5962,12 @@ export async function executeAssistantMcpDiscoveryPilot(
outgoingResult, outgoingResult,
lifecycleResult, lifecycleResult,
taxResult, taxResult,
accountingFinancialResultResult,
tradingMarginResult, tradingMarginResult,
receivablesResult, receivablesResult,
payablesResult, payablesResult,
openContractsResult, openContractsResult,
dueDateAgingResult,
documentActivityProfileResult, documentActivityProfileResult,
counterpartyProfileResult, counterpartyProfileResult,
contractUsageProfileResult, contractUsageProfileResult,
@ -5451,6 +6004,9 @@ export async function executeAssistantMcpDiscoveryPilot(
if (derivedBusinessOverview.tax_position) { if (derivedBusinessOverview.tax_position) {
pushReason(reasonCodes, "pilot_derived_business_overview_tax_position_from_confirmed_rows"); pushReason(reasonCodes, "pilot_derived_business_overview_tax_position_from_confirmed_rows");
} }
if (derivedBusinessOverview.accounting_financial_result) {
pushReason(reasonCodes, "pilot_derived_business_overview_accounting_financial_result_from_confirmed_rows");
}
if (derivedBusinessOverview.trading_margin_proxy) { if (derivedBusinessOverview.trading_margin_proxy) {
pushReason(reasonCodes, "pilot_derived_business_overview_trading_margin_proxy_from_confirmed_rows"); pushReason(reasonCodes, "pilot_derived_business_overview_trading_margin_proxy_from_confirmed_rows");
} }
@ -5466,6 +6022,10 @@ export async function executeAssistantMcpDiscoveryPilot(
if (derivedBusinessOverview.debt_staleness_risk_proxy) { if (derivedBusinessOverview.debt_staleness_risk_proxy) {
pushReason(reasonCodes, "pilot_derived_business_overview_debt_staleness_risk_proxy_from_confirmed_rows"); pushReason(reasonCodes, "pilot_derived_business_overview_debt_staleness_risk_proxy_from_confirmed_rows");
} }
if (derivedBusinessOverview.debt_due_date_aging) {
pushReason(reasonCodes, "pilot_derived_business_overview_debt_due_date_aging_from_confirmed_rows");
pushReason(reasonCodes, `pilot_derived_business_overview_debt_due_date_aging_${derivedBusinessOverview.debt_due_date_aging.evidence_status}`);
}
if (derivedBusinessOverview.inventory_position) { if (derivedBusinessOverview.inventory_position) {
pushReason(reasonCodes, "pilot_derived_business_overview_inventory_position_from_confirmed_rows"); pushReason(reasonCodes, "pilot_derived_business_overview_inventory_position_from_confirmed_rows");
} }
@ -5484,10 +6044,12 @@ export async function executeAssistantMcpDiscoveryPilot(
outgoingResult, outgoingResult,
lifecycleResult, lifecycleResult,
taxResult, taxResult,
accountingFinancialResultResult,
tradingMarginResult, tradingMarginResult,
receivablesResult, receivablesResult,
payablesResult, payablesResult,
openContractsResult, openContractsResult,
dueDateAgingResult,
documentActivityProfileResult, documentActivityProfileResult,
counterpartyProfileResult, counterpartyProfileResult,
contractUsageProfileResult, contractUsageProfileResult,

View File

@ -108,7 +108,7 @@ export interface AssistantMcpDiscoveryEvidenceContract {
const DEFAULT_DISCOVERY_BUDGET: AssistantMcpDiscoveryExecutionBudget = { const DEFAULT_DISCOVERY_BUDGET: AssistantMcpDiscoveryExecutionBudget = {
max_probe_count: 3, max_probe_count: 3,
max_rows_per_probe: 100 max_rows_per_probe: 200
}; };
const MAX_PROBE_COUNT = 36; const MAX_PROBE_COUNT = 36;

View File

@ -1,4 +1,5 @@
import type { AssistantMcpDiscoveryRuntimeEntryPointContract } from "./assistantMcpDiscoveryRuntimeEntryPoint"; import type { AssistantMcpDiscoveryRuntimeEntryPointContract } from "./assistantMcpDiscoveryRuntimeEntryPoint";
import { isLikelyFinancialInstitutionCounterparty } from "./counterpartyRoleHeuristics";
export const ASSISTANT_MCP_DISCOVERY_RESPONSE_CANDIDATE_SCHEMA_VERSION = export const ASSISTANT_MCP_DISCOVERY_RESPONSE_CANDIDATE_SCHEMA_VERSION =
"assistant_mcp_discovery_response_candidate_v1" as const; "assistant_mcp_discovery_response_candidate_v1" as const;
@ -97,7 +98,27 @@ function userFacingLines(values: string[]): string[] {
return uniqueStrings(values).filter((line) => !hasInternalMechanics(line)); return uniqueStrings(values).filter((line) => !hasInternalMechanics(line));
} }
function sanitizeUserFacingMechanics(value: string): string {
return 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С";
});
}
function localizeLine(value: string): string { function localizeLine(value: string): string {
const sanitizedValue = sanitizeUserFacingMechanics(value);
if (/^1C activity rows were found for the requested counterparty scope$/i.test(value)) { if (/^1C activity rows were found for the requested counterparty scope$/i.test(value)) {
return "В 1С найдены строки активности в запрошенном срезе."; return "В 1С найдены строки активности в запрошенном срезе.";
} }
@ -126,7 +147,7 @@ function localizeLine(value: string): string {
value value
) )
) { ) {
return "Запрошенный период уперся в лимит строк MCP; доступного бюджета помесячных дозапросов не хватило, чтобы покрыть все подпериоды."; return "Запрошенный период достиг лимита строк; доступного бюджета помесячных дозапросов не хватило, чтобы покрыть все подпериоды.";
} }
const counterpartyMatch = value.match(/^1C activity rows were found for counterparty\s+(.+)$/i); const counterpartyMatch = value.match(/^1C activity rows were found for counterparty\s+(.+)$/i);
if (counterpartyMatch) { if (counterpartyMatch) {
@ -151,10 +172,10 @@ function localizeLine(value: string): string {
} }
const movementRowsMatch = value.match(/^1C movement rows were found for counterparty\s+(.+)$/i); const movementRowsMatch = value.match(/^1C movement rows were found for counterparty\s+(.+)$/i);
if (movementRowsMatch) { if (movementRowsMatch) {
return `Р 1РЎ найдены строки движений РїРѕ контрагенту ${movementRowsMatch[1]}.`; return `В 1С найдены строки движений по контрагенту ${movementRowsMatch[1]}.`;
} }
if (/^1C movement rows were found for the requested scope$/i.test(value)) { if (/^1C movement rows were found for the requested scope$/i.test(value)) {
return "Р 1РЎ найдены строки движений РїРѕ запрошенному контуру."; return "В 1С найдены строки движений по запрошенному контуру.";
} }
const supplierPayoutMatch = value.match(/^1C supplier-payout rows were found for counterparty\s+(.+)$/i); const supplierPayoutMatch = value.match(/^1C supplier-payout rows were found for counterparty\s+(.+)$/i);
if (supplierPayoutMatch) { if (supplierPayoutMatch) {
@ -186,7 +207,7 @@ function localizeLine(value: string): string {
return "Срез документов ограничен только подтвержденными строками документов в проверенном окне."; return "Срез документов ограничен только подтвержденными строками документов в проверенном окне.";
} }
if (/^Counterparty movement evidence is limited to confirmed 1C movement rows in the checked scope$/i.test(value)) { if (/^Counterparty movement evidence is limited to confirmed 1C movement rows in the checked scope$/i.test(value)) {
return "Срез движений ограничен только подтвержденными строками движений РІ проверенном РѕРєРЅРµ."; return "Срез движений ограничен только подтвержденными строками движений в проверенном окне.";
} }
if (/^Counterparty value-flow total was calculated from confirmed 1C movement rows$/i.test(value)) { if (/^Counterparty value-flow total was calculated from confirmed 1C movement rows$/i.test(value)) {
return "Сумма входящих поступлений рассчитана только по подтвержденным строкам поступлений в 1С."; return "Сумма входящих поступлений рассчитана только по подтвержденным строкам поступлений в 1С.";
@ -274,10 +295,10 @@ function localizeLine(value: string): string {
return "Полный срез документов без явно проверенного периода не подтвержден."; return "Полный срез документов без явно проверенного периода не подтвержден.";
} }
if (/^Full movement history outside the checked period is not proven by this MCP discovery pilot$/i.test(value)) { if (/^Full movement history outside the checked period is not proven by this MCP discovery pilot$/i.test(value)) {
return "Полный исторический срез движений РІРЅРµ проверенного периода этим РїРѕРёСЃРєРѕРј РЅРµ подтвержден."; return "Полный исторический срез движений вне проверенного периода этим поиском не подтвержден.";
} }
if (/^Full movement history is not proven without an explicit checked period$/i.test(value)) { if (/^Full movement history is not proven without an explicit checked period$/i.test(value)) {
return "Полный срез движений без СЏРІРЅРѕ проверенного периода РЅРµ подтвержден."; return "Полный срез движений без явно проверенного периода не подтвержден.";
} }
if (/^Full supplier-payout amount outside the checked period is not proven by this MCP discovery pilot$/i.test(value)) { if (/^Full supplier-payout amount outside the checked period is not proven by this MCP discovery pilot$/i.test(value)) {
return "Полный объем исходящих платежей вне проверенного периода этим поиском не подтвержден."; return "Полный объем исходящих платежей вне проверенного периода этим поиском не подтвержден.";
@ -296,14 +317,14 @@ function localizeLine(value: string): string {
value value
) )
) { ) {
return "Покрытие запрошенного периода восстановлено помесячными проверками 1С после того, как общая выборка уперлась в лимит строк."; return "Покрытие запрошенного периода восстановлено помесячными проверками 1С после того, как общая выборка достигла лимита строк.";
} }
if ( 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( /^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 value
) )
) { ) {
return "Покрытие запрошенного периода по двустороннему денежному потоку восстановлено помесячными проверками 1С после того, как общая выборка уперлась в лимит строк хотя бы по одной стороне."; return "Покрытие запрошенного периода по двустороннему денежному потоку восстановлено помесячными проверками 1С после того, как общая выборка достигла лимита строк хотя бы по одной стороне.";
} }
if (/^Requested period coverage was recovered through monthly 1C value-flow probes$/i.test(value)) { if (/^Requested period coverage was recovered through monthly 1C value-flow probes$/i.test(value)) {
return "Покрытие запрошенного периода восстановлено помесячными проверками 1С."; return "Покрытие запрошенного периода восстановлено помесячными проверками 1С.";
@ -323,7 +344,7 @@ function localizeLine(value: string): string {
if (/^Complete requested-period coverage for bidirectional value-flow is not proven by the available checked rows$/i.test(value)) { if (/^Complete requested-period coverage for bidirectional value-flow is not proven by the available checked rows$/i.test(value)) {
return "Полное покрытие запрошенного периода по двустороннему денежному потоку не подтверждено доступными проверенными строками."; return "Полное покрытие запрошенного периода по двустороннему денежному потоку не подтверждено доступными проверенными строками.";
} }
return value; return sanitizedValue;
} }
function section(title: string, lines: string[]): string | null { function section(title: string, lines: string[]): string | null {
@ -408,7 +429,7 @@ function businessOverviewCoverageLimitLine(overview: Record<string, unknown>): s
limited.push("исходящие"); limited.push("исходящие");
} }
return limited.length > 0 return limited.length > 0
? `Важно: ${limited.join(" и ")} уперлись в лимит выборки MCP, поэтому это проверенный срез найденных строк, а не гарантированно полный бухгалтерский оборот.` ? `Важно: по направлению ${limited.join(" и ")} проверка достигла лимита строк; это расширенный проверенный срез найденных строк, но не гарантия полного бухгалтерского оборота без отдельной полной выгрузки.`
: null; : null;
} }
@ -437,6 +458,34 @@ function firstOverviewAxisLabel(rows: unknown, amountKey = "total_amount_human_r
return label && amount ? `${label}${sentenceAmount(amount) ?? amount}` : null; 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 businessOverviewTaxLine(overview: Record<string, unknown>): string | null { function businessOverviewTaxLine(overview: Record<string, unknown>): string | null {
const tax = toRecordObject(overview.tax_position); const tax = toRecordObject(overview.tax_position);
if (!tax) { if (!tax) {
@ -568,7 +617,7 @@ function buildCompactBidirectionalValueFlowReply(
lines.push(`Основа: ${basis.join("; ")}.`); lines.push(`Основа: ${basis.join("; ")}.`);
} }
if (flow.coverage_limited_by_probe_limit === true) { if (flow.coverage_limited_by_probe_limit === true) {
lines.push("Важно: часть проверки уперлась в лимит строк, поэтому это проверенный срез найденных движений, а не гарантия полного периода."); lines.push("Важно: часть проверки достигла лимита строк, поэтому это проверенный срез найденных движений, а не гарантия полного периода.");
} }
lines.push("Метод: нетто рассчитано как подтвержденные входящие строки 1С минус подтвержденные исходящие строки; это не полное бухгалтерское сальдо вне проверенного окна."); lines.push("Метод: нетто рассчитано как подтвержденные входящие строки 1С минус подтвержденные исходящие строки; это не полное бухгалтерское сальдо вне проверенного окна.");
@ -721,17 +770,202 @@ function buildCompactBusinessOverviewReply(
const topCustomer = toRecordObject(Array.isArray(overview.top_customers) ? overview.top_customers[0] : null); const topCustomer = toRecordObject(Array.isArray(overview.top_customers) ? overview.top_customers[0] : null);
const customerName = toNonEmptyString(topCustomer?.axis_value); const customerName = toNonEmptyString(topCustomer?.axis_value);
const customerAmount = moneyText(topCustomer?.total_amount_human_ru); const customerAmount = moneyText(topCustomer?.total_amount_human_ru);
const topCustomerLooksFinancial = overviewAxisLooksFinancial(topCustomer);
const nonFinancialCustomer = firstNonFinancialOverviewAxisLabel(
topCustomerLooksFinancial ? overview.top_customers : []
);
const topCustomerLead = const topCustomerLead =
customerName && customerAmount customerName && customerAmount
? `; крупнейший источник входящих денег: ${customerName}${sentenceAmount(customerAmount) ?? 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 topSupplier = firstOverviewAxisLabel(overview.top_suppliers);
const topSupplierLead = topSupplier ? `; крупнейший получатель исходящих денег: ${topSupplier}` : ""; const topSupplierLooksFinancial = overviewAxisLooksFinancial(topSupplierRecord);
const nonFinancialSupplier = firstNonFinancialOverviewAxisLabel(
topSupplierLooksFinancial ? overview.top_suppliers : []
);
const topSupplierLead = topSupplier
? topSupplierLooksFinancial
? `; крупнейший получатель исходящих денег: ${topSupplier} (похоже на банк/финорганизацию, не называю это обычным поставщиком без назначения платежа/договора)${nonFinancialSupplier ? `; крупнейший небанковский получатель исходящих денег: ${nonFinancialSupplier}` : ""}`
: `; крупнейший получатель исходящих денег: ${topSupplier}`
: "";
const roleBoundaryLead = topCustomer || topSupplier ? "; клиент/поставщик как бизнес-роли этим денежным срезом не подтверждены" : ""; const roleBoundaryLead = topCustomer || topSupplier ? "; клиент/поставщик как бизнес-роли этим денежным срезом не подтверждены" : "";
const graphReasonCodes = toStringList(graph?.reason_codes); const graphReasonCodes = toStringList(graph?.reason_codes);
const directMoneyAnswer = graphReasonCodes.includes("data_need_graph_business_overview_direct_money_answer"); const directMoneyAnswer = graphReasonCodes.includes("data_need_graph_business_overview_direct_money_answer");
const crossScopeExecutiveSummary = Boolean(separateSubject && previousCounterpartySummary); const crossScopeExecutiveSummary = Boolean(separateSubject && previousCounterpartySummary);
const lines: string[] = []; 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 за ${periodScope} подтвержден ${directionText}: ${amountText}${marginPct ? `; маржа к выручке 90.01 ${marginPct}` : "; маржа к выручке 90.01 не рассчитана"}.`
);
lines.push(
"Это учетный финрезультат по найденным строкам закрытия периода в 1С, а не внешний аудит и не юридически подтвержденная отчетность."
);
const reply = lines.join("\n").trim();
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
}
const headline = toNonEmptyString(draft.headline);
const cleanHeadline = headline?.replace(/^Коротко:\s*/iu, "").trim();
lines.push(
cleanHeadline
? `Коротко: ${localizeLine(cleanHeadline)}`
: "Коротко: нельзя точно подтвердить чистую прибыль и маржу по текущему срезу 1С; есть только bounded operating-flow/trading-margin proxy, не P&L и не бухгалтерский финансовый результат."
);
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(
"Для точного P&L нужны отдельный маршрут по себестоимости, расходам, закрытию периода и финрезультату; текущий proxy нельзя выдавать за подтвержденную чистую прибыль или маржу."
);
if (limitLine) {
lines.push(limitLine);
}
const reply = lines.join("\n").trim();
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
}
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;
if (status === "confirmed_overdue") {
lines.push(
`Коротко: на ${asOfDate} подтвержденная просрочка есть: ${overdueAmount ?? "сумма не распознана"} по ${dueDateAging.overdue_rows ?? "найденным"} строкам.`
);
lines.push("Основа ответа: открытые расчеты 60/62/76, договорный срок оплаты и дата расчетного документа; это уже due-date route, не старение договора как proxy.");
} else if (status === "no_payment_terms_configured") {
lines.push(
`Коротко: на ${asOfDate} подтвержденной просрочки нет: открытые расчеты проверены${grossAmount ? ` на ${grossAmount}` : ""}, но в найденных договорах срок оплаты не установлен.`
);
lines.push(
rowsWithAmount !== null
? `Проверено строк с суммой: ${rowsWithAmount}. Без установленного срока оплаты нельзя честно назвать эти остатки просрочкой.`
: "Без установленного срока оплаты нельзя честно назвать эти остатки просрочкой."
);
} else if (status === "insufficient_due_date_basis") {
lines.push(
`Коротко: due-date route запущен на ${asOfDate}, но просрочка не подтверждена: по строкам с установленным сроком оплаты не хватило даты расчетного документа.`
);
if (rowsWithPaymentTerms !== null) {
lines.push(`Строк с установленным сроком оплаты: ${rowsWithPaymentTerms}; нужен документ-основание с датой для расчета due date.`);
}
} else {
lines.push(
`Коротко: due-date route на ${asOfDate} проверен, подтвержденной просрочки не найдено${rowsWithPaymentTerms !== null ? `; строк с установленным сроком оплаты ${rowsWithPaymentTerms}` : ""}.`
);
}
const reply = lines.join("\n").trim();
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
}
const headline = toNonEmptyString(draft.headline);
const cleanHeadline = headline?.replace(/^Коротко:\s*/iu, "").trim();
lines.push(
cleanHeadline
? `Коротко: ${localizeLine(cleanHeadline)}`
: "Коротко: нельзя точно определить, какая дебиторка просрочена, по текущему срезу 1С; есть только debt-quality proxy, но нет проверенного due-date маршрута."
);
lines.push(
"Проверить нужно отдельно: договоры, сроки оплаты, погашение и закрытие задолженности; без этого нельзя доказать overdue/due-date aging."
);
const reply = lines.join("\n").trim();
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
}
if (vendorRiskBoundary) {
const supplierBasis = topSupplier
? topSupplierLooksFinancial
? `крупнейший получатель исходящих денег: ${topSupplier}; по названию это банк/финансовая организация, поэтому это не доказанная зависимость от обычного поставщика${nonFinancialSupplier ? `; крупнейший небанковский получатель исходящих денег: ${nonFinancialSupplier}` : ""}`
: `крупнейший подтвержденный поставщик/получатель исходящих платежей: ${topSupplier}`
: outgoingAmount
? `исходящие платежи/закупочный поток в проверенном срезе: ${outgoingAmount}`
: "есть только ограниченный срез исходящих платежей без полного vendor-risk профиля";
const proxyLabel = topSupplierLooksFinancial ? "outgoing cash concentration proxy" : "procurement concentration proxy";
lines.push(
`Коротко: точный риск зависимости от одного поставщика по текущим данным не подтвержден; есть только ${proxyLabel}: ${supplierBasis}.`
);
lines.push(
"Это сигнал концентрации закупок/исходящих платежей, а не полный аудит надежности поставщиков, условий, качества и структуры всех расходов."
);
lines.push(
"Для точного вывода нужен отдельный reviewed vendor-risk route: поставщики, договорные условия, качество поставок, сроки, доля в закупках и полная структура расходов."
);
const reply = lines.join("\n").trim();
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
}
if (inventoryReserveBoundary) {
const headline = toNonEmptyString(draft.headline);
const cleanHeadline = headline?.replace(/^Коротко:\s*/iu, "").trim();
lines.push(
cleanHeadline
? `Коротко: ${localizeLine(cleanHeadline)}`
: "Коротко: точно подтвердить резерв под неликвиды по текущим данным нельзя."
);
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(
"Проверить нужно отдельно: складской срез на дату, учетную политику резервов, списания и ликвидационную стоимость; proxy-сигналы нельзя выдавать за доказанный факт резерва."
);
const reply = lines.join("\n").trim();
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
}
if (crossScopeExecutiveSummary && separateSubject && previousCounterpartySummary && (incomingAmount || outgoingAmount || netAmount)) { if (crossScopeExecutiveSummary && separateSubject && previousCounterpartySummary && (incomingAmount || outgoingAmount || netAmount)) {
lines.push( lines.push(
@ -761,7 +995,7 @@ function buildCompactBusinessOverviewReply(
return null; return null;
} }
lines.push( lines.push(
`Коротко: в доступном проверенном MCP-срезе по входящим денежным строкам лидирует ${leaderYear}: ${leaderAmount}${Number.isFinite(leaderRows) && leaderRows > 0 ? ` по ${leaderRows} строкам с суммой` : ""}; это не полный бухгалтерский рейтинг доходности.` `Коротко: в доступном проверенном срезе 1С по входящим денежным строкам лидирует ${leaderYear}: ${leaderAmount}${Number.isFinite(leaderRows) && leaderRows > 0 ? ` по ${leaderRows} строкам с суммой` : ""}; это не полный бухгалтерский рейтинг доходности.`
); );
const netYear = toNonEmptyString(netLeader?.year_bucket); const netYear = toNonEmptyString(netLeader?.year_bucket);
const netYearAmount = moneyText(netLeader?.net_amount_human_ru); const netYearAmount = moneyText(netLeader?.net_amount_human_ru);
@ -783,7 +1017,11 @@ function buildCompactBusinessOverviewReply(
); );
lines.push('Метод: "заработали" здесь считаю как денежный operating-flow proxy по 1С; это не чистая прибыль и не финрезультат.'); lines.push('Метод: "заработали" здесь считаю как денежный operating-flow proxy по 1С; это не чистая прибыль и не финрезультат.');
if (!directMoneyAnswer && customerName && customerAmount) { if (!directMoneyAnswer && customerName && customerAmount) {
lines.push(`Крупнейший подтвержденный источник входящих денег в этом срезе: ${customerName}${sentenceAmount(customerAmount) ?? customerAmount}.`); lines.push(
topCustomerLooksFinancial
? `Крупнейший входящий денежный источник в этом срезе: ${customerName}${sentenceAmount(customerAmount) ?? customerAmount}. По названию это банк/финансовая организация, поэтому без назначения платежа не называю это клиентской выручкой.${nonFinancialCustomer ? ` Крупнейший небанковский входящий контрагент: ${nonFinancialCustomer}.` : ""}`
: `Крупнейший подтвержденный источник входящих денег в этом срезе: ${customerName}${sentenceAmount(customerAmount) ?? customerAmount}.`
);
} }
} else { } else {
return null; return null;
@ -797,10 +1035,18 @@ function buildCompactBusinessOverviewReply(
} }
if (!directMoneyAnswer && topSupplier) { if (!directMoneyAnswer && topSupplier) {
lines.push(`Крупнейший подтвержденный получатель исходящих денег: ${topSupplier}.`); lines.push(
topSupplierLooksFinancial
? `Крупнейший получатель исходящих денег: ${topSupplier}. По названию это банк/финансовая организация, поэтому без назначения платежа/договора не считаю это обычным поставщиком.${nonFinancialSupplier ? ` Крупнейший небанковский получатель исходящих денег: ${nonFinancialSupplier}.` : ""}`
: `Крупнейший подтвержденный получатель исходящих денег: ${topSupplier}.`
);
} }
if (!directMoneyAnswer && (topCustomer || topSupplier)) { if (!directMoneyAnswer && (topCustomer || topSupplier)) {
lines.push("Важно по ролям: текущий денежный срез подтверждает денежные источники и получателей, но не доказывает, что это главный клиент или главный поставщик как бизнес-роль."); lines.push(
topCustomerLooksFinancial || topSupplierLooksFinancial
? "Важно по ролям: текущий денежный срез подтверждает источники и получателей денег, но банковские контрагенты требуют проверки назначения платежа/счетов и не доказывают роль клиента или поставщика."
: "Важно по ролям: текущий денежный срез подтверждает денежные источники и получателей, но не доказывает, что это главный клиент или главный поставщик как бизнес-роль."
);
} }
if (!directMoneyAnswer) { if (!directMoneyAnswer) {
lines.push( lines.push(

View File

@ -20,6 +20,8 @@ export const ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION =
"assistant_mcp_discovery_runtime_bridge_v1" as const; "assistant_mcp_discovery_runtime_bridge_v1" as const;
export const ASSISTANT_MCP_DISCOVERY_LOOP_STATE_SCHEMA_VERSION = export const ASSISTANT_MCP_DISCOVERY_LOOP_STATE_SCHEMA_VERSION =
"assistant_mcp_discovery_loop_state_v1" as const; "assistant_mcp_discovery_loop_state_v1" as const;
export const ASSISTANT_MCP_ROUTE_CANDIDATE_SCHEMA_VERSION =
"assistant_mcp_route_candidate_v1" as const;
export type AssistantMcpDiscoveryRuntimeBridgeStatus = export type AssistantMcpDiscoveryRuntimeBridgeStatus =
| "answer_draft_ready" | "answer_draft_ready"
@ -31,6 +33,11 @@ export type AssistantMcpDiscoveryLoopStatus =
| "awaiting_clarification" | "awaiting_clarification"
| "ready_for_next_hop" | "ready_for_next_hop"
| "blocked"; | "blocked";
export type AssistantMcpRouteCandidateStatus =
| "ready_for_reviewed_execution"
| "needs_user_scope"
| "needs_route_enablement"
| "blocked";
export interface AssistantMcpDiscoveryRuntimeBridgeInput { export interface AssistantMcpDiscoveryRuntimeBridgeInput {
semanticDataNeed?: string | null; semanticDataNeed?: string | null;
@ -61,6 +68,26 @@ export interface AssistantMcpDiscoveryLoopStateContract {
explicit_date_scope: string | null; explicit_date_scope: string | null;
} }
export interface AssistantMcpRouteCandidateContract {
schema_version: typeof ASSISTANT_MCP_ROUTE_CANDIDATE_SCHEMA_VERSION;
policy_owner: "assistantMcpDiscoveryRuntimeBridge";
candidate_status: AssistantMcpRouteCandidateStatus;
selected_chain_id: AssistantMcpDiscoveryChainId;
selected_chain_summary: string;
nearest_catalog_chain_template: AssistantMcpDiscoveryPlannerContract["catalog_chain_template_alignment"]["top_chain_template_match"];
catalog_alignment_status: AssistantMcpDiscoveryPlannerContract["catalog_chain_template_alignment"]["alignment_status"];
business_fact_family: string | null;
action_family: string | null;
proof_expectation: string | null;
required_axes: string[];
provided_axes: string[];
missing_axes: string[];
executable_now: boolean;
enablement_reason: string | null;
recommended_next_action: string;
forbidden_overclaim_flags: string[];
}
export interface AssistantMcpDiscoveryRuntimeBridgeContract { export interface AssistantMcpDiscoveryRuntimeBridgeContract {
schema_version: typeof ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION; schema_version: typeof ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION;
policy_owner: "assistantMcpDiscoveryRuntimeBridge"; policy_owner: "assistantMcpDiscoveryRuntimeBridge";
@ -70,6 +97,7 @@ export interface AssistantMcpDiscoveryRuntimeBridgeContract {
pilot: AssistantMcpDiscoveryPilotExecutionContract; pilot: AssistantMcpDiscoveryPilotExecutionContract;
answer_draft: AssistantMcpDiscoveryAnswerDraftContract; answer_draft: AssistantMcpDiscoveryAnswerDraftContract;
loop_state: AssistantMcpDiscoveryLoopStateContract; loop_state: AssistantMcpDiscoveryLoopStateContract;
route_candidate: AssistantMcpRouteCandidateContract;
user_facing_response_allowed: boolean; user_facing_response_allowed: boolean;
business_fact_answer_allowed: boolean; business_fact_answer_allowed: boolean;
requires_user_clarification: boolean; requires_user_clarification: boolean;
@ -138,6 +166,26 @@ function loopStatusFor(
return "ready_for_next_hop"; return "ready_for_next_hop";
} }
function routeCandidateStatusFor(
bridgeStatus: AssistantMcpDiscoveryRuntimeBridgeStatus,
pilot: AssistantMcpDiscoveryPilotExecutionContract,
missingProofFamily: AssistantMcpDiscoveryBusinessOverviewMissingProofFamily | null
): AssistantMcpRouteCandidateStatus {
if (bridgeStatus === "blocked" || pilot.pilot_status === "blocked") {
return "blocked";
}
if (bridgeStatus === "needs_clarification" || pilot.pilot_status === "skipped_needs_clarification") {
return "needs_user_scope";
}
if (bridgeStatus === "unsupported" || pilot.pilot_status === "unsupported") {
return "needs_route_enablement";
}
if (missingProofFamily) {
return "needs_route_enablement";
}
return "ready_for_reviewed_execution";
}
function flattenAxes( function flattenAxes(
pilot: AssistantMcpDiscoveryPilotExecutionContract, pilot: AssistantMcpDiscoveryPilotExecutionContract,
source: "provided_axes" | "missing_axis_options" source: "provided_axes" | "missing_axis_options"
@ -168,6 +216,144 @@ function entityCandidatesFromPlanner(planner: AssistantMcpDiscoveryPlannerContra
return uniqueStrings(values); return uniqueStrings(values);
} }
function firstNonEmpty(values: Array<string | null | undefined>): string | null {
for (const value of values) {
const text = String(value ?? "").trim();
if (text) {
return text;
}
}
return null;
}
type AssistantMcpDiscoveryBusinessOverviewMissingProofFamily = NonNullable<
AssistantMcpDiscoveryPilotExecutionContract["derived_business_overview"]
>["missing_proof_families"][number];
function routeCandidateProofFamiliesFor(actionFamily: string | null, proofExpectation: string | null): string[] {
const combined = `${actionFamily ?? ""} ${proofExpectation ?? ""}`.trim().toLowerCase();
const result: string[] = [];
const add = (family: string) => {
if (!result.includes(family)) {
result.push(family);
}
};
if (!combined || combined === "broad_evaluation bounded_inference") {
return result;
}
if (/(?:inventory|stock|warehouse|reserve|liquidation|write[-_ ]?off|obsolete|obsolescence)/iu.test(combined)) {
add("inventory_reserve_liquidation_quality");
}
if (/(?:debt|due[-_ ]?date|overdue|aging|credit[-_ ]?risk)/iu.test(combined)) {
add("debt_due_date_aging_quality");
}
if (/(?:vendor|supplier|procurement|sourcing)/iu.test(combined)) {
add("vendor_risk_procurement_quality");
}
if (/(?:profit|margin|pnl|p&l|financial[-_ ]?result)/iu.test(combined)) {
add("accounting_profit_margin");
}
return result;
}
function routeCandidateMissingProofFamily(
planner: AssistantMcpDiscoveryPlannerContract,
pilot: AssistantMcpDiscoveryPilotExecutionContract
): AssistantMcpDiscoveryBusinessOverviewMissingProofFamily | null {
if (planner.data_need_graph?.business_fact_family !== "business_overview") {
return null;
}
const wantedFamilies = routeCandidateProofFamiliesFor(
planner.data_need_graph?.action_family ?? null,
planner.data_need_graph?.proof_expectation ?? null
);
if (wantedFamilies.length <= 0) {
return null;
}
const missingProofFamilies = pilot.derived_business_overview?.missing_proof_families ?? [];
return missingProofFamilies.find((item) => wantedFamilies.includes(item.family)) ?? null;
}
function routeCandidateEnablementReason(
status: AssistantMcpRouteCandidateStatus,
pilot: AssistantMcpDiscoveryPilotExecutionContract,
missingAxes: string[],
missingProofFamily: AssistantMcpDiscoveryBusinessOverviewMissingProofFamily | null
): string | null {
if (status === "ready_for_reviewed_execution") {
return null;
}
if (status === "needs_user_scope") {
return missingAxes.length > 0
? `Missing scope axes: ${missingAxes.join(", ")}`
: "Selected chain needs user clarification before MCP execution";
}
if (missingProofFamily) {
return [
`Missing reviewed proof family: ${missingProofFamily.family}`,
`next_required_evidence=${missingProofFamily.next_required_evidence}`,
missingProofFamily.current_supported_evidence
? `current_supported_evidence=${missingProofFamily.current_supported_evidence}`
: null,
`must_not_claim=${missingProofFamily.must_not_claim}`
]
.filter((item): item is string => Boolean(item))
.join("; ");
}
return firstNonEmpty([
...pilot.query_limitations,
...pilot.evidence.unknown_facts,
"Selected chain is not safely executable by the reviewed MCP runtime yet"
]);
}
function routeCandidateNextAction(status: AssistantMcpRouteCandidateStatus): string {
if (status === "ready_for_reviewed_execution") {
return "Execute through the reviewed runtime bridge and truth gate.";
}
if (status === "needs_user_scope") {
return "Ask the user for the missing scope axes before MCP execution.";
}
if (status === "needs_route_enablement") {
return "Create or wire a reviewed exact route for the selected chain before treating the fact as answerable.";
}
return "Do not execute until the blocking reason is resolved.";
}
function buildRouteCandidate(
planner: AssistantMcpDiscoveryPlannerContract,
pilot: AssistantMcpDiscoveryPilotExecutionContract,
bridgeStatus: AssistantMcpDiscoveryRuntimeBridgeStatus
): AssistantMcpRouteCandidateContract {
const plannerClarificationGaps = planner.discovery_plan.clarification_gaps ?? [];
const providedAxes = flattenAxes(pilot, "provided_axes");
const missingAxes = plannerClarificationGaps.length > 0 ? plannerClarificationGaps : flattenAxes(pilot, "missing_axis_options");
const missingProofFamily = routeCandidateMissingProofFamily(planner, pilot);
const candidateStatus = routeCandidateStatusFor(bridgeStatus, pilot, missingProofFamily);
return {
schema_version: ASSISTANT_MCP_ROUTE_CANDIDATE_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryRuntimeBridge",
candidate_status: candidateStatus,
selected_chain_id: planner.selected_chain_id,
selected_chain_summary: planner.selected_chain_summary,
nearest_catalog_chain_template: planner.catalog_chain_template_alignment.top_chain_template_match,
catalog_alignment_status: planner.catalog_chain_template_alignment.alignment_status,
business_fact_family: planner.data_need_graph?.business_fact_family ?? null,
action_family: planner.data_need_graph?.action_family ?? null,
proof_expectation: planner.data_need_graph?.proof_expectation ?? null,
required_axes: [...planner.required_axes],
provided_axes: providedAxes,
missing_axes: missingAxes,
executable_now: candidateStatus === "ready_for_reviewed_execution",
enablement_reason: routeCandidateEnablementReason(candidateStatus, pilot, missingAxes, missingProofFamily),
recommended_next_action: routeCandidateNextAction(candidateStatus),
forbidden_overclaim_flags: uniqueStrings([
...(planner.data_need_graph?.forbidden_overclaim_flags ?? []),
...(missingProofFamily ? [missingProofFamily.must_not_claim] : [])
])
};
}
function buildLoopState( function buildLoopState(
planner: AssistantMcpDiscoveryPlannerContract, planner: AssistantMcpDiscoveryPlannerContract,
pilot: AssistantMcpDiscoveryPilotExecutionContract, pilot: AssistantMcpDiscoveryPilotExecutionContract,
@ -215,11 +401,14 @@ export async function runAssistantMcpDiscoveryRuntimeBridge(
const answerDraft = buildAssistantMcpDiscoveryAnswerDraft(pilot); const answerDraft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
const bridgeStatus = bridgeStatusFor(pilot, answerDraft); const bridgeStatus = bridgeStatusFor(pilot, answerDraft);
const loopState = buildLoopState(planner, pilot, bridgeStatus); const loopState = buildLoopState(planner, pilot, bridgeStatus);
const routeCandidate = buildRouteCandidate(planner, pilot, bridgeStatus);
const reasonCodes = uniqueStrings([...planner.reason_codes, ...pilot.reason_codes, ...answerDraft.reason_codes]); const reasonCodes = uniqueStrings([...planner.reason_codes, ...pilot.reason_codes, ...answerDraft.reason_codes]);
pushReason(reasonCodes, `runtime_bridge_status_${bridgeStatus}`); pushReason(reasonCodes, `runtime_bridge_status_${bridgeStatus}`);
pushReason(reasonCodes, "runtime_bridge_not_wired_to_hot_assistant_answer"); pushReason(reasonCodes, "runtime_bridge_not_wired_to_hot_assistant_answer");
pushReason(reasonCodes, `runtime_bridge_loop_state_${loopState.loop_status}`); pushReason(reasonCodes, `runtime_bridge_loop_state_${loopState.loop_status}`);
pushReason(reasonCodes, "runtime_bridge_route_candidate_built");
pushReason(reasonCodes, `runtime_bridge_route_candidate_${routeCandidate.candidate_status}`);
return { return {
schema_version: ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION, schema_version: ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION,
@ -230,6 +419,7 @@ export async function runAssistantMcpDiscoveryRuntimeBridge(
pilot, pilot,
answer_draft: answerDraft, answer_draft: answerDraft,
loop_state: loopState, loop_state: loopState,
route_candidate: routeCandidate,
user_facing_response_allowed: bridgeStatus !== "blocked", user_facing_response_allowed: bridgeStatus !== "blocked",
business_fact_answer_allowed: businessFactAnswerAllowed(answerDraft), business_fact_answer_allowed: businessFactAnswerAllowed(answerDraft),
requires_user_clarification: bridgeStatus === "needs_clarification", requires_user_clarification: bridgeStatus === "needs_clarification",

View File

@ -180,6 +180,11 @@ function isGarbageSemanticAnchorCandidate(value: string | null): boolean {
"всему", "всему",
"всей", "всей",
"всем", "всем",
"год",
"года",
"году",
"годом",
"годы",
"выводу", "выводу",
"выводам", "выводам",
"аудиту", "аудиту",
@ -824,6 +829,51 @@ function hasOrganizationLevelEarningsOverviewSignal(text: string): boolean {
return hasYearRankingCue || hasCompanyEarningsCue || hasCompanyProfitMarginCue; return hasYearRankingCue || hasCompanyEarningsCue || hasCompanyProfitMarginCue;
} }
function hasOrganizationLevelProfitMarginBoundaryOverviewSignal(text: string): boolean {
if (!text) {
return false;
}
const hasProfitMarginCue =
/(?:\u043f\u0440\u0438\u0431\u044b\u043b\w*|\u043c\u0430\u0440\u0436\w*|\u0440\u0435\u043d\u0442\u0430\u0431\w*|\u0444\u0438\u043d(?:\u0430\u043d\u0441\w*)?\s*[- ]?\s*\u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442|p\s*&\s*l|profit(?:ability)?|margin|financial\s+result)/iu.test(
text
);
const hasCompanyScopeCue =
/(?:\u0443\s+\u043d\u0430\u0441|\u043d\u0430\u0448\w*|\u043c\u044b\b|\u043f\u043e\s+\u043a\u043e\u043c\u043f\u0430\u043d|\u043a\u043e\u043c\u043f\u0430\u043d|\u0431\u0438\u0437\u043d\u0435\u0441|\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446|\u0432\s+\u0446\u0435\u043b\u043e\u043c|\b(?:\u043e\u043e\u043e|\u0438\u043f|\u0430\u043e|\u043f\u0430\u043e|\u0437\u0430\u043e|\u043e\u0430\u043e)\b|(?:19|20)\d{2}|company|business|organization|our|we|us)/iu.test(
text
);
return hasProfitMarginCue && hasCompanyScopeCue;
}
function hasProfitMarginBoundaryFollowupSignal(text: string): boolean {
if (!text) {
return false;
}
const hasProfitOrResultCue =
/(?:\u043f\u0440\u0438\u0431\u044b\u043b\w*|\u0443\u0431\u044b\u0442\w*|\u043c\u0430\u0440\u0436\w*|\u0440\u0435\u043d\u0442\u0430\u0431\w*|\u0444\u0438\u043d(?:\u0430\u043d\u0441\w*)?\s*[- ]?\s*\u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442|p\s*&\s*l|profit|loss|margin|financial\s+result)/iu.test(
text
);
const hasFollowupShape =
/(?:\u044d\u0442\u043e|\u0438\u0442\u043e\u0433|\u0438\u0442\u043e\u0433\u043e|\u043f\u043e\u043b\u0443\u0447\w*|\u043a\u043e\u0440\u043e\u0442\w*|\u0432\s+\u0438\u0442\u043e\u0433\u0435|\u043c\u043e\u0436\u043d\u043e\s+(?:\u043b\u0438\s+)?\u0441\u043a\u0430\u0437\u0430\u0442\u044c|is\s+it|result|short|brief)/iu.test(
text
);
return hasProfitOrResultCue && hasFollowupShape;
}
function hasDebtDueDateBoundaryFollowupSignal(text: string): boolean {
if (!text) {
return false;
}
const hasDueDateCue =
/(?:\u043f\u0440\u043e\u0441\u0440\u043e\u0447\w*|\u0441\u0440\u043e\u043a\w*\s+\u043e\u043f\u043b\u0430\u0442|\u0434\u043e\u043a\u0430\u0437\w*|\u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\w*|due[-\s]?date|overdue|debt\s+aging|aging)/iu.test(
text
);
const hasFollowupShape =
/(?:\u0442\u043e\s+\u0435\u0441\u0442\u044c|\u043f\u043e\u0447\u0435\u043c\u0443|\u043a\u043e\u0440\u043e\u0442\w*|\u043d\u0435\u043b\u044c\u0437\u044f|\u043c\u043e\u0436\u043d\u043e\s+\u043b\u0438|\u0437\u043d\u0430\u0447\u0438\u0442|\u0432\u044b\u0445\u043e\u0434\u0438\u0442|why|short|brief|so)/iu.test(
text
);
return hasDueDateCue && hasFollowupShape;
}
function hasOrganizationLevelDebtDueDateOverviewSignal(text: string): boolean { function hasOrganizationLevelDebtDueDateOverviewSignal(text: string): boolean {
if (!text) { if (!text) {
return false; return false;
@ -999,6 +1049,13 @@ function hasExplicitVatQuestionSignal(text: string): boolean {
); );
} }
function hasExplicitVatMovementEvidenceSignal(text: string): boolean {
if (!/(?:\u043d\u0434\u0441|vat)/iu.test(text)) {
return false;
}
return hasMovementEvidenceFollowupSignal(text);
}
function hasBusinessOverviewSeparateCounterpartySignal(text: string): boolean { function hasBusinessOverviewSeparateCounterpartySignal(text: string): boolean {
if (!text) { if (!text) {
return false; return false;
@ -1534,6 +1591,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
); );
const rawPrimaryBusinessOverviewSignal = hasBusinessOverviewSignal(rawText); const rawPrimaryBusinessOverviewSignal = hasBusinessOverviewSignal(rawText);
const explicitVatQuestionSignal = hasExplicitVatQuestionSignal(rawText); const explicitVatQuestionSignal = hasExplicitVatQuestionSignal(rawText);
const explicitVatMovementEvidenceSignal = hasExplicitVatMovementEvidenceSignal(rawText);
const explicitVatSuppressesBusinessOverviewContinuation = Boolean( const explicitVatSuppressesBusinessOverviewContinuation = Boolean(
explicitVatQuestionSignal && !rawPrimaryBusinessOverviewSignal explicitVatQuestionSignal && !rawPrimaryBusinessOverviewSignal
); );
@ -1552,6 +1610,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
const rawMetadataSignal = const rawMetadataSignal =
!rawLifecycleSignal && !rawLifecycleSignal &&
!rawValueFlowSignal && !rawValueFlowSignal &&
!explicitVatMovementEvidenceSignal &&
!rawReferentialDocumentExclusionSignal && !rawReferentialDocumentExclusionSignal &&
hasMetadataSignal(rawText); hasMetadataSignal(rawText);
const rawEntityResolutionSignal = const rawEntityResolutionSignal =
@ -1569,7 +1628,8 @@ export function buildAssistantMcpDiscoveryTurnInput(
const explicitDateScopeLiteralDetected = hasExplicitDateScopeLiteral(dateScopeSignalText); const explicitDateScopeLiteralDetected = hasExplicitDateScopeLiteral(dateScopeSignalText);
const relativeCurrentDateHintDetected = hasRelativeCurrentDateHint(rawText); const relativeCurrentDateHintDetected = hasRelativeCurrentDateHint(rawText);
const rawDateScope = collectDateScopeFromRawText(dateScopeSignalText); const rawDateScope = collectDateScopeFromRawText(dateScopeSignalText);
const rawMetadataScopeHint = rawMetadataSignal ? metadataScopeHintFromRawText(rawText) : null; const rawMetadataScopeHint =
rawMetadataSignal || explicitVatMovementEvidenceSignal ? metadataScopeHintFromRawText(rawText) : null;
const rawTopicSwitchSignal = hasExplicitTopicSwitchSignal(rawText); const rawTopicSwitchSignal = hasExplicitTopicSwitchSignal(rawText);
const rawEntityCandidate = rawEntityResolutionSignal ? rawEntityResolutionCandidate(rawEntitySourceText) : null; const rawEntityCandidate = rawEntityResolutionSignal ? rawEntityResolutionCandidate(rawEntitySourceText) : null;
const rawScopedEntityCandidate = const rawScopedEntityCandidate =
@ -1594,12 +1654,49 @@ export function buildAssistantMcpDiscoveryTurnInput(
const rawAggregationAxis = toNonEmptyString(assistantTurnMeaning?.asked_aggregation_axis); const rawAggregationAxis = toNonEmptyString(assistantTurnMeaning?.asked_aggregation_axis);
const unsupported = toNonEmptyString(assistantTurnMeaning?.unsupported_but_understood_family); const unsupported = toNonEmptyString(assistantTurnMeaning?.unsupported_but_understood_family);
const broadBusinessEvaluationUnsupported = unsupported === "broad_business_evaluation"; const broadBusinessEvaluationUnsupported = unsupported === "broad_business_evaluation";
const businessOverviewSignal = const seededBusinessOverviewSignal =
rawBusinessOverviewSignal ||
broadBusinessEvaluationUnsupported || broadBusinessEvaluationUnsupported ||
rawDomain === "business_summary" || rawDomain === "business_summary" ||
rawDomain === "business_overview" || rawDomain === "business_overview" ||
rawAction === "broad_evaluation"; rawAction === "broad_evaluation";
const inventoryReserveBusinessOverviewSignal =
(rawBusinessOverviewSignal || seededBusinessOverviewSignal) &&
hasOrganizationLevelInventoryReserveLiquidationOverviewSignal(rawText);
const debtDueDateFollowupBusinessOverviewSignal =
businessOverviewContinuationSignal && hasDebtDueDateBoundaryFollowupSignal(rawText);
const debtDueDateBusinessOverviewSignal =
((rawBusinessOverviewSignal || seededBusinessOverviewSignal) &&
hasOrganizationLevelDebtDueDateOverviewSignal(rawText)) ||
debtDueDateFollowupBusinessOverviewSignal;
const supplierQualityBusinessOverviewSignal =
(rawBusinessOverviewSignal || seededBusinessOverviewSignal) && hasOrganizationLevelSupplierQualityOverviewSignal(rawText);
const profitMarginFollowupBusinessOverviewSignal =
businessOverviewContinuationSignal && hasProfitMarginBoundaryFollowupSignal(rawText);
const profitMarginBusinessOverviewSignal =
((rawBusinessOverviewSignal || seededBusinessOverviewSignal) &&
hasOrganizationLevelProfitMarginBoundaryOverviewSignal(rawText)) ||
profitMarginFollowupBusinessOverviewSignal;
const businessOverviewActionFamily = inventoryReserveBusinessOverviewSignal
? "inventory_reserve_boundary"
: debtDueDateBusinessOverviewSignal
? "debt_due_date_boundary"
: supplierQualityBusinessOverviewSignal
? "vendor_risk_procurement_boundary"
: profitMarginBusinessOverviewSignal
? "profit_margin_boundary"
: "broad_evaluation";
const businessOverviewUnsupportedFamily = inventoryReserveBusinessOverviewSignal
? "inventory_reserve_liquidation_boundary"
: debtDueDateBusinessOverviewSignal
? "debt_due_date_boundary"
: supplierQualityBusinessOverviewSignal
? "vendor_risk_procurement_boundary"
: profitMarginBusinessOverviewSignal
? "profit_margin_boundary"
: "broad_business_evaluation";
const businessOverviewSignal =
rawBusinessOverviewSignal ||
seededBusinessOverviewSignal;
const businessOverviewSeparateCounterpartySignal = Boolean( const businessOverviewSeparateCounterpartySignal = Boolean(
businessOverviewSignal && hasBusinessOverviewSeparateCounterpartySignal(rawText) businessOverviewSignal && hasBusinessOverviewSeparateCounterpartySignal(rawText)
); );
@ -1630,7 +1727,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
: rawAssistantTurnMeaningOrganizationScope; : rawAssistantTurnMeaningOrganizationScope;
const rawOrganizationMentionSignal = hasOrganizationScopeSignalUtf8(rawText); const rawOrganizationMentionSignal = hasOrganizationScopeSignalUtf8(rawText);
const rawOrganizationScope = extractOrganizationScopeFromRawText(rawUserText ?? rawEffectiveText ?? rawSignalSourceText); const rawOrganizationScope = extractOrganizationScopeFromRawText(rawUserText ?? rawEffectiveText ?? rawSignalSourceText);
const currentTurnFreshOrganizationScope = rawOrganizationScope ?? predecomposeEntities.organization; const currentTurnFreshOrganizationScope = predecomposeEntities.organization ?? rawOrganizationScope;
const currentTurnOrganizationScope = const currentTurnOrganizationScope =
currentTurnFreshOrganizationScope ?? assistantTurnMeaningOrganizationScope; currentTurnFreshOrganizationScope ?? assistantTurnMeaningOrganizationScope;
const followupCounterpartyIsMetadataOrganizationScope = Boolean( const followupCounterpartyIsMetadataOrganizationScope = Boolean(
@ -2029,9 +2126,21 @@ export function buildAssistantMcpDiscoveryTurnInput(
const semanticDataNeed = metadataAmbiguityLaneClarificationApplicable const semanticDataNeed = metadataAmbiguityLaneClarificationApplicable
? "metadata lane clarification" ? "metadata lane clarification"
: semanticNeedFor({ : semanticNeedFor({
domain: businessOverviewSignal ? "business_overview" : rawDomain ?? seededDomain, domain: explicitVatMovementEvidenceSignal
action: businessOverviewSignal ? "broad_evaluation" : rawAction ?? seededAction, ? "movements"
unsupported: businessOverviewSignal ? "broad_business_evaluation" : unsupported ?? seededUnsupported, : businessOverviewSignal
? "business_overview"
: rawDomain ?? seededDomain,
action: explicitVatMovementEvidenceSignal
? "list_movements"
: businessOverviewSignal
? businessOverviewActionFamily
: rawAction ?? seededAction,
unsupported: explicitVatMovementEvidenceSignal
? "movement_evidence"
: businessOverviewSignal
? businessOverviewUnsupportedFamily
: unsupported ?? seededUnsupported,
lifecycleSignal, lifecycleSignal,
valueFlowSignal, valueFlowSignal,
metadataSignal: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable, metadataSignal: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable,
@ -2065,7 +2174,9 @@ export function buildAssistantMcpDiscoveryTurnInput(
followupSeed.metadataSelectedEntitySet ?? followupSeed.metadataSelectedEntitySet ??
null; null;
const metadataScopedLaneWithoutSubject = Boolean( const metadataScopedLaneWithoutSubject = Boolean(
(metadataGroundedMovementLaneApplicable || metadataGroundedDocumentLaneApplicable) && (metadataGroundedMovementLaneApplicable ||
metadataGroundedDocumentLaneApplicable ||
explicitVatMovementEvidenceSignal) &&
!effectiveFollowupCounterparty && !effectiveFollowupCounterparty &&
metadataLaneCarryoverAvailable metadataLaneCarryoverAvailable
); );
@ -2278,11 +2389,13 @@ export function buildAssistantMcpDiscoveryTurnInput(
const turnMeaning: AssistantMcpDiscoveryTurnMeaningRef = { const turnMeaning: AssistantMcpDiscoveryTurnMeaningRef = {
asked_domain_family: asked_domain_family:
businessOverviewSignal businessOverviewSignal
? "business_overview" ? "business_overview"
: lifecycleSignal : lifecycleSignal
? "counterparty_lifecycle" ? "counterparty_lifecycle"
: valueFlowSignal : valueFlowSignal
? "counterparty_value" ? "counterparty_value"
: explicitVatMovementEvidenceSignal
? "movements"
: metadataGroundedMovementLaneApplicable : metadataGroundedMovementLaneApplicable
? "movements" ? "movements"
: metadataGroundedDocumentLaneApplicable : metadataGroundedDocumentLaneApplicable
@ -2293,7 +2406,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
? "metadata" ? "metadata"
: rawDomain ?? seededDomain, : rawDomain ?? seededDomain,
asked_action_family: businessOverviewSignal asked_action_family: businessOverviewSignal
? "broad_evaluation" ? businessOverviewActionFamily
: lifecycleSignal : lifecycleSignal
? "activity_duration" ? "activity_duration"
: valueFlowSignal : valueFlowSignal
@ -2302,6 +2415,8 @@ export function buildAssistantMcpDiscoveryTurnInput(
: payoutSignal : payoutSignal
? "payout" ? "payout"
: rawAction ?? seededAction ?? "turnover" : rawAction ?? seededAction ?? "turnover"
: explicitVatMovementEvidenceSignal
? "list_movements"
: metadataGroundedMovementLaneApplicable : metadataGroundedMovementLaneApplicable
? "list_movements" ? "list_movements"
: metadataGroundedDocumentLaneApplicable : metadataGroundedDocumentLaneApplicable
@ -2334,7 +2449,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
subject_resolution_optional: metadataScopedLaneWithoutSubject || undefined, subject_resolution_optional: metadataScopedLaneWithoutSubject || undefined,
unsupported_but_understood_family: unsupported_but_understood_family:
businessOverviewSignal businessOverviewSignal
? "broad_business_evaluation" ? businessOverviewUnsupportedFamily
: unsupported ?? : unsupported ??
(lifecycleSignal (lifecycleSignal
? "counterparty_lifecycle" ? "counterparty_lifecycle"
@ -2346,8 +2461,10 @@ export function buildAssistantMcpDiscoveryTurnInput(
: seededUnsupported ?? "counterparty_value_or_turnover" : seededUnsupported ?? "counterparty_value_or_turnover"
: metadataGroundedMovementLaneApplicable : metadataGroundedMovementLaneApplicable
? "movement_evidence" ? "movement_evidence"
: metadataGroundedDocumentLaneApplicable : metadataGroundedDocumentLaneApplicable
? "document_evidence" ? "document_evidence"
: explicitVatMovementEvidenceSignal
? "movement_evidence"
: metadataAmbiguityLaneClarificationApplicable : metadataAmbiguityLaneClarificationApplicable
? "metadata_lane_choice_clarification" ? "metadata_lane_choice_clarification"
: entityResolutionSignal : entityResolutionSignal
@ -2363,6 +2480,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
unsupported || unsupported ||
lifecycleSignal || lifecycleSignal ||
valueFlowSignal || valueFlowSignal ||
explicitVatMovementEvidenceSignal ||
metadataGroundedMovementLaneApplicable || metadataGroundedMovementLaneApplicable ||
metadataGroundedDocumentLaneApplicable || metadataGroundedDocumentLaneApplicable ||
metadataAmbiguityLaneClarificationApplicable || metadataAmbiguityLaneClarificationApplicable ||
@ -2423,13 +2541,17 @@ export function buildAssistantMcpDiscoveryTurnInput(
const currentTurnValueFlowExactOverrideApplicable = Boolean( const currentTurnValueFlowExactOverrideApplicable = Boolean(
valueFlowSignal && valueFlowSignal &&
explicitIntentCandidate && explicitIntentCandidate &&
rawValueFlowAggregateQuestionSignal && (rawValueFlowAggregateQuestionSignal || hasValueRankingSignal(rawText)) &&
semanticDataNeed && semanticDataNeed &&
(entityCandidates.length > 0 || explicitOrganizationScope || openScopeValueFlowWithoutResolvedCounterparty) (entityCandidates.length > 0 || explicitOrganizationScope || openScopeValueFlowWithoutResolvedCounterparty)
); );
const runDiscovery = shouldRunDiscovery({ const runDiscovery = shouldRunDiscovery({
unsupported: businessOverviewSignal ? "broad_business_evaluation" : unsupported ?? seededUnsupported, unsupported: explicitVatMovementEvidenceSignal
? "movement_evidence"
: businessOverviewSignal
? "broad_business_evaluation"
: unsupported ?? seededUnsupported,
lifecycleSignal, lifecycleSignal,
valueFlowSignal, valueFlowSignal,
metadataSignal: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable, metadataSignal: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable,
@ -2446,6 +2568,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
groundedValueFlowFollowupApplicable, groundedValueFlowFollowupApplicable,
forceDiscoveryOverExplicitIntent: forceDiscoveryOverExplicitIntent:
businessOverviewSignal || businessOverviewSignal ||
explicitVatMovementEvidenceSignal ||
Boolean(entityResolutionClarificationCandidate) || Boolean(entityResolutionClarificationCandidate) ||
organizationClarificationFollowupApplicable || organizationClarificationFollowupApplicable ||
periodClarificationFollowupApplicable || periodClarificationFollowupApplicable ||
@ -2469,6 +2592,8 @@ export function buildAssistantMcpDiscoveryTurnInput(
? "followup_context" ? "followup_context"
: metadataGroundedDocumentLaneApplicable : metadataGroundedDocumentLaneApplicable
? "followup_context" ? "followup_context"
: explicitVatMovementEvidenceSignal
? "raw_text"
: predecomposeContract : predecomposeContract
? "predecompose_contract" ? "predecompose_contract"
: lifecycleSignal : lifecycleSignal
@ -2490,6 +2615,9 @@ export function buildAssistantMcpDiscoveryTurnInput(
if (rawMetadataSignal) { if (rawMetadataSignal) {
pushReason(reasonCodes, "mcp_discovery_metadata_signal_detected"); pushReason(reasonCodes, "mcp_discovery_metadata_signal_detected");
} }
if (explicitVatMovementEvidenceSignal) {
pushReason(reasonCodes, "mcp_discovery_vat_movement_evidence_signal_detected");
}
if (entityResolutionSignal) { if (entityResolutionSignal) {
pushReason(reasonCodes, "mcp_discovery_entity_resolution_signal_detected"); pushReason(reasonCodes, "mcp_discovery_entity_resolution_signal_detected");
} }
@ -2613,6 +2741,12 @@ export function buildAssistantMcpDiscoveryTurnInput(
if (businessOverviewContinuationSignal) { if (businessOverviewContinuationSignal) {
pushReason(reasonCodes, "mcp_discovery_business_overview_continuation_from_followup_context"); pushReason(reasonCodes, "mcp_discovery_business_overview_continuation_from_followup_context");
} }
if (profitMarginFollowupBusinessOverviewSignal) {
pushReason(reasonCodes, "mcp_discovery_business_overview_profit_margin_followup_boundary");
}
if (debtDueDateFollowupBusinessOverviewSignal) {
pushReason(reasonCodes, "mcp_discovery_business_overview_debt_due_date_followup_boundary");
}
if (explicitVatSuppressesBusinessOverviewContinuation) { if (explicitVatSuppressesBusinessOverviewContinuation) {
pushReason(reasonCodes, "mcp_discovery_business_overview_continuation_suppressed_by_explicit_vat_question"); pushReason(reasonCodes, "mcp_discovery_business_overview_continuation_suppressed_by_explicit_vat_question");
} }

View File

@ -395,6 +395,13 @@ export function createAssistantRoutePolicy(deps) {
const hasTemporalCue = /(?:на\s+эту\s+же\s+дат[ауеы]|на\s+тот\s+же\s+период|за\s+этот\s+же\s+период|за\s+этот\s+период|март|апрел|ма[йя]|июн|июл|август|сентябр|октябр|ноябр|декабр|\b(?:19|20)\d{2}\b)/iu.test(normalized); const hasTemporalCue = /(?:на\s+эту\s+же\s+дат[ауеы]|на\s+тот\s+же\s+период|за\s+этот\s+же\s+период|за\s+этот\s+период|март|апрел|ма[йя]|июн|июл|август|сентябр|октябр|ноябр|декабр|\b(?:19|20)\d{2}\b)/iu.test(normalized);
return hasRequestCue && hasTemporalCue; return hasRequestCue && hasTemporalCue;
} }
function hasOrganizationClarificationTextCue(text) {
const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase());
if (!normalized) {
return false;
}
return /(?<!\p{L})(?:\u043e\u043e\u043e|\u0438\u043f|\u0430\u043e|\u043f\u0430\u043e|\u0437\u0430\u043e)(?!\p{L})|(?:\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446|\u043a\u043e\u043c\u043f\u0430\u043d|llc|company|organization)/iu.test(normalized);
}
function resolveAssistantOrchestrationDecision(input) { function resolveAssistantOrchestrationDecision(input) {
const rawUserMessage = String(input?.rawUserMessage ?? input?.userMessage ?? ""); const rawUserMessage = String(input?.rawUserMessage ?? input?.userMessage ?? "");
const effectiveAddressUserMessage = String(input?.effectiveAddressUserMessage ?? rawUserMessage); const effectiveAddressUserMessage = String(input?.effectiveAddressUserMessage ?? rawUserMessage);
@ -559,6 +566,27 @@ export function createAssistantRoutePolicy(deps) {
const followupPreviousFilters = followupContext?.previous_filters && typeof followupContext.previous_filters === "object" const followupPreviousFilters = followupContext?.previous_filters && typeof followupContext.previous_filters === "object"
? followupContext.previous_filters ? followupContext.previous_filters
: null; : null;
const followupLoopStatus = toNonEmptyString(followupContext?.previous_discovery_loop_status);
const followupLoopSelectedChainId = toNonEmptyString(followupContext?.previous_discovery_loop_selected_chain_id);
const followupLoopPendingAxes = Array.isArray(followupContext?.previous_discovery_loop_pending_axes)
? followupContext.previous_discovery_loop_pending_axes.map((item) => toNonEmptyString(item)).filter(Boolean)
: [];
const currentTurnPredecomposeOrganization = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.entities?.organization) ??
(toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.semantics?.anchor_kind) === "organization"
? toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.semantics?.anchor_value)
: null);
const routeCandidateOrganizationClarificationDetected = Boolean(followupContext &&
followupLoopStatus === "awaiting_clarification" &&
followupLoopSelectedChainId &&
followupLoopPendingAxes.includes("organization") &&
(currentTurnPredecomposeOrganization ||
explicitOrganizationClarificationSelection ||
[
rawUserMessage,
repairedRawUserMessage,
effectiveAddressUserMessage,
repairedEffectiveAddressUserMessage
].some((message) => hasOrganizationClarificationTextCue(message))));
const protectedInventoryShortFollowup = Boolean(followupContext && const protectedInventoryShortFollowup = Boolean(followupContext &&
(isInventorySelectedObjectIntent(followupPreviousIntent) || (isInventorySelectedObjectIntent(followupPreviousIntent) ||
(followupPreviousIntent === "inventory_on_hand_as_of_date" && (followupPreviousIntent === "inventory_on_hand_as_of_date" &&
@ -608,6 +636,8 @@ export function createAssistantRoutePolicy(deps) {
"net_value_flow" "net_value_flow"
].includes(String(toNonEmptyString(assistantTurnMeaning?.asked_action_family) ?? "")) || ].includes(String(toNonEmptyString(assistantTurnMeaning?.asked_action_family) ?? "")) ||
/(?:нетто|сальдо|сколько\s+мы\s+(?:получили|заплатили)|incoming|outgoing)/iu.test(analyticsSample))); /(?:нетто|сальдо|сколько\s+мы\s+(?:получили|заплатили)|incoming|outgoing)/iu.test(analyticsSample)));
const effectiveGroundedValueFlowFollowupContextDetected =
groundedValueFlowFollowupContextDetected || routeCandidateOrganizationClarificationDetected;
const baseToolGatePreservesAddressLane = Boolean(baseToolGate?.runAddressLane && const baseToolGatePreservesAddressLane = Boolean(baseToolGate?.runAddressLane &&
[ [
"address_intent_resolver_detected", "address_intent_resolver_detected",
@ -617,14 +647,15 @@ export function createAssistantRoutePolicy(deps) {
].includes(String(baseToolGate?.reason ?? ""))) || ].includes(String(baseToolGate?.reason ?? ""))) ||
Boolean(baseToolGate?.runAddressLane && Boolean(baseToolGate?.runAddressLane &&
String(baseToolGate?.reason ?? "") === "followup_context_detected" && String(baseToolGate?.reason ?? "") === "followup_context_detected" &&
groundedValueFlowFollowupContextDetected); effectiveGroundedValueFlowFollowupContextDetected);
const nonDomainQueryIndexed = Boolean(!llmFirstAddressCandidate && const nonDomainQueryIndexed = Boolean(!llmFirstAddressCandidate &&
deterministicNonDomainGuard && deterministicNonDomainGuard &&
(llmFirstUnsupportedCandidate || llmContractMode === null) && (llmFirstUnsupportedCandidate || llmContractMode === null) &&
!baseToolGatePreservesAddressLane && !baseToolGatePreservesAddressLane &&
!groundedValueFlowFollowupContextDetected && !effectiveGroundedValueFlowFollowupContextDetected &&
!protectedInventoryShortFollowup && !protectedInventoryShortFollowup &&
!organizationClarificationContinuationDetected); !organizationClarificationContinuationDetected &&
!routeCandidateOrganizationClarificationDetected);
const lastAddressAssistantDebug = sessionItems const lastAddressAssistantDebug = sessionItems
? findLastAddressAssistantItem(sessionItems)?.debug ?? null ? findLastAddressAssistantItem(sessionItems)?.debug ?? null
: null; : null;
@ -668,7 +699,7 @@ export function createAssistantRoutePolicy(deps) {
!turnMeaningIntentCandidate && !turnMeaningIntentCandidate &&
!dataScopeMetaQuery && !dataScopeMetaQuery &&
!dangerOrCoercionSignal && !dangerOrCoercionSignal &&
!groundedValueFlowFollowupContextDetected && !effectiveGroundedValueFlowFollowupContextDetected &&
!organizationClarificationContinuationDetected); !organizationClarificationContinuationDetected);
const hardMetaMode = resolveHardMetaMode({ const hardMetaMode = resolveHardMetaMode({
dataScopeMetaQuery, dataScopeMetaQuery,
@ -834,7 +865,7 @@ export function createAssistantRoutePolicy(deps) {
!dataScopeMetaQuery && !dataScopeMetaQuery &&
!capabilityMetaQuery && !capabilityMetaQuery &&
!dangerOrCoercionSignal && !dangerOrCoercionSignal &&
!groundedValueFlowFollowupContextDetected && !effectiveGroundedValueFlowFollowupContextDetected &&
!organizationClarificationContinuationDetected); !organizationClarificationContinuationDetected);
if (unsupportedCurrentTurnMeaningBoundary) { if (unsupportedCurrentTurnMeaningBoundary) {
return { return {

View File

@ -2079,6 +2079,9 @@ function isAddressLaneDebugPayload(debug) {
if (typeof debug.mcp_call_status === "string" && debug.mcp_call_status.trim().length > 0) { if (typeof debug.mcp_call_status === "string" && debug.mcp_call_status.trim().length > 0) {
return true; return true;
} }
if (debug.mcp_discovery_response_applied === true && debug.assistant_mcp_discovery_entry_point_v1) {
return true;
}
if (typeof debug.anchor_type === "string" && debug.anchor_type.trim().length > 0) { if (typeof debug.anchor_type === "string" && debug.anchor_type.trim().length > 0) {
return true; return true;
} }

View File

@ -208,6 +208,34 @@ export function createAssistantTransitionPolicy(deps) {
); );
} }
function hasBusinessOverviewBoundaryFollowupCue(text) {
const normalized = normalizeFollowupText(text);
if (!normalized) {
return false;
}
const hasBoundaryCue =
/(?:\u043f\u0440\u043e\u0441\u0440\u043e\u0447|\u0441\u0440\u043e\u043a\w*\s+\u043e\u043f\u043b\u0430\u0442|\u0434\u043e\u043a\u0430\u0437|\u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0434|\u043f\u0440\u0438\u0431\u044b\u043b|\u0443\u0431\u044b\u0442|\u043c\u0430\u0440\u0436|\u0440\u0435\u0437\u0435\u0440\u0432|\u043d\u0435\u043b\u0438\u043a\u0432\u0438\u0434|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u0432\u0435\u043d\u0434\u043e\u0440|due[-\s]?date|overdue|aging|profit|loss|margin|vendor|risk)/iu.test(
normalized
);
const hasFollowupShape =
/(?:\u0442\u043e\s+\u0435\u0441\u0442\u044c|\u043f\u043e\u0447\u0435\u043c\u0443|\u043a\u043e\u0440\u043e\u0442\u043a|\u043d\u0435\u043b\u044c\u0437\u044f|\u043c\u043e\u0436\u043d\u043e\s+\u043b\u0438|\u0437\u043d\u0430\u0447\u0438\u0442|\u0432\u044b\u0445\u043e\u0434\u0438\u0442|\u0438\u0442\u043e\u0433|why|short|brief|so)/iu.test(
normalized
);
return hasBoundaryCue && hasFollowupShape;
}
function hasOrganizationClarificationTextCue(text) {
const normalized = deps.compactWhitespace(
deps.repairAddressMojibake(String(text ?? "")).toLowerCase()
);
if (!normalized) {
return false;
}
return /(?<!\p{L})(?:\u043e\u043e\u043e|\u0438\u043f|\u0430\u043e|\u043f\u0430\u043e|\u0437\u0430\u043e)(?!\p{L})|(?:\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446|\u043a\u043e\u043c\u043f\u0430\u043d|llc|company|organization)/iu.test(
normalized
);
}
function parseDmyDateToIso(value) { function parseDmyDateToIso(value) {
const match = String(value ?? "").trim().match(/^(\d{2})\.(\d{2})\.(\d{4})$/); const match = String(value ?? "").trim().match(/^(\d{2})\.(\d{2})\.(\d{4})$/);
if (!match) { if (!match) {
@ -584,7 +612,12 @@ export function createAssistantTransitionPolicy(deps) {
(deps.toNonEmptyString(alternateMessage) (deps.toNonEmptyString(alternateMessage)
? deps.hasDataRetrievalRequestSignal(String(alternateMessage ?? "")) ? deps.hasDataRetrievalRequestSignal(String(alternateMessage ?? ""))
: false); : false);
if (rawCapabilityMetaQuery && !rawDataRetrievalSignal) { const rawBusinessOverviewBoundaryFollowupCue =
hasBusinessOverviewBoundaryFollowupCue(userMessage) ||
(deps.toNonEmptyString(alternateMessage)
? hasBusinessOverviewBoundaryFollowupCue(String(alternateMessage ?? ""))
: false);
if (rawCapabilityMetaQuery && !rawDataRetrievalSignal && !rawBusinessOverviewBoundaryFollowupCue) {
return null; return null;
} }
const assistantTurnMeaning = const assistantTurnMeaning =
@ -660,11 +693,42 @@ export function createAssistantTransitionPolicy(deps) {
carryoverSourceDebug, carryoverSourceDebug,
deps.toNonEmptyString deps.toNonEmptyString
); );
const sourceDiscoveryLoopStatusHint = readAssistantMcpDiscoveryLoopStatus(
carryoverSourceDebug,
deps.toNonEmptyString
);
const sourceDiscoveryLoopSelectedChainIdHint = readAssistantMcpDiscoveryLoopSelectedChainId(
carryoverSourceDebug,
deps.toNonEmptyString
);
const sourceDiscoveryLoopPendingAxesHint = readAssistantMcpDiscoveryLoopPendingAxes(
carryoverSourceDebug,
deps.toNonEmptyString
);
const sourceDiscoveryLoopProvidedAxesHint = readAssistantMcpDiscoveryLoopProvidedAxes(
carryoverSourceDebug,
deps.toNonEmptyString
);
const currentTurnPredecomposeOrganization =
deps.toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.entities?.organization) ??
(deps.toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.semantics?.anchor_kind) === "organization"
? deps.toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.semantics?.anchor_value)
: null);
const mcpDiscoveryOrganizationClarificationContinuation = Boolean(
sourceDiscoveryLoopStatusHint === "awaiting_clarification" &&
sourceDiscoveryLoopSelectedChainIdHint &&
sourceDiscoveryLoopPendingAxesHint.includes("organization") &&
(currentTurnPredecomposeOrganization ||
explicitOrganizationClarificationSelection ||
[userMessage, alternateMessage].some((message) => hasOrganizationClarificationTextCue(message)))
);
const hasValueFlowCarryoverSourceHint = const hasValueFlowCarryoverSourceHint =
sourceIntentHint === "customer_revenue_and_payments" || sourceIntentHint === "customer_revenue_and_payments" ||
sourceDiscoveryPilotScopeHint === "counterparty_value_flow_query_movements_v1" || sourceDiscoveryPilotScopeHint === "counterparty_value_flow_query_movements_v1" ||
sourceDiscoveryPilotScopeHint === "counterparty_supplier_payout_query_movements_v1" || sourceDiscoveryPilotScopeHint === "counterparty_supplier_payout_query_movements_v1" ||
sourceDiscoveryPilotScopeHint === "counterparty_bidirectional_value_flow_query_movements_v1"; sourceDiscoveryPilotScopeHint === "counterparty_bidirectional_value_flow_query_movements_v1";
const hasBusinessOverviewCarryoverSourceHint =
sourceDiscoveryPilotScopeHint === "business_overview_route_template_v1";
const navigationSessionState = resolveNavigationSessionContextState( const navigationSessionState = resolveNavigationSessionContextState(
addressNavigationState, addressNavigationState,
deps.toNonEmptyString, deps.toNonEmptyString,
@ -706,21 +770,31 @@ export function createAssistantTransitionPolicy(deps) {
hasValueFlowCarryoverSourceHint && deps.toNonEmptyString(alternateMessage) hasValueFlowCarryoverSourceHint && deps.toNonEmptyString(alternateMessage)
? hasShortValueFlowRetargetCue(String(alternateMessage ?? "")) ? hasShortValueFlowRetargetCue(String(alternateMessage ?? ""))
: false; : false;
const businessOverviewBoundaryFollowupPrimary =
hasBusinessOverviewCarryoverSourceHint && hasBusinessOverviewBoundaryFollowupCue(userMessage);
const businessOverviewBoundaryFollowupAlternate =
hasBusinessOverviewCarryoverSourceHint && deps.toNonEmptyString(alternateMessage)
? hasBusinessOverviewBoundaryFollowupCue(String(alternateMessage ?? ""))
: false;
const explicitSummaryBundleReuseSignal = hasExplicitSummaryBundleReuseSignal(userMessage, alternateMessage); const explicitSummaryBundleReuseSignal = hasExplicitSummaryBundleReuseSignal(userMessage, alternateMessage);
let hasPrimaryFollowupSignal = let hasPrimaryFollowupSignal =
deps.hasAddressFollowupContextSignal(userMessage) || deps.hasAddressFollowupContextSignal(userMessage) ||
Boolean(debtRoleSwapPrimary) || Boolean(debtRoleSwapPrimary) ||
shortValueFlowRetargetPrimary || shortValueFlowRetargetPrimary ||
businessOverviewBoundaryFollowupPrimary ||
inventoryShortFollowupPrimary || inventoryShortFollowupPrimary ||
inventoryPurchaseDateVatBridge || inventoryPurchaseDateVatBridge ||
explicitSummaryBundleReuseSignal; explicitSummaryBundleReuseSignal ||
mcpDiscoveryOrganizationClarificationContinuation;
let hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage) let hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage)
? deps.hasAddressFollowupContextSignal(alternateMessage) || ? deps.hasAddressFollowupContextSignal(alternateMessage) ||
Boolean(debtRoleSwapAlternate) || Boolean(debtRoleSwapAlternate) ||
shortValueFlowRetargetAlternate || shortValueFlowRetargetAlternate ||
businessOverviewBoundaryFollowupAlternate ||
inventoryShortFollowupAlternate || inventoryShortFollowupAlternate ||
inventoryPurchaseDateVatBridge || inventoryPurchaseDateVatBridge ||
explicitSummaryBundleReuseSignal explicitSummaryBundleReuseSignal ||
mcpDiscoveryOrganizationClarificationContinuation
: false; : false;
const hasPrimaryIndexReferenceSignal = deps.extractDisplayedEntityIndexMention(userMessage) !== null; const hasPrimaryIndexReferenceSignal = deps.extractDisplayedEntityIndexMention(userMessage) !== null;
const hasAlternateIndexReferenceSignal = deps.toNonEmptyString(alternateMessage) const hasAlternateIndexReferenceSignal = deps.toNonEmptyString(alternateMessage)
@ -760,6 +834,7 @@ export function createAssistantTransitionPolicy(deps) {
hasPrimaryIndexReferenceSignal || hasPrimaryIndexReferenceSignal ||
hasAlternateIndexReferenceSignal || hasAlternateIndexReferenceSignal ||
hasOrganizationClarificationContinuation || hasOrganizationClarificationContinuation ||
mcpDiscoveryOrganizationClarificationContinuation ||
hasImplicitContinuationSignal || hasImplicitContinuationSignal ||
hasSuggestedIntentPivotSignal || hasSuggestedIntentPivotSignal ||
inventoryShortFollowupPrimary || inventoryShortFollowupPrimary ||
@ -773,6 +848,8 @@ export function createAssistantTransitionPolicy(deps) {
Boolean(debtRoleSwapIntent) || Boolean(debtRoleSwapIntent) ||
shortValueFlowRetargetPrimary || shortValueFlowRetargetPrimary ||
shortValueFlowRetargetAlternate || shortValueFlowRetargetAlternate ||
businessOverviewBoundaryFollowupPrimary ||
businessOverviewBoundaryFollowupAlternate ||
deps.hasFollowupMarker(userMessage) || deps.hasFollowupMarker(userMessage) ||
deps.hasReferentialPointer(userMessage) || deps.hasReferentialPointer(userMessage) ||
(deps.toNonEmptyString(alternateMessage) (deps.toNonEmptyString(alternateMessage)
@ -783,6 +860,7 @@ export function createAssistantTransitionPolicy(deps) {
hasPrimaryIndexReferenceSignal || hasPrimaryIndexReferenceSignal ||
hasAlternateIndexReferenceSignal || hasAlternateIndexReferenceSignal ||
hasOrganizationClarificationContinuation || hasOrganizationClarificationContinuation ||
mcpDiscoveryOrganizationClarificationContinuation ||
inventoryShortFollowupPrimary || inventoryShortFollowupPrimary ||
inventoryShortFollowupAlternate || inventoryShortFollowupAlternate ||
hasInventoryRootTemporalFollowupPrimary || hasInventoryRootTemporalFollowupPrimary ||
@ -794,6 +872,8 @@ export function createAssistantTransitionPolicy(deps) {
Boolean(debtRoleSwapIntent) || Boolean(debtRoleSwapIntent) ||
shortValueFlowRetargetPrimary || shortValueFlowRetargetPrimary ||
shortValueFlowRetargetAlternate || shortValueFlowRetargetAlternate ||
businessOverviewBoundaryFollowupPrimary ||
businessOverviewBoundaryFollowupAlternate ||
deps.hasFollowupMarker(userMessage) || deps.hasFollowupMarker(userMessage) ||
deps.hasReferentialPointer(userMessage) || deps.hasReferentialPointer(userMessage) ||
(deps.toNonEmptyString(alternateMessage) (deps.toNonEmptyString(alternateMessage)
@ -826,6 +906,7 @@ export function createAssistantTransitionPolicy(deps) {
!hasImplicitContinuationSignal && !hasImplicitContinuationSignal &&
!hasSuggestedIntentPivotSignal && !hasSuggestedIntentPivotSignal &&
!hasOrganizationClarificationContinuation && !hasOrganizationClarificationContinuation &&
!mcpDiscoveryOrganizationClarificationContinuation &&
!hasIndexReferenceSignal && !hasIndexReferenceSignal &&
!explicitSummaryBundleReuseSignal !explicitSummaryBundleReuseSignal
) { ) {
@ -843,6 +924,7 @@ export function createAssistantTransitionPolicy(deps) {
!hasImplicitContinuationSignal && !hasImplicitContinuationSignal &&
!hasSuggestedIntentPivotSignal && !hasSuggestedIntentPivotSignal &&
!hasOrganizationClarificationContinuation && !hasOrganizationClarificationContinuation &&
!mcpDiscoveryOrganizationClarificationContinuation &&
!hasIndexReferenceSignal && !hasIndexReferenceSignal &&
!explicitSummaryBundleReuseSignal !explicitSummaryBundleReuseSignal
) { ) {
@ -884,19 +966,10 @@ export function createAssistantTransitionPolicy(deps) {
carryoverSourceDebug, carryoverSourceDebug,
deps.toNonEmptyString deps.toNonEmptyString
); );
const sourceDiscoveryLoopStatus = readAssistantMcpDiscoveryLoopStatus(carryoverSourceDebug, deps.toNonEmptyString); const sourceDiscoveryLoopStatus = sourceDiscoveryLoopStatusHint;
const sourceDiscoveryLoopSelectedChainId = readAssistantMcpDiscoveryLoopSelectedChainId( const sourceDiscoveryLoopSelectedChainId = sourceDiscoveryLoopSelectedChainIdHint;
carryoverSourceDebug, const sourceDiscoveryLoopPendingAxes = sourceDiscoveryLoopPendingAxesHint;
deps.toNonEmptyString const sourceDiscoveryLoopProvidedAxes = sourceDiscoveryLoopProvidedAxesHint;
);
const sourceDiscoveryLoopPendingAxes = readAssistantMcpDiscoveryLoopPendingAxes(
carryoverSourceDebug,
deps.toNonEmptyString
);
const sourceDiscoveryLoopProvidedAxes = readAssistantMcpDiscoveryLoopProvidedAxes(
carryoverSourceDebug,
deps.toNonEmptyString
);
const sourceDiscoveryLoopAskedDomainFamily = readAssistantMcpDiscoveryLoopAskedDomainFamily( const sourceDiscoveryLoopAskedDomainFamily = readAssistantMcpDiscoveryLoopAskedDomainFamily(
carryoverSourceDebug, carryoverSourceDebug,
deps.toNonEmptyString deps.toNonEmptyString
@ -959,6 +1032,7 @@ export function createAssistantTransitionPolicy(deps) {
explicitIntentFamily && explicitIntentFamily &&
sourceIntentFamily !== explicitIntentFamily && sourceIntentFamily !== explicitIntentFamily &&
!hasOrganizationClarificationContinuation && !hasOrganizationClarificationContinuation &&
!mcpDiscoveryOrganizationClarificationContinuation &&
!hasImplicitContinuationSignal && !hasImplicitContinuationSignal &&
!hasIndexReferenceSignal && !hasIndexReferenceSignal &&
!hasInventoryRootTemporalFollowupPrimary && !hasInventoryRootTemporalFollowupPrimary &&
@ -967,6 +1041,8 @@ export function createAssistantTransitionPolicy(deps) {
!hasInventoryRootRestatementAlternate && !hasInventoryRootRestatementAlternate &&
!inventoryShortFollowupPrimary && !inventoryShortFollowupPrimary &&
!inventoryShortFollowupAlternate && !inventoryShortFollowupAlternate &&
!businessOverviewBoundaryFollowupPrimary &&
!businessOverviewBoundaryFollowupAlternate &&
!foreignAccountingPivotOverInventory && !foreignAccountingPivotOverInventory &&
!deps.hasFollowupMarker(userMessage) && !deps.hasFollowupMarker(userMessage) &&
!deps.hasReferentialPointer(userMessage) && !deps.hasReferentialPointer(userMessage) &&
@ -1027,24 +1103,29 @@ export function createAssistantTransitionPolicy(deps) {
hasSuggestedIntentPivotSignal || hasSuggestedIntentPivotSignal ||
Boolean(debtRoleSwapPrimary) || Boolean(debtRoleSwapPrimary) ||
shortValueFlowRetargetPrimary || shortValueFlowRetargetPrimary ||
businessOverviewBoundaryFollowupPrimary ||
inventoryShortFollowupPrimary || inventoryShortFollowupPrimary ||
inventoryPurchaseDateVatBridge || inventoryPurchaseDateVatBridge ||
explicitSummaryBundleReuseSignal || explicitSummaryBundleReuseSignal ||
hasInventoryRootTemporalFollowupPrimary; hasInventoryRootTemporalFollowupPrimary ||
mcpDiscoveryOrganizationClarificationContinuation;
hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage) hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage)
? deps.hasAddressFollowupContextSignal(alternateMessage) || ? deps.hasAddressFollowupContextSignal(alternateMessage) ||
hasSuggestedIntentPivotSignal || hasSuggestedIntentPivotSignal ||
Boolean(debtRoleSwapAlternate) || Boolean(debtRoleSwapAlternate) ||
shortValueFlowRetargetAlternate || shortValueFlowRetargetAlternate ||
businessOverviewBoundaryFollowupAlternate ||
inventoryShortFollowupAlternate || inventoryShortFollowupAlternate ||
inventoryPurchaseDateVatBridge || inventoryPurchaseDateVatBridge ||
explicitSummaryBundleReuseSignal || explicitSummaryBundleReuseSignal ||
hasInventoryRootTemporalFollowupAlternate hasInventoryRootTemporalFollowupAlternate ||
mcpDiscoveryOrganizationClarificationContinuation
: false; : false;
hasStrongFollowupReference = hasStrongFollowupReference =
hasPrimaryIndexReferenceSignal || hasPrimaryIndexReferenceSignal ||
hasAlternateIndexReferenceSignal || hasAlternateIndexReferenceSignal ||
hasOrganizationClarificationContinuation || hasOrganizationClarificationContinuation ||
mcpDiscoveryOrganizationClarificationContinuation ||
hasSuggestedIntentPivotSignal || hasSuggestedIntentPivotSignal ||
hasImplicitContinuationSignal || hasImplicitContinuationSignal ||
inventoryShortFollowupPrimary || inventoryShortFollowupPrimary ||
@ -1056,6 +1137,8 @@ export function createAssistantTransitionPolicy(deps) {
Boolean(debtRoleSwapIntent) || Boolean(debtRoleSwapIntent) ||
shortValueFlowRetargetPrimary || shortValueFlowRetargetPrimary ||
shortValueFlowRetargetAlternate || shortValueFlowRetargetAlternate ||
businessOverviewBoundaryFollowupPrimary ||
businessOverviewBoundaryFollowupAlternate ||
deps.hasFollowupMarker(userMessage) || deps.hasFollowupMarker(userMessage) ||
deps.hasReferentialPointer(userMessage) || deps.hasReferentialPointer(userMessage) ||
(deps.toNonEmptyString(alternateMessage) (deps.toNonEmptyString(alternateMessage)

View File

@ -0,0 +1,47 @@
export type CounterpartyRoleHint = "ordinary_counterparty" | "bank_or_financial_institution";
const FINANCIAL_INSTITUTION_PATTERNS: RegExp[] = [
/(?:^|[\s"«(,-])банк(?:$|[\s"»),.-])/u,
/сбербанк/u,
/(?:^|[\s"«(,-])сбер(?:$|[\s"»),.-])/u,
/(?:^|[\s"«(,-])втб(?:$|[\s"»),.-])/u,
/альфа[\s-]*банк/u,
/тинькофф/u,
/(?:^|[\s"«(,-])т[\s-]*банк(?:$|[\s"»),.-])/u,
/газпромбанк/u,
/росбанк/u,
/райффайзен/u,
/совкомбанк/u,
/промсвязьбанк/u,
/(?:^|[\s"«(,-])псб(?:$|[\s"»),.-])/u,
/(?:^|[\s"«(,-])мкб(?:$|[\s"»),.-])/u,
/ак[\s-]*барс/u,
/уралсиб/u,
/юникредит/u,
/почта[\s-]*банк/u,
/(?:^|[\s"«(,-])открытие(?:$|[\s"»),.-])/u,
/кредитн(?:ая|ый|ое|ые)\s+организац/u
];
function normalizeCounterpartyRoleText(value: unknown): string {
return String(value ?? "")
.toLowerCase()
.replace(/ё/g, "е")
.replace(/[._]+/g, " ")
.replace(/\s+/g, " ")
.trim();
}
export function isLikelyFinancialInstitutionCounterparty(value: unknown): boolean {
const normalized = normalizeCounterpartyRoleText(value);
if (!normalized) {
return false;
}
return FINANCIAL_INSTITUTION_PATTERNS.some((pattern) => pattern.test(normalized));
}
export function counterpartyRoleHintForName(value: unknown): CounterpartyRoleHint {
return isLikelyFinancialInstitutionCounterparty(value)
? "bank_or_financial_institution"
: "ordinary_counterparty";
}

View File

@ -18,6 +18,8 @@ export type AddressIntent =
| "vat_payable_forecast" | "vat_payable_forecast"
| "vat_liability_confirmed_for_tax_period" | "vat_liability_confirmed_for_tax_period"
| "vat_payable_confirmed_as_of_date" | "vat_payable_confirmed_as_of_date"
| "accounting_financial_result_for_organization"
| "debt_due_date_aging_for_organization"
| "open_contracts_confirmed_as_of_date" | "open_contracts_confirmed_as_of_date"
| "list_contracts_by_counterparty" | "list_contracts_by_counterparty"
| "list_open_contracts" | "list_open_contracts"
@ -189,6 +191,8 @@ export interface AddressRecipeDefinition {
| "vat_payable_forecast_profile" | "vat_payable_forecast_profile"
| "vat_liability_confirmed_tax_period_profile" | "vat_liability_confirmed_tax_period_profile"
| "vat_payable_confirmed_as_of_balance_profile" | "vat_payable_confirmed_as_of_balance_profile"
| "accounting_financial_result_profile"
| "debt_due_date_aging_profile"
| "open_contracts_confirmed_as_of_balance_profile" | "open_contracts_confirmed_as_of_balance_profile"
| "payables_confirmed_as_of_balance_profile" | "payables_confirmed_as_of_balance_profile"
| "receivables_confirmed_as_of_balance_profile" | "receivables_confirmed_as_of_balance_profile"

View File

@ -8,6 +8,24 @@ describe("addressIntentResolver regression bridges", () => {
expect(result.intent).toBe("vat_liability_confirmed_for_tax_period"); expect(result.intent).toBe("vat_liability_confirmed_for_tax_period");
}); });
it("detects VAT movement inspection wording with an explicit year", () => {
const result = resolveAddressIntent(
"\u043f\u043e\u043a\u0430\u0436\u0438 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u044f \u043f\u043e \u041d\u0414\u0421 \u0437\u0430 2020 \u043f\u043e \u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441"
);
expect(result.intent).toBe("vat_liability_confirmed_for_tax_period");
expect(result.reasons).toContain("vat_period_inspection_bridge_signal_detected");
});
it("detects canonical VAT charged-or-paid wording with an explicit year", () => {
const result = resolveAddressIntent(
"\u041a\u0430\u043a\u043e\u0439 \u041d\u0414\u0421 \u0431\u044b\u043b \u043d\u0430\u0447\u0438\u0441\u043b\u0435\u043d \u0438\u043b\u0438 \u0443\u043f\u043b\u0430\u0447\u0435\u043d \u0432 2020 \u0433\u043e\u0434\u0443 \u043f\u043e \u043a\u043e\u043c\u043f\u0430\u043d\u0438\u0438 \u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441?"
);
expect(result.intent).toBe("vat_liability_confirmed_for_tax_period");
expect(result.reasons).toContain("vat_liability_explicit_period_bridge_signal_detected");
});
it("detects payables snapshot wording in plain human form", () => { it("detects payables snapshot wording in plain human form", () => {
const result = resolveAddressIntent("мы должны комуто денег на сегодня?"); const result = resolveAddressIntent("мы должны комуто денег на сегодня?");

View File

@ -1336,9 +1336,9 @@ describe("address compose stage utf8 headers", () => {
{ userMessage: "Сколько у нас заказчиков, поставщиков и смешанных контрагентов?" } { userMessage: "Сколько у нас заказчиков, поставщиков и смешанных контрагентов?" }
); );
expect(reply.text).toContain("Роли контрагентов по активности:"); expect(reply.text).toContain("Распределение ролей по активности:");
expect(reply.text).toContain("Заказчики (только customer-роль): 122."); expect(reply.text).toContain("Заказчики с ролью покупателя: 122.");
expect(reply.text).toContain("Поставщики (только supplier-роль): 71."); expect(reply.text).toContain("Поставщики с ролью поставщика: 71.");
expect(reply.text).toContain("Смешанные (и покупатель, и поставщик): 23."); expect(reply.text).toContain("Смешанные (и покупатель, и поставщик): 23.");
expect(reply.text).not.toContain("Всего уникальных контрагентов в базе"); expect(reply.text).not.toContain("Всего уникальных контрагентов в базе");
}); });
@ -1391,7 +1391,7 @@ describe("address compose stage utf8 headers", () => {
{ userMessage: "скока поставщиков в базе" } { userMessage: "скока поставщиков в базе" }
); );
expect(reply.text).toContain("Поставщиков (только supplier-роль): 71."); expect(reply.text).toContain("Поставщиков с ролью поставщика: 71.");
expect(reply.text).not.toContain("Роли контрагентов по активности:"); expect(reply.text).not.toContain("Роли контрагентов по активности:");
expect(reply.text).not.toContain("Всего уникальных контрагентов в базе"); expect(reply.text).not.toContain("Всего уникальных контрагентов в базе");
}); });
@ -1444,7 +1444,7 @@ describe("address compose stage utf8 headers", () => {
{ userMessage: "скок клиентов" } { userMessage: "скок клиентов" }
); );
expect(reply.text).toContain("Заказчиков (только customer-роль): 122."); expect(reply.text).toContain("Заказчиков с ролью покупателя: 122.");
expect(reply.text).not.toContain("Роли контрагентов по активности:"); expect(reply.text).not.toContain("Роли контрагентов по активности:");
expect(reply.text).not.toContain("Всего уникальных контрагентов в базе"); expect(reply.text).not.toContain("Всего уникальных контрагентов в базе");
}); });
@ -1711,7 +1711,7 @@ describe("address compose stage utf8 headers", () => {
expect(reply.responseType).toBe("FACTUAL_LIST"); expect(reply.responseType).toBe("FACTUAL_LIST");
expect(reply.text).toContain("по максимальной сумме одной входящей операции"); expect(reply.text).toContain("по максимальной сумме одной входящей операции");
expect(reply.text).toContain("1. Клиент Б | max single: 1200"); expect(reply.text).toContain("1. Клиент Б | максимальная разовая сумма: 1.200,00 ₽");
}); });
it("renders supplier payout list by operations count", () => { it("renders supplier payout list by operations count", () => {
@ -2033,7 +2033,7 @@ describe("address compose stage utf8 headers", () => {
); );
expect(reply.responseType).toBe("FACTUAL_SUMMARY"); expect(reply.responseType).toBe("FACTUAL_SUMMARY");
expect(reply.text).toContain("Покрытие VAT-источников через MCP"); expect(reply.text).toContain("Покрытие VAT-источников в 1С");
expect(reply.text).toContain("Найдено VAT-объектов: 5"); expect(reply.text).toContain("Найдено VAT-объектов: 5");
expect(reply.text).toContain("РегистрНакопления.НДСПродажи"); expect(reply.text).toContain("РегистрНакопления.НДСПродажи");
}); });
@ -2063,12 +2063,14 @@ describe("address compose stage utf8 headers", () => {
userMessage: "сколько платить ндс в налоговую за декабрь 2019", userMessage: "сколько платить ндс в налоговую за декабрь 2019",
periodFrom: "2019-10-01", periodFrom: "2019-10-01",
periodTo: "2019-12-31", periodTo: "2019-12-31",
organizationHint: "ООО Альтернатива Плюс",
useRubCurrency: true useRubCurrency: true
} }
); );
expect(reply.responseType).toBe("FACTUAL_SUMMARY"); expect(reply.responseType).toBe("FACTUAL_SUMMARY");
expect(reply.text).toContain("Коротко: подтвержденный НДС к уплате за налоговый период"); expect(reply.text).toContain("Коротко: подтвержденный НДС к уплате за налоговый период по организации ООО Альтернатива Плюс");
expect(reply.text).toContain("- Организация: ООО Альтернатива Плюс.");
expect(reply.text).toContain("50.000,00 ₽"); expect(reply.text).toContain("50.000,00 ₽");
expect(reply.semantics?.result_mode).toBe("confirmed_balance"); expect(reply.semantics?.result_mode).toBe("confirmed_balance");
expect(reply.semantics?.balance_confirmed).toBe(true); expect(reply.semantics?.balance_confirmed).toBe(true);
@ -2214,7 +2216,7 @@ describe("address compose stage utf8 headers", () => {
); );
expect(reply.responseType).toBe("FACTUAL_LIST"); expect(reply.responseType).toBe("FACTUAL_LIST");
expect(reply.text).toContain("Блок 2.1. MCP-проверка VAT-источников"); expect(reply.text).toContain("Блок 2.1. Проверка VAT-источников в 1С");
expect(reply.text).toContain("VAT-объектов в метаданных 1С: 3"); expect(reply.text).toContain("VAT-объектов в метаданных 1С: 3");
expect(reply.text).toContain("Источников с движениями до даты среза: 1"); expect(reply.text).toContain("Источников с движениями до даты среза: 1");
expect(reply.text).toContain("РегистрНакопления.НДСНачисленный"); expect(reply.text).toContain("РегистрНакопления.НДСНачисленный");
@ -2247,7 +2249,7 @@ describe("address compose stage utf8 headers", () => {
); );
expect(reply.responseType).toBe("FACTUAL_LIST"); expect(reply.responseType).toBe("FACTUAL_LIST");
expect(reply.text).toContain("Probe VAT-источников завершился ошибкой"); expect(reply.text).toContain("Дополнительная проверка VAT-источников завершилась ошибкой");
}); });
}); });
@ -5099,13 +5101,13 @@ describe("address recipe catalog counterparty filtering", () => {
expect(aging.extracted_filters.limit).toBeUndefined(); expect(aging.extracted_filters.limit).toBeUndefined();
}); });
it("selects customer value recipe and keeps top-20 default", () => { it("selects customer value recipe and keeps expanded top-200 default", () => {
const selected = selectAddressRecipe("customer_revenue_and_payments", {}); const selected = selectAddressRecipe("customer_revenue_and_payments", {});
expect(selected.selected_recipe).toBeTruthy(); expect(selected.selected_recipe).toBeTruthy();
const plan = buildAddressRecipePlan(selected.selected_recipe!, {}); const plan = buildAddressRecipePlan(selected.selected_recipe!, {});
expect(plan.recipe.recipe_id).toBe("address_customer_revenue_and_payments_v1"); expect(plan.recipe.recipe_id).toBe("address_customer_revenue_and_payments_v1");
expect(plan.limit).toBe(20); expect(plan.limit).toBe(200);
expect(plan.query).toContain("ПоступлениеНаРасчетныйСчет"); expect(plan.query).toContain("ПоступлениеНаРасчетныйСчет");
expect(plan.query).toContain("БанкПоступление.ДоговорКонтрагента"); expect(plan.query).toContain("БанкПоступление.ДоговорКонтрагента");
}); });
@ -5138,13 +5140,13 @@ describe("address recipe catalog counterparty filtering", () => {
expect(plan.limit).toBe(1000); expect(plan.limit).toBe(1000);
}); });
it("selects supplier payouts recipe and keeps top-20 default", () => { it("selects supplier payouts recipe and keeps expanded top-200 default", () => {
const selected = selectAddressRecipe("supplier_payouts_profile", {}); const selected = selectAddressRecipe("supplier_payouts_profile", {});
expect(selected.selected_recipe).toBeTruthy(); expect(selected.selected_recipe).toBeTruthy();
const plan = buildAddressRecipePlan(selected.selected_recipe!, {}); const plan = buildAddressRecipePlan(selected.selected_recipe!, {});
expect(plan.recipe.recipe_id).toBe("address_supplier_payouts_profile_v1"); expect(plan.recipe.recipe_id).toBe("address_supplier_payouts_profile_v1");
expect(plan.limit).toBe(20); expect(plan.limit).toBe(200);
expect(plan.query).toContain("СписаниеСРасчетногоСчета"); expect(plan.query).toContain("СписаниеСРасчетногоСчета");
expect(plan.query).toContain("БанкСписание.ДоговорКонтрагента"); expect(plan.query).toContain("БанкСписание.ДоговорКонтрагента");
}); });
@ -5343,6 +5345,26 @@ describe("address recipe catalog counterparty filtering", () => {
expect(plan.query).toContain("ПОДСТРОКА(ЕСТЬNULL(Остатки.Счет.Код, \"\"), 1, 2) = \"60\""); expect(plan.query).toContain("ПОДСТРОКА(ЕСТЬNULL(Остатки.Счет.Код, \"\"), 1, 2) = \"60\"");
}); });
it("builds debt due-date aging query without carrying noisy organization suffix", () => {
const selected = selectAddressRecipe("debt_due_date_aging_for_organization", {
as_of_date: "2020-12-31",
organization: "ООО Альтернатива Плюс на конец 2020 можно точно понять"
});
expect(selected.selected_recipe?.recipe_id).toBe("address_debt_due_date_aging_for_organization_v1");
const plan = buildAddressRecipePlan(selected.selected_recipe!, {
as_of_date: "2020-12-31",
organization: "ООО Альтернатива Плюс на конец 2020 можно точно понять"
});
expect(plan.query).toContain("УстановленСрокОплаты");
expect(plan.query).toContain('Наименование ПОДОБНО "%Альтернатива%"');
expect(plan.query).toContain('Наименование ПОДОБНО "%Плюс%"');
expect(plan.query).not.toContain('Наименование ПОДОБНО "%конец%"');
expect(plan.query).not.toContain('Наименование ПОДОБНО "%2020%"');
expect(plan.query).not.toContain('Наименование ПОДОБНО "%можно%"');
expect(plan.query).not.toContain('Наименование ПОДОБНО "%понять%"');
});
it("injects account condition into movements query for account snapshot", () => { it("injects account condition into movements query for account snapshot", () => {
const filters = extractAddressFilters( const filters = extractAddressFilters(
"Какой остаток по счету 60 на дату 2020-07-31", "Какой остаток по счету 60 на дату 2020-07-31",

View File

@ -180,6 +180,76 @@ describe("assistant MCP discovery answer adapter", () => {
expect(draft.must_not_claim).toContain("Do not present the confirmed movement rows as a complete movement universe."); expect(draft.must_not_claim).toContain("Do not present the confirmed movement rows as a complete movement universe.");
}); });
it("does not present bank-like ranked value-flow leaders as ordinary customers", () => {
const draft = buildAssistantMcpDiscoveryAnswerDraft({
pilot_status: "executed",
pilot_scope: "counterparty_ranked_value_flow_query_movements_v1",
dry_run: false,
mcp_execution_performed: true,
executed_primitives: ["query_movements"],
skipped_primitives: [],
probe_results: [],
evidence: {
evidence_status: "confirmed",
answer_permission: "confirmed_answer",
confirmed_facts: [],
inferred_facts: [],
unknown_facts: [],
query_limitations: [],
reason_codes: [],
query_plan: {}
},
source_rows_summary: "2 MCP movement rows fetched, 2 matched ranking scope",
derived_metadata_surface: null,
derived_entity_resolution: null,
derived_activity_period: null,
derived_value_flow: null,
derived_bidirectional_value_flow: null,
derived_business_overview: null,
derived_ranked_value_flow: {
organization_scope: "ООО Альтернатива Плюс",
period_scope: "2020",
value_flow_direction: "incoming_customer_revenue",
ranking_need: "top_desc",
aggregation_axis: "counterparty",
coverage_limited_by_probe_limit: false,
ranked_values: [
{
axis_value: "СБЕРБАНК, ПАО",
total_amount: 12792194.31,
total_amount_human_ru: "12 792 194,31 руб.",
rows_with_amount: 3,
rows_matched: 3,
first_movement_date: "2020-01-15",
latest_movement_date: "2020-12-20",
counterparty_role_hint: "bank_or_financial_institution"
},
{
axis_value: "Группа СВК",
total_amount: 12093465,
total_amount_human_ru: "12 093 465 руб.",
rows_with_amount: 2,
rows_matched: 2,
first_movement_date: "2020-02-15",
latest_movement_date: "2020-11-10",
counterparty_role_hint: "ordinary_counterparty"
}
],
inference_basis: "confirmed_1c_movement_rows_grouped_by_counterparty"
},
query_limitations: [],
reason_codes: ["pilot_derived_ranked_value_flow_from_confirmed_rows"]
} as any);
const confirmed = draft.confirmed_lines.join("\n");
expect(confirmed).toContain("Крупнейший входящий денежный источник СБЕРБАНК, ПАО");
expect(confirmed).toContain("не называю это клиентской выручкой");
expect(confirmed).not.toContain("Больше всего денег принёс контрагент СБЕРБАНК");
expect(draft.must_not_claim).toContain(
"Do not present bank-like counterparties as ordinary customers, suppliers, revenue, procurement dependency, or business quality evidence without payment-purpose/contract proof."
);
});
it("turns business overview multi-probe evidence into an analyst-safe draft", async () => { it("turns business overview multi-probe evidence into an analyst-safe draft", async () => {
const planner = planAssistantMcpDiscovery({ const planner = planAssistantMcpDiscovery({
dataNeedGraph: { dataNeedGraph: {
@ -355,6 +425,7 @@ describe("assistant MCP discovery answer adapter", () => {
{ rows: [] }, { rows: [] },
{ rows: [] }, { rows: [] },
{ rows: [] }, { rows: [] },
{ rows: [] },
{ {
rows: [ rows: [
{ Период: "2020-01-15T00:00:00", Регистратор: "Поступление 1" }, { Период: "2020-01-15T00:00:00", Регистратор: "Поступление 1" },
@ -429,6 +500,7 @@ describe("assistant MCP discovery answer adapter", () => {
{ rows: [{ Period: "2020-01-15T00:00:00", Amount: 120000, Counterparty: "Клиент А" }] }, { rows: [{ Period: "2020-01-15T00:00:00", Amount: 120000, Counterparty: "Клиент А" }] },
{ rows: [{ Period: "2020-01-20T00:00:00", Amount: 50000, Counterparty: "Поставщик А" }] }, { rows: [{ Period: "2020-01-20T00:00:00", Amount: 50000, Counterparty: "Поставщик А" }] },
{ rows: [] }, { rows: [] },
{ rows: [] },
{ {
rows: [ rows: [
{ Period: "2020-12-31T00:00:00", Amount: 100000, Counterparty: "Клиент А" } { Period: "2020-12-31T00:00:00", Amount: 100000, Counterparty: "Клиент А" }
@ -528,6 +600,7 @@ describe("assistant MCP discovery answer adapter", () => {
{ rows: [] }, { rows: [] },
{ rows: [] }, { rows: [] },
{ rows: [] }, { rows: [] },
{ rows: [] },
{ {
rows: [ rows: [
{ Period: "2020-12-31T00:00:00", Amount: 250000, Quantity: 10, Item: "Товар А" }, { Period: "2020-12-31T00:00:00", Amount: 250000, Quantity: 10, Item: "Товар А" },
@ -576,6 +649,73 @@ describe("assistant MCP discovery answer adapter", () => {
expect(draft.must_not_claim).toContain("Do not present business overview inventory staleness risk proxy as confirmed obsolete stock, reserve, write-off, or liquidation value."); expect(draft.must_not_claim).toContain("Do not present business overview inventory staleness risk proxy as confirmed obsolete stock, reserve, write-off, or liquidation value.");
}); });
it("answers inventory reserve boundary questions direct-first before broad overview detail", async () => {
const planner = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
business_fact_family: "business_overview",
action_family: "inventory_reserve_boundary",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: null,
ranking_need: null,
proof_expectation: "bounded_inference",
clarification_gaps: [],
decomposition_candidates: ["collect_scoped_movements", "probe_coverage", "explain_evidence_basis"],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_profit_or_margin_claim_without_evidence"],
reason_codes: ["data_need_graph_built", "data_need_graph_family_business_overview"]
},
turnMeaning: {
asked_domain_family: "business_overview",
asked_action_family: "inventory_reserve_boundary",
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020",
unsupported_but_understood_family: "inventory_reserve_liquidation_boundary"
}
});
const pilot = await executeAssistantMcpDiscoveryPilot(
planner,
buildSequentialDeps([
{ rows: [{ Period: "2020-01-15T00:00:00", Amount: 120000, Counterparty: "Клиент А" }] },
{ rows: [{ Period: "2020-01-20T00:00:00", Amount: 50000, Counterparty: "Поставщик А" }] },
{ rows: [] },
{ rows: [] },
{ rows: [] },
{ rows: [] },
{ rows: [] },
{
rows: [
{ Period: "2020-12-31T00:00:00", Amount: 250000, Quantity: 10, Item: "Товар А" }
]
},
{
rows: [
{ Period: "2020-01-10T00:00:00", Amount: 200000, Quantity: 8, Item: "Товар А" }
]
},
{ rows: [{ Period: "2020-01-15T00:00:00", Registrator: "Поступление 1" }] },
{
rows: [
{ Period: "2020-03-01T00:00:00", Amount: 600000, Item: "Товар А", Counterparty: "Клиент А", AccountKt: "41.01" },
{ Period: "2020-02-01T00:00:00", Amount: 240000, Item: "Товар А", Counterparty: "Поставщик А", AccountDt: "41.01" }
]
}
])
);
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
expect(draft.headline).toContain(
"\u0442\u043e\u0447\u043d\u043e \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c \u0440\u0435\u0437\u0435\u0440\u0432"
);
expect(draft.headline).toContain("\u043d\u0435\u043b\u044c\u0437\u044f");
expect(draft.headline).toContain("staleness-risk proxy");
expect(draft.headline).not.toContain("бизнес-обзор");
expect(draft.must_not_claim).toContain("Do not present business overview inventory staleness risk proxy as confirmed obsolete stock, reserve, write-off, or liquidation value.");
});
it("renders metadata-scoped movement all-time follow-up as an all-time bounded answer", async () => { it("renders metadata-scoped movement all-time follow-up as an all-time bounded answer", async () => {
const planner = planAssistantMcpDiscovery({ const planner = planAssistantMcpDiscovery({
dataNeedGraph: { dataNeedGraph: {
@ -796,7 +936,9 @@ describe("assistant MCP discovery answer adapter", () => {
expect(draft.answer_mode).toBe("confirmed_with_bounded_inference"); expect(draft.answer_mode).toBe("confirmed_with_bounded_inference");
expect(draft.confirmed_lines).toHaveLength(1); expect(draft.confirmed_lines).toHaveLength(1);
expect(userText).toContain("\u0411\u043e\u043b\u044c\u0448\u0435 \u0432\u0441\u0435\u0433\u043e \u0434\u0435\u043d\u0435\u0433 \u043f\u0440\u0438\u043d\u0451\u0441 \u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442"); expect(userText).toContain("Крупнейший входящий денежный источник");
expect(userText).toContain("не называю это клиентской выручкой");
expect(userText).not.toContain("\u0411\u043e\u043b\u044c\u0448\u0435 \u0432\u0441\u0435\u0433\u043e \u0434\u0435\u043d\u0435\u0433 \u043f\u0440\u0438\u043d\u0451\u0441 \u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442 \u0421\u0411\u0415\u0420\u0411\u0410\u041d\u041a");
expect(userText).toContain("\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441"); expect(userText).toContain("\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441");
expect(userText).not.toContain("1C incoming value-flow"); expect(userText).not.toContain("1C incoming value-flow");
expect(userText).not.toContain("Full ranking outside"); expect(userText).not.toContain("Full ranking outside");
@ -1409,7 +1551,7 @@ describe("assistant MCP discovery answer adapter", () => {
unsupported_but_understood_family: "counterparty_payouts_or_outflow" unsupported_but_understood_family: "counterparty_payouts_or_outflow"
} }
}); });
const broadRows = Array.from({ length: 100 }, (_, index) => ({ const broadRows = Array.from({ length: 200 }, (_, index) => ({
Period: `2020-01-${String((index % 28) + 1).padStart(2, "0")}T00:00:00`, Period: `2020-01-${String((index % 28) + 1).padStart(2, "0")}T00:00:00`,
Amount: 10, Amount: 10,
Counterparty: "SVK" Counterparty: "SVK"

View File

@ -26,6 +26,25 @@ function entryPointContract(overrides: Record<string, unknown> = {}) {
selected_chain_matches_top: true selected_chain_matches_top: true
} }
}, },
route_candidate: {
schema_version: "assistant_mcp_route_candidate_v1",
policy_owner: "assistantMcpDiscoveryRuntimeBridge",
candidate_status: "ready_for_reviewed_execution",
selected_chain_id: "value_flow_ranking",
selected_chain_summary: "Rank value flow",
nearest_catalog_chain_template: "value_flow_ranking",
catalog_alignment_status: "selected_matches_top",
business_fact_family: "value_flow",
action_family: "turnover",
proof_expectation: "coverage_checked_fact",
required_axes: ["organization", "period"],
provided_axes: ["organization", "period"],
missing_axes: [],
executable_now: true,
enablement_reason: null,
recommended_next_action: "Execute through the reviewed runtime bridge and truth gate.",
forbidden_overclaim_flags: ["no_unchecked_fact_totals"]
},
answer_draft: { answer_draft: {
answer_mode: "confirmed_with_bounded_inference" answer_mode: "confirmed_with_bounded_inference"
} }
@ -54,6 +73,15 @@ describe("assistant MCP discovery debug attachment", () => {
expect(debug.mcp_discovery_catalog_chain_alignment_status).toBe("selected_matches_top"); expect(debug.mcp_discovery_catalog_chain_alignment_status).toBe("selected_matches_top");
expect(debug.mcp_discovery_catalog_chain_top_match).toBe("value_flow_ranking"); expect(debug.mcp_discovery_catalog_chain_top_match).toBe("value_flow_ranking");
expect(debug.mcp_discovery_catalog_chain_selected_matches_top).toBe(true); expect(debug.mcp_discovery_catalog_chain_selected_matches_top).toBe(true);
expect(debug.mcp_discovery_route_candidate_status).toBe("ready_for_reviewed_execution");
expect(debug.mcp_discovery_route_candidate_fact_family).toBe("value_flow");
expect(debug.mcp_discovery_route_candidate_action_family).toBe("turnover");
expect(debug.mcp_discovery_route_candidate_missing_axes).toEqual([]);
expect(debug.mcp_discovery_route_candidate_provided_axes).toEqual(["organization", "period"]);
expect(debug.mcp_discovery_route_candidate_executable_now).toBe(true);
expect(debug.mcp_discovery_route_candidate_next_action).toBe(
"Execute through the reviewed runtime bridge and truth gate."
);
expect(debug.mcp_discovery_answer_mode).toBe("confirmed_with_bounded_inference"); expect(debug.mcp_discovery_answer_mode).toBe("confirmed_with_bounded_inference");
expect(debug.mcp_discovery_business_fact_answer_allowed).toBe(true); expect(debug.mcp_discovery_business_fact_answer_allowed).toBe(true);
expect(debug.mcp_discovery_user_facing_response_allowed).toBe(true); expect(debug.mcp_discovery_user_facing_response_allowed).toBe(true);
@ -76,6 +104,11 @@ describe("assistant MCP discovery debug attachment", () => {
expect(debug.mcp_discovery_catalog_chain_alignment_status).toBeNull(); expect(debug.mcp_discovery_catalog_chain_alignment_status).toBeNull();
expect(debug.mcp_discovery_catalog_chain_top_match).toBeNull(); expect(debug.mcp_discovery_catalog_chain_top_match).toBeNull();
expect(debug.mcp_discovery_catalog_chain_selected_matches_top).toBe(false); expect(debug.mcp_discovery_catalog_chain_selected_matches_top).toBe(false);
expect(debug.mcp_discovery_route_candidate_v1).toBeNull();
expect(debug.mcp_discovery_route_candidate_status).toBeNull();
expect(debug.mcp_discovery_route_candidate_missing_axes).toEqual([]);
expect(debug.mcp_discovery_route_candidate_provided_axes).toEqual([]);
expect(debug.mcp_discovery_route_candidate_executable_now).toBe(false);
expect(debug.mcp_discovery_answer_mode).toBeNull(); expect(debug.mcp_discovery_answer_mode).toBeNull();
expect(debug.mcp_discovery_business_fact_answer_allowed).toBe(false); expect(debug.mcp_discovery_business_fact_answer_allowed).toBe(false);
expect(debug.mcp_discovery_user_facing_response_allowed).toBe(false); expect(debug.mcp_discovery_user_facing_response_allowed).toBe(false);

View File

@ -293,6 +293,80 @@ describe("assistant MCP discovery pilot executor", () => {
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(6); expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(6);
}); });
it("marks bank-like counterparties in business-overview rankings before evidence wording", async () => {
const planner = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
business_fact_family: "business_overview",
action_family: "broad_evaluation",
aggregation_need: null,
time_scope_need: "all_time_scope",
comparison_need: null,
ranking_need: null,
proof_expectation: "bounded_inference",
clarification_gaps: [],
decomposition_candidates: [
"collect_scoped_movements",
"aggregate_checked_amounts",
"aggregate_ranked_axis_values",
"fetch_supporting_documents",
"probe_coverage",
"explain_evidence_basis"
],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_profit_or_margin_claim_without_evidence"],
reason_codes: ["data_need_graph_built", "data_need_graph_family_business_overview"]
},
turnMeaning: {
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation",
explicit_organization_scope: "ООО Альтернатива Плюс"
}
});
const deps = buildSequentialDeps([
{
rows: [
{ Period: "2020-01-15T00:00:00", Amount: 1200000, Counterparty: "СБЕРБАНК, ПАО" },
{ Period: "2020-02-15T00:00:00", Amount: 800000, Counterparty: "Группа СВК" }
]
},
{
rows: [
{ Period: "2020-01-20T00:00:00", Amount: 650000, Counterparty: "СБЕРБАНК, ПАО" },
{ Period: "2020-03-20T00:00:00", Amount: 50000, Counterparty: "ООО Поставщик" }
]
},
{ rows: [] },
{ rows: [] },
{ rows: [] },
{ rows: [] }
]);
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
expect(result.derived_business_overview?.top_customers[0]).toMatchObject({
axis_value: "СБЕРБАНК, ПАО",
counterparty_role_hint: "bank_or_financial_institution"
});
expect(result.derived_business_overview?.top_customers[1]).toMatchObject({
axis_value: "Группа СВК",
counterparty_role_hint: "ordinary_counterparty"
});
expect(result.derived_business_overview?.top_suppliers[0]).toMatchObject({
axis_value: "СБЕРБАНК, ПАО",
counterparty_role_hint: "bank_or_financial_institution"
});
const confirmedFacts = result.evidence.confirmed_facts.join("\n");
const inferredFacts = result.evidence.inferred_facts.join("\n");
expect(confirmedFacts).toContain("Крупнейший входящий денежный источник");
expect(confirmedFacts).toContain("Крупнейший небанковский входящий контрагент");
expect(confirmedFacts).toContain("Крупнейший получатель исходящих денег");
expect(confirmedFacts).not.toContain("Самый крупный подтвержденный клиент в проверенном срезе: СБЕРБАНК");
expect(confirmedFacts).not.toContain("Самый крупный подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: СБЕРБАНК");
expect(inferredFacts).toContain("outgoing cash concentration proxy");
});
it("adds a checked VAT/tax family to business overview only when an explicit period is available", async () => { it("adds a checked VAT/tax family to business overview only when an explicit period is available", async () => {
const planner = planAssistantMcpDiscovery({ const planner = planAssistantMcpDiscovery({
dataNeedGraph: { dataNeedGraph: {
@ -347,6 +421,7 @@ describe("assistant MCP discovery pilot executor", () => {
{ rows: [] }, { rows: [] },
{ rows: [] }, { rows: [] },
{ rows: [] }, { rows: [] },
{ rows: [] },
{ {
rows: [ rows: [
{ Период: "2020-01-15T00:00:00", Регистратор: "Поступление 1" }, { Период: "2020-01-15T00:00:00", Регистратор: "Поступление 1" },
@ -402,9 +477,9 @@ describe("assistant MCP discovery pilot executor", () => {
expect(result.reason_codes).toContain("pilot_derived_business_overview_tax_position_from_confirmed_rows"); expect(result.reason_codes).toContain("pilot_derived_business_overview_tax_position_from_confirmed_rows");
expect(result.reason_codes).toContain("pilot_business_overview_trading_margin_query_mcp_executed"); expect(result.reason_codes).toContain("pilot_business_overview_trading_margin_query_mcp_executed");
expect(result.reason_codes).toContain("pilot_derived_business_overview_trading_margin_proxy_from_confirmed_rows"); expect(result.reason_codes).toContain("pilot_derived_business_overview_trading_margin_proxy_from_confirmed_rows");
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(13); expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(14);
const taxCall = deps.executeAddressMcpQuery.mock.calls[2]?.[0]; const taxCall = deps.executeAddressMcpQuery.mock.calls[2]?.[0];
const tradingMarginCall = deps.executeAddressMcpQuery.mock.calls[9]?.[0]; const tradingMarginCall = deps.executeAddressMcpQuery.mock.calls[10]?.[0];
expect(String(taxCall?.query ?? "")).toContain("НДСЗаписиКнигиПродаж"); expect(String(taxCall?.query ?? "")).toContain("НДСЗаписиКнигиПродаж");
expect(String(taxCall?.query ?? "")).toContain("НДСЗаписиКнигиПокупок"); expect(String(taxCall?.query ?? "")).toContain("НДСЗаписиКнигиПокупок");
expect(String(tradingMarginCall?.query ?? "")).toContain("Документ.РеализацияТоваровУслуг.Товары"); expect(String(tradingMarginCall?.query ?? "")).toContain("Документ.РеализацияТоваровУслуг.Товары");
@ -453,6 +528,7 @@ describe("assistant MCP discovery pilot executor", () => {
{ rows: [] }, { rows: [] },
{ rows: [] }, { rows: [] },
{ rows: [] }, { rows: [] },
{ rows: [] },
{ {
rows: [ rows: [
{ Period: "2020-03-01T00:00:00", Amount: 100000, Item: "Товар А", Counterparty: "Клиент А", AccountKt: "41.01" }, { Period: "2020-03-01T00:00:00", Amount: 100000, Item: "Товар А", Counterparty: "Клиент А", AccountKt: "41.01" },
@ -510,6 +586,7 @@ describe("assistant MCP discovery pilot executor", () => {
{ rows: [{ Period: "2020-01-15T00:00:00", Amount: 120000, Counterparty: "Клиент А" }] }, { rows: [{ Period: "2020-01-15T00:00:00", Amount: 120000, Counterparty: "Клиент А" }] },
{ rows: [{ Period: "2020-01-20T00:00:00", Amount: 50000, Counterparty: "Поставщик А" }] }, { rows: [{ Period: "2020-01-20T00:00:00", Amount: 50000, Counterparty: "Поставщик А" }] },
{ rows: [] }, { rows: [] },
{ rows: [] },
{ {
rows: [ rows: [
{ Period: "2020-12-31T00:00:00", Amount: 70000, Counterparty: "Клиент А" }, { Period: "2020-12-31T00:00:00", Amount: 70000, Counterparty: "Клиент А" },
@ -602,15 +679,107 @@ describe("assistant MCP discovery pilot executor", () => {
expect(result.reason_codes).toContain("pilot_derived_business_overview_open_settlement_quality_from_confirmed_rows"); expect(result.reason_codes).toContain("pilot_derived_business_overview_open_settlement_quality_from_confirmed_rows");
expect(result.reason_codes).toContain("pilot_derived_business_overview_debt_age_signal_from_contract_dates"); expect(result.reason_codes).toContain("pilot_derived_business_overview_debt_age_signal_from_contract_dates");
expect(result.reason_codes).toContain("pilot_derived_business_overview_debt_staleness_risk_proxy_from_confirmed_rows"); expect(result.reason_codes).toContain("pilot_derived_business_overview_debt_staleness_risk_proxy_from_confirmed_rows");
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(13); expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(14);
const receivablesCall = deps.executeAddressMcpQuery.mock.calls[3]?.[0]; const receivablesCall = deps.executeAddressMcpQuery.mock.calls[4]?.[0];
const payablesCall = deps.executeAddressMcpQuery.mock.calls[4]?.[0]; const payablesCall = deps.executeAddressMcpQuery.mock.calls[5]?.[0];
const openContractsCall = deps.executeAddressMcpQuery.mock.calls[5]?.[0]; const openContractsCall = deps.executeAddressMcpQuery.mock.calls[6]?.[0];
expect(String(receivablesCall?.query ?? "")).toContain("62"); expect(String(receivablesCall?.query ?? "")).toContain("62");
expect(String(payablesCall?.query ?? "")).toContain("60"); expect(String(payablesCall?.query ?? "")).toContain("60");
expect(String(openContractsCall?.query ?? "")).toContain("СуммаРазвернутыйОстатокКт"); expect(String(openContractsCall?.query ?? "")).toContain("СуммаРазвернутыйОстатокКт");
}); });
it("checks debt due-date aging with contract payment terms before claiming overdue debt", async () => {
const planner = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
business_fact_family: "business_overview",
action_family: "debt_due_date_boundary",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: null,
ranking_need: null,
proof_expectation: "due_date_aging",
clarification_gaps: [],
decomposition_candidates: [
"collect_scoped_movements",
"aggregate_checked_amounts",
"aggregate_ranked_axis_values",
"fetch_supporting_documents",
"probe_coverage",
"explain_evidence_basis"
],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_overdue_claim"],
reason_codes: ["data_need_graph_built", "data_need_graph_family_business_overview"]
},
turnMeaning: {
asked_domain_family: "business_overview",
asked_action_family: "debt_due_date_boundary",
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020"
}
});
const deps = buildSequentialDeps([
{ rows: [] },
{ rows: [] },
{ rows: [] },
{ rows: [] },
{ rows: [] },
{ rows: [] },
{
rows: [
{ Period: "2020-12-31T00:00:00", Amount: 100000, Counterparty: "Клиент А", Contract: "Договор А от 10.02.2019" }
]
},
{
rows: [
{
Period: "2020-12-31T00:00:00",
Amount: 100000,
Counterparty: "Клиент А",
Contract: "Договор А от 10.02.2019",
ДокументРасчетов: "Реализация товаров от 10.03.2020",
УстановленСрокОплаты: false,
СрокОплаты: 0
}
]
},
{ rows: [] },
{ rows: [] },
{ rows: [] },
{ rows: [] },
{ rows: [] },
{ rows: [] },
{ rows: [] }
]);
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
expect(result.derived_business_overview?.debt_due_date_aging).toMatchObject({
as_of_date: "2020-12-31",
rows_with_amount: 1,
gross_open_amount: 100000,
rows_with_payment_terms: 0,
rows_without_payment_terms: 1,
overdue_rows: 0,
evidence_status: "no_payment_terms_configured"
});
expect(result.derived_business_overview?.missing_signal_families).not.toContain("debt_due_date_aging_quality");
expect(result.derived_business_overview?.missing_proof_families.map((item) => item.family)).not.toContain(
"debt_due_date_aging_quality"
);
expect(result.evidence.confirmed_facts.join("\n")).toContain("срок оплаты не установлен");
expect(result.evidence.confirmed_facts.join("\n")).toContain("Подтвержденной просрочки");
expect(result.evidence.unknown_facts.join("\n")).not.toContain("due-date aging этим бизнес-обзором не подтверждены");
expect(result.reason_codes).toContain("pilot_business_overview_debt_due_date_aging_query_mcp_executed");
expect(result.reason_codes).toContain("pilot_derived_business_overview_debt_due_date_aging_from_confirmed_rows");
expect(result.reason_codes).toContain("pilot_derived_business_overview_debt_due_date_aging_no_payment_terms_configured");
const dueDateCall = deps.executeAddressMcpQuery.mock.calls[7]?.[0];
expect(String(dueDateCall?.query ?? "")).toContain("УстановленСрокОплаты");
expect(String(dueDateCall?.query ?? "")).toContain("СрокОплаты");
});
it("adds a checked inventory-position family to business overview only as an as-of-date snapshot", async () => { it("adds a checked inventory-position family to business overview only as an as-of-date snapshot", async () => {
const planner = planAssistantMcpDiscovery({ const planner = planAssistantMcpDiscovery({
dataNeedGraph: { dataNeedGraph: {
@ -650,6 +819,7 @@ describe("assistant MCP discovery pilot executor", () => {
{ rows: [] }, { rows: [] },
{ rows: [] }, { rows: [] },
{ rows: [] }, { rows: [] },
{ rows: [] },
{ {
rows: [ rows: [
{ Period: "2020-12-31T00:00:00", Amount: 250000, Quantity: 10, Item: "Товар А" }, { Period: "2020-12-31T00:00:00", Amount: 250000, Quantity: 10, Item: "Товар А" },
@ -729,8 +899,8 @@ describe("assistant MCP discovery pilot executor", () => {
expect(result.reason_codes).toContain("pilot_derived_business_overview_inventory_position_from_confirmed_rows"); expect(result.reason_codes).toContain("pilot_derived_business_overview_inventory_position_from_confirmed_rows");
expect(result.reason_codes).toContain("pilot_derived_business_overview_inventory_turnover_proxy_from_confirmed_rows"); expect(result.reason_codes).toContain("pilot_derived_business_overview_inventory_turnover_proxy_from_confirmed_rows");
expect(result.reason_codes).toContain("pilot_derived_business_overview_inventory_staleness_risk_proxy_from_confirmed_rows"); expect(result.reason_codes).toContain("pilot_derived_business_overview_inventory_staleness_risk_proxy_from_confirmed_rows");
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(13); expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(14);
const inventoryCall = deps.executeAddressMcpQuery.mock.calls[6]?.[0]; const inventoryCall = deps.executeAddressMcpQuery.mock.calls[7]?.[0];
expect(inventoryCall?.account_scope).toContain("41.01"); expect(inventoryCall?.account_scope).toContain("41.01");
}); });
@ -1335,7 +1505,7 @@ describe("assistant MCP discovery pilot executor", () => {
unsupported_but_understood_family: "counterparty_payouts_or_outflow" unsupported_but_understood_family: "counterparty_payouts_or_outflow"
} }
}); });
const rows = Array.from({ length: 100 }, (_, index) => ({ const rows = Array.from({ length: 200 }, (_, index) => ({
Period: `2020-01-${String((index % 28) + 1).padStart(2, "0")}T00:00:00`, Period: `2020-01-${String((index % 28) + 1).padStart(2, "0")}T00:00:00`,
Amount: 10, Amount: 10,
Counterparty: "SVK" Counterparty: "SVK"
@ -1359,7 +1529,7 @@ describe("assistant MCP discovery pilot executor", () => {
unsupported_but_understood_family: "counterparty_payouts_or_outflow" unsupported_but_understood_family: "counterparty_payouts_or_outflow"
} }
}); });
const broadRows = Array.from({ length: 100 }, (_, index) => ({ const broadRows = Array.from({ length: 200 }, (_, index) => ({
Period: `2020-01-${String((index % 28) + 1).padStart(2, "0")}T00:00:00`, Period: `2020-01-${String((index % 28) + 1).padStart(2, "0")}T00:00:00`,
Amount: 10, Amount: 10,
Counterparty: "SVK" Counterparty: "SVK"
@ -1560,7 +1730,7 @@ describe("assistant MCP discovery pilot executor", () => {
unsupported_but_understood_family: "counterparty_bidirectional_value_flow_or_netting" unsupported_but_understood_family: "counterparty_bidirectional_value_flow_or_netting"
} }
}); });
const outgoingBroadRows = Array.from({ length: 100 }, (_, index) => ({ const outgoingBroadRows = Array.from({ length: 200 }, (_, index) => ({
Period: `2020-01-${String((index % 28) + 1).padStart(2, "0")}T00:00:00`, Period: `2020-01-${String((index % 28) + 1).padStart(2, "0")}T00:00:00`,
Amount: 10, Amount: 10,
Counterparty: "SVK" Counterparty: "SVK"

View File

@ -44,6 +44,177 @@ describe("assistant MCP discovery response candidate", () => {
expect(candidate.reply_text).not.toContain("primitive"); expect(candidate.reply_text).not.toContain("primitive");
}); });
it("keeps inventory reserve boundary answers direct instead of compacting into a money overview", () => {
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
entryPoint({
turn_input: {
adapter_status: "ready",
turn_meaning_ref: {
asked_domain_family: "business_overview",
asked_action_family: "inventory_reserve_boundary",
unsupported_but_understood_family: "inventory_reserve_liquidation_boundary"
},
data_need_graph: {
business_fact_family: "business_overview",
ranking_need: null,
reason_codes: ["data_need_graph_family_business_overview"]
}
},
bridge: {
bridge_status: "answer_draft_ready",
user_facing_response_allowed: true,
business_fact_answer_allowed: true,
requires_user_clarification: false,
pilot: {
pilot_scope: "business_overview_route_template_v1",
derived_business_overview: {
period_scope: null,
incoming_customer_revenue: {
total_amount_human_ru: "157 192 981,43 руб.",
coverage_limited_by_probe_limit: true
},
outgoing_supplier_payout: {
total_amount_human_ru: "35 439 044,74 руб.",
coverage_limited_by_probe_limit: true
},
net_amount_human_ru: "121 753 936,69 руб.",
net_direction: "net_incoming"
}
},
answer_draft: {
answer_mode: "confirmed_with_bounded_inference",
headline:
"\u041a\u043e\u0440\u043e\u0442\u043a\u043e: \u0442\u043e\u0447\u043d\u043e \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c \u0440\u0435\u0437\u0435\u0440\u0432 \u043f\u043e\u0434 \u043d\u0435\u043b\u0438\u043a\u0432\u0438\u0434\u044b \u043f\u043e \u0442\u0435\u043a\u0443\u0449\u0438\u043c \u0434\u0430\u043d\u043d\u044b\u043c \u043d\u0435\u043b\u044c\u0437\u044f.",
confirmed_lines: ["Денежный обзор здесь не является ответом на складской резерв."],
inference_lines: [],
unknown_lines: [
"\u0420\u0435\u0437\u0435\u0440\u0432\u044b, \u0441\u043f\u0438\u0441\u0430\u043d\u0438\u044f \u0438 \u043b\u0438\u043a\u0432\u0438\u0434\u0430\u0446\u0438\u043e\u043d\u043d\u0430\u044f \u0441\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u044c \u0441\u043a\u043b\u0430\u0434\u0430 \u043d\u0435 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u044b."
],
limitation_lines: [],
next_step_line: null
}
}
})
);
expect(candidate.reply_text).toContain(
"\u0442\u043e\u0447\u043d\u043e \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c \u0440\u0435\u0437\u0435\u0440\u0432"
);
expect(candidate.reply_text).toContain("\u041f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u043d\u0443\u0436\u043d\u043e");
expect(candidate.reply_text).not.toContain("157 192 981");
expect(candidate.reply_text).not.toContain("лимит");
});
it("keeps profit/margin boundary answers direct instead of compacting into a money overview", () => {
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
entryPoint({
turn_input: {
adapter_status: "ready",
turn_meaning_ref: {
asked_domain_family: "business_overview",
asked_action_family: "profit_margin_boundary",
unsupported_but_understood_family: "profit_margin_boundary"
},
data_need_graph: {
business_fact_family: "business_overview",
ranking_need: null,
reason_codes: ["data_need_graph_family_business_overview"]
}
},
bridge: {
bridge_status: "answer_draft_ready",
user_facing_response_allowed: true,
business_fact_answer_allowed: true,
requires_user_clarification: false,
pilot: {
pilot_scope: "business_overview_route_template_v1",
derived_business_overview: {
incoming_customer_revenue: {
total_amount_human_ru: "47 628 853,03 руб."
},
outgoing_supplier_payout: {
total_amount_human_ru: "19 568 878,06 руб."
},
net_amount_human_ru: "28 059 974,97 руб.",
net_direction: "net_incoming"
}
},
answer_draft: {
answer_mode: "confirmed_with_bounded_inference",
headline:
"Коротко: нельзя точно подтвердить чистую прибыль и маржу по текущему срезу 1С; есть только bounded operating-flow/trading-margin proxy, не P&L и не бухгалтерский финансовый результат.",
confirmed_lines: ["Денежный обзор здесь не является ответом на чистую прибыль."],
inference_lines: [],
unknown_lines: ["Для точного P&L нужны себестоимость, расходы и закрытие периода."],
limitation_lines: [],
next_step_line: null
}
}
})
);
expect(candidate.reply_text).toContain("нельзя точно подтвердить чистую прибыль");
expect(candidate.reply_text).toContain("P&L");
expect(candidate.reply_text).toContain("себестоимости");
expect(candidate.reply_text).not.toContain("47 628 853");
});
it("keeps vendor-risk boundary answers direct instead of compacting into a money overview", () => {
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
entryPoint({
turn_input: {
adapter_status: "ready",
turn_meaning_ref: {
asked_domain_family: "business_overview",
asked_action_family: "vendor_risk_procurement_boundary",
unsupported_but_understood_family: "vendor_risk_procurement_boundary"
},
data_need_graph: {
business_fact_family: "business_overview",
ranking_need: null,
reason_codes: ["data_need_graph_family_business_overview"]
}
},
bridge: {
bridge_status: "answer_draft_ready",
user_facing_response_allowed: true,
business_fact_answer_allowed: true,
requires_user_clarification: false,
pilot: {
pilot_scope: "business_overview_route_template_v1",
derived_business_overview: {
outgoing_supplier_payout: {
total_amount_human_ru: "19 568 878,06 руб."
},
top_suppliers: [
{
axis_value: "СБЕРБАНК, ПАО",
total_amount_human_ru: "6 653 022,52 руб."
}
]
}
},
answer_draft: {
answer_mode: "confirmed_with_bounded_inference",
headline: "Коротко: точный риск зависимости от одного поставщика по текущим данным не подтвержден.",
confirmed_lines: [],
inference_lines: [],
unknown_lines: ["Vendor-risk route не подключен."],
limitation_lines: [],
next_step_line: null
}
}
})
);
expect(candidate.reply_text).toContain("риск зависимости");
expect(candidate.reply_text).toContain("outgoing cash concentration proxy");
expect(candidate.reply_text).toContain("банк/финансовая организация");
expect(candidate.reply_text).toContain("не доказанная зависимость от обычного поставщика");
expect(candidate.reply_text).not.toContain("крупнейший подтвержденный поставщик/получатель исходящих платежей: СБЕРБАНК");
expect(candidate.reply_text).not.toContain("операционное нетто");
});
it("uses a compact direct answer for business-overview top year questions", () => { it("uses a compact direct answer for business-overview top year questions", () => {
const candidate = buildAssistantMcpDiscoveryResponseCandidate( const candidate = buildAssistantMcpDiscoveryResponseCandidate(
entryPoint({ entryPoint({
@ -114,13 +285,15 @@ describe("assistant MCP discovery response candidate", () => {
}) })
); );
expect(candidate.reply_text).toContain("в доступном проверенном MCP-срезе"); expect(candidate.reply_text).toContain("в доступном проверенном срезе 1С");
expect(candidate.reply_text).toContain("лидирует 2015"); expect(candidate.reply_text).toContain("лидирует 2015");
expect(candidate.reply_text).toContain("2015"); expect(candidate.reply_text).toContain("2015");
expect(candidate.reply_text).toContain("136 723 459,73 руб."); expect(candidate.reply_text).toContain("136 723 459,73 руб.");
expect(candidate.reply_text).toContain("не полный бухгалтерский рейтинг доходности"); expect(candidate.reply_text).toContain("не полный бухгалтерский рейтинг доходности");
expect(candidate.reply_text).toContain("не как чистую бухгалтерскую прибыль"); expect(candidate.reply_text).toContain("не как чистую бухгалтерскую прибыль");
expect(candidate.reply_text).toContain("лимит выборки MCP"); expect(candidate.reply_text).toContain("проверка достигла лимита строк");
expect(candidate.reply_text).not.toContain("лимит выборки MCP");
expect(candidate.reply_text).not.toContain("MCP-срез");
expect(candidate.reply_text).not.toContain("Что подтверждено:"); expect(candidate.reply_text).not.toContain("Что подтверждено:");
expect(candidate.reply_text).not.toContain("Профиль операционной активности"); expect(candidate.reply_text).not.toContain("Профиль операционной активности");
}); });
@ -197,6 +370,76 @@ describe("assistant MCP discovery response candidate", () => {
expect(candidate.reply_text).not.toContain("Складской срез"); expect(candidate.reply_text).not.toContain("Складской срез");
}); });
it("does not present bank-like incoming leaders as ordinary client revenue", () => {
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
entryPoint({
turn_input: {
adapter_status: "ready",
data_need_graph: {
business_fact_family: "business_overview",
ranking_need: null,
reason_codes: [
"data_need_graph_family_business_overview",
"data_need_graph_business_overview_direct_money_answer"
]
}
},
bridge: {
bridge_status: "answer_draft_ready",
user_facing_response_allowed: true,
business_fact_answer_allowed: true,
requires_user_clarification: false,
pilot: {
pilot_scope: "business_overview_route_template_v1",
derived_business_overview: {
period_scope: "2020",
incoming_customer_revenue: {
total_amount_human_ru: "24 885 659,31 руб.",
coverage_limited_by_probe_limit: false
},
outgoing_supplier_payout: {
total_amount_human_ru: "19 568 878,06 руб.",
coverage_limited_by_probe_limit: false
},
net_amount_human_ru: "5 316 781,25 руб.",
net_direction: "net_incoming",
top_customers: [
{
axis_value: "СБЕРБАНК, ПАО",
total_amount_human_ru: "12 792 194,31 руб.",
counterparty_role_hint: "bank_or_financial_institution"
},
{
axis_value: "Группа СВК",
total_amount_human_ru: "12 093 465 руб.",
counterparty_role_hint: "ordinary_counterparty"
}
],
top_suppliers: [],
yearly_breakdown: []
}
},
answer_draft: {
answer_mode: "confirmed_with_bounded_inference",
headline: "Ограниченный бизнес-обзор.",
confirmed_lines: [],
inference_lines: [],
unknown_lines: [],
limitation_lines: [],
next_step_line: null
}
}
})
);
const firstLine = candidate.reply_text?.split("\n")[0] ?? "";
expect(firstLine).toContain("крупнейший входящий денежный источник: СБЕРБАНК, ПАО");
expect(firstLine).toContain("не называю это клиентской выручкой");
expect(firstLine).toContain("крупнейший небанковский входящий контрагент: Группа СВК");
expect(candidate.reply_text).not.toContain("крупнейший источник входящих денег: СБЕРБАНК");
expect(candidate.reply_text).not.toContain("Самый крупный подтвержденный клиент");
});
it("mentions separate counterparty scope in company plus counterparty business summaries", () => { it("mentions separate counterparty scope in company plus counterparty business summaries", () => {
const candidate = buildAssistantMcpDiscoveryResponseCandidate( const candidate = buildAssistantMcpDiscoveryResponseCandidate(
entryPoint({ entryPoint({
@ -607,7 +850,7 @@ describe("assistant MCP discovery response candidate", () => {
requires_user_clarification: false, requires_user_clarification: false,
answer_draft: { answer_draft: {
answer_mode: "confirmed_with_bounded_inference", answer_mode: "confirmed_with_bounded_inference",
headline: "РџРѕ данным 1РЎ найдены строки исходящих платежей/списаний; СЃСѓРјРјСѓ РјРѕР¶РЅРѕ называть только РІ рамках проверенного периода Рё найденных строк.", headline: "По данным 1С найдены строки исходящих платежей/списаний; сумму можно называть только в рамках проверенного периода и найденных строк.",
confirmed_lines: ["1C supplier-payout rows were found for counterparty SVK"], confirmed_lines: ["1C supplier-payout rows were found for counterparty SVK"],
inference_lines: [ inference_lines: [
"Requested period coverage was recovered through monthly 1C value-flow probes after the broad probe hit the row limit" "Requested period coverage was recovered through monthly 1C value-flow probes after the broad probe hit the row limit"
@ -661,7 +904,7 @@ describe("assistant MCP discovery response candidate", () => {
requires_user_clarification: false, requires_user_clarification: false,
answer_draft: { answer_draft: {
answer_mode: "confirmed_with_bounded_inference", answer_mode: "confirmed_with_bounded_inference",
headline: "РџРѕ данным 1РЎ найдены строки движений; ответ ограничен проверенным периодом Рё найденными строками.", headline: "По данным 1С найдены строки движений; ответ ограничен проверенным периодом и найденными строками.",
confirmed_lines: ["1C movement rows were found for counterparty SVK"], confirmed_lines: ["1C movement rows were found for counterparty SVK"],
inference_lines: ["Counterparty movement evidence is limited to confirmed 1C movement rows in the checked scope"], inference_lines: ["Counterparty movement evidence is limited to confirmed 1C movement rows in the checked scope"],
unknown_lines: ["Full movement history outside the checked period is not proven by this MCP discovery pilot"], unknown_lines: ["Full movement history outside the checked period is not proven by this MCP discovery pilot"],
@ -672,9 +915,9 @@ describe("assistant MCP discovery response candidate", () => {
}) })
); );
expect(candidate.reply_text).toContain("Р 1РЎ найдены строки движений РїРѕ контрагенту SVK."); expect(candidate.reply_text).toContain("В 1С найдены строки движений по контрагенту SVK.");
expect(candidate.reply_text).toContain("Срез движений ограничен только подтвержденными строками движений"); expect(candidate.reply_text).toContain("Срез движений ограничен только подтвержденными строками движений");
expect(candidate.reply_text).toContain("Полный исторический срез движений РІРЅРµ проверенного периода этим РїРѕРёСЃРєРѕРј РЅРµ подтвержден."); expect(candidate.reply_text).toContain("Полный исторический срез движений вне проверенного периода этим поиском не подтвержден.");
expect(candidate.reply_text).not.toContain("1C movement rows were found"); expect(candidate.reply_text).not.toContain("1C movement rows were found");
}); });
@ -841,9 +1084,8 @@ describe("assistant MCP discovery response candidate", () => {
expect(candidate.reply_text).toContain( expect(candidate.reply_text).toContain(
"\u0412 1\u0421 \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u043d\u044b \u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0435 \u0438 \u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0435 \u0434\u0435\u043d\u0435\u0436\u043d\u044b\u0435 \u0441\u0442\u0440\u043e\u043a\u0438 \u0432 \u0437\u0430\u043f\u0440\u043e\u0448\u0435\u043d\u043d\u043e\u043c \u0441\u0440\u0435\u0437\u0435" "\u0412 1\u0421 \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u043d\u044b \u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0435 \u0438 \u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0435 \u0434\u0435\u043d\u0435\u0436\u043d\u044b\u0435 \u0441\u0442\u0440\u043e\u043a\u0438 \u0432 \u0437\u0430\u043f\u0440\u043e\u0448\u0435\u043d\u043d\u043e\u043c \u0441\u0440\u0435\u0437\u0435"
); );
expect(candidate.reply_text).toContain( expect(candidate.reply_text).toContain("Запрошенный период достиг лимита строк");
"\u0417\u0430\u043f\u0440\u043e\u0448\u0435\u043d\u043d\u044b\u0439 \u043f\u0435\u0440\u0438\u043e\u0434 \u0443\u043f\u0435\u0440\u0441\u044f \u0432 \u043b\u0438\u043c\u0438\u0442 \u0441\u0442\u0440\u043e\u043a MCP" expect(candidate.reply_text).not.toContain("уперся в лимит строк MCP");
);
expect(candidate.reply_text).not.toContain( expect(candidate.reply_text).not.toContain(
"\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0441\u043a\u043e\u043c\u0443 \u043a\u043e\u043d\u0442\u0443\u0440\u0443" "\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0441\u043a\u043e\u043c\u0443 \u043a\u043e\u043d\u0442\u0443\u0440\u0443"
); );

View File

@ -621,7 +621,7 @@ describe("assistant MCP discovery response policy", () => {
answer_mode: "confirmed_with_bounded_inference", answer_mode: "confirmed_with_bounded_inference",
headline: "Рейтинг по контрагентам построен по подтвержденным строкам 1С.", headline: "Рейтинг по контрагентам построен по подтвержденным строкам 1С.",
confirmed_lines: [ confirmed_lines: [
"Больше всего денег принёс контрагент СБЕРБАНК, ПАО по организации ООО Альтернатива Плюс за период 2020: 12 792 194,31 руб. по 9 строкам с суммой." "Крупнейший входящий денежный источник — СБЕРБАНК, ПАО по организации ООО Альтернатива Плюс за период 2020: 12 792 194,31 руб. по 9 строкам с суммой. Это банк/финансовая организация; без назначения платежа или договора не считаю это обычным клиентом или выручкой."
], ],
inference_lines: [ inference_lines: [
"Рейтинг по контрагентам по организации ООО Альтернатива Плюс за период 2020 рассчитан только по подтвержденным строкам 1С." "Рейтинг по контрагентам по организации ООО Альтернатива Плюс за период 2020 рассчитан только по подтвержденным строкам 1С."
@ -639,6 +639,7 @@ describe("assistant MCP discovery response policy", () => {
expect(result.decision).toBe("apply_candidate"); expect(result.decision).toBe("apply_candidate");
expect(result.reply_text).toContain("ООО Альтернатива Плюс"); expect(result.reply_text).toContain("ООО Альтернатива Плюс");
expect(result.reply_text).toContain("2020"); expect(result.reply_text).toContain("2020");
expect(result.reply_text).toContain("не считаю это обычным клиентом");
expect(result.reason_codes).toContain("mcp_discovery_response_policy_semantic_conflict_allows_candidate_override"); expect(result.reason_codes).toContain("mcp_discovery_response_policy_semantic_conflict_allows_candidate_override");
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_aligned_factual_address_reply"); expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_aligned_factual_address_reply");
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_factual_address_continuation_target"); expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_factual_address_continuation_target");

View File

@ -37,6 +37,22 @@ function buildBidirectionalDeps(
}; };
} }
function buildSequentialDeps(results: Array<{ rows: Array<Record<string, unknown>>; error?: string | null }>) {
const executeAddressMcpQuery = vi.fn(async () => {
const next = results.shift() ?? { rows: [] };
const rows = next.rows;
const error = next.error ?? null;
return {
fetched_rows: rows.length,
matched_rows: error ? 0 : rows.length,
raw_rows: rows,
rows: error ? [] : rows,
error
};
});
return { executeAddressMcpQuery };
}
describe("assistant MCP discovery runtime bridge", () => { describe("assistant MCP discovery runtime bridge", () => {
it("composes planner, pilot executor, and answer draft without wiring the hot runtime", async () => { it("composes planner, pilot executor, and answer draft without wiring the hot runtime", async () => {
const result = await runAssistantMcpDiscoveryRuntimeBridge({ const result = await runAssistantMcpDiscoveryRuntimeBridge({
@ -150,8 +166,24 @@ describe("assistant MCP discovery runtime bridge", () => {
expect(result.loop_state.catalog_chain_template_matches[0]).toBe("value_flow_ranking"); expect(result.loop_state.catalog_chain_template_matches[0]).toBe("value_flow_ranking");
expect(result.loop_state.catalog_chain_template_alignment.alignment_status).toBe("selected_matches_top"); expect(result.loop_state.catalog_chain_template_alignment.alignment_status).toBe("selected_matches_top");
expect(result.loop_state.catalog_chain_template_alignment.selected_chain_matches_top).toBe(true); expect(result.loop_state.catalog_chain_template_alignment.selected_chain_matches_top).toBe(true);
expect(result.route_candidate).toMatchObject({
schema_version: "assistant_mcp_route_candidate_v1",
candidate_status: "needs_user_scope",
selected_chain_id: "value_flow_ranking",
nearest_catalog_chain_template: "value_flow_ranking",
catalog_alignment_status: "selected_matches_top",
business_fact_family: "value_flow",
action_family: "turnover",
executable_now: false
});
expect(result.route_candidate.missing_axes).toContain("organization");
expect(result.route_candidate.provided_axes).toContain("aggregate_axis");
expect(result.route_candidate.recommended_next_action).toBe(
"Ask the user for the missing scope axes before MCP execution."
);
expect(result.reason_codes).toContain("planner_selected_chain_matches_catalog_top"); expect(result.reason_codes).toContain("planner_selected_chain_matches_catalog_top");
expect(result.reason_codes).toContain("runtime_bridge_loop_state_awaiting_clarification"); expect(result.reason_codes).toContain("runtime_bridge_loop_state_awaiting_clarification");
expect(result.reason_codes).toContain("runtime_bridge_route_candidate_needs_user_scope");
}); });
it("produces a bounded ranked value-flow answer when period and organization are known", async () => { it("produces a bounded ranked value-flow answer when period and organization are known", async () => {
@ -192,6 +224,12 @@ describe("assistant MCP discovery runtime bridge", () => {
axis_value: "СВК-А", axis_value: "СВК-А",
total_amount: 2100 total_amount: 2100
}); });
expect(result.route_candidate).toMatchObject({
candidate_status: "ready_for_reviewed_execution",
selected_chain_id: "value_flow_ranking",
executable_now: true,
enablement_reason: null
});
expect(result.answer_draft.confirmed_lines.join("\n")).toContain("СВК-А"); expect(result.answer_draft.confirmed_lines.join("\n")).toContain("СВК-А");
}); });
@ -437,6 +475,288 @@ describe("assistant MCP discovery runtime bridge", () => {
expect(userFacing).not.toContain("MCP discovery pilot"); expect(userFacing).not.toContain("MCP discovery pilot");
}); });
it("marks exact business-overview proof gaps as route enablement instead of reviewed execution", async () => {
const deps = buildSequentialDeps([
{ rows: [{ Period: "2020-01-15T00:00:00", Amount: 120000, Counterparty: "Client A" }] },
{ rows: [{ Period: "2020-01-20T00:00:00", Amount: 50000, Counterparty: "Supplier A" }] },
{ rows: [] },
{ rows: [] },
{ rows: [] },
{ rows: [] },
{ rows: [] },
{
rows: [
{ Period: "2020-12-31T00:00:00", Amount: 250000, Quantity: 10, Item: "Widget A" },
{ Period: "2020-12-31T00:00:00", Amount: 50000, Quantity: 5, Item: "Widget B" }
]
},
{
rows: [
{ Period: "2020-01-10T00:00:00", Amount: 200000, Quantity: 8, Item: "Widget A" },
{ Period: "2020-11-01T00:00:00", Amount: 50000, Quantity: 2, Item: "Widget B" }
]
},
{
rows: [
{ Period: "2020-01-15T00:00:00", Registrator: "Purchase 1" },
{ Period: "2020-12-15T00:00:00", Registrator: "Purchase 2" }
]
},
{
rows: [
{ Period: "2020-03-01T00:00:00", Amount: 600000, Item: "Widget A", Counterparty: "Client A", AccountKt: "41.01" },
{ Period: "2020-02-01T00:00:00", Amount: 240000, Item: "Widget A", Counterparty: "Supplier A", AccountDt: "41.01" }
]
}
]);
const result = await runAssistantMcpDiscoveryRuntimeBridge({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
business_fact_family: "business_overview",
action_family: "inventory_reserve_boundary",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: null,
ranking_need: null,
proof_expectation: "bounded_inference",
clarification_gaps: [],
decomposition_candidates: [
"collect_scoped_movements",
"aggregate_checked_amounts",
"fetch_supporting_documents",
"probe_coverage",
"explain_evidence_basis"
],
forbidden_overclaim_flags: [
"no_raw_model_claims",
"no_unchecked_fact_totals",
"no_unchecked_business_health_claim"
],
reason_codes: ["data_need_graph_built", "data_need_graph_family_business_overview"]
},
turnMeaning: {
asked_domain_family: "business_overview",
asked_action_family: "inventory_reserve_boundary",
explicit_organization_scope: "OOO Alternative Plus",
explicit_date_scope: "2020"
},
deps
});
expect(result.bridge_status).toBe("answer_draft_ready");
expect(result.business_fact_answer_allowed).toBe(true);
expect(result.route_candidate).toMatchObject({
candidate_status: "needs_route_enablement",
selected_chain_id: "business_overview",
business_fact_family: "business_overview",
action_family: "inventory_reserve_boundary",
executable_now: false
});
expect(result.route_candidate.enablement_reason).toContain("inventory_reserve_liquidation_quality");
expect(result.route_candidate.enablement_reason).toContain(
"reviewed_inventory_quality_route_with_reserves_writeoffs_obsolescence_and_liquidation_value"
);
expect(result.route_candidate.forbidden_overclaim_flags).toContain(
"confirmed_obsolete_stock_reserve_writeoff_or_liquidation_value"
);
expect(result.reason_codes).toContain("runtime_bridge_route_candidate_needs_route_enablement");
});
it("promotes profit-margin boundary when accounting 90/91/99 proof is available", async () => {
const deps = buildSequentialDeps([
{ rows: [{ Period: "2020-01-15T00:00:00", Amount: 12012833.72, Counterparty: "Client A" }] },
{ rows: [{ Period: "2020-01-20T00:00:00", Amount: 6430415.34, Counterparty: "Supplier A" }] },
{ rows: [] },
{
rows: [
{ Period: "2020-12-31T00:00:00", Registrator: "ACC90_REVENUE_KT", Amount: 12012833.72 },
{ Period: "2020-12-31T00:00:00", Registrator: "ACC90_COST_DT", Amount: 6430415.34 },
{ Period: "2020-12-31T00:00:00", Registrator: "ACC90_SELLING_DT", Amount: 1715450.75 },
{ Period: "2020-12-31T00:00:00", Registrator: "ACC90_RESULT_TO_99_PROFIT", Amount: 3053486.79 },
{ Period: "2020-12-31T00:00:00", Registrator: "ACC90_RESULT_FROM_99_LOSS", Amount: 1188658.09 },
{ Period: "2020-12-31T00:00:00", Registrator: "ACC91_RESULT_FROM_99_LOSS", Amount: 9001644.55 },
{ Period: "2020-12-31T00:00:00", Registrator: "ACC84_TO99_LOSS_TRANSFER", Amount: 7136815.85 }
]
},
{ rows: [] },
{ rows: [] },
{ rows: [] },
{ rows: [] },
{ rows: [] },
{ rows: [{ Period: "2020-01-15T00:00:00", Registrator: "Purchase 1" }] },
{ rows: [] },
{ rows: [] },
{ rows: [] },
{ rows: [] }
]);
const result = await runAssistantMcpDiscoveryRuntimeBridge({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
business_fact_family: "business_overview",
action_family: "profit_margin_boundary",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: null,
ranking_need: null,
proof_expectation: "bounded_inference",
clarification_gaps: [],
decomposition_candidates: [
"collect_scoped_movements",
"aggregate_checked_amounts",
"fetch_supporting_documents",
"probe_coverage",
"explain_evidence_basis"
],
forbidden_overclaim_flags: [
"no_raw_model_claims",
"no_unchecked_fact_totals",
"no_unchecked_business_health_claim"
],
reason_codes: ["data_need_graph_built", "data_need_graph_family_business_overview"]
},
turnMeaning: {
asked_domain_family: "business_overview",
asked_action_family: "profit_margin_boundary",
explicit_organization_scope: "OOO Alternative Plus",
explicit_date_scope: "2020"
},
deps
});
const overview = result.pilot.derived_business_overview;
const missingFamilies = overview?.missing_proof_families.map((item) => item.family) ?? [];
const userFacing = [
result.answer_draft.headline,
...result.answer_draft.confirmed_lines,
...result.answer_draft.inference_lines,
...result.answer_draft.unknown_lines,
...result.answer_draft.limitation_lines,
result.answer_draft.next_step_line ?? ""
].join("\n");
expect(result.bridge_status).toBe("answer_draft_ready");
expect(result.business_fact_answer_allowed).toBe(true);
expect(result.route_candidate).toMatchObject({
candidate_status: "ready_for_reviewed_execution",
selected_chain_id: "business_overview",
business_fact_family: "business_overview",
action_family: "profit_margin_boundary",
executable_now: true
});
expect(overview?.accounting_financial_result).toMatchObject({
period_scope: "2020",
final_result_amount: -7136815.85,
final_result_direction: "loss",
final_transfer_basis: "account_99_to_84_period_close",
period_close_rows_with_amount: 4
});
expect(overview?.accounting_financial_result?.net_margin_to_revenue_pct).toBe(-59.41);
expect(missingFamilies).not.toContain("accounting_profit_margin");
expect(userFacing).toContain("90/91/99");
expect(userFacing).toContain("90.01");
expect(userFacing).toContain("7 136 815,85");
expect(userFacing).not.toContain("operating-flow/trading-margin proxy");
expect(result.reason_codes).toContain("pilot_derived_business_overview_accounting_financial_result_from_confirmed_rows");
expect(result.reason_codes).toContain("answer_contains_business_overview_accounting_financial_result");
expect(result.reason_codes).toContain("runtime_bridge_route_candidate_ready_for_reviewed_execution");
});
it("promotes debt due-date boundary after reviewed payment-term route returns a checked negative", async () => {
const deps = buildSequentialDeps([
{ rows: [] },
{ rows: [] },
{ rows: [] },
{ rows: [] },
{ rows: [] },
{ rows: [] },
{
rows: [
{ Period: "2020-12-31T00:00:00", Amount: 100000, Counterparty: "Клиент А", Contract: "Договор А" }
]
},
{
rows: [
{
Period: "2020-12-31T00:00:00",
Amount: 100000,
Counterparty: "Клиент А",
Contract: "Договор А",
ДокументРасчетов: "Реализация товаров от 10.03.2020",
УстановленСрокОплаты: false,
СрокОплаты: 0
}
]
},
{ rows: [] },
{ rows: [] },
{ rows: [] },
{ rows: [] },
{ rows: [] },
{ rows: [] },
{ rows: [] }
]);
const result = await runAssistantMcpDiscoveryRuntimeBridge({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
business_fact_family: "business_overview",
action_family: "debt_due_date_boundary",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: null,
ranking_need: null,
proof_expectation: "due_date_aging",
clarification_gaps: [],
decomposition_candidates: [
"collect_scoped_movements",
"aggregate_checked_amounts",
"fetch_supporting_documents",
"probe_coverage",
"explain_evidence_basis"
],
forbidden_overclaim_flags: [
"no_raw_model_claims",
"no_unchecked_overdue_claim"
],
reason_codes: ["data_need_graph_built", "data_need_graph_family_business_overview"]
},
turnMeaning: {
asked_domain_family: "business_overview",
asked_action_family: "debt_due_date_boundary",
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020"
},
deps
});
const userFacing = [
result.answer_draft.headline,
...result.answer_draft.confirmed_lines,
...result.answer_draft.unknown_lines
].join("\n");
expect(result.bridge_status).toBe("answer_draft_ready");
expect(result.route_candidate).toMatchObject({
candidate_status: "ready_for_reviewed_execution",
selected_chain_id: "business_overview",
business_fact_family: "business_overview",
action_family: "debt_due_date_boundary",
executable_now: true
});
expect(result.pilot.derived_business_overview?.debt_due_date_aging).toMatchObject({
evidence_status: "no_payment_terms_configured",
rows_without_payment_terms: 1,
overdue_rows: 0
});
expect(userFacing).toContain("срок оплаты не установлен");
expect(userFacing).toContain("Подтвержденной просрочки");
expect(result.reason_codes).toContain("runtime_bridge_route_candidate_ready_for_reviewed_execution");
expect(result.reason_codes).toContain("answer_contains_business_overview_debt_due_date_aging_no_payment_terms_configured");
});
it("bridges selected-item inventory provenance templates through exact document evidence", async () => { it("bridges selected-item inventory provenance templates through exact document evidence", async () => {
const deps = buildDeps([ const deps = buildDeps([
{ {
@ -711,5 +1031,15 @@ describe("assistant MCP discovery runtime bridge", () => {
}); });
expect(result.loop_state.pending_axes).toEqual(["organization", "period"]); expect(result.loop_state.pending_axes).toEqual(["organization", "period"]);
expect(result.loop_state.explicit_entity_candidates).toEqual([]); expect(result.loop_state.explicit_entity_candidates).toEqual([]);
expect(result.route_candidate).toMatchObject({
candidate_status: "needs_user_scope",
selected_chain_id: "movement_evidence",
business_fact_family: "movement_evidence",
action_family: "list_movements",
proof_expectation: "clarification_required",
executable_now: false
});
expect(result.route_candidate.missing_axes).toEqual(["organization", "period"]);
expect(result.route_candidate.forbidden_overclaim_flags).toContain("no_unchecked_fact_totals");
}); });
}); });

View File

@ -1505,6 +1505,36 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.reason_codes).toContain("mcp_discovery_not_applicable_for_supported_exact_turn"); expect(result.reason_codes).toContain("mcp_discovery_not_applicable_for_supported_exact_turn");
}); });
it("forces discovery over an exact ranking intent when organization scope is missing", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage:
"\u043a\u0430\u043a\u043e\u0439 \u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442 \u043f\u0440\u0438\u043d\u0435\u0441 \u0431\u043e\u043b\u044c\u0448\u0435 \u0432\u0441\u0435\u0433\u043e \u0434\u0435\u043d\u0435\u0433 \u0437\u0430 2020 \u0433\u043e\u0434?",
assistantTurnMeaning: {
asked_domain_family: "counterparty",
asked_action_family: "counterparty_value_or_turnover",
explicit_intent_candidate: "customer_revenue_and_payments",
explicit_entity_candidates: [{ type: "counterparty", value: "\u0433\u043e\u0434\u0443", source: "current_turn_loose_entity_tail" }],
explicit_date_scope: "2020",
stale_replay_forbidden: false
}
});
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.semantic_data_need).toBe("counterparty value-flow evidence");
expect(result.data_need_graph?.business_fact_family).toBe("value_flow");
expect(result.data_need_graph?.ranking_need).toBe("top_desc");
expect(result.data_need_graph?.clarification_gaps).toEqual(["organization"]);
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "counterparty_value",
asked_action_family: "counterparty_value_or_turnover",
explicit_date_scope: "2020",
stale_replay_forbidden: true
});
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.reason_codes).not.toContain("mcp_discovery_not_applicable_for_supported_exact_turn");
});
it("routes broad business evaluation into business overview discovery without metadata drift", () => { it("routes broad business evaluation into business overview discovery without metadata drift", () => {
const result = buildAssistantMcpDiscoveryTurnInput({ const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: userMessage:
@ -1536,6 +1566,46 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.reason_codes).not.toContain("mcp_discovery_not_applicable_for_supported_exact_turn"); expect(result.reason_codes).not.toContain("mcp_discovery_not_applicable_for_supported_exact_turn");
}); });
it("keeps inventory reserve questions as a bounded boundary check instead of generic overview", () => {
const orgName =
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage:
"\u041c\u043e\u0436\u043d\u043e \u043b\u0438 \u043f\u043e \u044d\u0442\u0438\u043c \u0434\u0430\u043d\u043d\u044b\u043c \u0442\u043e\u0447\u043d\u043e \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c \u0440\u0435\u0437\u0435\u0440\u0432 \u043f\u043e\u0434 \u043d\u0435\u043b\u0438\u043a\u0432\u0438\u0434\u044b \u043d\u0430 \u0441\u043a\u043b\u0430\u0434\u0435?",
assistantTurnMeaning: {
asked_domain_family: "business_summary",
asked_action_family: "broad_evaluation",
unsupported_but_understood_family: "broad_business_evaluation",
stale_replay_forbidden: true
},
followupContext: {
previous_discovery_pilot_scope: "business_overview_route_template_v1",
previous_filters: {
organization: orgName,
period_from: "2020-01-01",
period_to: "2020-12-31"
}
}
});
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.semantic_data_need).toBe("business overview evidence with bounded analyst interpretation");
expect(result.data_need_graph?.business_fact_family).toBe("business_overview");
expect(result.data_need_graph?.subject_candidates).toEqual([]);
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "business_overview",
asked_action_family: "inventory_reserve_boundary",
explicit_organization_scope: orgName,
explicit_date_scope: "2020",
unsupported_but_understood_family: "inventory_reserve_liquidation_boundary",
stale_replay_forbidden: true
});
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.reason_codes).toContain("mcp_discovery_broad_business_evaluation_route_candidate");
expect(result.reason_codes).toContain("mcp_discovery_data_need_graph_built");
});
it("lets raw business-overview wording override stale exact turnover meaning", () => { it("lets raw business-overview wording override stale exact turnover meaning", () => {
const result = buildAssistantMcpDiscoveryTurnInput({ const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: userMessage:
@ -2170,10 +2240,10 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.data_need_graph?.business_fact_family).toBe("business_overview"); expect(result.data_need_graph?.business_fact_family).toBe("business_overview");
expect(result.turn_meaning_ref).toMatchObject({ expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "business_overview", asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation", asked_action_family: "profit_margin_boundary",
explicit_organization_scope: orgName, explicit_organization_scope: orgName,
explicit_date_scope: "2020", explicit_date_scope: "2020",
unsupported_but_understood_family: "broad_business_evaluation", unsupported_but_understood_family: "profit_margin_boundary",
stale_replay_forbidden: true stale_replay_forbidden: true
}); });
expect(result.reason_codes).toContain("mcp_discovery_broad_business_evaluation_route_candidate"); expect(result.reason_codes).toContain("mcp_discovery_broad_business_evaluation_route_candidate");
@ -2181,6 +2251,23 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.data_need_graph?.clarification_gaps).toEqual([]); expect(result.data_need_graph?.clarification_gaps).toEqual([]);
}); });
it("routes legal-entity profit and margin wording to the missing P&L proof-family boundary", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "по ООО Альтернатива Плюс за 2020 можно точно сказать чистую прибыль и маржу?"
});
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.data_need_graph?.business_fact_family).toBe("business_overview");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "business_overview",
asked_action_family: "profit_margin_boundary",
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020",
unsupported_but_understood_family: "profit_margin_boundary"
});
});
it("routes organization-level overdue debt wording to business overview instead of exact receivables recipes", () => { it("routes organization-level overdue debt wording to business overview instead of exact receivables recipes", () => {
const orgName = "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441"; const orgName = "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
const result = buildAssistantMcpDiscoveryTurnInput({ const result = buildAssistantMcpDiscoveryTurnInput({
@ -2201,10 +2288,10 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.data_need_graph?.business_fact_family).toBe("business_overview"); expect(result.data_need_graph?.business_fact_family).toBe("business_overview");
expect(result.turn_meaning_ref).toMatchObject({ expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "business_overview", asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation", asked_action_family: "debt_due_date_boundary",
explicit_organization_scope: orgName, explicit_organization_scope: orgName,
explicit_date_scope: "2020", explicit_date_scope: "2020",
unsupported_but_understood_family: "broad_business_evaluation", unsupported_but_understood_family: "debt_due_date_boundary",
stale_replay_forbidden: true stale_replay_forbidden: true
}); });
expect(result.reason_codes).toContain("mcp_discovery_broad_business_evaluation_route_candidate"); expect(result.reason_codes).toContain("mcp_discovery_broad_business_evaluation_route_candidate");
@ -2232,10 +2319,10 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.data_need_graph?.business_fact_family).toBe("business_overview"); expect(result.data_need_graph?.business_fact_family).toBe("business_overview");
expect(result.turn_meaning_ref).toMatchObject({ expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "business_overview", asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation", asked_action_family: "inventory_reserve_boundary",
explicit_organization_scope: orgName, explicit_organization_scope: orgName,
explicit_date_scope: "2020", explicit_date_scope: "2020",
unsupported_but_understood_family: "broad_business_evaluation", unsupported_but_understood_family: "inventory_reserve_liquidation_boundary",
stale_replay_forbidden: true stale_replay_forbidden: true
}); });
expect(result.reason_codes).toContain("mcp_discovery_broad_business_evaluation_route_candidate"); expect(result.reason_codes).toContain("mcp_discovery_broad_business_evaluation_route_candidate");
@ -2263,10 +2350,10 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.data_need_graph?.business_fact_family).toBe("business_overview"); expect(result.data_need_graph?.business_fact_family).toBe("business_overview");
expect(result.turn_meaning_ref).toMatchObject({ expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "business_overview", asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation", asked_action_family: "vendor_risk_procurement_boundary",
explicit_organization_scope: orgName, explicit_organization_scope: orgName,
explicit_date_scope: "2020", explicit_date_scope: "2020",
unsupported_but_understood_family: "broad_business_evaluation", unsupported_but_understood_family: "vendor_risk_procurement_boundary",
stale_replay_forbidden: true stale_replay_forbidden: true
}); });
expect(result.reason_codes).toContain("mcp_discovery_broad_business_evaluation_route_candidate"); expect(result.reason_codes).toContain("mcp_discovery_broad_business_evaluation_route_candidate");
@ -2708,6 +2795,42 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.data_need_graph?.clarification_gaps).toEqual(["period"]); expect(result.data_need_graph?.clarification_gaps).toEqual(["period"]);
}); });
it("routes explicit VAT movement wording to metadata-scoped movement evidence over exact VAT aggregate", () => {
const orgName =
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage:
"\u043f\u043e\u043a\u0430\u0436\u0438 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u044f \u043f\u043e \u041d\u0414\u0421 \u0437\u0430 2020 \u043f\u043e \u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441",
assistantTurnMeaning: {
explicit_intent_candidate: "vat_liability_confirmed_for_tax_period"
},
predecomposeContract: {
entities: { organization: orgName },
period: {
period_from: "2020-01-01",
period_to: "2020-12-31"
}
}
});
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.semantic_data_need).toBe("movement evidence");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "movements",
asked_action_family: "list_movements",
metadata_scope_hint: "\u041d\u0414\u0421",
subject_resolution_optional: true,
explicit_organization_scope: orgName,
explicit_date_scope: "2020",
unsupported_but_understood_family: "movement_evidence",
stale_replay_forbidden: true
});
expect(result.data_need_graph?.business_fact_family).toBe("movement_evidence");
expect(result.data_need_graph?.clarification_gaps).toEqual([]);
expect(result.reason_codes).toContain("mcp_discovery_vat_movement_evidence_signal_detected");
});
it("pivots a metadata-scoped subjectless movement retrieval into documents without inventing a counterparty", () => { it("pivots a metadata-scoped subjectless movement retrieval into documents without inventing a counterparty", () => {
const orgName = const orgName =
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441"; "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
@ -2867,6 +2990,62 @@ describe("assistant MCP discovery turn input adapter", () => {
}); });
}); });
it("prefers semantic organization over noisy raw organization tail for debt due-date overview", () => {
const orgName = "ООО Альтернатива Плюс";
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "по ООО Альтернатива Плюс на конец 2020 можно точно понять, какая дебиторка просрочена?",
predecomposeContract: {
entities: { counterparty: orgName, organization: orgName },
period: { period_from: "2020-01-01", period_to: "2020-12-31" }
}
});
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.semantic_data_need).toBe("business overview evidence with bounded analyst interpretation");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "business_overview",
asked_action_family: "debt_due_date_boundary",
explicit_organization_scope: orgName,
explicit_date_scope: "2020",
unsupported_but_understood_family: "debt_due_date_boundary",
stale_replay_forbidden: true
});
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
});
it("routes debt due-date boundary follow-up through the business overview lane", () => {
const orgName =
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage:
"\u0442\u043e \u0435\u0441\u0442\u044c \u043f\u0440\u043e\u0441\u0440\u043e\u0447\u043a\u0443 \u0434\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u043d\u0435\u043b\u044c\u0437\u044f, \u043a\u043e\u0440\u043e\u0442\u043a\u043e \u043f\u043e\u0447\u0435\u043c\u0443?",
followupContext: {
previous_discovery_pilot_scope: "business_overview_route_template_v1",
previous_filters: {
organization: orgName,
period_from: "2020-01-01",
period_to: "2020-12-31"
}
}
});
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.semantic_data_need).toBe("business overview evidence with bounded analyst interpretation");
expect(result.data_need_graph?.business_fact_family).toBe("business_overview");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "business_overview",
asked_action_family: "debt_due_date_boundary",
explicit_organization_scope: orgName,
explicit_date_scope: "2020",
unsupported_but_understood_family: "debt_due_date_boundary",
stale_replay_forbidden: true
});
expect(result.reason_codes).toContain("mcp_discovery_business_overview_continuation_from_followup_context");
expect(result.reason_codes).toContain("mcp_discovery_business_overview_debt_due_date_followup_boundary");
});
it("lets raw metadata scope override stale document evidence subject on topic switch", () => { it("lets raw metadata scope override stale document evidence subject on topic switch", () => {
const orgName = const orgName =
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441"; "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
@ -2946,6 +3125,44 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.reason_codes).toContain("mcp_discovery_business_overview_continuation_from_followup_context"); expect(result.reason_codes).toContain("mcp_discovery_business_overview_continuation_from_followup_context");
}); });
it("keeps short profit/loss follow-up on the accounting profit-margin boundary", () => {
const orgName =
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage:
"\u0430 \u044d\u0442\u043e \u043f\u0440\u0438\u0431\u044b\u043b\u044c \u0438\u043b\u0438 \u0443\u0431\u044b\u0442\u043e\u043a, \u043a\u043e\u0440\u043e\u0442\u043a\u043e?",
assistantTurnMeaning: {
asked_domain_family: "unknown",
asked_action_family: "unknown"
},
followupContext: {
previous_discovery_pilot_scope: "business_overview_route_template_v1",
previous_filters: {
organization: orgName,
period_from: "2020-01-01",
period_to: "2020-12-31"
}
}
});
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.semantic_data_need).toBe("business overview evidence with bounded analyst interpretation");
expect(result.data_need_graph?.business_fact_family).toBe("business_overview");
expect(result.data_need_graph?.subject_candidates).toEqual([]);
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "business_overview",
asked_action_family: "profit_margin_boundary",
explicit_organization_scope: orgName,
explicit_date_scope: "2020",
unsupported_but_understood_family: "profit_margin_boundary",
stale_replay_forbidden: true
});
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.reason_codes).toContain("mcp_discovery_business_overview_continuation_from_followup_context");
expect(result.reason_codes).toContain("mcp_discovery_business_overview_profit_margin_followup_boundary");
});
it("keeps detailed money-breakdown follow-up in business overview without pseudo counterparty anchors", () => { it("keeps detailed money-breakdown follow-up in business overview without pseudo counterparty anchors", () => {
const orgName = const orgName =
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441"; "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";

View File

@ -557,6 +557,45 @@ describe("assistantRoutePolicy", () => {
expect(decision.orchestrationContract?.followup_context_detected).toBe(false); expect(decision.orchestrationContract?.followup_context_detected).toBe(false);
}); });
it("keeps plain organization clarification in address lane for pending route-candidate scope", () => {
const orgName =
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
const policy = buildPolicy({
resolveAddressToolGateDecision: undefined
});
const decision = policy.resolveAssistantOrchestrationDecision({
rawUserMessage: orgName,
effectiveAddressUserMessage: orgName,
followupContext: {
previous_intent: "customer_revenue_and_payments",
target_intent: "customer_revenue_and_payments",
previous_discovery_pilot_scope: "counterparty_value_flow_query_movements_v1",
previous_discovery_loop_status: "awaiting_clarification",
previous_discovery_loop_selected_chain_id: "value_flow_ranking",
previous_discovery_loop_pending_axes: ["organization"],
previous_discovery_loop_asked_domain_family: "counterparty_value",
previous_discovery_loop_asked_action_family: "turnover",
previous_discovery_loop_unsupported_family: "counterparty_value_or_turnover"
},
llmPreDecomposeMeta: {
predecomposeContract: {
mode: "unsupported",
intent: "unknown",
entities: { organization: orgName },
semantics: { anchor_kind: "organization", anchor_value: orgName }
}
},
useMock: false
});
expect(decision.runAddressLane).toBe(true);
expect(decision.toolGateReason).toBe("followup_context_detected");
expect(decision.livingMode).toBe("address_data");
expect(decision.livingReason).toBe("address_lane_triggered");
expect(decision.orchestrationContract?.hard_meta_mode).toBeNull();
});
it("does not turn short entity follow-up into organization switch just because scope already has an active company", () => { it("does not turn short entity follow-up into organization switch just because scope already has an active company", () => {
const policy = buildPolicy({ const policy = buildPolicy({
resolveAddressToolGateDecision: undefined, resolveAddressToolGateDecision: undefined,

View File

@ -1206,6 +1206,67 @@ describe("assistantTransitionPolicy", () => {
period_to: "2020-12-31" period_to: "2020-12-31"
}); });
}); });
it("carries business overview boundary context through short capability-shaped follow-up", () => {
const policy = buildPolicy({
findLastAddressAssistantItem: () => null,
shouldHandleAsAssistantCapabilityMetaQuery: () => true,
hasDataRetrievalRequestSignal: () => false,
hasAddressFollowupContextSignal: () => false
});
const organization = "ООО Альтернатива Плюс";
const carryover = policy.resolveAddressFollowupCarryoverContext(
"то есть просрочку доказать нельзя, коротко почему?",
[
{
role: "assistant",
text: "На 2020-12-31 подтвержденной просрочки нет: в договорах срок оплаты не установлен.",
debug: {
execution_lane: "living_chat",
mcp_discovery_response_applied: true,
assistant_active_organization: organization,
assistant_mcp_discovery_entry_point_v1: {
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
entry_status: "bridge_executed",
turn_input: {
turn_meaning_ref: {
asked_domain_family: "business_overview",
asked_action_family: "debt_due_date_boundary",
unsupported_but_understood_family: "debt_due_date_boundary",
explicit_organization_scope: organization,
explicit_date_scope: "2020"
}
},
bridge: {
bridge_status: "answer_draft_ready",
business_fact_answer_allowed: true,
pilot: {
pilot_scope: "business_overview_route_template_v1"
},
answer_draft: {
answer_mode: "confirmed_with_bounded_inference"
}
}
}
}
}
],
null,
null,
null
);
expect(carryover?.followupSelectionMode).toBe("carry_previous_intent");
expect(carryover?.followupContext?.previous_discovery_pilot_scope).toBe(
"business_overview_route_template_v1"
);
expect(carryover?.followupContext?.previous_filters).toMatchObject({
organization,
period_from: "2020-01-01",
period_to: "2020-12-31"
});
});
it("carries resolved entity candidates from grounded entity-resolution discovery into followup context", () => { it("carries resolved entity candidates from grounded entity-resolution discovery into followup context", () => {
const policy = buildPolicy({ const policy = buildPolicy({
findLastAddressAssistantItem: () => null, findLastAddressAssistantItem: () => null,
@ -1506,6 +1567,67 @@ describe("assistantTransitionPolicy", () => {
); );
}); });
it("treats a plain organization reply as continuation of a pending route-candidate organization scope", () => {
const orgName =
"\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
const policy = buildPolicy({
findLastAddressAssistantItem: () => ({
role: "assistant",
text: "\u041d\u0443\u0436\u043d\u043e \u0443\u0442\u043e\u0447\u043d\u0438\u0442\u044c \u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\u0438\u044e.",
debug: {
execution_lane: "living_chat",
detected_intent: "customer_revenue_and_payments",
mcp_discovery_response_applied: true,
assistant_mcp_discovery_entry_point_v1: {
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
entry_status: "bridge_executed",
bridge: {
bridge_status: "needs_clarification",
business_fact_answer_allowed: false,
pilot: {
pilot_scope: "counterparty_value_flow_query_movements_v1"
},
loop_state: {
schema_version: "assistant_mcp_discovery_loop_state_v1",
loop_status: "awaiting_clarification",
selected_chain_id: "value_flow_ranking",
pilot_scope: "counterparty_value_flow_query_movements_v1",
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
unsupported_but_understood_family: "counterparty_value_or_turnover",
ranking_need: "top_desc",
pending_axes: ["organization"],
provided_axes: ["aggregate_axis", "amount", "coverage_target"],
explicit_date_scope: "2020"
}
}
}
}
}),
hasAddressFollowupContextSignal: () => false
});
const carryover = policy.resolveAddressFollowupCarryoverContext(
orgName,
[],
null,
{
predecomposeContract: {
mode: "unsupported",
intent: "unknown",
entities: { organization: orgName },
semantics: { anchor_kind: "organization", anchor_value: orgName }
}
},
null
);
expect(carryover?.followupContext?.previous_discovery_loop_status).toBe("awaiting_clarification");
expect(carryover?.followupContext?.previous_discovery_loop_selected_chain_id).toBe("value_flow_ranking");
expect(carryover?.followupContext?.previous_discovery_loop_pending_axes).toEqual(["organization"]);
expect(carryover?.followupContext?.target_intent).toBe("customer_revenue_and_payments");
});
it("carries grounded metadata downstream route hints into followup context", () => { it("carries grounded metadata downstream route hints into followup context", () => {
const policy = buildPolicy({ const policy = buildPolicy({
findLastAddressAssistantItem: () => null, findLastAddressAssistantItem: () => null,

View File

@ -114,6 +114,40 @@ describe("counterparty analytics reply builders", () => {
expect(reply.text).not.toContain("Топ-"); expect(reply.text).not.toContain("Топ-");
}); });
it("does not mistake a year period in top-counterparty wording for a top-year question", () => {
const reply = composeFactualReply(
"customer_revenue_and_payments",
[
{
period: "2020-03-01T00:00:00Z",
registrator: "Поступление 1",
account_dt: "",
account_kt: "",
amount: 500,
analytics: ["Клиент А", "Договор А-1"]
},
{
period: "2020-03-02T00:00:00Z",
registrator: "Поступление 2",
account_dt: "",
account_kt: "",
amount: 1200,
analytics: ["Клиент Б", "Договор Б-1"]
}
],
{
userMessage: "какой контрагент принес больше всего денег за 2020 год?",
periodFrom: "2020-01-01",
periodTo: "2020-12-31"
}
);
expect(reply.responseType).toBe("FACTUAL_LIST");
expect(reply.text).toContain("Клиент Б");
expect(reply.text).not.toContain("Клиент А");
expect(reply.text).not.toContain("1. 2020 |");
});
it("explains organization activity age as 1C activity rather than legal age", () => { it("explains organization activity age as 1C activity rather than legal age", () => {
const reply = composeFactualReply( const reply = composeFactualReply(
"counterparty_activity_lifecycle", "counterparty_activity_lifecycle",