Укрепить автономные reviewed-маршруты и срезы задолженности

This commit is contained in:
2026-05-12 23:26:20 +03:00
parent 5dedfeee86
commit b99a3be083
46 changed files with 3234 additions and 142 deletions
@@ -2315,6 +2315,18 @@ function resolveUnicodeAddressIntentBridge(text: string): AddressIntentResolutio
);
}
const hasTopYearRevenueRankingCue =
/(?:(?:\u043a\u0430\u043a\u043e\u0439|\u043a\u0430\u043a\u0438\u0435|\u043a\u0430\u043a\u0430\u044f|what|which)[\s\S]{0,80}(?:\u0441\u0430\u043c\p{L}*|top|best|most)[\s\S]{0,80}(?:\u0434\u043e\u0445\u043e\u0434\u043d|\u0432\u044b\u0440\u0443\u0447\u043a|\u043e\u0431\u043e\u0440\u043e\u0442|revenue|turnover)[\s\S]{0,60}(?:\u0433\u043e\u0434|year)|(?:\u0434\u043e\u0445\u043e\u0434\u043d|\u0432\u044b\u0440\u0443\u0447\u043a|\u043e\u0431\u043e\u0440\u043e\u0442|revenue|turnover)[\s\S]{0,60}(?:\u0441\u0430\u043c\p{L}*|top|best|most)[\s\S]{0,60}(?:\u0433\u043e\u0434|year))/iu.test(
normalized
);
if (!hasContractCue && (hasTopYearRevenueRankingCue || hasCustomerRevenueRankingBridgeSignal(normalized))) {
return unicodeBridgeResolution(
"customer_revenue_and_payments",
"high",
"unicode_customer_revenue_ranking_bridge_signal_detected"
);
}
if (hasOrganizationLevelEarningsOverviewBridgeSignal(normalized)) {
return unicodeBridgeResolution("unknown", "high", "unicode_business_overview_earnings_deferred_to_discovery");
}
@@ -3007,16 +3019,67 @@ function resolveUnicodeAddressIntentBridge(text: string): AddressIntentResolutio
return null;
}
function resolveDirectDebtSnapshotIntent(text: string): AddressIntentResolution | null {
const normalized = String(text ?? "").trim().toLowerCase();
if (!normalized) {
return null;
}
if (/(?:ндс|vat)/iu.test(normalized)) {
return null;
}
const hasSnapshotCue =
/(?:кто|сколько|есть\s+ли|по\s+состоянию|на\s+сегодня|на\s+дату|срез|остаток|сальдо|баланс|на\s+(?:январ|феврал|март|апрел|ма[йя]|июн|июл|август|сентябр|октябр|ноябр|декабр)\S*(?:\s+(?:19|20)\d{2})?|на\s+(?:19|20)\d{2}|as\s+of|today|current|balance)/iu.test(
normalized
);
const hasReceivablesCue =
/(?:кто\s+(?:является\s+)?дебитором|дебитор(?:[а-яё]{0,8})?|дебиторск(?:[а-яё]{0,8})?|кто\s+нам\s+долж(?:ен|ны|на|но)?|нам\s+(?:кто-то\s+|кто\s+)?долж(?:ен|ны|на|но)?|нам\s+торч(?:ат|ит|ишь|у|али)?|к\s+получению|к\s+взысканию|who\s+owes\s+us|receivables?|accounts\s+receivable)/iu.test(
normalized
);
const hasPayablesCue =
/(?:кто\s+(?:является\s+)?кредитором|кредитор(?:[а-яё]{0,8})?|кому\s+мы\s+долж(?:ны|н[ао])?|мы\s+долж(?:ны|н[ао])?\s+кому|мы\s+долж(?:ны|н[ао])?|к\s+оплате|who\s+we\s+owe|payables?|accounts\s+payable)/iu.test(
normalized
);
if (hasReceivablesCue && !hasPayablesCue && hasSnapshotCue) {
return {
intent: "receivables_confirmed_as_of_date",
confidence: "high",
reasons: ["receivables_debt_lifecycle_signal_detected", "direct_debt_snapshot_signal_detected"]
};
}
if (hasPayablesCue && !hasReceivablesCue && hasSnapshotCue) {
return {
intent: "payables_confirmed_as_of_date",
confidence: "high",
reasons: ["payables_debt_lifecycle_signal_detected", "direct_debt_snapshot_signal_detected"]
};
}
return null;
}
export function resolveAddressIntent(userMessage: string): AddressIntentResolution {
const text = String(userMessage ?? "").trim().toLowerCase();
const repairedText = repairLikelyUtf8Mojibake(text).trim().toLowerCase();
const bridgeText = repairedText && repairedText !== text ? `${text} ${repairedText}` : text;
const turnNoiseNormalizedBridgeText = bridgeText
.replace(/(^|[^\p{L}0-9_])\u043d\u0430\u043c\u0441(?=$|[^\p{L}0-9_])/giu, "$1\u043d\u0430\u043c")
.replace(/(^|[^\p{L}0-9_])\u043a\u0430\u043a\u0438\u0435\u043a(?=$|[^\p{L}0-9_])/giu, "$1\u043a\u0430\u043a\u0438\u0435");
.replace(/(^|[^\p{L}0-9_])намс(?=$|[^\p{L}0-9_])/giu, "$1нам")
.replace(/(^|[^\p{L}0-9_])какиек(?=$|[^\p{L}0-9_])/giu, "$1какие");
const currentTurnBridgeText =
turnNoiseNormalizedBridgeText !== bridgeText ? `${bridgeText} ${turnNoiseNormalizedBridgeText}` : bridgeText;
const directDebtSnapshotIntent = resolveDirectDebtSnapshotIntent(currentTurnBridgeText);
if (directDebtSnapshotIntent) {
const reasons = [...directDebtSnapshotIntent.reasons];
if (currentTurnBridgeText !== bridgeText && !reasons.includes("current_turn_noise_normalized")) {
reasons.push("current_turn_noise_normalized");
}
return {
...directDebtSnapshotIntent,
reasons
};
}
const unicodeAddressIntent = resolveUnicodeAddressIntentBridge(currentTurnBridgeText);
if (unicodeAddressIntent) {
const reasons = [...unicodeAddressIntent.reasons];
@@ -2071,10 +2071,10 @@ export function buildAddressRecipePlan(
? toDateTimeExpr(filters.period_from, true)
: null) ??
"ТЕКУЩАЯДАТА()";
return PAYABLES_CONFIRMED_AS_OF_QUERY_TEMPLATE
return OPEN_CONTRACTS_CONFIRMED_AS_OF_QUERY_TEMPLATE
.replaceAll("__LIMIT__", String(resolvedLimit))
.replaceAll("__AS_OF_EXPR__", asOfExpr)
.replaceAll("__PAYABLE_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["60", "76"]))
.replaceAll("__OPEN_CONTRACT_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["60", "76"]))
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
})()
: recipe.query_template === "receivables_confirmed_as_of_balance_profile"
@@ -2090,10 +2090,10 @@ export function buildAddressRecipePlan(
? 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("Остатки.Счет", ["62", "76"]))
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
})()
: MOVEMENTS_QUERY_TEMPLATE
@@ -401,6 +401,21 @@ function normalizeQuestionText(value: string | null | undefined): string {
.trim();
}
function isReportStyleBusinessQuestion(userMessage: string | null | undefined): boolean {
const text = normalizeQuestionText(userMessage);
return /(?:обзор|анализ|подроб|разверн|оцен|аудит|report|review|analysis)/iu.test(text);
}
function isDirectBalanceQuestion(userMessage: string | null | undefined): boolean {
const text = normalizeQuestionText(userMessage);
if (!text || isReportStyleBusinessQuestion(text)) {
return false;
}
return /(?:кто|кому|сколько|какой|какая|какие|есть\s+ли|долж|дебитор|кредитор|payables?|receivables?|who|how\s+much)/iu.test(
text
);
}
function hasInventoryPurchaseDateActionFocus(userMessage: string | null | undefined): boolean {
const text = normalizeQuestionText(userMessage);
if (!text) {
@@ -780,27 +795,46 @@ function extractRequestedYearFromQuestion(userMessage: string | null | undefined
}
function extractCounterpartyName(row: ComposeStageRow): string | null {
const isCounterpartyLikeToken = (value: unknown, skipPattern: RegExp): string | null => {
const normalized = String(value ?? "").trim();
if (!normalized) {
return null;
}
if (/^\d{4}-\d{2}-\d{2}/.test(normalized)) {
return null;
}
if (/^\d+(?:[./-]\d+)*$/.test(normalized)) {
return null;
}
if (!/[a-zа-я]/iu.test(normalized)) {
return null;
}
if (skipPattern.test(normalized)) {
return null;
}
return normalized;
};
const hardSkipTokenPattern =
/(?:^0$|^<пусто>$|^пустая ссылка$|договор|contract|документ|операц|счет[-\s]?фактур|накладн|акт|поступлен|списани|плат[её]ж|перевод|касса|расчетн|проводк|movement|invoice|payment)/iu;
const skipTokenPattern =
/(?:^0$|^<пусто>$|^пустая ссылка$|договор|contract|документ|операц|счет[-\s]?фактур|накладн|акт|поступлен|списани|плат[её]ж|перевод|банк|касса|расчетн|проводк|movement|invoice|payment)/iu;
const directCounterparty = isCounterpartyLikeToken(row.counterparty, hardSkipTokenPattern);
if (directCounterparty) {
return directCounterparty;
}
if (/остатки\s+на\s+дату/iu.test(row.registrator)) {
const balancePrimaryCounterparty = isCounterpartyLikeToken(row.analytics[0], hardSkipTokenPattern);
if (balancePrimaryCounterparty) {
return balancePrimaryCounterparty;
}
}
for (const token of row.analytics) {
const normalized = String(token ?? "").trim();
if (!normalized) {
continue;
const normalized = isCounterpartyLikeToken(token, skipTokenPattern);
if (normalized) {
return normalized;
}
if (/^\d{4}-\d{2}-\d{2}/.test(normalized)) {
continue;
}
if (/^\d+(?:[./-]\d+)*$/.test(normalized)) {
continue;
}
if (!/[a-zа-я]/iu.test(normalized)) {
continue;
}
if (skipTokenPattern.test(normalized)) {
continue;
}
return normalized;
}
for (const token of row.analytics) {
const normalized = String(token ?? "").trim();
@@ -1457,6 +1491,25 @@ interface PayablesConfirmedBalanceAggregate {
sourceRefs: string[];
}
interface DebtMirrorBalanceGroup {
name: string;
account: string | null;
contract: string | null;
organization: string | null;
debitAmount: number;
creditAmount: number;
offsetAmount: number;
netAmount: number;
operations: number;
sourceRefs: string[];
}
interface ConfirmedDebtBalanceSnapshot {
balances: PayablesConfirmedBalanceAggregate[];
mirrorGroups: DebtMirrorBalanceGroup[];
mirroredOffsetAmount: number;
}
function liabilityCategoryLabel(category: PayablesLiabilityCategory): string {
if (category === "supplier_or_contractor") {
return "поставщики/подрядчики";
@@ -1572,6 +1625,18 @@ function hasReceivablesSectionPrefix(account: string | null): boolean {
return section === "62" || section === "76";
}
function normalizeSettlementAccount(value: string | null | undefined): string | null {
const normalized = String(value ?? "")
.trim()
.replace(",", ".");
return normalized || null;
}
function extractSettlementOrganizationName(row: ComposeStageRow): string | null {
const direct = String(row.organization ?? "").trim();
return direct || null;
}
function resolvePayablesAsOfDate(options: ComposeFactualReplyOptions): string {
const explicit = normalizeIsoDateOnly(options.asOfDate);
if (explicit) {
@@ -1906,6 +1971,259 @@ function buildReceivablesConfirmedBalanceAggregate(
});
}
function buildConfirmedDebtBalanceSnapshot(
rows: ComposeStageRow[],
asOfDate: string,
hasRelevantSectionPrefix: (account: string | null) => boolean,
positiveSide: "debit" | "credit"
): ConfirmedDebtBalanceSnapshot {
const bySettlementKey = new Map<
string,
{
name: string;
account: string | null;
contract: string | null;
organization: string | null;
debitAmount: number;
creditAmount: number;
operations: number;
firstPeriod: string | null;
lastPeriod: string | null;
categoryScores: Record<PayablesLiabilityCategory, number>;
reasons: Set<string>;
contracts: Set<string>;
documents: Set<string>;
sourceRefs: Set<string>;
}
>();
const asOfTimestamp = toUtcDayTimestamp(asOfDate);
for (const row of rows) {
const name = extractCounterpartyName(row);
if (!name) {
continue;
}
const rowTimestamp = toUtcDayTimestamp(row.period);
if (asOfTimestamp !== null && rowTimestamp !== null && rowTimestamp > asOfTimestamp) {
continue;
}
const amount = row.amount;
if (typeof amount !== "number" || !Number.isFinite(amount)) {
continue;
}
const absAmount = Math.abs(amount);
const debitAccount = normalizeSettlementAccount(row.account_dt);
const creditAccount = normalizeSettlementAccount(row.account_kt);
const contributions: Array<{ side: "debit" | "credit"; account: string }> = [];
if (debitAccount && hasRelevantSectionPrefix(debitAccount)) {
contributions.push({ side: "debit", account: debitAccount });
}
if (creditAccount && hasRelevantSectionPrefix(creditAccount)) {
contributions.push({ side: "credit", account: creditAccount });
}
if (contributions.length === 0) {
continue;
}
const contract = extractSettlementBalanceAnalyticKey(row, name);
const organization = extractSettlementOrganizationName(row);
const classified = classifyPayablesLiabilityCategory(row, name);
const sourceRefs = extractPayablesSourceRefs(row, name, contract);
for (const contribution of contributions) {
const key = [
normalizeEntityToken(organization),
normalizeEntityToken(contribution.account),
normalizeEntityToken(name),
normalizeEntityToken(contract)
].join("|");
const current = bySettlementKey.get(key);
if (!current) {
bySettlementKey.set(key, {
name,
account: contribution.account,
contract,
organization,
debitAmount: contribution.side === "debit" ? absAmount : 0,
creditAmount: contribution.side === "credit" ? absAmount : 0,
operations: 1,
firstPeriod: row.period,
lastPeriod: row.period,
categoryScores: {
supplier_or_contractor: classified.scores.supplier_or_contractor,
bank_or_credit: classified.scores.bank_or_credit,
tax_or_state: classified.scores.tax_or_state,
other: classified.scores.other
},
reasons: new Set(classified.reasons),
contracts: new Set(contract ? [contract] : []),
documents: new Set(row.registrator ? [row.registrator] : []),
sourceRefs: new Set(sourceRefs)
});
continue;
}
if (contribution.side === "debit") {
current.debitAmount += absAmount;
} else {
current.creditAmount += absAmount;
}
current.operations += 1;
if ((row.period ?? "") < (current.firstPeriod ?? "")) {
current.firstPeriod = row.period;
}
if ((row.period ?? "") > (current.lastPeriod ?? "")) {
current.lastPeriod = row.period;
}
current.categoryScores.supplier_or_contractor += classified.scores.supplier_or_contractor;
current.categoryScores.bank_or_credit += classified.scores.bank_or_credit;
current.categoryScores.tax_or_state += classified.scores.tax_or_state;
current.categoryScores.other += classified.scores.other;
for (const reason of classified.reasons) {
current.reasons.add(reason);
}
if (contract) {
current.contracts.add(contract);
}
if (row.registrator) {
current.documents.add(row.registrator);
}
for (const ref of sourceRefs) {
current.sourceRefs.add(ref);
}
}
}
const byCounterparty = new Map<
string,
{
outstandingAmount: number;
operations: number;
firstPeriod: string | null;
lastPeriod: string | null;
categoryScores: Record<PayablesLiabilityCategory, number>;
reasons: Set<string>;
contracts: Set<string>;
documents: Set<string>;
sourceRefs: Set<string>;
}
>();
const mirrorGroups: DebtMirrorBalanceGroup[] = [];
let mirroredOffsetAmount = 0;
for (const group of bySettlementKey.values()) {
const offsetAmount = Math.min(group.debitAmount, group.creditAmount);
const netDebitMinusCredit = group.debitAmount - group.creditAmount;
if (offsetAmount > 0.005) {
mirroredOffsetAmount += offsetAmount;
mirrorGroups.push({
name: group.name,
account: group.account,
contract: group.contract,
organization: group.organization,
debitAmount: group.debitAmount,
creditAmount: group.creditAmount,
offsetAmount,
netAmount: netDebitMinusCredit,
operations: group.operations,
sourceRefs: Array.from(group.sourceRefs).slice(0, 3)
});
}
const sideNetAmount = positiveSide === "credit" ? group.creditAmount - group.debitAmount : group.debitAmount - group.creditAmount;
if (sideNetAmount <= 0.005) {
continue;
}
const current = byCounterparty.get(group.name);
if (!current) {
byCounterparty.set(group.name, {
outstandingAmount: sideNetAmount,
operations: group.operations,
firstPeriod: group.firstPeriod,
lastPeriod: group.lastPeriod,
categoryScores: {
supplier_or_contractor: group.categoryScores.supplier_or_contractor,
bank_or_credit: group.categoryScores.bank_or_credit,
tax_or_state: group.categoryScores.tax_or_state,
other: group.categoryScores.other
},
reasons: new Set(group.reasons),
contracts: new Set(group.contracts),
documents: new Set(group.documents),
sourceRefs: new Set(group.sourceRefs)
});
continue;
}
current.outstandingAmount += sideNetAmount;
current.operations += group.operations;
if ((group.firstPeriod ?? "") < (current.firstPeriod ?? "")) {
current.firstPeriod = group.firstPeriod;
}
if ((group.lastPeriod ?? "") > (current.lastPeriod ?? "")) {
current.lastPeriod = group.lastPeriod;
}
current.categoryScores.supplier_or_contractor += group.categoryScores.supplier_or_contractor;
current.categoryScores.bank_or_credit += group.categoryScores.bank_or_credit;
current.categoryScores.tax_or_state += group.categoryScores.tax_or_state;
current.categoryScores.other += group.categoryScores.other;
for (const reason of group.reasons) {
current.reasons.add(reason);
}
for (const contract of group.contracts) {
current.contracts.add(contract);
}
for (const document of group.documents) {
current.documents.add(document);
}
for (const ref of group.sourceRefs) {
current.sourceRefs.add(ref);
}
}
return {
balances: Array.from(byCounterparty.entries())
.map(([name, item]) => ({
name,
outstandingAmount: item.outstandingAmount,
operations: item.operations,
firstPeriod: item.firstPeriod,
lastPeriod: item.lastPeriod,
category: resolvePayablesLiabilityCategory(item.categoryScores),
categoryReasons: Array.from(item.reasons).slice(0, 2),
contracts: Array.from(item.contracts).slice(0, 2),
documents: Array.from(item.documents).slice(0, 2),
sourceRefs: Array.from(item.sourceRefs).slice(0, 3)
}))
.filter((item) => item.outstandingAmount > 0.005)
.sort((left, right) => {
if (right.outstandingAmount !== left.outstandingAmount) {
return right.outstandingAmount - left.outstandingAmount;
}
if (right.operations !== left.operations) {
return right.operations - left.operations;
}
return left.name.localeCompare(right.name);
}),
mirrorGroups: mirrorGroups.sort((left, right) => {
if (right.offsetAmount !== left.offsetAmount) {
return right.offsetAmount - left.offsetAmount;
}
return left.name.localeCompare(right.name);
}),
mirroredOffsetAmount
};
}
function buildPayablesConfirmedBalanceSnapshot(rows: ComposeStageRow[], asOfDate: string): ConfirmedDebtBalanceSnapshot {
return buildConfirmedDebtBalanceSnapshot(rows, asOfDate, hasPayablesSectionPrefix, "credit");
}
function buildReceivablesConfirmedBalanceSnapshot(rows: ComposeStageRow[], asOfDate: string): ConfirmedDebtBalanceSnapshot {
return buildConfirmedDebtBalanceSnapshot(rows, asOfDate, hasReceivablesSectionPrefix, "debit");
}
function buildCounterpartyRiskAggregate(rows: ComposeStageRow[]): CounterpartyRiskAggregate[] {
const byCounterparty = new Map<string, CounterpartyRiskAggregate>();
@@ -2109,6 +2427,51 @@ function extractContractName(row: ComposeStageRow): string | null {
return null;
}
function extractSettlementBalanceAnalyticKey(row: ComposeStageRow, counterparty: string): string | null {
const counterpartyToken = normalizeSettlementComparableToken(counterparty);
const organizationToken = normalizeSettlementComparableToken(extractSettlementOrganizationName(row));
const contract = extractContractName(row);
if (contract) {
const contractToken = normalizeSettlementComparableToken(contract);
if (
contractToken &&
contractToken !== counterpartyToken &&
contractToken !== organizationToken &&
!(Boolean(organizationToken) && contractToken.includes(organizationToken)) &&
!/^организац/.test(contractToken)
) {
return contract;
}
}
for (const token of row.analytics) {
const normalized = String(token ?? "").trim();
const normalizedToken = normalizeSettlementComparableToken(normalized);
if (!normalized || !normalizedToken) {
continue;
}
if (/^(?:0|<пусто>|пустая ссылка)$/iu.test(normalized)) {
continue;
}
if (/^\d{4}-\d{2}-\d{2}/.test(normalized) || /^\d+(?:[.,]\d+)?$/.test(normalized)) {
continue;
}
if (/^\d{2}(?:\.\d{1,2})?$/.test(normalized)) {
continue;
}
if (
normalizedToken === counterpartyToken ||
normalizedToken === organizationToken ||
(Boolean(organizationToken) && normalizedToken.includes(organizationToken)) ||
/^организац/.test(normalizedToken)
) {
continue;
}
return normalized;
}
return null;
}
function normalizeEntityToken(value: string | null | undefined): string {
return String(value ?? "")
.toLowerCase()
@@ -2117,6 +2480,13 @@ function normalizeEntityToken(value: string | null | undefined): string {
.trim();
}
function normalizeSettlementComparableToken(value: string | null | undefined): string {
return normalizeEntityToken(value)
.replace(/[^\p{L}0-9]+/giu, " ")
.replace(/\s+/g, " ")
.trim();
}
function extractPayablesSourceRefs(
row: ComposeStageRow,
counterparty: string,
@@ -2172,6 +2542,58 @@ function formatPayablesEvidenceSuffix(item: PayablesConfirmedBalanceAggregate):
return parts.length > 0 ? ` | ${parts.join(" | ")}` : "";
}
function formatDebtMirrorGroupLine(item: DebtMirrorBalanceGroup): string {
const details = [
item.account ? `счет ${item.account}` : null,
item.contract ? `договор/аналитика: ${item.contract}` : null,
item.organization ? `организация: ${item.organization}` : null
].filter((part): part is string => Boolean(part));
const netText =
Math.abs(item.netAmount) <= 0.005
? "чисто: 0 ₽"
: item.netAmount > 0
? `чисто к получению: ${formatMoneyRub(item.netAmount)}`
: `чисто к оплате: ${formatMoneyRub(Math.abs(item.netAmount))}`;
return `${item.name}${details.length > 0 ? ` (${details.join(", ")})` : ""}: дебет ${formatMoneyRub(item.debitAmount)} / кредит ${formatMoneyRub(item.creditAmount)}, ${netText}.`;
}
function debtMirrorCleanScopeLabel(kind: "payables" | "receivables"): string {
return kind === "payables" ? "чистый долг к оплате" : "чистую дебиторку к получению";
}
function appendDebtMirrorCompactDisclosure(
lines: string[],
snapshot: ConfirmedDebtBalanceSnapshot,
kind: "payables" | "receivables"
): void {
if (snapshot.mirroredOffsetAmount <= 0.005) {
return;
}
lines.push(
`Отдельно сверено встречных остатков: ${formatMoneyRub(snapshot.mirroredOffsetAmount)}; они не включены в ${debtMirrorCleanScopeLabel(kind)}.`
);
const leadingMirror = snapshot.mirrorGroups[0] ?? null;
if (leadingMirror) {
lines.push(`Крупнейший встречный хвост: ${formatDebtMirrorGroupLine(leadingMirror)}`);
}
}
function appendDebtMirrorDisclosure(
lines: string[],
snapshot: ConfirmedDebtBalanceSnapshot,
kind: "payables" | "receivables"
): void {
if (snapshot.mirroredOffsetAmount <= 0.005) {
return;
}
lines.push("");
lines.push("Встречные остатки к сверке");
lines.push(
`- Встречная часть: ${formatMoneyRub(snapshot.mirroredOffsetAmount)}; она исключена из ${debtMirrorCleanScopeLabel(kind)}.`
);
lines.push(...snapshot.mirrorGroups.slice(0, 3).map((item, index) => `${index + 1}. ${formatDebtMirrorGroupLine(item)}`));
}
function deriveOperationalYearWindow(
yearDocs: YearAggPoint[],
yearOps: YearAggPoint[]
@@ -3813,7 +4235,8 @@ function composeFactualReplyBody(
if (intent === "payables_confirmed_as_of_date") {
const payablesAsOfDate = resolvePayablesAsOfDate(options);
const confirmedBalances = buildPayablesConfirmedBalanceAggregate(rows, payablesAsOfDate);
const balanceSnapshot = buildPayablesConfirmedBalanceSnapshot(rows, payablesAsOfDate);
const confirmedBalances = balanceSnapshot.balances;
const asOfDate = normalizeIsoDateOnly(options.asOfDate);
const periodFrom = normalizeIsoDateOnly(options.periodFrom);
const periodTo = normalizeIsoDateOnly(options.periodTo);
@@ -3834,6 +4257,39 @@ function composeFactualReplyBody(
{ supplier_or_contractor: 0, bank_or_credit: 0, tax_or_state: 0, other: 0 }
);
if (isDirectBalanceQuestion(options.userMessage)) {
const leading = confirmedBalances[0] ?? null;
const compactLines: string[] = leading
? [
`Коротко: на ${formatDateRu(payablesAsOfDate)} мы должны ${formatMoneyRub(totalOutstandingAmount)}; крупнейшая позиция — ${leading.name} (${formatMoneyRub(leading.outstandingAmount)}).`,
"Крупнейшие позиции к оплате:"
]
: [`Коротко: на ${formatDateRu(payablesAsOfDate)} подтвержденных обязательств к оплате не найдено.`];
if (leading) {
compactLines.push(
...confirmedBalances.slice(0, 5).map((item, index) => {
const lastPeriod = item.lastPeriod ? `, последнее движение: ${item.lastPeriod}` : "";
return `${index + 1}. ${item.name}${formatMoneyRub(item.outstandingAmount)} (${formatNumberWithDots(item.operations)} опер.${lastPeriod}).`;
})
);
if (confirmedBalances.length > 5) {
compactLines.push(`Показаны первые 5 из ${formatNumberWithDots(confirmedBalances.length)} подтвержденных позиций.`);
}
}
appendDebtMirrorCompactDisclosure(compactLines, balanceSnapshot, "payables");
compactLines.push(`Основа: подтвержденный остаток по счетам 60/76, срез ${formatDateRu(payablesAsOfDate)}.`);
return {
responseType: confirmedBalances.length > 0 ? "FACTUAL_LIST" : "FACTUAL_SUMMARY",
text: joinLines(compactLines),
semantics: {
result_mode: "confirmed_balance",
evidence_strength: confirmedBalances.length > 0 ? "strong" : "medium",
balance_confirmed: true
}
};
}
const lines: string[] = [
`Коротко: подтвержденный долг к оплате на ${formatDateRu(payablesAsOfDate)}${formatMoneyRub(totalOutstandingAmount)}.`,
"Это подтвержденный срез обязательств к оплате по точному остатку."
@@ -3854,6 +4310,7 @@ function composeFactualReplyBody(
lines.push("Сводка");
lines.push(`- Строк в выборке: ${formatNumberWithDots(rows.length)}.`);
lines.push(`- Контрагентов с подтвержденным остатком к оплате: ${formatNumberWithDots(confirmedBalances.length)}.`);
appendDebtMirrorDisclosure(lines, balanceSnapshot, "payables");
lines.push("");
lines.push("Категории обязательств");
@@ -3891,7 +4348,8 @@ function composeFactualReplyBody(
if (intent === "receivables_confirmed_as_of_date") {
const receivablesAsOfDate = resolveReceivablesAsOfDate(options);
const confirmedBalances = buildReceivablesConfirmedBalanceAggregate(rows, receivablesAsOfDate);
const balanceSnapshot = buildReceivablesConfirmedBalanceSnapshot(rows, receivablesAsOfDate);
const confirmedBalances = balanceSnapshot.balances;
const asOfDate = normalizeIsoDateOnly(options.asOfDate);
const periodFrom = normalizeIsoDateOnly(options.periodFrom);
const periodTo = normalizeIsoDateOnly(options.periodTo);
@@ -3912,6 +4370,39 @@ function composeFactualReplyBody(
{ supplier_or_contractor: 0, bank_or_credit: 0, tax_or_state: 0, other: 0 }
);
if (isDirectBalanceQuestion(options.userMessage)) {
const leading = confirmedBalances[0] ?? null;
const compactLines: string[] = leading
? [
`Коротко: на ${formatDateRu(receivablesAsOfDate)} нам должны ${formatMoneyRub(totalOutstandingAmount)}; крупнейшая позиция — ${leading.name} (${formatMoneyRub(leading.outstandingAmount)}).`,
"Крупнейшие позиции к получению:"
]
: [`Коротко: на ${formatDateRu(receivablesAsOfDate)} подтвержденной дебиторской задолженности не найдено.`];
if (leading) {
compactLines.push(
...confirmedBalances.slice(0, 5).map((item, index) => {
const lastPeriod = item.lastPeriod ? `, последнее движение: ${item.lastPeriod}` : "";
return `${index + 1}. ${item.name}${formatMoneyRub(item.outstandingAmount)} (${formatNumberWithDots(item.operations)} опер.${lastPeriod}).`;
})
);
if (confirmedBalances.length > 5) {
compactLines.push(`Показаны первые 5 из ${formatNumberWithDots(confirmedBalances.length)} подтвержденных позиций.`);
}
}
appendDebtMirrorCompactDisclosure(compactLines, balanceSnapshot, "receivables");
compactLines.push(`Основа: подтвержденный остаток по счетам 62/76, срез ${formatDateRu(receivablesAsOfDate)}.`);
return {
responseType: confirmedBalances.length > 0 ? "FACTUAL_LIST" : "FACTUAL_SUMMARY",
text: joinLines(compactLines),
semantics: {
result_mode: "confirmed_balance",
evidence_strength: confirmedBalances.length > 0 ? "strong" : "medium",
balance_confirmed: true
}
};
}
const lines: string[] = [
`Коротко: подтвержденная дебиторская задолженность на ${formatDateRu(receivablesAsOfDate)}${formatMoneyRub(totalOutstandingAmount)}.`,
"Это подтвержденный срез дебиторской задолженности, а не эвристический shortlist."
@@ -3932,6 +4423,7 @@ function composeFactualReplyBody(
lines.push("Сводка");
lines.push(`- Строк в выборке: ${formatNumberWithDots(rows.length)}.`);
lines.push(`- Контрагентов с подтвержденным остатком к получению: ${formatNumberWithDots(confirmedBalances.length)}.`);
appendDebtMirrorDisclosure(lines, balanceSnapshot, "receivables");
lines.push("");
lines.push("Категории дебиторской задолженности");
@@ -4062,7 +4554,8 @@ function composeFactualReplyBody(
};
if (options.requestedResultMode === "confirmed_balance") {
const confirmedBalances = buildPayablesConfirmedBalanceAggregate(rows, payablesAsOfDate);
const balanceSnapshot = buildPayablesConfirmedBalanceSnapshot(rows, payablesAsOfDate);
const confirmedBalances = balanceSnapshot.balances;
if (confirmedBalances.length > 0) {
const categoryCounts = confirmedBalances.reduce<Record<PayablesLiabilityCategory, number>>(
(acc, item) => {
@@ -4103,6 +4596,7 @@ function composeFactualReplyBody(
`${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток к оплате: ${formatMoneyRub(item.outstandingAmount)} | операций в срезе: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`
)
];
appendDebtMirrorDisclosure(lines, balanceSnapshot, "payables");
return {
responseType: "FACTUAL_LIST",
text: joinLines(lines),
@@ -534,7 +534,18 @@ export function composeCounterpartyAnalyticsReply(
!/(?:\btop\b|топ|рейтинг|список|первые|покажи\s+топ|дай\s+топ|РїРѕРєР°Р\w*\s+СРѕРї|РґР°Р\s+СРѕРї)/iu.test(
normalizedQuestion
);
const effectiveLimit = asksSingleBestCounterparty ? 1 : limit;
const asksExplicitRankingList =
/(?:\btop\b|топ|рейтинг|список|первые|покажи\s+(?:топ|список)|дай\s+(?:топ|список)|show\s+(?:top|list))/iu.test(
normalizedQuestion
);
const hasSingleBestCounterpartyCue =
/(?:сам\p{L}*|больше\s+всего|наибольш|прин[её]с|определ\p{L}*|найд\p{L}*|highest|largest|most)/iu.test(
normalizedQuestion
) &&
/(?:клиент|заказчик|покупател|контрагент|customer|client|counterparty|buyer)/iu.test(normalizedQuestion);
const semanticSingleBestCounterparty =
focus === "top_by_total" && hasSingleBestCounterpartyCue && !asksExplicitRankingList;
const effectiveLimit = asksSingleBestCounterparty || semanticSingleBestCounterparty ? 1 : limit;
const byCounterparty = new Map<string, CounterpartyValuePoint>();
const byYear = new Map<number, CounterpartyYearPoint>();
@@ -220,6 +220,14 @@ function hasExplicitLooseByAnchorToken(text: string): boolean {
}
const FOLLOWUP_LOW_QUALITY_COUNTERPARTY_TOKENS = new Set([
"мы",
"нам",
"нас",
"наш",
"наша",
"наше",
"наши",
"унас",
"есть",
"же",
"что",
@@ -1445,6 +1453,12 @@ function mergeFollowupFilters(
previousOrganization ??
(followupContext.previous_anchor_type === "organization" ? previousAnchorValue : null);
const finalCounterparty = toNonEmptyString(merged.counterparty);
if (finalCounterparty && isLowQualityCounterpartyAnchor(finalCounterparty)) {
delete merged.counterparty;
if (!reasons.includes("counterparty_cleared_low_quality_followup_anchor")) {
reasons.push("counterparty_cleared_low_quality_followup_anchor");
}
}
if (shouldSuppressInventoryCounterpartyAlias(intent, finalCounterparty, finalOrganizationReference)) {
delete merged.counterparty;
if (!reasons.includes("counterparty_cleared_as_organization_scope_alias")) {
@@ -575,6 +575,14 @@ function isReferentialCounterpartyPlaceholder(
return false;
}
return new Set([
"мы",
"нам",
"нас",
"наш",
"наша",
"наше",
"наши",
"унас",
"он",
"она",
"оно",
@@ -733,6 +733,37 @@ function businessOverviewDebtDueDateAgingText(overview: BusinessOverview): strin
return `Due-date aging на ${aging.as_of_date} проверен: строк с установленным сроком оплаты ${aging.rows_with_payment_terms}, подтвержденной просрочки не найдено; не просрочено по расчету ${aging.not_yet_due_amount_human_ru}.`;
}
function businessOverviewVendorProcurementQualityText(overview: BusinessOverview): string | null {
const quality = overview.vendor_procurement_quality;
if (!quality) {
return null;
}
const period = quality.period_scope ?? "проверенное окно";
const total = quality.total_outgoing_amount_human_ru;
const top = quality.top_outgoing_counterparty;
const topName = top?.axis_value ?? "получатель не распознан";
const topShare = quality.top_outgoing_share_pct === null ? "" : `, около ${quality.top_outgoing_share_pct}%`;
const topAmount = top?.total_amount_human_ru ? ` (${top.total_amount_human_ru})` : "";
const nonFinancial = quality.top_non_financial_supplier;
const nonFinancialShare =
quality.top_non_financial_supplier_share_pct === null ? "" : `, около ${quality.top_non_financial_supplier_share_pct}%`;
const nonFinancialText = nonFinancial
? ` Крупнейший небанковский получатель: ${nonFinancial.axis_value}${nonFinancialShare}${nonFinancial.total_amount_human_ru ? ` (${nonFinancial.total_amount_human_ru})` : ""}.`
: "";
const contractText = quality.used_contracts === null
? ""
: quality.total_contracts === null
? ` Договорный профиль: используется ${quality.used_contracts} договоров.`
: ` Договорный профиль: используется ${quality.used_contracts}/${quality.total_contracts} договоров${quality.used_contract_share_pct === null ? "" : ` (${quality.used_contract_share_pct}%)`}.`;
if (quality.evidence_status === "financial_institution_leads_outgoing_cash") {
return `Проверенный procurement-concentration route за ${period}: крупнейший получатель исходящих денег ${topName}${topShare}${topAmount}, всего исходящих платежей ${total}. По названию это банк/финансовая организация, поэтому зависимость от обычного поставщика этим не подтверждается.${nonFinancialText}${contractText} Надежность поставщиков, качество поставок, назначение каждого платежа и полная структура расходов этим маршрутом не доказаны.`;
}
if (quality.evidence_status === "reviewed_procurement_concentration") {
return `Проверенный procurement-concentration route за ${period}: крупнейший поставщик/получатель исходящих платежей ${topName}${topShare}${topAmount}, всего исходящих платежей ${total}.${contractText} Это проверенный сигнал концентрации закупок/исходящих платежей, но не аудит надежности поставщика, качества поставок и полной структуры расходов.`;
}
return `Procurement-concentration route за ${period} отработал по исходящим платежам на ${total}, но надежной небанковской концентрации поставщика по найденным строкам не хватает.${contractText} Полный vendor-risk аудит не подтвержден.`;
}
function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpDiscoveryPilotExecutionContract): string {
const askedMonthlyBreakdown =
pilot.derived_bidirectional_value_flow?.aggregation_axis === "month" ||
@@ -774,6 +805,10 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
return `Коротко: точно подтвердить резерв под неликвиды по текущим данным нельзя; ${inventoryBasis}. Можно честно говорить только о необходимости отдельной проверки склада, списаний/резервов и ликвидационной стоимости, не превращая proxy в доказанный факт резерва.`;
}
if (isVendorRiskBoundaryTurn(pilot)) {
const vendorQualityText = businessOverviewVendorProcurementQualityText(overview);
if (vendorQualityText) {
return vendorQualityText;
}
const supplierLeader = overview.top_suppliers?.[0] ?? null;
const proxyLabel = isFinancialInstitutionBucket(supplierLeader)
? "outgoing cash concentration proxy"
@@ -1051,7 +1086,11 @@ function buildMustNotClaim(pilot: AssistantMcpDiscoveryPilotExecutionContract):
claims.push("Do not present business overview cash-flow spread as profit or margin.");
claims.push("Do not present business overview yearly operating-flow breakdown as profit, financial result, or a complete annual P&L.");
claims.push("Do not present business overview trading-margin proxy as clean profit, accounting financial result, or exact cost-of-sales margin.");
claims.push("Do not present business overview supplier concentration as vendor-risk audit, procurement quality, or full expense structure.");
if (pilot.derived_business_overview?.vendor_procurement_quality) {
claims.push("Do not present reviewed procurement concentration as supplier reliability, delivery quality, payment-purpose classification, or full expense structure.");
} else {
claims.push("Do not present business overview supplier concentration as vendor-risk audit, procurement quality, or full expense structure.");
}
claims.push("Do not present business overview document/account-section activity profile as process quality, accounting correctness, or completeness of all 1C activity.");
claims.push("Do not present business overview counterparty or contract profile as CRM quality, counterparty due diligence, contract-risk audit, or legal completeness.");
claims.push("Do not claim debt quality, VAT position, inventory health, or company health unless those contours were separately checked.");
@@ -1486,6 +1525,10 @@ function derivedBusinessOverviewConfirmedLines(pilot: AssistantMcpDiscoveryPilot
if (outgoingLeaderLine) {
lines.push(outgoingLeaderLine);
}
const vendorQualityText = businessOverviewVendorProcurementQualityText(overview);
if (vendorQualityText) {
lines.push(vendorQualityText);
}
if (overview.yearly_breakdown?.length) {
lines.push(
`Годовая раскладка операционного денежного потока построена по подтвержденным строкам 1С за ${yearCountHumanRu(overview.yearly_breakdown.length)}.`
@@ -1936,6 +1979,10 @@ export function buildAssistantMcpDiscoveryAnswerDraft(
if (pilot.derived_business_overview?.top_suppliers?.length) {
pushReason(reasonCodes, "answer_contains_business_overview_supplier_concentration");
}
if (pilot.derived_business_overview?.vendor_procurement_quality) {
pushReason(reasonCodes, "answer_contains_business_overview_vendor_procurement_quality");
pushReason(reasonCodes, `answer_contains_business_overview_vendor_procurement_quality_${pilot.derived_business_overview.vendor_procurement_quality.evidence_status}`);
}
if (pilot.derived_business_overview?.yearly_breakdown?.length) {
pushReason(reasonCodes, "answer_contains_business_overview_yearly_operating_breakdown");
}
@@ -188,6 +188,28 @@ export interface AssistantMcpDiscoveryDerivedBusinessOverviewContractUsageProfil
inference_basis: "contract_usage_overview_confirmed_1c_rows";
}
export interface AssistantMcpDiscoveryDerivedBusinessOverviewVendorProcurementQuality {
period_scope: string | null;
rows_with_amount: number;
total_outgoing_amount: number;
total_outgoing_amount_human_ru: string;
top_outgoing_counterparty: AssistantMcpDiscoveryRankedValueFlowBucket | null;
top_outgoing_share_pct: number | null;
top_non_financial_supplier: AssistantMcpDiscoveryRankedValueFlowBucket | null;
top_non_financial_supplier_share_pct: number | null;
financial_institution_leads_outgoing_cash: boolean;
supplier_only_count: number | null;
mixed_role_count: number | null;
used_contracts: number | null;
total_contracts: number | null;
used_contract_share_pct: number | null;
evidence_status:
| "reviewed_procurement_concentration"
| "financial_institution_leads_outgoing_cash"
| "insufficient_supplier_concentration_basis";
inference_basis: "supplier_payout_concentration_counterparty_contract_profile_confirmed_1c_rows";
}
export type AssistantMcpDiscoveryBusinessOverviewMissingProofFamilyId =
| "accounting_profit_margin"
| "debt_due_date_aging_quality"
@@ -246,6 +268,7 @@ export interface AssistantMcpDiscoveryDerivedBusinessOverview {
document_activity_profile: AssistantMcpDiscoveryDerivedBusinessOverviewDocumentActivityProfile | null;
counterparty_profile: AssistantMcpDiscoveryDerivedBusinessOverviewCounterpartyProfile | null;
contract_usage_profile: AssistantMcpDiscoveryDerivedBusinessOverviewContractUsageProfile | null;
vendor_procurement_quality: AssistantMcpDiscoveryDerivedBusinessOverviewVendorProcurementQuality | null;
coverage_limited_by_probe_limit: boolean;
checked_signal_count: number;
missing_signal_families: string[];
@@ -4125,6 +4148,59 @@ function deriveBusinessOverviewInventoryStalenessRiskProxy(input: {
};
}
function deriveBusinessOverviewVendorProcurementQuality(input: {
rankedOutgoing: AssistantMcpDiscoveryDerivedRankedValueFlow | null;
outgoing: AssistantMcpDiscoveryValueFlowSideSummary;
counterpartyProfile: AssistantMcpDiscoveryDerivedBusinessOverviewCounterpartyProfile | null;
contractUsageProfile: AssistantMcpDiscoveryDerivedBusinessOverviewContractUsageProfile | null;
periodScope: string | null;
}): AssistantMcpDiscoveryDerivedBusinessOverviewVendorProcurementQuality | null {
if (
!input.rankedOutgoing ||
input.rankedOutgoing.ranked_values.length <= 0 ||
input.outgoing.rows_with_amount <= 0 ||
input.outgoing.total_amount <= 0
) {
return null;
}
const topOutgoingCounterparty = input.rankedOutgoing.ranked_values[0] ?? null;
const topNonFinancialSupplier =
input.rankedOutgoing.ranked_values.find((item) => !isLikelyFinancialInstitutionCounterparty(item.axis_value)) ?? null;
const financialInstitutionLeadsOutgoingCash = topOutgoingCounterparty
? isLikelyFinancialInstitutionCounterparty(topOutgoingCounterparty.axis_value)
: false;
const evidenceStatus: AssistantMcpDiscoveryDerivedBusinessOverviewVendorProcurementQuality["evidence_status"] =
!topNonFinancialSupplier
? "insufficient_supplier_concentration_basis"
: financialInstitutionLeadsOutgoingCash
? "financial_institution_leads_outgoing_cash"
: "reviewed_procurement_concentration";
return {
period_scope: input.periodScope,
rows_with_amount: input.outgoing.rows_with_amount,
total_outgoing_amount: input.outgoing.total_amount,
total_outgoing_amount_human_ru: input.outgoing.total_amount_human_ru,
top_outgoing_counterparty: topOutgoingCounterparty,
top_outgoing_share_pct: topOutgoingCounterparty
? percentageOfTotal(topOutgoingCounterparty.total_amount, input.outgoing.total_amount)
: null,
top_non_financial_supplier: topNonFinancialSupplier,
top_non_financial_supplier_share_pct: topNonFinancialSupplier
? percentageOfTotal(topNonFinancialSupplier.total_amount, input.outgoing.total_amount)
: null,
financial_institution_leads_outgoing_cash: financialInstitutionLeadsOutgoingCash,
supplier_only_count: input.counterpartyProfile?.supplier_only_count ?? null,
mixed_role_count: input.counterpartyProfile?.mixed_role_count ?? null,
used_contracts: input.contractUsageProfile?.used_contracts ?? null,
total_contracts: input.contractUsageProfile?.total_contracts ?? null,
used_contract_share_pct: input.contractUsageProfile?.used_contract_share_pct ?? null,
evidence_status: evidenceStatus,
inference_basis: "supplier_payout_concentration_counterparty_contract_profile_confirmed_1c_rows"
};
}
function inventoryStalenessRiskBandRu(
riskBand: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryStalenessRiskProxy["risk_band"]
): string {
@@ -4150,6 +4226,7 @@ function buildBusinessOverviewMissingProofFamilies(input: {
inventoryPosition: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryPosition | null;
inventoryTurnoverProxy: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryTurnoverProxy | null;
inventoryStalenessRiskProxy: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryStalenessRiskProxy | null;
vendorProcurementQuality: AssistantMcpDiscoveryDerivedBusinessOverviewVendorProcurementQuality | null;
hasSupplierConcentrationSignal: boolean;
}): AssistantMcpDiscoveryBusinessOverviewMissingProofFamily[] {
const missing = new Set(input.missingSignalFamilies);
@@ -4215,7 +4292,7 @@ function buildBusinessOverviewMissingProofFamilies(input: {
});
}
if (input.hasSupplierConcentrationSignal) {
if (input.hasSupplierConcentrationSignal && !input.vendorProcurementQuality) {
pushUnique({
family: "vendor_risk_procurement_quality",
current_status: "proxy_only_currently",
@@ -4313,6 +4390,13 @@ function deriveBusinessOverview(input: {
inventoryPosition,
inventoryTurnoverProxy
});
const vendorProcurementQuality = deriveBusinessOverviewVendorProcurementQuality({
rankedOutgoing,
outgoing,
counterpartyProfile,
contractUsageProfile,
periodScope: input.periodScope
});
const checkedSignalCount = [
incoming.rows_with_amount > 0,
outgoing.rows_with_amount > 0,
@@ -4329,7 +4413,8 @@ function deriveBusinessOverview(input: {
Boolean(contractUsageProfile),
Boolean(inventoryPosition),
Boolean(inventoryTurnoverProxy),
Boolean(inventoryStalenessRiskProxy)
Boolean(inventoryStalenessRiskProxy),
Boolean(vendorProcurementQuality)
].filter(Boolean).length;
if (checkedSignalCount <= 0) {
return null;
@@ -4363,6 +4448,7 @@ function deriveBusinessOverview(input: {
inventoryPosition,
inventoryTurnoverProxy,
inventoryStalenessRiskProxy,
vendorProcurementQuality,
hasSupplierConcentrationSignal: (rankedOutgoing?.ranked_values.length ?? 0) > 0
});
return {
@@ -4390,6 +4476,7 @@ function deriveBusinessOverview(input: {
document_activity_profile: documentActivityProfile,
counterparty_profile: counterpartyProfile,
contract_usage_profile: contractUsageProfile,
vendor_procurement_quality: vendorProcurementQuality,
coverage_limited_by_probe_limit:
incoming.coverage_limited_by_probe_limit || outgoing.coverage_limited_by_probe_limit,
checked_signal_count: checkedSignalCount,
@@ -5986,6 +6073,10 @@ export async function executeAssistantMcpDiscoveryPilot(
if (derivedBusinessOverview.top_suppliers.length > 0) {
pushReason(reasonCodes, "pilot_derived_business_overview_top_suppliers_from_confirmed_rows");
}
if (derivedBusinessOverview.vendor_procurement_quality) {
pushReason(reasonCodes, "pilot_derived_business_overview_vendor_procurement_quality_from_confirmed_rows");
pushReason(reasonCodes, `pilot_derived_business_overview_vendor_procurement_quality_${derivedBusinessOverview.vendor_procurement_quality.evidence_status}`);
}
if (derivedBusinessOverview.yearly_breakdown.length > 0) {
pushReason(reasonCodes, "pilot_derived_business_overview_yearly_operating_breakdown_from_confirmed_rows");
}
@@ -843,7 +843,7 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
extraReasons: primitiveSelection.reasonCodes
});
}
if (dataNeedGraph?.ranking_need && !hasSubjectCandidates(dataNeedGraph)) {
if (dataNeedGraph?.ranking_need) {
pushUnique(axes, "aggregate_axis");
pushUnique(axes, "amount");
pushUnique(axes, "coverage_target");
@@ -864,7 +864,10 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
dataNeedGraph.ranking_need === "bottom_asc"
? "planner_selected_bottom_ranked_value_flow_from_data_need_graph"
: "planner_selected_top_ranked_value_flow_from_data_need_graph",
extraReasons: primitiveSelection.reasonCodes
extraReasons: [
...primitiveSelection.reasonCodes,
...(hasSubjectCandidates(dataNeedGraph) ? ["planner_kept_ranking_over_subject_scope_candidates"] : [])
]
});
}
if (openScopeTotalWithoutSubject) {
@@ -922,6 +922,58 @@ function buildCompactBusinessOverviewReply(
}
if (vendorRiskBoundary) {
const vendorProcurementQuality = toRecordObject(overview.vendor_procurement_quality);
if (vendorProcurementQuality) {
const status = toNonEmptyString(vendorProcurementQuality.evidence_status);
const totalOutgoing = moneyText(vendorProcurementQuality.total_outgoing_amount_human_ru);
const topOutgoingRecord = toRecordObject(vendorProcurementQuality.top_outgoing_counterparty);
const topOutgoingName = toNonEmptyString(topOutgoingRecord?.axis_value);
const topOutgoingAmount = moneyText(topOutgoingRecord?.total_amount_human_ru);
const topOutgoingShare =
typeof vendorProcurementQuality.top_outgoing_share_pct === "number" &&
Number.isFinite(vendorProcurementQuality.top_outgoing_share_pct)
? `${vendorProcurementQuality.top_outgoing_share_pct}%`
: null;
const nonFinancialRecord = toRecordObject(vendorProcurementQuality.top_non_financial_supplier);
const nonFinancialName = toNonEmptyString(nonFinancialRecord?.axis_value);
const nonFinancialAmount = moneyText(nonFinancialRecord?.total_amount_human_ru);
const nonFinancialShare =
typeof vendorProcurementQuality.top_non_financial_supplier_share_pct === "number" &&
Number.isFinite(vendorProcurementQuality.top_non_financial_supplier_share_pct)
? `${vendorProcurementQuality.top_non_financial_supplier_share_pct}%`
: null;
const periodScope = toNonEmptyString(vendorProcurementQuality.period_scope) ?? period;
const totalText = totalOutgoing ? `; всего исходящих платежей в проверенном срезе ${totalOutgoing}` : "";
if (status === "financial_institution_leads_outgoing_cash") {
lines.push(
`Коротко: проверенный procurement-concentration route за ${periodScope} не подтверждает зависимость от обычного поставщика: крупнейший получатель исходящих денег ${topOutgoingName ?? "не распознан"}${topOutgoingShare ? ` держит около ${topOutgoingShare}` : ""}${topOutgoingAmount ? ` (${topOutgoingAmount})` : ""}, но по названию это банк/финансовая организация${totalText}.`
);
if (nonFinancialName) {
lines.push(
`Крупнейший небанковский получатель исходящих денег: ${nonFinancialName}${nonFinancialShare ? `, около ${nonFinancialShare}` : ""}${nonFinancialAmount ? ` (${nonFinancialAmount})` : ""}. Это уже сигнал закупочной/исходящей концентрации, но не аудит надежности поставщика.`
);
}
} else if (status === "reviewed_procurement_concentration") {
lines.push(
`Коротко: проверенный procurement-concentration route за ${periodScope} нашел основную зависимость исходящего потока: ${topOutgoingName ?? nonFinancialName ?? "получатель не распознан"}${topOutgoingShare ? ` держит около ${topOutgoingShare}` : nonFinancialShare ? ` держит около ${nonFinancialShare}` : ""}${topOutgoingAmount ? ` (${topOutgoingAmount})` : nonFinancialAmount ? ` (${nonFinancialAmount})` : ""}${totalText}.`
);
} else {
lines.push(
`Коротко: procurement-concentration route за ${periodScope} отработал, но надежной небанковской концентрации поставщика по найденным исходящим платежам не хватает${totalText}.`
);
}
const contractText =
typeof vendorProcurementQuality.used_contracts === "number" && Number.isFinite(vendorProcurementQuality.used_contracts)
? typeof vendorProcurementQuality.total_contracts === "number" && Number.isFinite(vendorProcurementQuality.total_contracts)
? ` Договорный профиль: используется ${vendorProcurementQuality.used_contracts}/${vendorProcurementQuality.total_contracts} договоров${typeof vendorProcurementQuality.used_contract_share_pct === "number" && Number.isFinite(vendorProcurementQuality.used_contract_share_pct) ? ` (${vendorProcurementQuality.used_contract_share_pct}%)` : ""}.`
: ` Договорный профиль: используется ${vendorProcurementQuality.used_contracts} договоров.`
: "";
lines.push(
`Что не доказано этим маршрутом: надежность поставщика, качество поставок, договорные условия, назначение каждого платежа и полная структура всех расходов.${contractText}`
);
const reply = lines.join("\n").trim();
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
}
const supplierBasis = topSupplier
? topSupplierLooksFinancial
? `крупнейший получатель исходящих денег: ${topSupplier}; по названию это банк/финансовая организация, поэтому это не доказанная зависимость от обычного поставщика${nonFinancialSupplier ? `; крупнейший небанковский получатель исходящих денег: ${nonFinancialSupplier}` : ""}`
@@ -255,6 +255,26 @@ function isMetadataDiscoveryTurn(
);
}
function isExplicitMetadataDiscoveryTurn(
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
): boolean {
const turnInput = toRecordObject(entryPoint?.turn_input);
const turnMeaning = readDiscoveryTurnMeaning(entryPoint);
const graph = readDiscoveryDataNeedGraph(entryPoint);
const reasonCodes = [
...(Array.isArray(entryPoint?.reason_codes) ? entryPoint.reason_codes : []),
...(Array.isArray(turnInput?.reason_codes) ? turnInput.reason_codes : [])
];
const decompositionCandidates = Array.isArray(graph?.decomposition_candidates) ? graph.decomposition_candidates : [];
return Boolean(
toNonEmptyString(turnMeaning?.asked_domain_family) === "metadata" ||
toNonEmptyString(turnMeaning?.unsupported_but_understood_family) === "1c_metadata_surface" ||
toNonEmptyString(graph?.business_fact_family) === "schema_surface" ||
decompositionCandidates.some((candidate) => toNonEmptyString(candidate) === "inspect_metadata_surface") ||
reasonCodes.some((reason) => toNonEmptyString(reason) === "mcp_discovery_metadata_signal_detected")
);
}
function isInventoryExactAddressIntent(intent: string | null): boolean {
return /^(?:inventory_purchase_provenance_for_item|inventory_purchase_documents_for_item|inventory_sale_trace_for_item|inventory_profitability_for_item|inventory_purchase_to_sale_chain|inventory_aging_by_purchase_date|inventory_on_hand_as_of_date)$/u.test(
String(intent ?? "")
@@ -274,6 +294,9 @@ function hasMetadataDiscoveryPriority(
if (!isMetadataDiscoveryTurn(entryPoint)) {
return false;
}
if (!isExplicitMetadataDiscoveryTurn(entryPoint)) {
return false;
}
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
return !isInventoryExactAddressIntent(detectedIntent);
}
@@ -363,6 +386,75 @@ function readStringArray(value: unknown): string[] {
: [];
}
function hasConfirmedAddressExecution(input: ApplyAssistantMcpDiscoveryResponsePolicyInput): boolean {
const mcpCallStatus = toNonEmptyString(input.addressRuntimeMeta?.mcp_call_status);
const truthMode = toNonEmptyString(input.addressRuntimeMeta?.truth_mode);
const selectedRecipe = toNonEmptyString(input.addressRuntimeMeta?.selected_recipe);
const bindingStatus = toNonEmptyString(input.addressRuntimeMeta?.capability_binding_status);
const bindingViolations = readStringArray(input.addressRuntimeMeta?.capability_binding_violations);
return Boolean(
mcpCallStatus === "matched_non_empty" &&
truthMode === "confirmed" &&
selectedRecipe?.startsWith("address_") &&
(bindingStatus === "bound" || bindingStatus === "bound_with_limits") &&
bindingViolations.length === 0
);
}
function hasStaleMetadataDiscoveryFallbackAgainstExactAddressReply(
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
): boolean {
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
return false;
}
if (!hasEffectivelyFactualAddressReply(input)) {
return false;
}
if (!isMetadataDiscoveryTurn(entryPoint) || isExplicitMetadataDiscoveryTurn(entryPoint)) {
return false;
}
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
return Boolean(
detectedIntent &&
hasConfirmedAddressExecution(input) &&
isDetectedIntentAlignedWithTurnMeaning(detectedIntent, readDiscoveryTurnMeaning(entryPoint))
);
}
function hasBusinessOverviewDirectMoneyClarification(
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
): boolean {
const graph = readDiscoveryDataNeedGraph(entryPoint);
const businessFactFamily = toNonEmptyString(graph?.business_fact_family);
const reasonCodes = readStringArray(graph?.reason_codes);
const clarificationGaps = readStringArray(graph?.clarification_gaps);
return Boolean(
businessFactFamily === "business_overview" &&
reasonCodes.includes("data_need_graph_business_overview_direct_money_answer") &&
(toNonEmptyString(graph?.ranking_need) || reasonCodes.includes("data_need_graph_ranking_top_desc")) &&
clarificationGaps.includes("organization")
);
}
function hasExactValueFlowReplyForBusinessOverviewDirectMoneyNeed(
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
): boolean {
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
return false;
}
if (!hasEffectivelyFactualAddressReply(input)) {
return false;
}
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
return Boolean(
detectedIntent === "customer_revenue_and_payments" &&
hasConfirmedAddressExecution(input) &&
hasBusinessOverviewDirectMoneyClarification(entryPoint)
);
}
function hasValueFlowActionConflictWithDiscoveryTurnMeaning(
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
@@ -443,18 +535,7 @@ function hasExactMatchedFactualAddressReply(
return false;
}
}
const mcpCallStatus = toNonEmptyString(input.addressRuntimeMeta?.mcp_call_status);
const truthMode = toNonEmptyString(input.addressRuntimeMeta?.truth_mode);
const selectedRecipe = toNonEmptyString(input.addressRuntimeMeta?.selected_recipe);
const bindingStatus = toNonEmptyString(input.addressRuntimeMeta?.capability_binding_status);
const bindingViolations = readStringArray(input.addressRuntimeMeta?.capability_binding_violations);
return Boolean(
mcpCallStatus === "matched_non_empty" &&
truthMode === "confirmed" &&
selectedRecipe?.startsWith("address_") &&
(bindingStatus === "bound" || bindingStatus === "bound_with_limits") &&
bindingViolations.length === 0
);
return hasConfirmedAddressExecution(input);
}
function hasOpenScopeValueFlowDiscoveryPriority(
@@ -682,6 +763,14 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
const exactMatchedFactualAddressReply = hasExactMatchedFactualAddressReply(input, entryPoint);
const runtimeAdjustedExactReply = hasRuntimeAdjustedExactReply(input, entryPoint);
const runtimeMatchedExactReply = hasRuntimeMatchedExactReply(input, entryPoint);
const staleMetadataDiscoveryFallbackAgainstExactAddressReply = hasStaleMetadataDiscoveryFallbackAgainstExactAddressReply(
input,
entryPoint
);
const exactValueFlowReplyForBusinessOverviewDirectMoneyNeed = hasExactValueFlowReplyForBusinessOverviewDirectMoneyNeed(
input,
entryPoint
);
const openScopeValueFlowDiscoveryPriority = hasOpenScopeValueFlowDiscoveryPriority(input, entryPoint);
const metadataDiscoveryPriority = hasMetadataDiscoveryPriority(input, entryPoint);
const valueFlowActionConflictWithDiscoveryTurnMeaning = hasValueFlowActionConflictWithDiscoveryTurnMeaning(
@@ -750,6 +839,18 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
"mcp_discovery_response_policy_keep_runtime_matched_exact_reply_over_stale_discovery_turn_meaning"
);
}
if (staleMetadataDiscoveryFallbackAgainstExactAddressReply) {
pushReason(
reasonCodes,
"mcp_discovery_response_policy_keep_exact_address_reply_over_stale_metadata_discovery"
);
}
if (exactValueFlowReplyForBusinessOverviewDirectMoneyNeed) {
pushReason(
reasonCodes,
"mcp_discovery_response_policy_keep_exact_value_flow_reply_over_business_overview_direct_money_clarification"
);
}
if (deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") {
pushReason(
reasonCodes,
@@ -779,6 +880,8 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
!exactMatchedFactualAddressReply &&
!runtimeAdjustedExactReply &&
!runtimeMatchedExactReply &&
!staleMetadataDiscoveryFallbackAgainstExactAddressReply &&
!exactValueFlowReplyForBusinessOverviewDirectMoneyNeed &&
!(deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") &&
ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) &&
candidate.eligible_for_future_hot_runtime &&