Закрепить reviewed routes и защиту бизнес-ответов
This commit is contained in:
+1
-1
@@ -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_CHANNEL = process.env.ASSISTANT_MCP_CHANNEL ?? "default";
|
||||
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_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");
|
||||
|
||||
@@ -1733,8 +1733,7 @@ function extractAddressFilters(userMessage, intent) {
|
||||
const periodWasDerivedHeuristically = warnings.includes("period_derived_from_month_phrase") ||
|
||||
warnings.includes("period_derived_from_year_range_phrase") ||
|
||||
warnings.includes("period_derived_from_year_phrase");
|
||||
const preserveDerivedPeriodWindow = usesAsOfPrimaryWindow(intent) ||
|
||||
intent === "inventory_on_hand_as_of_date" ||
|
||||
const preserveDerivedPeriodWindow = intent === "inventory_on_hand_as_of_date" ||
|
||||
intent === "inventory_supplier_stock_overlap_as_of_date";
|
||||
if (periodWasDerivedHeuristically && !warnings.includes("exact_historical_period_window_requested")) {
|
||||
warnings.push("exact_historical_period_window_requested");
|
||||
|
||||
@@ -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 hasInspectionCue = /(?:что\s+с|позици|основан|не\s+хватает|налогов[а-яё]*\s+вывод|вывод|декларац|книга\s+(?:продаж|покупок)|расшифр|разбор)/iu.test(normalized);
|
||||
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) {
|
||||
const normalized = String(text ?? "").trim().toLowerCase();
|
||||
@@ -2044,6 +2045,16 @@ function resolveAddressIntent(userMessage) {
|
||||
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) &&
|
||||
/(?:\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);
|
||||
|
||||
+376
-105
@@ -197,6 +197,51 @@ const OPEN_CONTRACTS_CONFIRMED_AS_OF_QUERY_TEMPLATE = `
|
||||
УПОРЯДОЧИТЬ ПО
|
||||
Сумма __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 = `
|
||||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
||||
__AS_OF_EXPR__ КАК Период,
|
||||
@@ -723,7 +768,7 @@ const BASE_RECIPES = [
|
||||
purpose: "Build customer value ranking and incoming deal profile from bank inflow docs",
|
||||
required_filters: [],
|
||||
optional_filters: ["period_from", "period_to", "organization", "counterparty", "contract", "limit", "sort"],
|
||||
default_limit: 20,
|
||||
default_limit: 200,
|
||||
account_scope_mode: "preferred",
|
||||
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",
|
||||
required_filters: [],
|
||||
optional_filters: ["period_from", "period_to", "organization", "counterparty", "contract", "limit", "sort"],
|
||||
default_limit: 20,
|
||||
default_limit: 200,
|
||||
account_scope_mode: "preferred",
|
||||
query_template: "supplier_payout_profile"
|
||||
},
|
||||
@@ -778,6 +823,17 @@ const BASE_RECIPES = [
|
||||
account_scope_mode: "preferred",
|
||||
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",
|
||||
intent: "inventory_on_hand_as_of_date",
|
||||
@@ -888,6 +944,17 @@ const BASE_RECIPES = [
|
||||
account_scope_mode: "strict",
|
||||
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",
|
||||
intent: "list_contracts_by_counterparty",
|
||||
@@ -1093,6 +1160,32 @@ function buildContractValueWhereClause(filters, fieldPath, 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) {
|
||||
const source = String(value ?? "").trim().replace(",", ".");
|
||||
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}"`);
|
||||
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) {
|
||||
const debitPredicate = buildAccountPrefixPredicate("Движения.СчетДт", ["41.01"]);
|
||||
const creditPredicate = buildAccountPrefixPredicate("Движения.СчетКт", ["41.01"]);
|
||||
@@ -1246,6 +1368,148 @@ function buildCounterpartyReferenceCondition(filters, fieldPaths) {
|
||||
}
|
||||
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) {
|
||||
const itemCondition = buildInventoryItemReferenceCondition(filters, ["Товары.Номенклатура"]);
|
||||
return INVENTORY_SALE_DOCUMENTS_QUERY_TEMPLATE
|
||||
@@ -1324,6 +1588,8 @@ function maxLimitForIntent(intent) {
|
||||
intent === "contract_usage_and_value" ||
|
||||
intent === "vat_payable_forecast" ||
|
||||
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_purchase_provenance_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 === "contract_usage_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)
|
||||
? Math.max(1, Math.min(maxLimit, Math.trunc(filters.limit)))
|
||||
: recipe.default_limit;
|
||||
@@ -1467,97 +1734,65 @@ function buildAddressRecipePlan(recipe, filters) {
|
||||
.replaceAll("__WHERE_CLAUSE__", buildManagementWhereClause(filters, "Движения.Период"))
|
||||
.replaceAll("__PERIOD_TO_EXPR__", periodToExpr);
|
||||
})()
|
||||
: recipe.query_template === "vat_payable_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 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)
|
||||
: 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"
|
||||
? (() => {
|
||||
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_from === "string" && filters.period_from.trim().length > 0
|
||||
? 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)
|
||||
: recipe.query_template === "inventory_trading_margin_proxy_profile"
|
||||
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
||||
: recipe.query_template === "inventory_purchase_to_sale_chain_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 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) ??
|
||||
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0
|
||||
? 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)
|
||||
: recipe.query_template === "inventory_aging_by_purchase_date_profile"
|
||||
? buildInventoryMovementQuery(filters, resolvedLimit, "dt")
|
||||
: recipe.query_template === "contracts_by_counterparty_profile"
|
||||
? CONTRACTS_BY_COUNTERPARTY_QUERY_TEMPLATE.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||
: recipe.query_template === "open_contracts_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 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"
|
||||
: recipe.query_template === "inventory_trading_margin_proxy_profile"
|
||||
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
||||
: recipe.query_template === "inventory_purchase_to_sale_chain_profile"
|
||||
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
||||
: recipe.query_template === "inventory_aging_by_purchase_date_profile"
|
||||
? buildInventoryMovementQuery(filters, resolvedLimit, "dt")
|
||||
: recipe.query_template === "contracts_by_counterparty_profile"
|
||||
? CONTRACTS_BY_COUNTERPARTY_QUERY_TEMPLATE.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||
: recipe.query_template === "open_contracts_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)
|
||||
@@ -1569,23 +1804,59 @@ function buildAddressRecipePlan(recipe, filters) {
|
||||
? toDateTimeExpr(filters.period_from, true)
|
||||
: null) ??
|
||||
"ТЕКУЩАЯДАТА()";
|
||||
return RECEIVABLES_CONFIRMED_AS_OF_QUERY_TEMPLATE
|
||||
return OPEN_CONTRACTS_CONFIRMED_AS_OF_QUERY_TEMPLATE
|
||||
.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||
.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));
|
||||
})()
|
||||
: 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));
|
||||
: 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
|
||||
? 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 {
|
||||
recipe,
|
||||
query,
|
||||
|
||||
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.contractCandidatesFromRows = contractCandidatesFromRows;
|
||||
exports.composeFactualReply = composeFactualReply;
|
||||
exports.inferReplyType = inferReplyType;
|
||||
const assistantOrganizationMatcher_1 = require("../assistantOrganizationMatcher");
|
||||
const replyPackaging_1 = require("./replyPackaging");
|
||||
const counterpartyAnalyticsReplyBuilders_1 = require("./counterpartyAnalyticsReplyBuilders");
|
||||
const inventoryReplyBuilders_1 = require("./inventoryReplyBuilders");
|
||||
@@ -515,10 +516,12 @@ function detectValueRankingFocus(userMessage) {
|
||||
if (asksTotalMoneyEarned) {
|
||||
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) &&
|
||||
/(?:год|года|годы|year|years|по\s+годам)/iu.test(text) &&
|
||||
/(?:сам(?:ый|ая|ое|ые)|топ|луч|best|max|наибольш|больше)/iu.test(text);
|
||||
if (asksYearlyRevenueRanking) {
|
||||
if (asksYearlyRevenueRanking && (!hasCounterpartyRankingSubject || asksExplicitYearBreakdown)) {
|
||||
return "top_years_by_total";
|
||||
}
|
||||
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 visibleProbeRows = (nonErrorProbeRows.length > 0 ? nonErrorProbeRows : orderedProbeRows).slice(0, 6);
|
||||
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) {
|
||||
lines.push(...visibleProbeRows.map((item, index) => {
|
||||
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-источники показаны для проверки покрытия.");
|
||||
}
|
||||
else if (vatProbe && vatProbe.status === "error") {
|
||||
lines.push("", "Покрытие VAT-источников через MCP: probe завершился ошибкой, поэтому использован только базовый контур 68.02*/19*.");
|
||||
lines.push("", "Покрытие VAT-источников в 1С: дополнительная проверка завершилась ошибкой, поэтому использован только базовый контур 68.02*/19*.");
|
||||
}
|
||||
if (!vatActivityDetected) {
|
||||
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 formatConfirmedMoney = (value) => (options.useRubCurrency ? formatMoneyRub(value) : formatMoney(value));
|
||||
const vatProbe = options.vatDirectSourceProbe ?? null;
|
||||
const organizationLabel = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeValue)(options.organizationHint);
|
||||
const organizationScopeLabel = organizationLabel ? ` по организации ${organizationLabel}` : "";
|
||||
const lines = [
|
||||
`Коротко: подтвержденный НДС к уплате за налоговый период — ${formatConfirmedMoney(vatToPay)}.`,
|
||||
`Коротко: подтвержденный НДС к уплате за налоговый период${organizationScopeLabel} — ${formatConfirmedMoney(vatToPay)}.`,
|
||||
`Если смотреть на возможный перенос или переплату, получается ${formatConfirmedMoney(carryoverOrOverpayment)}.`,
|
||||
"Это подтвержденный расчет по регистрам книг продаж и покупок, без surrogate-формулы 68/19.",
|
||||
"",
|
||||
"Что вошло в расчет:",
|
||||
...(organizationLabel ? [`- Организация: ${organizationLabel}.`] : []),
|
||||
`- Налоговый период расчета: ${periodWindowLabel ?? "не задан (нужен явный период)"}.`,
|
||||
`- НДС по книге продаж: ${formatConfirmedMoney(salesVat)}.`,
|
||||
`- НДС по книге покупок (вычеты): ${formatConfirmedMoney(purchaseVat)}.`,
|
||||
@@ -2602,14 +2608,14 @@ function composeFactualReplyBody(intent, rows, options = {}) {
|
||||
if (vatProbe && vatProbe.status === "ok") {
|
||||
const nonEmptySources = vatProbe.probedSources.filter((item) => item.status === "ok").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) {
|
||||
lines.push(`- Ограничения probe: ${vatProbe.errors.slice(0, 2).join("; ")}.`);
|
||||
}
|
||||
lines.push("- Сумма расчета выше получена по книгам продаж/покупок; probe использован для контроля полноты VAT-источников.");
|
||||
lines.push("- Сумма расчета выше получена по книгам продаж/покупок; дополнительная проверка использована для контроля полноты VAT-источников.");
|
||||
}
|
||||
else if (vatProbe && vatProbe.status === "error") {
|
||||
lines.push("", "Покрытие VAT-источников через MCP: дополнительный probe недоступен (например, timeout metadata).", "Итоговая сумма НДС выше рассчитана по основному маршруту книг продаж/покупок; probe влияет только на диагностику покрытия.");
|
||||
lines.push("", "Покрытие VAT-источников в 1С: дополнительная проверка недоступна, поэтому использован основной бухгалтерский срез.", "Итоговая сумма НДС выше рассчитана по основному маршруту книг продаж/покупок; probe влияет только на диагностику покрытия.");
|
||||
if (vatProbe.errors.length > 0) {
|
||||
lines.push(`- Детали probe: ${vatProbe.errors.slice(0, 2).join("; ")}.`);
|
||||
}
|
||||
@@ -2679,7 +2685,7 @@ function composeFactualReplyBody(intent, rows, options = {}) {
|
||||
const vatProbe = options.vatDirectSourceProbe ?? null;
|
||||
if (vatProbe && vatProbe.status === "ok") {
|
||||
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) {
|
||||
lines.push(...vatProbe.probedSources.slice(0, 4).map((item, index) => {
|
||||
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") {
|
||||
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. Подтвержденные позиции");
|
||||
if (accountRows.length > 0) {
|
||||
|
||||
+22
-11
@@ -48,9 +48,9 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
|
||||
const includeTotal = focus === "full_profile" || focus === "total_only";
|
||||
const includeRoles = focus === "full_profile" || focus === "roles_only";
|
||||
const directLead = focus === "suppliers_only"
|
||||
? `Поставщиков (только supplier-роль): ${supplierOnly}.`
|
||||
? `Поставщиков с ролью поставщика: ${supplierOnly}.`
|
||||
: focus === "customers_only"
|
||||
? `Заказчиков (только customer-роль): ${customerOnly}.`
|
||||
? `Заказчиков с ролью покупателя: ${customerOnly}.`
|
||||
: focus === "mixed_only"
|
||||
? `Контрагентов со смешанной ролью: ${mixedActive}.`
|
||||
: includeTotal && totalCounterparties > 0
|
||||
@@ -74,9 +74,9 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
|
||||
}
|
||||
if (includeRoles) {
|
||||
if (resolvedActive > 0 || activeCounterparties > 0) {
|
||||
lines.push("Роли контрагентов по активности:");
|
||||
lines.push(`Заказчики (только customer-роль): ${customerOnly}.`);
|
||||
lines.push(`Поставщики (только supplier-роль): ${supplierOnly}.`);
|
||||
lines.push("Распределение ролей по активности:");
|
||||
lines.push(`Заказчики с ролью покупателя: ${customerOnly}.`);
|
||||
lines.push(`Поставщики с ролью поставщика: ${supplierOnly}.`);
|
||||
lines.push(`Смешанные (и покупатель, и поставщик): ${mixedActive}.`);
|
||||
lines.push(`4. Всего активных контрагентов: ${activeCounterparties}.`);
|
||||
if (otherCounterparties !== null) {
|
||||
@@ -88,10 +88,10 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
|
||||
}
|
||||
}
|
||||
if (focus === "suppliers_only") {
|
||||
lines.push(`Поставщиков (только supplier-роль): ${supplierOnly}.`);
|
||||
lines.push(`Поставщиков с ролью поставщика: ${supplierOnly}.`);
|
||||
}
|
||||
if (focus === "customers_only") {
|
||||
lines.push(`Заказчиков (только customer-роль): ${customerOnly}.`);
|
||||
lines.push(`Заказчиков с ролью покупателя: ${customerOnly}.`);
|
||||
}
|
||||
if (focus === "mixed_only") {
|
||||
lines.push(`Контрагентов со смешанной ролью: ${mixedActive}.`);
|
||||
@@ -387,6 +387,11 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
|
||||
const limit = deps.detectRankingLimit(options.userMessage, 20);
|
||||
const minOpsForAvgCheck = deps.detectMinOpsForAvgCheck(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 byYear = new Map();
|
||||
const deals = [];
|
||||
@@ -554,7 +559,7 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
|
||||
? `Топ-${visible.length} поставщиков по максимальной разовой выплате:`
|
||||
: `Топ-${visible.length} заказчиков по максимальной сумме одной входящей операции:`;
|
||||
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);
|
||||
}
|
||||
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)}`));
|
||||
return (0, replyContracts_1.buildFactualListReply)(lines);
|
||||
}
|
||||
const visible = rankedByTotal.slice(0, limit);
|
||||
const visible = rankedByTotal.slice(0, effectiveLimit);
|
||||
const singleCandidateOnly = rankedByTotal.length === 1;
|
||||
const rankingPeriodLabel = options.periodFrom && options.periodTo
|
||||
? `за период ${deps.formatDateRu(options.periodFrom)}..${deps.formatDateRu(options.periodTo)}`
|
||||
: "за доступное время";
|
||||
const heading = singleCandidateOnly
|
||||
? isSupplier
|
||||
? "Найденный поставщик по сумме выплат:"
|
||||
@@ -603,14 +611,17 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
|
||||
: `Топ-${visible.length} заказчиков по сумме поступлений:`;
|
||||
const leadingCounterparty = visible[0] ?? null;
|
||||
lines.unshift(heading);
|
||||
if (options.periodFrom && options.periodTo) {
|
||||
lines.push(`Период рейтинга: ${rankingPeriodLabel}.`);
|
||||
}
|
||||
if (leadingCounterparty) {
|
||||
const directAnswerLine = singleCandidateOnly
|
||||
? isSupplier
|
||||
? `В выбранном срезе найден один поставщик: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг.`
|
||||
: `В выбранном срезе найден один клиент: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг; сумма является денежным потоком, а не чистой прибылью.`
|
||||
: isSupplier
|
||||
? `Крупнейший поставщик по подтвержденным выплатам за доступное время: ${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} операциям).`
|
||||
: `Самый доходный клиент ${rankingPeriodLabel} по подтвержденным поступлениям: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это денежный поток, а не чистая прибыль.`;
|
||||
lines.unshift(directAnswerLine);
|
||||
}
|
||||
lines.push(...visible.map((item, index) => {
|
||||
|
||||
+278
-24
@@ -2,6 +2,7 @@
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION = void 0;
|
||||
exports.buildAssistantMcpDiscoveryAnswerDraft = buildAssistantMcpDiscoveryAnswerDraft;
|
||||
const counterpartyRoleHeuristics_1 = require("./counterpartyRoleHeuristics");
|
||||
exports.ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION = "assistant_mcp_discovery_answer_draft_v1";
|
||||
function normalizeReasonCode(value) {
|
||||
const normalized = value
|
||||
@@ -371,6 +372,26 @@ function metadataRouteFamilyLabelRu(routeFamily) {
|
||||
}
|
||||
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) {
|
||||
if (overview.inventory_staleness_risk_proxy) {
|
||||
return "резервы/списания/ликвидационная стоимость склада";
|
||||
@@ -433,6 +454,67 @@ function inlineBusinessOverviewAmount(value) {
|
||||
.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) {
|
||||
const parts = [];
|
||||
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) {
|
||||
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);
|
||||
if (strongestIncomingYear) {
|
||||
parts.push(`самый сильный год по подтвержденным входящим поступлениям ${strongestIncomingYear.year_bucket}: ${inlineBusinessOverviewAmount(strongestIncomingYear.incoming_total_amount_human_ru)}`);
|
||||
}
|
||||
return parts.length > 0
|
||||
? `${parts.join("; ")}. Это operating-flow proxy по найденным строкам, не бухгалтерская прибыль и не финрезультат`
|
||||
? overview.accounting_financial_result
|
||||
? `${parts.join("; ")}. Финрезультат ограничен найденными строками 1С и не является внешним аудитом или юридически подтвержденной отчетностью`
|
||||
: `${parts.join("; ")}. Это operating-flow proxy по найденным строкам, не бухгалтерская прибыль и не финрезультат`
|
||||
: 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) {
|
||||
const askedMonthlyBreakdown = pilot.derived_bidirectional_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") {
|
||||
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 = [];
|
||||
if (overview.incoming_customer_revenue.rows_with_amount > 0 ||
|
||||
overview.outgoing_supplier_payout.rows_with_amount > 0) {
|
||||
@@ -492,6 +661,9 @@ function headlineFor(mode, pilot) {
|
||||
if (overview.tax_position) {
|
||||
families.push("НДС-позиция");
|
||||
}
|
||||
if (overview.accounting_financial_result) {
|
||||
families.push("учетный финрезультат 90/91/99");
|
||||
}
|
||||
if (overview.trading_margin_proxy) {
|
||||
families.push("торговый margin proxy");
|
||||
}
|
||||
@@ -507,6 +679,9 @@ function headlineFor(mode, pilot) {
|
||||
if (overview.debt_staleness_risk_proxy) {
|
||||
families.push("staleness risk proxy открытых расчетов");
|
||||
}
|
||||
if (overview.debt_due_date_aging) {
|
||||
families.push("due-date aging открытых расчетов");
|
||||
}
|
||||
if (overview.inventory_position) {
|
||||
families.push("складской срез на дату");
|
||||
}
|
||||
@@ -516,18 +691,22 @@ function headlineFor(mode, pilot) {
|
||||
if (overview.inventory_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) {
|
||||
unknownFamilies.push("НДС");
|
||||
}
|
||||
if (!overview.debt_position) {
|
||||
unknownFamilies.push("долговой срез");
|
||||
}
|
||||
unknownFamilies.push(overview.debt_staleness_risk_proxy
|
||||
? "договорные сроки оплаты/due-date просрочка"
|
||||
: overview.debt_open_settlement_quality
|
||||
? "due-date просрочка"
|
||||
: "качество открытых расчетов");
|
||||
if (!overview.debt_due_date_aging) {
|
||||
unknownFamilies.push(overview.debt_staleness_risk_proxy
|
||||
? "договорные сроки оплаты/due-date просрочка"
|
||||
: overview.debt_open_settlement_quality
|
||||
? "due-date просрочка"
|
||||
: "качество открытых расчетов");
|
||||
}
|
||||
unknownFamilies.push(businessOverviewInventoryUnknownLabel(overview));
|
||||
const metricLead = businessOverviewHeadlineMetricsLine(overview);
|
||||
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 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 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 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.");
|
||||
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) {
|
||||
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) {
|
||||
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.");
|
||||
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)) {
|
||||
claims.push("Do not claim full document history outside the checked period.");
|
||||
@@ -885,24 +1072,38 @@ function derivedRankedValueFlowConfirmedLine(pilot) {
|
||||
return null;
|
||||
}
|
||||
const leader = ranking.ranked_values[0];
|
||||
const leaderLooksFinancial = isFinancialInstitutionBucket(leader);
|
||||
const organization = ranking.organization_scope ? ` по организации ${ranking.organization_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) {
|
||||
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
|
||||
? " Лимит строк проверки достигнут; сравнение с другими контрагентами может быть неполным."
|
||||
: " Других контрагентов в этом проверенном срезе не найдено, поэтому это не полноценный сравнительный рейтинг.";
|
||||
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.ranking_need === "bottom_asc"
|
||||
? ranking.value_flow_direction === "outgoing_supplier_payout"
|
||||
? "Меньше всего заплатили контрагенту"
|
||||
: "Меньше всего денег принёс контрагент"
|
||||
: ranking.value_flow_direction === "outgoing_supplier_payout"
|
||||
? "Больше всего заплатили контрагенту"
|
||||
: "Больше всего денег принёс контрагент";
|
||||
const tail = ranking.ranked_values
|
||||
.slice(1, 3)
|
||||
.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
|
||||
? " Лимит строк проверки достигнут; рейтинг может быть неполным."
|
||||
: "";
|
||||
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) {
|
||||
const flow = pilot.derived_value_flow;
|
||||
@@ -1070,13 +1271,13 @@ function derivedBusinessOverviewConfirmedLines(pilot) {
|
||||
if (strongestIncomingYear) {
|
||||
lines.push(`Самый сильный год по подтвержденным входящим поступлениям: ${strongestIncomingYear.year_bucket} — ${strongestIncomingYear.incoming_total_amount_human_ru} по ${strongestIncomingYear.incoming_rows_with_amount} строкам с суммой. Это не бухгалтерская прибыль.`);
|
||||
}
|
||||
const leader = overview.top_customers[0];
|
||||
if (leader) {
|
||||
lines.push(`Самый крупный подтвержденный клиент в проверенном срезе: ${leader.axis_value} — ${leader.total_amount_human_ru}.`);
|
||||
const incomingLeaderLine = businessOverviewIncomingLeaderLine(overview);
|
||||
if (incomingLeaderLine) {
|
||||
lines.push(incomingLeaderLine);
|
||||
}
|
||||
const supplierLeader = overview.top_suppliers?.[0];
|
||||
if (supplierLeader) {
|
||||
lines.push(`Самый крупный подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${supplierLeader.axis_value} — ${supplierLeader.total_amount_human_ru}.`);
|
||||
const outgoingLeaderLine = businessOverviewOutgoingLeaderLine(overview);
|
||||
if (outgoingLeaderLine) {
|
||||
lines.push(outgoingLeaderLine);
|
||||
}
|
||||
if (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}.`);
|
||||
}
|
||||
if (overview.accounting_financial_result) {
|
||||
const accountingFinancialResultText = businessOverviewAccountingFinancialResultText(overview);
|
||||
if (accountingFinancialResultText) {
|
||||
lines.push(accountingFinancialResultText);
|
||||
}
|
||||
}
|
||||
if (overview.trading_margin_proxy) {
|
||||
const proxy = overview.trading_margin_proxy;
|
||||
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}` : "";
|
||||
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) {
|
||||
const leader = overview.inventory_position.top_items[0];
|
||||
const leaderText = leader
|
||||
@@ -1203,6 +1414,13 @@ function businessOverviewCustomerConcentrationLine(overview) {
|
||||
return null;
|
||||
}
|
||||
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
|
||||
? `Концентрация входящего потока: крупнейший подтвержденный клиент ${leader.axis_value} дает около ${share} проверенных входящих поступлений (${leader.total_amount_human_ru}). Это сигнал зависимости от клиента, а не полный customer-risk аудит.`
|
||||
: `Крупнейший подтвержденный клиент в проверенном срезе: ${leader.axis_value} — ${leader.total_amount_human_ru}.`;
|
||||
@@ -1213,6 +1431,13 @@ function businessOverviewSupplierConcentrationLine(overview) {
|
||||
return null;
|
||||
}
|
||||
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
|
||||
? `Концентрация исходящего потока: крупнейший подтвержденный поставщик/получатель исходящих платежей ${leader.axis_value} держит около ${share} проверенных исходящих платежей (${leader.total_amount_human_ru}). Это сигнал procurement concentration по найденным строкам, а не полный vendor-risk аудит или структура всех расходов.`
|
||||
: `Крупнейший подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${leader.axis_value} — ${leader.total_amount_human_ru}.`;
|
||||
@@ -1257,6 +1482,18 @@ function businessOverviewRiskSynthesisLine(overview) {
|
||||
: `маржинальность proxy ${overview.trading_margin_proxy.margin_to_revenue_pct}%`;
|
||||
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) {
|
||||
const debtDirection = overview.debt_position.net_debt_position_direction === "net_receivable"
|
||||
? `дебиторка больше кредиторки на ${overview.debt_position.net_debt_position_amount_human_ru}`
|
||||
@@ -1275,6 +1512,16 @@ function businessOverviewRiskSynthesisLine(overview) {
|
||||
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}%`);
|
||||
}
|
||||
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) {
|
||||
const topDocument = overview.document_activity_profile.top_document_types[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) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
pushReason(reasonCodes, "answer_contains_business_overview_inventory_position");
|
||||
}
|
||||
|
||||
@@ -33,6 +33,11 @@ function isMcpDiscoveryEntryPointContract(value) {
|
||||
return (record?.schema_version === "assistant_mcp_discovery_runtime_entry_point_v1" &&
|
||||
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) {
|
||||
if (isMcpDiscoveryEntryPointContract(input.entryPoint)) {
|
||||
return input.entryPoint;
|
||||
@@ -47,6 +52,7 @@ function buildAssistantMcpDiscoveryDebugAttachmentFields(input) {
|
||||
const bridge = toRecordObject(entryPoint?.bridge);
|
||||
const planner = toRecordObject(bridge?.planner);
|
||||
const chainAlignment = toRecordObject(planner?.catalog_chain_template_alignment);
|
||||
const routeCandidate = isRouteCandidateContract(bridge?.route_candidate) ? bridge.route_candidate : null;
|
||||
const answerDraft = toRecordObject(bridge?.answer_draft);
|
||||
return {
|
||||
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_top_match: toNonEmptyString(chainAlignment?.top_chain_template_match),
|
||||
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_business_fact_answer_allowed: bridge?.business_fact_answer_allowed === true,
|
||||
mcp_discovery_user_facing_response_allowed: bridge?.user_facing_response_allowed === true,
|
||||
|
||||
+448
-14
@@ -6,6 +6,7 @@ const addressMcpClient_1 = require("./addressMcpClient");
|
||||
const assistantMcpDiscoveryRuntimeAdapter_1 = require("./assistantMcpDiscoveryRuntimeAdapter");
|
||||
const assistantMcpDiscoveryPolicy_1 = require("./assistantMcpDiscoveryPolicy");
|
||||
const addressRecipeCatalog_1 = require("./addressRecipeCatalog");
|
||||
const counterpartyRoleHeuristics_1 = require("./counterpartyRoleHeuristics");
|
||||
exports.ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION = "assistant_mcp_discovery_pilot_executor_v1";
|
||||
const DEFAULT_DEPS = {
|
||||
executeAddressMcpQuery: addressMcpClient_1.executeAddressMcpQuery,
|
||||
@@ -200,6 +201,16 @@ function buildBusinessOverviewDebtFilters(planner) {
|
||||
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) {
|
||||
const meaning = planner.discovery_plan.turn_meaning_ref;
|
||||
const organization = toNonEmptyString(meaning?.explicit_organization_scope);
|
||||
@@ -231,6 +242,17 @@ function buildBusinessOverviewTradingMarginFilters(planner) {
|
||||
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) {
|
||||
const meaning = planner.discovery_plan.turn_meaning_ref;
|
||||
const subject = firstEntityCandidate(planner);
|
||||
@@ -715,7 +737,8 @@ async function executeCoverageAwareValueFlowQuery(input) {
|
||||
});
|
||||
executedProbeCount += 1;
|
||||
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) {
|
||||
pushUnique(queryLimitations, broadResult.error);
|
||||
return {
|
||||
@@ -772,7 +795,8 @@ async function executeCoverageAwareValueFlowQuery(input) {
|
||||
pushUnique(queryLimitations, chunkResult.error);
|
||||
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;
|
||||
}
|
||||
chunkResults.push(chunkResult);
|
||||
@@ -1604,6 +1628,13 @@ function extractContractDateFromText(value) {
|
||||
if (!text || !/(?:договор|contract|дог\.)/iu.test(text)) {
|
||||
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})/);
|
||||
if (isoLikeMatch) {
|
||||
return normalizeDateParts(isoLikeMatch[1], isoLikeMatch[2], isoLikeMatch[3]);
|
||||
@@ -1614,6 +1645,59 @@ function extractContractDateFromText(value) {
|
||||
}
|
||||
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) {
|
||||
if (!left) {
|
||||
return right;
|
||||
@@ -2038,7 +2122,8 @@ function deriveRankedValueFlow(result, input) {
|
||||
axis_value: axisValue,
|
||||
rows_with_amount: bucket.rows_with_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) => {
|
||||
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"
|
||||
};
|
||||
}
|
||||
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) {
|
||||
if (!result || result.error || result.matched_rows <= 0 || !periodScope) {
|
||||
return null;
|
||||
@@ -2554,6 +2732,121 @@ function deriveBusinessOverviewDebtStalenessRiskProxy(quality) {
|
||||
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) {
|
||||
if (riskBand === "high") {
|
||||
return "высокая зона внимания";
|
||||
@@ -2748,7 +3041,7 @@ function buildBusinessOverviewMissingProofFamilies(input) {
|
||||
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({
|
||||
family: "accounting_profit_margin",
|
||||
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"
|
||||
});
|
||||
}
|
||||
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({
|
||||
family: "debt_due_date_aging_quality",
|
||||
current_status: input.debtStalenessRiskProxy
|
||||
@@ -2830,6 +3123,7 @@ function deriveBusinessOverview(input) {
|
||||
});
|
||||
const activityPeriod = deriveActivityPeriod(input.lifecycleResult);
|
||||
const taxPosition = deriveBusinessOverviewTaxPosition(input.taxResult, input.periodScope);
|
||||
const accountingFinancialResult = deriveBusinessOverviewAccountingFinancialResult(input.accountingFinancialResultResult, input.periodScope);
|
||||
const tradingMarginProxy = deriveBusinessOverviewTradingMarginProxy(input.tradingMarginResult, input.periodScope);
|
||||
const debtPosition = deriveBusinessOverviewDebtPosition({
|
||||
receivablesResult: input.receivablesResult,
|
||||
@@ -2844,6 +3138,10 @@ function deriveBusinessOverview(input) {
|
||||
const counterpartyProfile = deriveBusinessOverviewCounterpartyProfile(input.counterpartyProfileResult, input.periodScope);
|
||||
const contractUsageProfile = deriveBusinessOverviewContractUsageProfile(input.contractUsageProfileResult, input.periodScope);
|
||||
const debtStalenessRiskProxy = deriveBusinessOverviewDebtStalenessRiskProxy(debtOpenSettlementQuality);
|
||||
const debtDueDateAging = deriveBusinessOverviewDebtDueDateAging({
|
||||
dueDateResult: input.dueDateAgingResult,
|
||||
debtAsOfDate: input.debtAsOfDate
|
||||
});
|
||||
const inventoryPosition = deriveBusinessOverviewInventoryPosition({
|
||||
inventoryOnHandResult: input.inventoryOnHandResult,
|
||||
inventoryAgingResult: input.inventoryAgingResult,
|
||||
@@ -2862,10 +3160,12 @@ function deriveBusinessOverview(input) {
|
||||
outgoing.rows_with_amount > 0,
|
||||
Boolean(activityPeriod),
|
||||
Boolean(taxPosition),
|
||||
Boolean(accountingFinancialResult),
|
||||
Boolean(tradingMarginProxy),
|
||||
Boolean(debtPosition),
|
||||
Boolean(debtOpenSettlementQuality),
|
||||
Boolean(debtStalenessRiskProxy),
|
||||
Boolean(debtDueDateAging),
|
||||
Boolean(documentActivityProfile),
|
||||
Boolean(counterpartyProfile),
|
||||
Boolean(contractUsageProfile),
|
||||
@@ -2879,9 +3179,9 @@ function deriveBusinessOverview(input) {
|
||||
const netAmount = incoming.total_amount - outgoing.total_amount;
|
||||
const hasBusinessOverviewProfileSignal = Boolean(documentActivityProfile || counterpartyProfile || contractUsageProfile);
|
||||
const missingSignalFamilies = [
|
||||
tradingMarginProxy ? "accounting_profit_margin" : "profit_margin",
|
||||
accountingFinancialResult ? null : tradingMarginProxy ? "accounting_profit_margin" : "profit_margin",
|
||||
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",
|
||||
inventoryPosition
|
||||
? inventoryStalenessRiskProxy
|
||||
@@ -2894,9 +3194,11 @@ function deriveBusinessOverview(input) {
|
||||
].filter((item) => Boolean(item));
|
||||
const missingProofFamilies = buildBusinessOverviewMissingProofFamilies({
|
||||
missingSignalFamilies,
|
||||
accountingFinancialResult,
|
||||
tradingMarginProxy,
|
||||
debtOpenSettlementQuality,
|
||||
debtStalenessRiskProxy,
|
||||
debtDueDateAging,
|
||||
inventoryPosition,
|
||||
inventoryTurnoverProxy,
|
||||
inventoryStalenessRiskProxy,
|
||||
@@ -2915,10 +3217,12 @@ function deriveBusinessOverview(input) {
|
||||
yearly_breakdown: yearlyBreakdown,
|
||||
activity_period: activityPeriod,
|
||||
tax_position: taxPosition,
|
||||
accounting_financial_result: accountingFinancialResult,
|
||||
trading_margin_proxy: tradingMarginProxy,
|
||||
debt_position: debtPosition,
|
||||
debt_open_settlement_quality: debtOpenSettlementQuality,
|
||||
debt_staleness_risk_proxy: debtStalenessRiskProxy,
|
||||
debt_due_date_aging: debtDueDateAging,
|
||||
inventory_position: inventoryPosition,
|
||||
inventory_turnover_proxy: inventoryTurnoverProxy,
|
||||
inventory_staleness_risk_proxy: inventoryStalenessRiskProxy,
|
||||
@@ -2929,9 +3233,9 @@ function deriveBusinessOverview(input) {
|
||||
checked_signal_count: checkedSignalCount,
|
||||
missing_signal_families: missingSignalFamilies,
|
||||
missing_proof_families: missingProofFamilies,
|
||||
inference_basis: hasBusinessOverviewProfileSignal || inventoryPosition
|
||||
inference_basis: hasBusinessOverviewProfileSignal || inventoryPosition || accountingFinancialResult
|
||||
? "business_overview_from_confirmed_1c_multi_family_rows"
|
||||
: debtOpenSettlementQuality
|
||||
: debtOpenSettlementQuality || debtDueDateAging
|
||||
? "business_overview_from_confirmed_1c_multi_family_rows"
|
||||
: taxPosition && debtPosition
|
||||
? "business_overview_from_confirmed_1c_money_activity_tax_and_debt_rows"
|
||||
@@ -2956,6 +3260,9 @@ function summarizeBusinessOverviewRows(input) {
|
||||
if (input.taxResult && !input.taxResult.error) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
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}` : "";
|
||||
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) {
|
||||
const leader = derived.inventory_position.top_items[0];
|
||||
const leaderText = leader
|
||||
@@ -3133,6 +3480,9 @@ function buildBusinessOverviewInferredFacts(derived) {
|
||||
const supplierSharePct = supplierLeader
|
||||
? percentageOfTotal(supplierLeader.total_amount, derived.outgoing_supplier_payout.total_amount)
|
||||
: null;
|
||||
const supplierLeaderIsFinancial = supplierLeader
|
||||
? (0, counterpartyRoleHeuristics_1.isLikelyFinancialInstitutionCounterparty)(supplierLeader.axis_value)
|
||||
: false;
|
||||
const strongestIncomingYear = [...derived.yearly_breakdown]
|
||||
.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];
|
||||
@@ -3142,9 +3492,13 @@ function buildBusinessOverviewInferredFacts(derived) {
|
||||
return [
|
||||
`Расчетное нетто по найденным строкам: ${derived.net_amount_human_ru}; ${direction}.`,
|
||||
supplierLeader
|
||||
? supplierSharePct !== null
|
||||
? `Крупнейший подтвержденный поставщик/получатель исходящих платежей ${supplierLeader.axis_value} держит около ${supplierSharePct}% проверенного исходящего потока (${supplierLeader.total_amount_human_ru}). Это procurement concentration proxy по найденным строкам, а не полный vendor-risk аудит.`
|
||||
: `Крупнейший подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${supplierLeader.axis_value} — ${supplierLeader.total_amount_human_ru}.`
|
||||
? supplierLeaderIsFinancial
|
||||
? supplierSharePct !== null
|
||||
? `Крупнейший получатель исходящих денег ${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,
|
||||
strongestIncomingYear
|
||||
? `Самый сильный год по подтвержденным входящим поступлениям: ${strongestIncomingYear.year_bucket} (${strongestIncomingYear.incoming_total_amount_human_ru}).`
|
||||
@@ -3803,10 +4157,12 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
||||
let outgoingResult = null;
|
||||
let lifecycleResult = null;
|
||||
let taxResult = null;
|
||||
let accountingFinancialResultResult = null;
|
||||
let tradingMarginResult = null;
|
||||
let receivablesResult = null;
|
||||
let payablesResult = null;
|
||||
let openContractsResult = null;
|
||||
let dueDateAgingResult = null;
|
||||
let documentActivityProfileResult = null;
|
||||
let counterpartyProfileResult = null;
|
||||
let contractUsageProfileResult = null;
|
||||
@@ -3816,8 +4172,10 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
||||
const lifecycleFilters = buildLifecycleFilters(planner);
|
||||
const profileFilters = buildBusinessOverviewProfileFilters(planner);
|
||||
const taxFilters = buildBusinessOverviewTaxFilters(planner);
|
||||
const accountingFinancialResultFilters = buildBusinessOverviewAccountingFinancialResultFilters(planner);
|
||||
const tradingMarginFilters = buildBusinessOverviewTradingMarginFilters(planner);
|
||||
const debtFilters = buildBusinessOverviewDebtFilters(planner);
|
||||
const debtDueDateAgingProbeEnabled = shouldRunDebtDueDateAgingProbe(planner);
|
||||
const inventoryFilters = buildBusinessOverviewInventoryFilters(planner);
|
||||
const debtAsOfDate = toNonEmptyString(debtFilters?.as_of_date);
|
||||
const inventoryAsOfDate = toNonEmptyString(inventoryFilters?.as_of_date);
|
||||
@@ -3830,6 +4188,9 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
||||
const taxSelection = taxFilters
|
||||
? (0, addressRecipeCatalog_1.selectAddressRecipe)("vat_liability_confirmed_for_tax_period", taxFilters)
|
||||
: null;
|
||||
const accountingFinancialResultSelection = accountingFinancialResultFilters
|
||||
? (0, addressRecipeCatalog_1.selectAddressRecipe)("accounting_financial_result_for_organization", accountingFinancialResultFilters)
|
||||
: null;
|
||||
const tradingMarginSelection = tradingMarginFilters
|
||||
? (0, addressRecipeCatalog_1.selectAddressRecipe)("inventory_trading_margin_proxy_for_organization", tradingMarginFilters)
|
||||
: null;
|
||||
@@ -3842,6 +4203,9 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
||||
const openContractsSelection = debtFilters
|
||||
? (0, addressRecipeCatalog_1.selectAddressRecipe)("open_contracts_confirmed_as_of_date", debtFilters)
|
||||
: null;
|
||||
const dueDateAgingSelection = debtFilters && debtDueDateAgingProbeEnabled
|
||||
? (0, addressRecipeCatalog_1.selectAddressRecipe)("debt_due_date_aging_for_organization", debtFilters)
|
||||
: null;
|
||||
const inventoryOnHandSelection = inventoryFilters
|
||||
? (0, addressRecipeCatalog_1.selectAddressRecipe)("inventory_on_hand_as_of_date", inventoryFilters)
|
||||
: null;
|
||||
@@ -3909,6 +4273,16 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
||||
pushReason(reasonCodes, "pilot_business_overview_tax_recipe_not_available");
|
||||
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) {
|
||||
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");
|
||||
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) {
|
||||
pushReason(reasonCodes, "pilot_business_overview_inventory_on_hand_recipe_selected");
|
||||
if (inventoryAgingSelection?.selected_recipe) {
|
||||
@@ -3982,6 +4369,14 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
||||
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) {
|
||||
const receivablesPlan = (0, addressRecipeCatalog_1.buildAddressRecipePlan)(receivablesSelection.selected_recipe, debtFilters);
|
||||
receivablesResult = await runtimeDeps.executeAddressMcpQuery({
|
||||
@@ -4004,6 +4399,14 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
||||
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) {
|
||||
const inventoryOnHandPlan = (0, addressRecipeCatalog_1.buildAddressRecipePlan)(inventoryOnHandSelection.selected_recipe, inventoryFilters);
|
||||
inventoryOnHandResult = await runtimeDeps.executeAddressMcpQuery({
|
||||
@@ -4025,6 +4428,9 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
||||
if (taxResult) {
|
||||
probeResults.push(queryResultToProbeResult(step.primitive_id, taxResult));
|
||||
}
|
||||
if (accountingFinancialResultResult) {
|
||||
probeResults.push(queryResultToProbeResult(step.primitive_id, accountingFinancialResultResult));
|
||||
}
|
||||
if (receivablesResult) {
|
||||
probeResults.push(queryResultToProbeResult(step.primitive_id, receivablesResult));
|
||||
}
|
||||
@@ -4034,6 +4440,9 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
||||
if (openContractsResult) {
|
||||
probeResults.push(queryResultToProbeResult(step.primitive_id, openContractsResult));
|
||||
}
|
||||
if (dueDateAgingResult) {
|
||||
probeResults.push(queryResultToProbeResult(step.primitive_id, dueDateAgingResult));
|
||||
}
|
||||
if (inventoryOnHandResult) {
|
||||
probeResults.push(queryResultToProbeResult(step.primitive_id, inventoryOnHandResult));
|
||||
}
|
||||
@@ -4059,6 +4468,13 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
||||
else if (taxResult) {
|
||||
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) {
|
||||
pushUnique(queryLimitations, receivablesResult.error);
|
||||
pushReason(reasonCodes, "pilot_business_overview_receivables_query_mcp_error");
|
||||
@@ -4084,6 +4500,13 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
||||
else if (openContractsResult) {
|
||||
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) {
|
||||
pushUnique(queryLimitations, inventoryOnHandResult.error);
|
||||
pushReason(reasonCodes, "pilot_business_overview_inventory_on_hand_query_mcp_error");
|
||||
@@ -4194,10 +4617,12 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
||||
outgoingResult,
|
||||
lifecycleResult,
|
||||
taxResult,
|
||||
accountingFinancialResultResult,
|
||||
tradingMarginResult,
|
||||
receivablesResult,
|
||||
payablesResult,
|
||||
openContractsResult,
|
||||
dueDateAgingResult,
|
||||
documentActivityProfileResult,
|
||||
counterpartyProfileResult,
|
||||
contractUsageProfileResult,
|
||||
@@ -4234,6 +4659,9 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
||||
if (derivedBusinessOverview.tax_position) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
pushReason(reasonCodes, "pilot_derived_business_overview_inventory_position_from_confirmed_rows");
|
||||
}
|
||||
@@ -4267,10 +4699,12 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
|
||||
outgoingResult,
|
||||
lifecycleResult,
|
||||
taxResult,
|
||||
accountingFinancialResultResult,
|
||||
tradingMarginResult,
|
||||
receivablesResult,
|
||||
payablesResult,
|
||||
openContractsResult,
|
||||
dueDateAgingResult,
|
||||
documentActivityProfileResult,
|
||||
counterpartyProfileResult,
|
||||
contractUsageProfileResult,
|
||||
|
||||
@@ -19,7 +19,7 @@ exports.ASSISTANT_MCP_DISCOVERY_PRIMITIVES = [
|
||||
];
|
||||
const DEFAULT_DISCOVERY_BUDGET = {
|
||||
max_probe_count: 3,
|
||||
max_rows_per_probe: 100
|
||||
max_rows_per_probe: 200
|
||||
};
|
||||
const MAX_PROBE_COUNT = 36;
|
||||
const MAX_ROWS_PER_PROBE = 500;
|
||||
|
||||
+209
-17
@@ -2,6 +2,7 @@
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.ASSISTANT_MCP_DISCOVERY_RESPONSE_CANDIDATE_SCHEMA_VERSION = void 0;
|
||||
exports.buildAssistantMcpDiscoveryResponseCandidate = buildAssistantMcpDiscoveryResponseCandidate;
|
||||
const counterpartyRoleHeuristics_1 = require("./counterpartyRoleHeuristics");
|
||||
exports.ASSISTANT_MCP_DISCOVERY_RESPONSE_CANDIDATE_SCHEMA_VERSION = "assistant_mcp_discovery_response_candidate_v1";
|
||||
function toRecordObject(value) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
@@ -67,7 +68,26 @@ function hasInternalMechanics(value) {
|
||||
function userFacingLines(values) {
|
||||
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) {
|
||||
const sanitizedValue = sanitizeUserFacingMechanics(value);
|
||||
if (/^1C activity rows were found for the requested counterparty scope$/i.test(value)) {
|
||||
return "В 1С найдены строки активности в запрошенном срезе.";
|
||||
}
|
||||
@@ -88,7 +108,7 @@ function localizeLine(value) {
|
||||
return `В 1С проверены входящие и исходящие денежные строки в запрошенном срезе: ${incoming}, ${outgoing}.`;
|
||||
}
|
||||
if (/^Requested period hit the MCP row limit, but the approved monthly recovery probe budget is smaller than the required subperiod count$/i.test(value)) {
|
||||
return "Запрошенный период уперся в лимит строк MCP; доступного бюджета помесячных дозапросов не хватило, чтобы покрыть все подпериоды.";
|
||||
return "Запрошенный период достиг лимита строк; доступного бюджета помесячных дозапросов не хватило, чтобы покрыть все подпериоды.";
|
||||
}
|
||||
const counterpartyMatch = value.match(/^1C activity rows were found for counterparty\s+(.+)$/i);
|
||||
if (counterpartyMatch) {
|
||||
@@ -113,10 +133,10 @@ function localizeLine(value) {
|
||||
}
|
||||
const movementRowsMatch = value.match(/^1C movement rows were found for counterparty\s+(.+)$/i);
|
||||
if (movementRowsMatch) {
|
||||
return `В 1С найдены строки движений по контрагенту ${movementRowsMatch[1]}.`;
|
||||
return `В 1С найдены строки движений по контрагенту ${movementRowsMatch[1]}.`;
|
||||
}
|
||||
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);
|
||||
if (supplierPayoutMatch) {
|
||||
@@ -144,7 +164,7 @@ function localizeLine(value) {
|
||||
return "Срез документов ограничен только подтвержденными строками документов в проверенном окне.";
|
||||
}
|
||||
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)) {
|
||||
return "Сумма входящих поступлений рассчитана только по подтвержденным строкам поступлений в 1С.";
|
||||
@@ -227,10 +247,10 @@ function localizeLine(value) {
|
||||
return "Полный срез документов без явно проверенного периода не подтвержден.";
|
||||
}
|
||||
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)) {
|
||||
return "Полный срез движений без явно проверенного периода не подтвержден.";
|
||||
return "Полный срез движений без явно проверенного периода не подтвержден.";
|
||||
}
|
||||
if (/^Full supplier-payout amount outside the checked period is not proven by this MCP discovery pilot$/i.test(value)) {
|
||||
return "Полный объем исходящих платежей вне проверенного периода этим поиском не подтвержден.";
|
||||
@@ -245,10 +265,10 @@ function localizeLine(value) {
|
||||
return "Полный двусторонний денежный поток за все время без явно проверенного периода не подтвержден.";
|
||||
}
|
||||
if (/^Requested period coverage was recovered through monthly 1C value-flow probes after the broad probe hit the row limit$/i.test(value)) {
|
||||
return "Покрытие запрошенного периода восстановлено помесячными проверками 1С после того, как общая выборка уперлась в лимит строк.";
|
||||
return "Покрытие запрошенного периода восстановлено помесячными проверками 1С после того, как общая выборка достигла лимита строк.";
|
||||
}
|
||||
if (/^Requested period coverage for bidirectional value-flow was recovered through monthly 1C side probes after a broad probe hit the row limit$/i.test(value)) {
|
||||
return "Покрытие запрошенного периода по двустороннему денежному потоку восстановлено помесячными проверками 1С после того, как общая выборка уперлась в лимит строк хотя бы по одной стороне.";
|
||||
return "Покрытие запрошенного периода по двустороннему денежному потоку восстановлено помесячными проверками 1С после того, как общая выборка достигла лимита строк хотя бы по одной стороне.";
|
||||
}
|
||||
if (/^Requested period coverage was recovered through monthly 1C value-flow probes$/i.test(value)) {
|
||||
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)) {
|
||||
return "Полное покрытие запрошенного периода по двустороннему денежному потоку не подтверждено доступными проверенными строками.";
|
||||
}
|
||||
return value;
|
||||
return sanitizedValue;
|
||||
}
|
||||
function section(title, lines) {
|
||||
const clean = userFacingLines(lines.map(localizeLine));
|
||||
@@ -345,7 +365,7 @@ function businessOverviewCoverageLimitLine(overview) {
|
||||
limited.push("исходящие");
|
||||
}
|
||||
return limited.length > 0
|
||||
? `Важно: ${limited.join(" и ")} уперлись в лимит выборки MCP, поэтому это проверенный срез найденных строк, а не гарантированно полный бухгалтерский оборот.`
|
||||
? `Важно: по направлению ${limited.join(" и ")} проверка достигла лимита строк; это расширенный проверенный срез найденных строк, но не гарантия полного бухгалтерского оборота без отдельной полной выгрузки.`
|
||||
: null;
|
||||
}
|
||||
function businessOverviewYearRowsLine(overview) {
|
||||
@@ -371,6 +391,30 @@ function firstOverviewAxisLabel(rows, amountKey = "total_amount_human_ru") {
|
||||
const amount = moneyText(first?.[amountKey]);
|
||||
return label && amount ? `${label} — ${sentenceAmount(amount) ?? amount}` : null;
|
||||
}
|
||||
function firstNonFinancialOverviewAxisLabel(rows, amountKey = "total_amount_human_ru") {
|
||||
if (!Array.isArray(rows)) {
|
||||
return null;
|
||||
}
|
||||
for (const row of rows) {
|
||||
const item = toRecordObject(row);
|
||||
const label = toNonEmptyString(item?.axis_value);
|
||||
if (!label || (0, counterpartyRoleHeuristics_1.isLikelyFinancialInstitutionCounterparty)(label)) {
|
||||
continue;
|
||||
}
|
||||
const amount = moneyText(item?.[amountKey]);
|
||||
if (amount) {
|
||||
return `${label} — ${sentenceAmount(amount) ?? amount}`;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function overviewAxisLooksFinancial(row) {
|
||||
if (!row) {
|
||||
return false;
|
||||
}
|
||||
return (row.counterparty_role_hint === "bank_or_financial_institution" ||
|
||||
(0, counterpartyRoleHeuristics_1.isLikelyFinancialInstitutionCounterparty)(row.axis_value));
|
||||
}
|
||||
function businessOverviewTaxLine(overview) {
|
||||
const tax = toRecordObject(overview.tax_position);
|
||||
if (!tax) {
|
||||
@@ -487,7 +531,7 @@ function buildCompactBidirectionalValueFlowReply(entryPoint, draft) {
|
||||
lines.push(`Основа: ${basis.join("; ")}.`);
|
||||
}
|
||||
if (flow.coverage_limited_by_probe_limit === true) {
|
||||
lines.push("Важно: часть проверки уперлась в лимит строк, поэтому это проверенный срез найденных движений, а не гарантия полного периода.");
|
||||
lines.push("Важно: часть проверки достигла лимита строк, поэтому это проверенный срез найденных движений, а не гарантия полного периода.");
|
||||
}
|
||||
lines.push("Метод: нетто рассчитано как подтвержденные входящие строки 1С минус подтвержденные исходящие строки; это не полное бухгалтерское сальдо вне проверенного окна.");
|
||||
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 customerName = toNonEmptyString(topCustomer?.axis_value);
|
||||
const customerAmount = moneyText(topCustomer?.total_amount_human_ru);
|
||||
const topCustomerLooksFinancial = overviewAxisLooksFinancial(topCustomer);
|
||||
const nonFinancialCustomer = firstNonFinancialOverviewAxisLabel(topCustomerLooksFinancial ? overview.top_customers : []);
|
||||
const topCustomerLead = customerName && customerAmount
|
||||
? `; крупнейший источник входящих денег: ${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 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 graphReasonCodes = toStringList(graph?.reason_codes);
|
||||
const directMoneyAnswer = graphReasonCodes.includes("data_need_graph_business_overview_direct_money_answer");
|
||||
const crossScopeExecutiveSummary = Boolean(separateSubject && previousCounterpartySummary);
|
||||
const lines = [];
|
||||
const actionFamily = toNonEmptyString(turnMeaning?.asked_action_family);
|
||||
const unsupportedFamily = toNonEmptyString(turnMeaning?.unsupported_but_understood_family);
|
||||
const profitMarginBoundary = actionFamily === "profit_margin_boundary" || unsupportedFamily === "profit_margin_boundary";
|
||||
const debtDueDateBoundary = actionFamily === "debt_due_date_boundary" || unsupportedFamily === "debt_due_date_boundary";
|
||||
const vendorRiskBoundary = actionFamily === "vendor_risk_procurement_boundary" || unsupportedFamily === "vendor_risk_procurement_boundary";
|
||||
const inventoryReserveBoundary = actionFamily === "inventory_reserve_boundary" || unsupportedFamily === "inventory_reserve_liquidation_boundary";
|
||||
if (profitMarginBoundary) {
|
||||
const accountingFinancialResult = toRecordObject(overview.accounting_financial_result);
|
||||
if (accountingFinancialResult) {
|
||||
const direction = toNonEmptyString(accountingFinancialResult.final_result_direction);
|
||||
const amount = moneyText(accountingFinancialResult.final_result_amount_human_ru);
|
||||
const periodScope = toNonEmptyString(accountingFinancialResult.period_scope) ?? period;
|
||||
const marginPct = typeof accountingFinancialResult.net_margin_to_revenue_pct === "number" &&
|
||||
Number.isFinite(accountingFinancialResult.net_margin_to_revenue_pct)
|
||||
? `${accountingFinancialResult.net_margin_to_revenue_pct}%`
|
||||
: null;
|
||||
const directionText = direction === "profit"
|
||||
? "учетная прибыль"
|
||||
: direction === "loss"
|
||||
? "учетный убыток"
|
||||
: "нулевой учетный финрезультат";
|
||||
const amountText = amount
|
||||
? direction === "loss"
|
||||
? `минус ${amount}`
|
||||
: amount
|
||||
: "сумма не распознана";
|
||||
lines.push(`Коротко: по бухгалтерскому маршруту 90/91/99 за ${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)) {
|
||||
lines.push(`Коротко: по компании ${organizationScope ?? "в выбранном контуре"} ${period} подтвержден денежный срез: получили ${incomingAmount ?? "0 руб."}, исходящие платежи/списания ${outgoingAmount ?? "0 руб."}, ${netDirection} ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}${previousCounterpartySummary.lead}; можно утверждать только эти подтвержденные срезы, нельзя называть это чистой прибылью, полным оборотом или доказанной ролью главного клиента/поставщика.`);
|
||||
lines.push(previousCounterpartySummary.line);
|
||||
@@ -640,7 +826,7 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
||||
if (!leaderYear || !leaderAmount) {
|
||||
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 netYearAmount = moneyText(netLeader?.net_amount_human_ru);
|
||||
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('Метод: "заработали" здесь считаю как денежный operating-flow proxy по 1С; это не чистая прибыль и не финрезультат.');
|
||||
if (!directMoneyAnswer && customerName && customerAmount) {
|
||||
lines.push(`Крупнейший подтвержденный источник входящих денег в этом срезе: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}.`);
|
||||
lines.push(topCustomerLooksFinancial
|
||||
? `Крупнейший входящий денежный источник в этом срезе: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}. По названию это банк/финансовая организация, поэтому без назначения платежа не называю это клиентской выручкой.${nonFinancialCustomer ? ` Крупнейший небанковский входящий контрагент: ${nonFinancialCustomer}.` : ""}`
|
||||
: `Крупнейший подтвержденный источник входящих денег в этом срезе: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}.`);
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -671,10 +859,14 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
||||
`Отдельно по контрагенту ${separateSubject}: этот итог не переносит суммы компании на контрагента. Можно утверждать только разделение контура; нельзя делать вывод о выручке, долге или прибыльности ${separateSubject} без отдельного контрагентского среза документов и движений.`);
|
||||
}
|
||||
if (!directMoneyAnswer && topSupplier) {
|
||||
lines.push(`Крупнейший подтвержденный получатель исходящих денег: ${topSupplier}.`);
|
||||
lines.push(topSupplierLooksFinancial
|
||||
? `Крупнейший получатель исходящих денег: ${topSupplier}. По названию это банк/финансовая организация, поэтому без назначения платежа/договора не считаю это обычным поставщиком.${nonFinancialSupplier ? ` Крупнейший небанковский получатель исходящих денег: ${nonFinancialSupplier}.` : ""}`
|
||||
: `Крупнейший подтвержденный получатель исходящих денег: ${topSupplier}.`);
|
||||
}
|
||||
if (!directMoneyAnswer && (topCustomer || topSupplier)) {
|
||||
lines.push("Важно по ролям: текущий денежный срез подтверждает денежные источники и получателей, но не доказывает, что это главный клиент или главный поставщик как бизнес-роль.");
|
||||
lines.push(topCustomerLooksFinancial || topSupplierLooksFinancial
|
||||
? "Важно по ролям: текущий денежный срез подтверждает источники и получателей денег, но банковские контрагенты требуют проверки назначения платежа/счетов и не доказывают роль клиента или поставщика."
|
||||
: "Важно по ролям: текущий денежный срез подтверждает денежные источники и получателей, но не доказывает, что это главный клиент или главный поставщик как бизнес-роль.");
|
||||
}
|
||||
if (!directMoneyAnswer) {
|
||||
lines.push(`Что подтверждено: денежный срез по компании${organizationScope ? ` ${organizationScope}` : ""}${period ? ` ${period}` : ""}${topCustomer ? ", крупнейший источник входящих денег" : ""}${topSupplier ? ", крупнейший получатель исходящих денег" : ""}.`);
|
||||
|
||||
+134
-1
@@ -1,12 +1,13 @@
|
||||
"use strict";
|
||||
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;
|
||||
const assistantMcpDiscoveryAnswerAdapter_1 = require("./assistantMcpDiscoveryAnswerAdapter");
|
||||
const assistantMcpDiscoveryPilotExecutor_1 = require("./assistantMcpDiscoveryPilotExecutor");
|
||||
const assistantMcpDiscoveryPlanner_1 = require("./assistantMcpDiscoveryPlanner");
|
||||
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_ROUTE_CANDIDATE_SCHEMA_VERSION = "assistant_mcp_route_candidate_v1";
|
||||
function normalizeReasonCode(value) {
|
||||
const normalized = value
|
||||
.trim()
|
||||
@@ -58,6 +59,21 @@ function loopStatusFor(bridgeStatus) {
|
||||
}
|
||||
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) {
|
||||
const result = [];
|
||||
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 ?? [];
|
||||
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) {
|
||||
const plannerClarificationGaps = planner.discovery_plan.clarification_gaps ?? [];
|
||||
return {
|
||||
@@ -120,10 +249,13 @@ async function runAssistantMcpDiscoveryRuntimeBridge(input) {
|
||||
const answerDraft = (0, assistantMcpDiscoveryAnswerAdapter_1.buildAssistantMcpDiscoveryAnswerDraft)(pilot);
|
||||
const bridgeStatus = bridgeStatusFor(pilot, answerDraft);
|
||||
const loopState = buildLoopState(planner, pilot, bridgeStatus);
|
||||
const routeCandidate = buildRouteCandidate(planner, pilot, bridgeStatus);
|
||||
const reasonCodes = uniqueStrings([...planner.reason_codes, ...pilot.reason_codes, ...answerDraft.reason_codes]);
|
||||
pushReason(reasonCodes, `runtime_bridge_status_${bridgeStatus}`);
|
||||
pushReason(reasonCodes, "runtime_bridge_not_wired_to_hot_assistant_answer");
|
||||
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 {
|
||||
schema_version: exports.ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION,
|
||||
policy_owner: "assistantMcpDiscoveryRuntimeBridge",
|
||||
@@ -133,6 +265,7 @@ async function runAssistantMcpDiscoveryRuntimeBridge(input) {
|
||||
pilot,
|
||||
answer_draft: answerDraft,
|
||||
loop_state: loopState,
|
||||
route_candidate: routeCandidate,
|
||||
user_facing_response_allowed: bridgeStatus !== "blocked",
|
||||
business_fact_answer_allowed: businessFactAnswerAllowed(answerDraft),
|
||||
requires_user_clarification: bridgeStatus === "needs_clarification",
|
||||
|
||||
+149
-45
@@ -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);
|
||||
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) {
|
||||
if (!text) {
|
||||
return false;
|
||||
@@ -711,6 +740,12 @@ function hasExplicitVatQuestionSignal(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));
|
||||
}
|
||||
function hasExplicitVatMovementEvidenceSignal(text) {
|
||||
if (!/(?:\u043d\u0434\u0441|vat)/iu.test(text)) {
|
||||
return false;
|
||||
}
|
||||
return hasMovementEvidenceFollowupSignal(text);
|
||||
}
|
||||
function hasBusinessOverviewSeparateCounterpartySignal(text) {
|
||||
if (!text) {
|
||||
return false;
|
||||
@@ -1102,6 +1137,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||
const rawReferentialDocumentExclusionSignal = hasReferentialDocumentExclusionFollowupSignal(repairedUserText ?? rawUserText ?? "");
|
||||
const rawPrimaryBusinessOverviewSignal = hasBusinessOverviewSignal(rawText);
|
||||
const explicitVatQuestionSignal = hasExplicitVatQuestionSignal(rawText);
|
||||
const explicitVatMovementEvidenceSignal = hasExplicitVatMovementEvidenceSignal(rawText);
|
||||
const explicitVatSuppressesBusinessOverviewContinuation = Boolean(explicitVatQuestionSignal && !rawPrimaryBusinessOverviewSignal);
|
||||
const businessOverviewContinuationSignal = hasBusinessOverviewFollowupSeed(followupSeed) &&
|
||||
hasBusinessOverviewContinuationSignal(rawText) &&
|
||||
@@ -1114,6 +1150,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||
(hasValueFlowSignal(rawText) || hasValueRankingSignal(rawText) || rawBidirectionalValueFlowSignal);
|
||||
const rawMetadataSignal = !rawLifecycleSignal &&
|
||||
!rawValueFlowSignal &&
|
||||
!explicitVatMovementEvidenceSignal &&
|
||||
!rawReferentialDocumentExclusionSignal &&
|
||||
hasMetadataSignal(rawText);
|
||||
const rawEntityResolutionSignal = !rawLifecycleSignal && !rawValueFlowSignal && !rawMetadataSignal && hasEntityResolutionSignal(rawText);
|
||||
@@ -1128,7 +1165,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||
const explicitDateScopeLiteralDetected = hasExplicitDateScopeLiteral(dateScopeSignalText);
|
||||
const relativeCurrentDateHintDetected = hasRelativeCurrentDateHint(rawText);
|
||||
const rawDateScope = collectDateScopeFromRawText(dateScopeSignalText);
|
||||
const rawMetadataScopeHint = rawMetadataSignal ? metadataScopeHintFromRawText(rawText) : null;
|
||||
const rawMetadataScopeHint = rawMetadataSignal || explicitVatMovementEvidenceSignal ? metadataScopeHintFromRawText(rawText) : null;
|
||||
const rawTopicSwitchSignal = hasExplicitTopicSwitchSignal(rawText);
|
||||
const rawEntityCandidate = rawEntityResolutionSignal ? rawEntityResolutionCandidate(rawEntitySourceText) : null;
|
||||
const rawScopedEntityCandidate = !predecomposeEntities.counterparty &&
|
||||
@@ -1147,11 +1184,41 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||
const rawAggregationAxis = toNonEmptyString(assistantTurnMeaning?.asked_aggregation_axis);
|
||||
const unsupported = toNonEmptyString(assistantTurnMeaning?.unsupported_but_understood_family);
|
||||
const broadBusinessEvaluationUnsupported = unsupported === "broad_business_evaluation";
|
||||
const businessOverviewSignal = rawBusinessOverviewSignal ||
|
||||
broadBusinessEvaluationUnsupported ||
|
||||
const seededBusinessOverviewSignal = broadBusinessEvaluationUnsupported ||
|
||||
rawDomain === "business_summary" ||
|
||||
rawDomain === "business_overview" ||
|
||||
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 businessOverviewSeparateCounterpartyCandidate = businessOverviewSeparateCounterpartySignal
|
||||
? businessOverviewSeparateCounterpartyCandidateFromText(rawText)
|
||||
@@ -1176,7 +1243,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||
: rawAssistantTurnMeaningOrganizationScope;
|
||||
const rawOrganizationMentionSignal = hasOrganizationScopeSignalUtf8(rawText);
|
||||
const rawOrganizationScope = extractOrganizationScopeFromRawText(rawUserText ?? rawEffectiveText ?? rawSignalSourceText);
|
||||
const currentTurnFreshOrganizationScope = rawOrganizationScope ?? predecomposeEntities.organization;
|
||||
const currentTurnFreshOrganizationScope = predecomposeEntities.organization ?? rawOrganizationScope;
|
||||
const currentTurnOrganizationScope = currentTurnFreshOrganizationScope ?? assistantTurnMeaningOrganizationScope;
|
||||
const followupCounterpartyIsMetadataOrganizationScope = Boolean(followupSeed.subjectResolutionOptional &&
|
||||
followupSeed.counterparty &&
|
||||
@@ -1500,9 +1567,21 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||
const semanticDataNeed = metadataAmbiguityLaneClarificationApplicable
|
||||
? "metadata lane clarification"
|
||||
: semanticNeedFor({
|
||||
domain: businessOverviewSignal ? "business_overview" : rawDomain ?? seededDomain,
|
||||
action: businessOverviewSignal ? "broad_evaluation" : rawAction ?? seededAction,
|
||||
unsupported: businessOverviewSignal ? "broad_business_evaluation" : unsupported ?? seededUnsupported,
|
||||
domain: explicitVatMovementEvidenceSignal
|
||||
? "movements"
|
||||
: businessOverviewSignal
|
||||
? "business_overview"
|
||||
: rawDomain ?? seededDomain,
|
||||
action: explicitVatMovementEvidenceSignal
|
||||
? "list_movements"
|
||||
: businessOverviewSignal
|
||||
? businessOverviewActionFamily
|
||||
: rawAction ?? seededAction,
|
||||
unsupported: explicitVatMovementEvidenceSignal
|
||||
? "movement_evidence"
|
||||
: businessOverviewSignal
|
||||
? businessOverviewUnsupportedFamily
|
||||
: unsupported ?? seededUnsupported,
|
||||
lifecycleSignal,
|
||||
valueFlowSignal,
|
||||
metadataSignal: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable,
|
||||
@@ -1530,7 +1609,9 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||
followupSeed.discoveryEntity ??
|
||||
followupSeed.metadataSelectedEntitySet ??
|
||||
null;
|
||||
const metadataScopedLaneWithoutSubject = Boolean((metadataGroundedMovementLaneApplicable || metadataGroundedDocumentLaneApplicable) &&
|
||||
const metadataScopedLaneWithoutSubject = Boolean((metadataGroundedMovementLaneApplicable ||
|
||||
metadataGroundedDocumentLaneApplicable ||
|
||||
explicitVatMovementEvidenceSignal) &&
|
||||
!effectiveFollowupCounterparty &&
|
||||
metadataLaneCarryoverAvailable);
|
||||
const groundedFollowupEntity = metadataScopedLaneWithoutSubject
|
||||
@@ -1711,17 +1792,19 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||
? "counterparty_lifecycle"
|
||||
: valueFlowSignal
|
||||
? "counterparty_value"
|
||||
: metadataGroundedMovementLaneApplicable
|
||||
: explicitVatMovementEvidenceSignal
|
||||
? "movements"
|
||||
: metadataGroundedDocumentLaneApplicable
|
||||
? "documents"
|
||||
: entityResolutionSignal
|
||||
? "entity_resolution"
|
||||
: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
|
||||
? "metadata"
|
||||
: rawDomain ?? seededDomain,
|
||||
: metadataGroundedMovementLaneApplicable
|
||||
? "movements"
|
||||
: metadataGroundedDocumentLaneApplicable
|
||||
? "documents"
|
||||
: entityResolutionSignal
|
||||
? "entity_resolution"
|
||||
: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
|
||||
? "metadata"
|
||||
: rawDomain ?? seededDomain,
|
||||
asked_action_family: businessOverviewSignal
|
||||
? "broad_evaluation"
|
||||
? businessOverviewActionFamily
|
||||
: lifecycleSignal
|
||||
? "activity_duration"
|
||||
: valueFlowSignal
|
||||
@@ -1730,15 +1813,17 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||
: payoutSignal
|
||||
? "payout"
|
||||
: rawAction ?? seededAction ?? "turnover"
|
||||
: metadataGroundedMovementLaneApplicable
|
||||
: explicitVatMovementEvidenceSignal
|
||||
? "list_movements"
|
||||
: metadataGroundedDocumentLaneApplicable
|
||||
? "list_documents"
|
||||
: entityResolutionSignal
|
||||
? "search_business_entity"
|
||||
: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
|
||||
? metadataActionFromRawText(rawText) ?? seededAction
|
||||
: rawAction ?? seededAction,
|
||||
: metadataGroundedMovementLaneApplicable
|
||||
? "list_movements"
|
||||
: metadataGroundedDocumentLaneApplicable
|
||||
? "list_documents"
|
||||
: entityResolutionSignal
|
||||
? "search_business_entity"
|
||||
: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
|
||||
? metadataActionFromRawText(rawText) ?? seededAction
|
||||
: rawAction ?? seededAction,
|
||||
asked_aggregation_axis: monthlyAggregationSignal ? "month" : rawAggregationAxis,
|
||||
seeded_ranking_need: valueFlowSignal && followupSeed.rankingNeed && !rawEntitySearchOverridesStaleScope
|
||||
? followupSeed.rankingNeed
|
||||
@@ -1757,7 +1842,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||
explicit_date_scope: explicitDateScope,
|
||||
subject_resolution_optional: metadataScopedLaneWithoutSubject || undefined,
|
||||
unsupported_but_understood_family: businessOverviewSignal
|
||||
? "broad_business_evaluation"
|
||||
? businessOverviewUnsupportedFamily
|
||||
: unsupported ??
|
||||
(lifecycleSignal
|
||||
? "counterparty_lifecycle"
|
||||
@@ -1771,20 +1856,23 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||
? "movement_evidence"
|
||||
: metadataGroundedDocumentLaneApplicable
|
||||
? "document_evidence"
|
||||
: metadataAmbiguityLaneClarificationApplicable
|
||||
? "metadata_lane_choice_clarification"
|
||||
: entityResolutionSignal
|
||||
? "entity_resolution"
|
||||
: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
|
||||
? "1c_metadata_surface"
|
||||
: followupDiscoverySeedApplicable
|
||||
? seededUnsupported
|
||||
: null),
|
||||
: explicitVatMovementEvidenceSignal
|
||||
? "movement_evidence"
|
||||
: metadataAmbiguityLaneClarificationApplicable
|
||||
? "metadata_lane_choice_clarification"
|
||||
: entityResolutionSignal
|
||||
? "entity_resolution"
|
||||
: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
|
||||
? "1c_metadata_surface"
|
||||
: followupDiscoverySeedApplicable
|
||||
? seededUnsupported
|
||||
: null),
|
||||
stale_replay_forbidden: Boolean(assistantTurnMeaning?.stale_replay_forbidden ||
|
||||
businessOverviewSignal ||
|
||||
unsupported ||
|
||||
lifecycleSignal ||
|
||||
valueFlowSignal ||
|
||||
explicitVatMovementEvidenceSignal ||
|
||||
metadataGroundedMovementLaneApplicable ||
|
||||
metadataGroundedDocumentLaneApplicable ||
|
||||
metadataAmbiguityLaneClarificationApplicable ||
|
||||
@@ -1841,11 +1929,15 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||
}
|
||||
const currentTurnValueFlowExactOverrideApplicable = Boolean(valueFlowSignal &&
|
||||
explicitIntentCandidate &&
|
||||
rawValueFlowAggregateQuestionSignal &&
|
||||
(rawValueFlowAggregateQuestionSignal || hasValueRankingSignal(rawText)) &&
|
||||
semanticDataNeed &&
|
||||
(entityCandidates.length > 0 || explicitOrganizationScope || openScopeValueFlowWithoutResolvedCounterparty));
|
||||
const runDiscovery = shouldRunDiscovery({
|
||||
unsupported: businessOverviewSignal ? "broad_business_evaluation" : unsupported ?? seededUnsupported,
|
||||
unsupported: explicitVatMovementEvidenceSignal
|
||||
? "movement_evidence"
|
||||
: businessOverviewSignal
|
||||
? "broad_business_evaluation"
|
||||
: unsupported ?? seededUnsupported,
|
||||
lifecycleSignal,
|
||||
valueFlowSignal,
|
||||
metadataSignal: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable,
|
||||
@@ -1860,6 +1952,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||
metadataGroundedDocumentLaneApplicable ||
|
||||
groundedValueFlowFollowupApplicable,
|
||||
forceDiscoveryOverExplicitIntent: businessOverviewSignal ||
|
||||
explicitVatMovementEvidenceSignal ||
|
||||
Boolean(entityResolutionClarificationCandidate) ||
|
||||
organizationClarificationFollowupApplicable ||
|
||||
periodClarificationFollowupApplicable ||
|
||||
@@ -1883,17 +1976,19 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||
? "followup_context"
|
||||
: metadataGroundedDocumentLaneApplicable
|
||||
? "followup_context"
|
||||
: predecomposeContract
|
||||
? "predecompose_contract"
|
||||
: lifecycleSignal
|
||||
? "raw_text"
|
||||
: valueFlowSignal
|
||||
: explicitVatMovementEvidenceSignal
|
||||
? "raw_text"
|
||||
: predecomposeContract
|
||||
? "predecompose_contract"
|
||||
: lifecycleSignal
|
||||
? "raw_text"
|
||||
: entityResolutionSignal
|
||||
: valueFlowSignal
|
||||
? "raw_text"
|
||||
: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
|
||||
: entityResolutionSignal
|
||||
? "raw_text"
|
||||
: "none";
|
||||
: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
|
||||
? "raw_text"
|
||||
: "none";
|
||||
if (lifecycleSignal) {
|
||||
pushReason(reasonCodes, "mcp_discovery_lifecycle_signal_detected");
|
||||
}
|
||||
@@ -1903,6 +1998,9 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||
if (rawMetadataSignal) {
|
||||
pushReason(reasonCodes, "mcp_discovery_metadata_signal_detected");
|
||||
}
|
||||
if (explicitVatMovementEvidenceSignal) {
|
||||
pushReason(reasonCodes, "mcp_discovery_vat_movement_evidence_signal_detected");
|
||||
}
|
||||
if (entityResolutionSignal) {
|
||||
pushReason(reasonCodes, "mcp_discovery_entity_resolution_signal_detected");
|
||||
}
|
||||
@@ -2026,6 +2124,12 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
||||
if (businessOverviewContinuationSignal) {
|
||||
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) {
|
||||
pushReason(reasonCodes, "mcp_discovery_business_overview_continuation_suppressed_by_explicit_vat_question");
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
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) {
|
||||
const rawUserMessage = String(input?.rawUserMessage ?? input?.userMessage ?? "");
|
||||
const effectiveAddressUserMessage = String(input?.effectiveAddressUserMessage ?? rawUserMessage);
|
||||
@@ -475,6 +482,27 @@ function createAssistantRoutePolicy(deps) {
|
||||
const followupPreviousFilters = followupContext?.previous_filters && typeof followupContext.previous_filters === "object"
|
||||
? followupContext.previous_filters
|
||||
: 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 &&
|
||||
(isInventorySelectedObjectIntent(followupPreviousIntent) ||
|
||||
(followupPreviousIntent === "inventory_on_hand_as_of_date" &&
|
||||
@@ -524,6 +552,7 @@ function createAssistantRoutePolicy(deps) {
|
||||
"net_value_flow"
|
||||
].includes(String(toNonEmptyString(assistantTurnMeaning?.asked_action_family) ?? "")) ||
|
||||
/(?:нетто|сальдо|сколько\s+мы\s+(?:получили|заплатили)|incoming|outgoing)/iu.test(analyticsSample)));
|
||||
const effectiveGroundedValueFlowFollowupContextDetected = groundedValueFlowFollowupContextDetected || routeCandidateOrganizationClarificationDetected;
|
||||
const baseToolGatePreservesAddressLane = Boolean(baseToolGate?.runAddressLane &&
|
||||
[
|
||||
"address_intent_resolver_detected",
|
||||
@@ -533,14 +562,15 @@ function createAssistantRoutePolicy(deps) {
|
||||
].includes(String(baseToolGate?.reason ?? ""))) ||
|
||||
Boolean(baseToolGate?.runAddressLane &&
|
||||
String(baseToolGate?.reason ?? "") === "followup_context_detected" &&
|
||||
groundedValueFlowFollowupContextDetected);
|
||||
effectiveGroundedValueFlowFollowupContextDetected);
|
||||
const nonDomainQueryIndexed = Boolean(!llmFirstAddressCandidate &&
|
||||
deterministicNonDomainGuard &&
|
||||
(llmFirstUnsupportedCandidate || llmContractMode === null) &&
|
||||
!baseToolGatePreservesAddressLane &&
|
||||
!groundedValueFlowFollowupContextDetected &&
|
||||
!effectiveGroundedValueFlowFollowupContextDetected &&
|
||||
!protectedInventoryShortFollowup &&
|
||||
!organizationClarificationContinuationDetected);
|
||||
!organizationClarificationContinuationDetected &&
|
||||
!routeCandidateOrganizationClarificationDetected);
|
||||
const lastAddressAssistantDebug = sessionItems
|
||||
? findLastAddressAssistantItem(sessionItems)?.debug ?? null
|
||||
: null;
|
||||
@@ -583,7 +613,7 @@ function createAssistantRoutePolicy(deps) {
|
||||
!turnMeaningIntentCandidate &&
|
||||
!dataScopeMetaQuery &&
|
||||
!dangerOrCoercionSignal &&
|
||||
!groundedValueFlowFollowupContextDetected &&
|
||||
!effectiveGroundedValueFlowFollowupContextDetected &&
|
||||
!organizationClarificationContinuationDetected);
|
||||
const hardMetaMode = resolveHardMetaMode({
|
||||
dataScopeMetaQuery,
|
||||
@@ -748,7 +778,7 @@ function createAssistantRoutePolicy(deps) {
|
||||
!dataScopeMetaQuery &&
|
||||
!capabilityMetaQuery &&
|
||||
!dangerOrCoercionSignal &&
|
||||
!groundedValueFlowFollowupContextDetected &&
|
||||
!effectiveGroundedValueFlowFollowupContextDetected &&
|
||||
!organizationClarificationContinuationDetected);
|
||||
if (unsupportedCurrentTurnMeaningBoundary) {
|
||||
return {
|
||||
|
||||
@@ -2123,6 +2123,9 @@ function isAddressLaneDebugPayload(debug) {
|
||||
if (typeof debug.mcp_call_status === "string" && debug.mcp_call_status.trim().length > 0) {
|
||||
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) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -120,6 +120,22 @@ function createAssistantTransitionPolicy(deps) {
|
||||
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));
|
||||
}
|
||||
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) {
|
||||
const match = String(value ?? "").trim().match(/^(\d{2})\.(\d{2})\.(\d{4})$/);
|
||||
if (!match) {
|
||||
@@ -433,7 +449,11 @@ function createAssistantTransitionPolicy(deps) {
|
||||
(deps.toNonEmptyString(alternateMessage)
|
||||
? deps.hasDataRetrievalRequestSignal(String(alternateMessage ?? ""))
|
||||
: false);
|
||||
if (rawCapabilityMetaQuery && !rawDataRetrievalSignal) {
|
||||
const rawBusinessOverviewBoundaryFollowupCue = hasBusinessOverviewBoundaryFollowupCue(userMessage) ||
|
||||
(deps.toNonEmptyString(alternateMessage)
|
||||
? hasBusinessOverviewBoundaryFollowupCue(String(alternateMessage ?? ""))
|
||||
: false);
|
||||
if (rawCapabilityMetaQuery && !rawDataRetrievalSignal && !rawBusinessOverviewBoundaryFollowupCue) {
|
||||
return null;
|
||||
}
|
||||
const assistantTurnMeaning = typeof deps.resolveAssistantTurnMeaning === "function"
|
||||
@@ -492,10 +512,25 @@ function createAssistantTransitionPolicy(deps) {
|
||||
: false));
|
||||
const sourceIntentHint = (0, assistantContinuityPolicy_1.readAddressDebugIntent)(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" ||
|
||||
sourceDiscoveryPilotScopeHint === "counterparty_value_flow_query_movements_v1" ||
|
||||
sourceDiscoveryPilotScopeHint === "counterparty_supplier_payout_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 navigationFocusObjectHint = navigationSessionState.focusObject;
|
||||
const hasNavigationInventoryItemFocusHint = Boolean(deps.toNonEmptyString(navigationFocusObjectHint?.label) &&
|
||||
@@ -521,20 +556,28 @@ function createAssistantTransitionPolicy(deps) {
|
||||
const shortValueFlowRetargetAlternate = hasValueFlowCarryoverSourceHint && deps.toNonEmptyString(alternateMessage)
|
||||
? hasShortValueFlowRetargetCue(String(alternateMessage ?? ""))
|
||||
: false;
|
||||
const businessOverviewBoundaryFollowupPrimary = hasBusinessOverviewCarryoverSourceHint && hasBusinessOverviewBoundaryFollowupCue(userMessage);
|
||||
const businessOverviewBoundaryFollowupAlternate = hasBusinessOverviewCarryoverSourceHint && deps.toNonEmptyString(alternateMessage)
|
||||
? hasBusinessOverviewBoundaryFollowupCue(String(alternateMessage ?? ""))
|
||||
: false;
|
||||
const explicitSummaryBundleReuseSignal = hasExplicitSummaryBundleReuseSignal(userMessage, alternateMessage);
|
||||
let hasPrimaryFollowupSignal = deps.hasAddressFollowupContextSignal(userMessage) ||
|
||||
Boolean(debtRoleSwapPrimary) ||
|
||||
shortValueFlowRetargetPrimary ||
|
||||
businessOverviewBoundaryFollowupPrimary ||
|
||||
inventoryShortFollowupPrimary ||
|
||||
inventoryPurchaseDateVatBridge ||
|
||||
explicitSummaryBundleReuseSignal;
|
||||
explicitSummaryBundleReuseSignal ||
|
||||
mcpDiscoveryOrganizationClarificationContinuation;
|
||||
let hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage)
|
||||
? deps.hasAddressFollowupContextSignal(alternateMessage) ||
|
||||
Boolean(debtRoleSwapAlternate) ||
|
||||
shortValueFlowRetargetAlternate ||
|
||||
businessOverviewBoundaryFollowupAlternate ||
|
||||
inventoryShortFollowupAlternate ||
|
||||
inventoryPurchaseDateVatBridge ||
|
||||
explicitSummaryBundleReuseSignal
|
||||
explicitSummaryBundleReuseSignal ||
|
||||
mcpDiscoveryOrganizationClarificationContinuation
|
||||
: false;
|
||||
const hasPrimaryIndexReferenceSignal = deps.extractDisplayedEntityIndexMention(userMessage) !== null;
|
||||
const hasAlternateIndexReferenceSignal = deps.toNonEmptyString(alternateMessage)
|
||||
@@ -557,6 +600,7 @@ function createAssistantTransitionPolicy(deps) {
|
||||
let hasStrongFollowupReference = hasPrimaryIndexReferenceSignal ||
|
||||
hasAlternateIndexReferenceSignal ||
|
||||
hasOrganizationClarificationContinuation ||
|
||||
mcpDiscoveryOrganizationClarificationContinuation ||
|
||||
hasImplicitContinuationSignal ||
|
||||
hasSuggestedIntentPivotSignal ||
|
||||
inventoryShortFollowupPrimary ||
|
||||
@@ -570,6 +614,8 @@ function createAssistantTransitionPolicy(deps) {
|
||||
Boolean(debtRoleSwapIntent) ||
|
||||
shortValueFlowRetargetPrimary ||
|
||||
shortValueFlowRetargetAlternate ||
|
||||
businessOverviewBoundaryFollowupPrimary ||
|
||||
businessOverviewBoundaryFollowupAlternate ||
|
||||
deps.hasFollowupMarker(userMessage) ||
|
||||
deps.hasReferentialPointer(userMessage) ||
|
||||
(deps.toNonEmptyString(alternateMessage)
|
||||
@@ -579,6 +625,7 @@ function createAssistantTransitionPolicy(deps) {
|
||||
const hasConcreteFollowupReference = hasPrimaryIndexReferenceSignal ||
|
||||
hasAlternateIndexReferenceSignal ||
|
||||
hasOrganizationClarificationContinuation ||
|
||||
mcpDiscoveryOrganizationClarificationContinuation ||
|
||||
inventoryShortFollowupPrimary ||
|
||||
inventoryShortFollowupAlternate ||
|
||||
hasInventoryRootTemporalFollowupPrimary ||
|
||||
@@ -590,6 +637,8 @@ function createAssistantTransitionPolicy(deps) {
|
||||
Boolean(debtRoleSwapIntent) ||
|
||||
shortValueFlowRetargetPrimary ||
|
||||
shortValueFlowRetargetAlternate ||
|
||||
businessOverviewBoundaryFollowupPrimary ||
|
||||
businessOverviewBoundaryFollowupAlternate ||
|
||||
deps.hasFollowupMarker(userMessage) ||
|
||||
deps.hasReferentialPointer(userMessage) ||
|
||||
(deps.toNonEmptyString(alternateMessage)
|
||||
@@ -617,6 +666,7 @@ function createAssistantTransitionPolicy(deps) {
|
||||
!hasImplicitContinuationSignal &&
|
||||
!hasSuggestedIntentPivotSignal &&
|
||||
!hasOrganizationClarificationContinuation &&
|
||||
!mcpDiscoveryOrganizationClarificationContinuation &&
|
||||
!hasIndexReferenceSignal &&
|
||||
!explicitSummaryBundleReuseSignal) {
|
||||
return null;
|
||||
@@ -632,6 +682,7 @@ function createAssistantTransitionPolicy(deps) {
|
||||
!hasImplicitContinuationSignal &&
|
||||
!hasSuggestedIntentPivotSignal &&
|
||||
!hasOrganizationClarificationContinuation &&
|
||||
!mcpDiscoveryOrganizationClarificationContinuation &&
|
||||
!hasIndexReferenceSignal &&
|
||||
!explicitSummaryBundleReuseSignal) {
|
||||
return null;
|
||||
@@ -650,10 +701,10 @@ function createAssistantTransitionPolicy(deps) {
|
||||
const sourceDiscoveryMetadataAmbiguityEntitySets = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryMetadataAmbiguityEntitySets)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const sourceDiscoveryEntityResolutionStatus = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryEntityResolutionStatus)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const sourceDiscoveryEntityCandidates = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryEntityCandidates)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const sourceDiscoveryLoopStatus = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopStatus)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const sourceDiscoveryLoopSelectedChainId = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopSelectedChainId)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const sourceDiscoveryLoopPendingAxes = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopPendingAxes)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const sourceDiscoveryLoopProvidedAxes = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopProvidedAxes)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const sourceDiscoveryLoopStatus = sourceDiscoveryLoopStatusHint;
|
||||
const sourceDiscoveryLoopSelectedChainId = sourceDiscoveryLoopSelectedChainIdHint;
|
||||
const sourceDiscoveryLoopPendingAxes = sourceDiscoveryLoopPendingAxesHint;
|
||||
const sourceDiscoveryLoopProvidedAxes = sourceDiscoveryLoopProvidedAxesHint;
|
||||
const sourceDiscoveryLoopAskedDomainFamily = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopAskedDomainFamily)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const sourceDiscoveryLoopAskedActionFamily = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopAskedActionFamily)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const sourceDiscoveryLoopUnsupportedFamily = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopUnsupportedFamily)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
@@ -689,6 +740,7 @@ function createAssistantTransitionPolicy(deps) {
|
||||
explicitIntentFamily &&
|
||||
sourceIntentFamily !== explicitIntentFamily &&
|
||||
!hasOrganizationClarificationContinuation &&
|
||||
!mcpDiscoveryOrganizationClarificationContinuation &&
|
||||
!hasImplicitContinuationSignal &&
|
||||
!hasIndexReferenceSignal &&
|
||||
!hasInventoryRootTemporalFollowupPrimary &&
|
||||
@@ -697,6 +749,8 @@ function createAssistantTransitionPolicy(deps) {
|
||||
!hasInventoryRootRestatementAlternate &&
|
||||
!inventoryShortFollowupPrimary &&
|
||||
!inventoryShortFollowupAlternate &&
|
||||
!businessOverviewBoundaryFollowupPrimary &&
|
||||
!businessOverviewBoundaryFollowupAlternate &&
|
||||
!foreignAccountingPivotOverInventory &&
|
||||
!deps.hasFollowupMarker(userMessage) &&
|
||||
!deps.hasReferentialPointer(userMessage) &&
|
||||
@@ -753,24 +807,29 @@ function createAssistantTransitionPolicy(deps) {
|
||||
hasSuggestedIntentPivotSignal ||
|
||||
Boolean(debtRoleSwapPrimary) ||
|
||||
shortValueFlowRetargetPrimary ||
|
||||
businessOverviewBoundaryFollowupPrimary ||
|
||||
inventoryShortFollowupPrimary ||
|
||||
inventoryPurchaseDateVatBridge ||
|
||||
explicitSummaryBundleReuseSignal ||
|
||||
hasInventoryRootTemporalFollowupPrimary;
|
||||
hasInventoryRootTemporalFollowupPrimary ||
|
||||
mcpDiscoveryOrganizationClarificationContinuation;
|
||||
hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage)
|
||||
? deps.hasAddressFollowupContextSignal(alternateMessage) ||
|
||||
hasSuggestedIntentPivotSignal ||
|
||||
Boolean(debtRoleSwapAlternate) ||
|
||||
shortValueFlowRetargetAlternate ||
|
||||
businessOverviewBoundaryFollowupAlternate ||
|
||||
inventoryShortFollowupAlternate ||
|
||||
inventoryPurchaseDateVatBridge ||
|
||||
explicitSummaryBundleReuseSignal ||
|
||||
hasInventoryRootTemporalFollowupAlternate
|
||||
hasInventoryRootTemporalFollowupAlternate ||
|
||||
mcpDiscoveryOrganizationClarificationContinuation
|
||||
: false;
|
||||
hasStrongFollowupReference =
|
||||
hasPrimaryIndexReferenceSignal ||
|
||||
hasAlternateIndexReferenceSignal ||
|
||||
hasOrganizationClarificationContinuation ||
|
||||
mcpDiscoveryOrganizationClarificationContinuation ||
|
||||
hasSuggestedIntentPivotSignal ||
|
||||
hasImplicitContinuationSignal ||
|
||||
inventoryShortFollowupPrimary ||
|
||||
@@ -782,6 +841,8 @@ function createAssistantTransitionPolicy(deps) {
|
||||
Boolean(debtRoleSwapIntent) ||
|
||||
shortValueFlowRetargetPrimary ||
|
||||
shortValueFlowRetargetAlternate ||
|
||||
businessOverviewBoundaryFollowupPrimary ||
|
||||
businessOverviewBoundaryFollowupAlternate ||
|
||||
deps.hasFollowupMarker(userMessage) ||
|
||||
deps.hasReferentialPointer(userMessage) ||
|
||||
(deps.toNonEmptyString(alternateMessage)
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
Reference in New Issue
Block a user