ГЛОБАЛЬНЫЙ РЕФАКТОРИНГ АРХИТЕКТУРЫ - fix(router): keep llm-first non-domain indexing and prevent followup force into address lane

This commit is contained in:
2026-04-12 17:29:35 +03:00
parent 143cf6efe1
commit a717ea6b26
5 changed files with 358 additions and 44 deletions
@@ -653,6 +653,9 @@ interface PayablesConfirmedBalanceAggregate {
lastPeriod: string | null;
category: PayablesLiabilityCategory;
categoryReasons: string[];
contracts: string[];
documents: string[];
sourceRefs: string[];
}
function liabilityCategoryLabel(category: PayablesLiabilityCategory): string {
@@ -680,6 +683,11 @@ function classifyPayablesLiabilityCategory(row: ComposeStageRow, counterparty: s
};
const reasons = new Set<string>();
const text = `${counterparty} ${row.registrator} ${row.analytics.join(" ")}`.toLowerCase();
const hasBankOrCreditSignal = /(?:банк|сбер|втб|альфа|газпромбанк|кредит|депозит|loan|overdraft|deposit|bank)/iu.test(text);
const hasTaxOrStateSignal =
/(?:уфк|ифнс|фнс|налог|пфр|фсс|сфр|казнач|бюджет|гос|департамент|министер|муницип|город москвы|федерал|налогов)/iu.test(text);
const hasCommercialCounterpartySignal =
/(?:\bип\b|ооо|ао|зао|пао|подряд|поставщик|supplier|vendor|contractor|заказчик|клиент|counterparty)/iu.test(text);
const accountPrefixes = [extractAccountSectionCode(row.account_dt), extractAccountSectionCode(row.account_kt)].filter(
(item): item is string => Boolean(item)
@@ -697,19 +705,21 @@ function classifyPayablesLiabilityCategory(row: ComposeStageRow, counterparty: s
reasons.add("участие счета 68/69");
}
if (accountPrefixes.includes("76")) {
scores.supplier_or_contractor += 1;
reasons.add("участие счета 76");
scores.other += 1;
reasons.add("участие счета 76 (прочие расчеты)");
}
if (/(?:банк|сбер|втб|альфа|газпромбанк|кредит|депозит|loan|overdraft|deposit)/iu.test(text)) {
scores.bank_or_credit += 3;
if (hasBankOrCreditSignal) {
scores.bank_or_credit += 6;
scores.supplier_or_contractor = Math.max(0, scores.supplier_or_contractor - 2);
reasons.add("банк/кредит в аналитике");
}
if (/(?:уфк|ифнс|фнс|налог|пфр|фсс|сфр|казнач|бюджет|гос|департамент|министер|муницип|город москвы|федерал)/iu.test(text)) {
scores.tax_or_state += 3;
if (hasTaxOrStateSignal) {
scores.tax_or_state += 6;
scores.supplier_or_contractor = Math.max(0, scores.supplier_or_contractor - 2);
reasons.add("налог/госорган в аналитике");
}
if (/(?:\bип\b|ооо|ао|зао|пао|подряд|поставщик|supplier|vendor|contractor)/iu.test(text)) {
if (hasCommercialCounterpartySignal && !hasBankOrCreditSignal && !hasTaxOrStateSignal) {
scores.supplier_or_contractor += 2;
reasons.add("коммерческий контрагент в аналитике");
}
@@ -852,6 +862,9 @@ function buildPayablesConfirmedBalanceAggregate(
lastPeriod: string | null;
categoryScores: Record<PayablesLiabilityCategory, number>;
reasons: Set<string>;
contracts: Set<string>;
documents: Set<string>;
sourceRefs: Set<string>;
}
>();
const asOfTimestamp = toUtcDayTimestamp(asOfDate);
@@ -882,6 +895,8 @@ function buildPayablesConfirmedBalanceAggregate(
}
const classified = classifyPayablesLiabilityCategory(row, name);
const contract = extractContractName(row);
const sourceRefs = extractPayablesSourceRefs(row, name, contract);
const current = byCounterparty.get(name);
if (!current) {
byCounterparty.set(name, {
@@ -895,7 +910,10 @@ function buildPayablesConfirmedBalanceAggregate(
tax_or_state: classified.scores.tax_or_state,
other: classified.scores.other
},
reasons: new Set(classified.reasons)
reasons: new Set(classified.reasons),
contracts: new Set(contract ? [contract] : []),
documents: new Set(row.registrator ? [row.registrator] : []),
sourceRefs: new Set(sourceRefs)
});
continue;
}
@@ -915,6 +933,15 @@ function buildPayablesConfirmedBalanceAggregate(
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);
}
}
return Array.from(byCounterparty.entries())
@@ -925,7 +952,10 @@ function buildPayablesConfirmedBalanceAggregate(
firstPeriod: item.firstPeriod,
lastPeriod: item.lastPeriod,
category: resolvePayablesLiabilityCategory(item.categoryScores),
categoryReasons: Array.from(item.reasons).slice(0, 2)
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) => {
@@ -1142,6 +1172,69 @@ function extractContractName(row: ComposeStageRow): string | null {
return null;
}
function normalizeEntityToken(value: string | null | undefined): string {
return String(value ?? "")
.toLowerCase()
.replace(/ё/g, "е")
.replace(/\s+/g, " ")
.trim();
}
function extractPayablesSourceRefs(
row: ComposeStageRow,
counterparty: string,
contract: string | null
): string[] {
const refs = new Set<string>();
const counterpartyToken = normalizeEntityToken(counterparty);
const contractToken = normalizeEntityToken(contract);
const sourceRefTokenPattern =
/(?:№|договор|contract|счет|сч[её]т|акт|накладн|плат[её]ж|invoice|payment|order|заявк|реестр|доп\.?\s*соглаш)/iu;
for (const token of row.analytics) {
const normalized = String(token ?? "").trim();
if (!normalized) {
continue;
}
if (/^(?:0|<пусто>|пустая ссылка)$/iu.test(normalized)) {
continue;
}
if (/^\d{4}-\d{2}-\d{2}/.test(normalized)) {
continue;
}
const tokenNormalized = normalizeEntityToken(normalized);
if (!tokenNormalized) {
continue;
}
if (tokenNormalized === counterpartyToken || (contractToken && tokenNormalized === contractToken)) {
continue;
}
if (sourceRefTokenPattern.test(normalized) || /(?:[a-zа-я].*\d|\d.*[a-zа-я])/iu.test(normalized)) {
refs.add(normalized);
}
if (refs.size >= 3) {
break;
}
}
return Array.from(refs);
}
function formatPayablesEvidenceSuffix(item: PayablesConfirmedBalanceAggregate): string {
const parts: string[] = [];
if (item.contracts.length > 0) {
parts.push(`договор: ${item.contracts.slice(0, 2).join("; ")}`);
}
if (item.documents.length > 0) {
const suffix = item.documents.length > 1 ? ` (+${item.documents.length - 1})` : "";
parts.push(`документ: ${item.documents[0]}${suffix}`);
}
if (item.sourceRefs.length > 0) {
parts.push(`source refs: ${item.sourceRefs.slice(0, 2).join("; ")}`);
}
return parts.length > 0 ? ` | ${parts.join(" | ")}` : "";
}
function deriveOperationalYearWindow(
yearDocs: YearAggPoint[],
yearOps: YearAggPoint[]
@@ -2319,9 +2412,8 @@ export function composeFactualReply(
const asOfDate = normalizeIsoDateOnly(options.asOfDate);
const periodFrom = normalizeIsoDateOnly(options.periodFrom);
const periodTo = normalizeIsoDateOnly(options.periodTo);
const scopeLine = asOfDate
? `- Дата среза: ${formatDateRu(asOfDate)}.`
: periodFrom || periodTo
const periodScopeLine =
!asOfDate && (periodFrom || periodTo)
? `- Период анализа: ${formatDateRu(periodFrom ?? "...")}..${formatDateRu(periodTo ?? "...")}.`
: null;
const carryoverLine =
@@ -2345,8 +2437,8 @@ export function composeFactualReply(
lines.push("");
lines.push("Блок 2. Что учтено");
lines.push(`- Дата среза: ${formatDateRu(payablesAsOfDate)}.`);
if (scopeLine) {
lines.push(scopeLine);
if (periodScopeLine) {
lines.push(periodScopeLine);
}
lines.push("- Контур: обязательства по счетам 60/76.");
if (carryoverLine) {
@@ -2371,7 +2463,7 @@ export function composeFactualReply(
lines.push(
...confirmedBalances.slice(0, 10).map(
(item, index) =>
`${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток: ${formatMoney(item.outstandingAmount)} | операций: ${item.operations}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}`
`${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток: ${formatMoney(item.outstandingAmount)} | операций: ${item.operations}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`
)
);
} else {
@@ -2516,10 +2608,10 @@ export function composeFactualReply(
`- ${liabilityCategoryLabel("tax_or_state")}: ${categoryCounts.tax_or_state}`,
`- ${liabilityCategoryLabel("other")}: ${categoryCounts.other}`,
"",
"Блок 5. Кому нужно заплатить в первую очередь (по сумме остатка):",
"Блок 5. Крупнейшие подтвержденные позиции к оплате (по сумме остатка):",
...confirmedBalances.slice(0, 10).map(
(item, index) =>
`${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток к оплате: ${formatMoney(item.outstandingAmount)} | операций в срезе: ${item.operations}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}`
`${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток к оплате: ${formatMoney(item.outstandingAmount)} | операций в срезе: ${item.operations}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`
)
];
return {