Этап 4 corrective pack 2 по family isolation после текущих routing fixes

This commit is contained in:
2026-03-29 15:27:01 +03:00
parent 133b6dca3c
commit f74e7b697a
30 changed files with 4427 additions and 259 deletions
@@ -112,6 +112,25 @@ function collectDateLikeSpans(text: string): Array<{ start: number; end: number
return spans;
}
function collectContractLikeSpans(text: string): Array<{ start: number; end: number }> {
const spans: Array<{ start: number; end: number }> = [];
const patterns = [
/(?:\b(?:договор(?:а|у|ом|е)?|contract)\b[^\r\n]{0,24}(?:№|#|n|no\.?)\s*[a-zа-я0-9][a-zа-я0-9/_-]{1,})/giu,
/(?:№|#)\s*[a-zа-я0-9_-]{1,10}\/[a-zа-я0-9_-]{1,12}/giu,
/\b\d{2}\/\d{2}(?:-[a-zа-я]{1,10})?\b/giu
];
for (const pattern of patterns) {
let match: RegExpExecArray | null = null;
while ((match = pattern.exec(text)) !== null) {
spans.push({
start: match.index,
end: match.index + match[0].length
});
}
}
return spans;
}
function collectAmountLikeSpans(text: string): Array<{ start: number; end: number }> {
const spans: Array<{ start: number; end: number }> = [];
const patterns = [/\b\d{1,3}(?:[ \u00A0]\d{3})+(?:[.,]\d{2})?\b/g, /\b\d+[.,]\d{2}\b/g];
@@ -154,7 +173,12 @@ function hasAccountContextAround(text: string, start: number, end: number): bool
function detectAccounts(text: string): string[] {
const lower = String(text ?? "").toLowerCase();
const blockedSpans = [...collectDateLikeSpans(lower), ...collectAmountLikeSpans(lower), ...collectPercentLikeSpans(lower)];
const blockedSpans = [
...collectDateLikeSpans(lower),
...collectAmountLikeSpans(lower),
...collectPercentLikeSpans(lower),
...collectContractLikeSpans(lower)
];
const hasAccountingLexeme =
/(?:\bсчет(?:а|у|ом|ов)?\b|\bсч\.?\b|\baccount(?:s)?\b|\bschet(?:a|u|om|ov)?\b|оплат|расчет|расч[её]т|аванс|долг|settlement|payment|supplier|customer|ндс|vat|рбп|deferred|амортиз)/iu.test(
lower
@@ -217,37 +241,47 @@ function detectPeriod(text: string): string | null {
return null;
}
function detectExplicitDomainHint(text: string): string | null {
export function inferP0DomainFromMessage(text: string): string | null {
const messageCorpus = String(text ?? "").toLowerCase();
const accounts = detectAccounts(text);
const hasSettlementSignal =
accounts.some((item) => isSettlementAccount(item)) ||
/(?:60(?:\.\d{2})?|62(?:\.\d{2})?|оплат|расч[её]т|зач[её]т|аванс|долг|поставщ|покупат|settlement|payment|supplier|customer)/i.test(
const hasSettlementAccount = accounts.some((item) => isSettlementAccount(item));
const hasVatAccount = accounts.some((item) => isVatAccount(item));
const hasCloseAccount = accounts.some((item) => isCloseCostsAccount(item));
const hasFixedAssetAccount = accounts.some((item) => isFixedAssetAccount(item));
const hasSettlementLexical = /(?:60(?:\.\d{2})?|62(?:\.\d{2})?|оплат|расч[её]т|зач[её]т|аванс|долг|поставщ|покупат|settlement|payment|supplier|customer)/i.test(
messageCorpus
);
const hasVatLexical = /(?:ндс|сч[её]т[\s-]?фактур|книг[аи]|vat|invoice|book|register)/i.test(messageCorpus);
const hasCloseLexical =
/(?:закрыти|месяц|затрат|распредел|списан|period\s*close|month\s*close|allocation|residual|cost|рбп)/i.test(messageCorpus);
const hasExplicitFixedAssetLexical =
/(?:амортиз|основн(ые|ых|ым)?\s+средств|объект[а-яё]*\s+ос|fixed\s*asset|depreciat|сч[её]т(?:а|у|ом|е)?\s*(?:01|02|08)|account\s*0[128])/i.test(
messageCorpus
);
if (hasSettlementSignal) {
const hasBroadMonthCloseLexical =
/(?:после\s+закрытия|косвенн|период(?:а)?\s+закрыт|month\s*close|period\s*close|регламентн)/i.test(messageCorpus);
// Keep settlement lane stable when 60/62 lexical/account anchors are explicit
// and there is no explicit VAT intent.
if ((hasSettlementAccount || hasSettlementLexical) && !hasVatLexical && !hasVatAccount && !hasExplicitFixedAssetLexical) {
return "settlements_60_62";
}
const hasVatSignal =
accounts.some((item) => isVatAccount(item)) ||
/(?:ндс|сч[её]т[\s-]?фактур|книг[аи]|vat|invoice|book|register)/i.test(messageCorpus);
if (hasVatSignal) {
return "vat_document_register_book";
}
const hasCloseSignal =
accounts.some((item) => isCloseCostsAccount(item)) ||
/(?:закрыти|месяц|затрат|распредел|списан|period\s*close|month\s*close|allocation|residual|cost|рбп)/i.test(messageCorpus);
if (hasCloseSignal) {
if ((hasCloseAccount || hasCloseLexical || hasBroadMonthCloseLexical) && !hasVatLexical && !hasVatAccount && !hasExplicitFixedAssetLexical && !hasFixedAssetAccount) {
return "month_close_costs_20_44";
}
const hasFixedAssetSignal =
accounts.some((item) => isFixedAssetAccount(item)) ||
/(?:амортиз|основн(ые|ых|ым)?\s+средств|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|объект[а-яё]*\s+ос|fixed\s*asset|depreciat)/i.test(
messageCorpus
);
if (hasFixedAssetSignal) {
if (hasVatAccount || hasVatLexical) {
return "vat_document_register_book";
}
if (hasFixedAssetAccount || hasExplicitFixedAssetLexical) {
return "fixed_asset_amortization";
}
if (hasCloseAccount || hasCloseLexical) {
return "month_close_costs_20_44";
}
if (hasSettlementAccount || hasSettlementLexical) {
return "settlements_60_62";
}
return null;
}
@@ -283,7 +317,7 @@ function deriveScopeOrigin(input: {
}
const hasExplicitPeriod = Boolean(detectPeriod(input.userMessage));
const hasExplicitAccounts = detectAccounts(input.userMessage).length > 0;
const explicitDomain = detectExplicitDomainHint(input.userMessage);
const explicitDomain = inferP0DomainFromMessage(input.userMessage);
if (hasExplicitPeriod || hasExplicitAccounts || explicitDomain) {
return "explicit_from_message";
}
@@ -406,7 +440,7 @@ function inferFollowupActiveDomain(input: {
: messageCorpus;
const hasFixedAssetLexicalSignal =
/(?:амортиз|основн(ые|ых|ым)?\s+средств|(?:^|[^a-zа-яё])ос(?:$|[^a-zа-яё])|объект[а-яё]*\s+ос|fixed\s*asset|depreciat)/i.test(
/(?:амортиз|основн(ые|ых|ым)?\s+средств|объект[а-яё]*\s+ос|fixed\s*asset|depreciat|сч[её]т(?:а|у|ом|е)?\s*(?:01|02|08)|account\s*0[128])/i.test(
messageCorpus
);
const hasFixedAssetAccountSignal =
@@ -414,6 +448,17 @@ function inferFollowupActiveDomain(input: {
/(?:сч[её]т(?:а|у|ом|е)?\s*(?:01|02|08)|(?:01|02|08)(?:\.\d{2})?\s*\/\s*(?:01|02|08)(?:\.\d{2})?|\b0[128](?:\.\d{2})?\b)/i.test(
messageCorpus
);
const hasBroadMonthCloseSignal =
/(?:после\s+закрытия|косвенн|период(?:а)?\s+закрыт|регламентн|month\s*close|period\s*close)/i.test(messageCorpus);
if (
(input.focusAccounts.some((item) => isCloseCostsAccount(item)) ||
/(?:закрыти|месяц|затрат|распредел|списан|period\s*close|month\s*close|allocation|residual|cost)/i.test(messageCorpus) ||
hasBroadMonthCloseSignal) &&
!hasFixedAssetLexicalSignal &&
!hasFixedAssetAccountSignal
) {
return "month_close_costs_20_44";
}
if (hasFixedAssetLexicalSignal || hasFixedAssetAccountSignal) {
return "fixed_asset_amortization";
}