Этап 4 / Волна 11: объектный трейс по бизнес-якорям, доменное заземление, cleanup утечки дебага

This commit is contained in:
2026-03-28 13:12:09 +03:00
parent 553a5c407a
commit 8b84f5e989
34 changed files with 5517 additions and 173 deletions
@@ -0,0 +1,181 @@
export interface CompanyAnchorSet {
contract_numbers: string[];
document_numbers: string[];
dates: string[];
amounts: string[];
accounts: string[];
periods: string[];
document_types: string[];
all: string[];
}
const CONTRACT_PATTERN =
/(?:\u0434\u043e\u0433\u043e\u0432\u043e\u0440(?:\u0430|\u0443|ом|е)?\s*(?:№|#|n)?\s*([a-zа-я0-9./_-]+))/giu;
const DOCUMENT_NUMBER_PATTERN =
/(?:(?:\u0441\u0447(?:\u0435|\u0451)\u0442(?:-\u0444\u0430\u043a\u0442\u0443\u0440(?:а|ы))?|\u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446(?:ия|ии)|\u0430\u043a\u0442)\s*(?:№|#|n)\s*([a-zа-я0-9./_-]+))/giu;
const DATE_PATTERN =
/\b(?:\d{1,2}[./]\d{1,2}[./]\d{2,4}|\d{1,2}\s+(?:\u044f\u043d\u0432\u0430\u0440\u044f|\u0444\u0435\u0432\u0440\u0430\u043b\u044f|\u043c\u0430\u0440\u0442\u0430|\u0430\u043f\u0440\u0435\u043b\u044f|\u043c\u0430\u044f|\u0438\u044e\u043d\u044f|\u0438\u044e\u043b\u044f|\u0430\u0432\u0433\u0443\u0441\u0442\u0430|\u0441\u0435\u043d\u0442\u044f\u0431\u0440\u044f|\u043e\u043a\u0442\u044f\u0431\u0440\u044f|\u043d\u043e\u044f\u0431\u0440\u044f|\u0434\u0435\u043a\u0430\u0431\u0440\u044f))\b/giu;
const AMOUNT_PATTERN =
/\b(?:\d{1,3}(?:[ \u00A0]\d{3})+(?:[.,]\d{2})?|\d+[.,]\d{2})\b/gu;
const CONTEXTUAL_ACCOUNT_PATTERN =
/(?:\b(?:\u0441\u0447(?:\u0435|\u0451)\u0442(?:а|у|ом|ов)?|account|schet)\b\s*(?:№|#|:)?\s*)(\d{2}(?:\.\d{2})?)/giu;
const ACCOUNT_PAIR_PATTERN = /\b(\d{2}\.\d{2})\s*\/\s*(\d{2}\.\d{2})\b/gu;
const PERIOD_PATTERN =
/\b(?:20\d{2}(?:[-./](?:0?[1-9]|1[0-2]))?|(?:\u0438\u044e\u043b\u044c|\u0438\u044e\u043d\u044c|\u0430\u0432\u0433\u0443\u0441\u0442|\u0441\u0435\u043d\u0442\u044f\u0431\u0440\u044c|\u043e\u043a\u0442\u044f\u0431\u0440\u044c|\u043d\u043e\u044f\u0431\u0440\u044c|\u0434\u0435\u043a\u0430\u0431\u0440\u044c|\u044f\u043d\u0432\u0430\u0440\u044c|\u0444\u0435\u0432\u0440\u0430\u043b\u044c|\u043c\u0430\u0440\u0442|\u0430\u043f\u0440\u0435\u043b\u044c|\u043c\u0430\u0439)\s+20\d{2})\b/giu;
const DOCUMENT_TYPE_PATTERNS: Array<{ name: string; pattern: RegExp }> = [
{ name: "invoice", pattern: /\b(?:\u0441\u0447(?:\u0435|\u0451)\u0442-\u0444\u0430\u043a\u0442\u0443\u0440|invoice)\b/iu },
{ name: "realization", pattern: /\b(?:\u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446|realization)\b/iu },
{ name: "payment", pattern: /\b(?:\u043e\u043f\u043b\u0430\u0442|payment|\u043f\u043b\u0430\u0442\u0435\u0436)\b/iu },
{ name: "receipt", pattern: /\b(?:\u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d|receipt)\b/iu },
{ name: "close", pattern: /\b(?:\u0437\u0430\u043a\u0440\u044b\u0442\u0438|\u0440\u0435\u0433\u043b\u0430\u043c\u0435\u043d\u0442)\b/iu },
{ name: "rbp_writeoff", pattern: /\b(?:\u0440\u0431\u043f|\u0441\u043f\u0438\u0441\u0430\u043d\u0438\u0435)\b/iu },
{ name: "amortization", pattern: /\b(?:\u0430\u043c\u043e\u0440\u0442\u0438\u0437|amortization)\b/iu }
];
const KNOWN_ACCOUNT_PREFIXES = new Set<string>([
"01",
"02",
"07",
"08",
"10",
"13",
"19",
"20",
"21",
"23",
"25",
"26",
"41",
"43",
"44",
"45",
"50",
"51",
"52",
"55",
"57",
"58",
"60",
"62",
"66",
"67",
"68",
"69",
"70",
"71",
"73",
"76",
"90",
"91",
"94",
"96",
"97"
]);
function uniqueStrings(values: string[], limit = 48): string[] {
return Array.from(new Set(values.map((item) => String(item ?? "").trim()).filter(Boolean))).slice(0, limit);
}
function normalizeAnchorToken(value: string): string {
return String(value ?? "")
.replace(/\s+/g, " ")
.trim();
}
function collectMatches(text: string, pattern: RegExp, useCaptures = true): string[] {
const values: string[] = [];
pattern.lastIndex = 0;
for (const match of text.matchAll(pattern)) {
if (!match) continue;
if (useCaptures && match.length > 1) {
for (let i = 1; i < match.length; i += 1) {
const token = normalizeAnchorToken(match[i] ?? "");
if (token) values.push(token);
}
continue;
}
const token = normalizeAnchorToken(match[0] ?? "");
if (token) values.push(token);
}
return uniqueStrings(values);
}
function isKnownAccount(value: string): boolean {
const token = String(value ?? "").trim();
const match = token.match(/^(\d{2})/);
if (!match) {
return false;
}
return KNOWN_ACCOUNT_PREFIXES.has(match[1]);
}
function collectAccountAnchors(text: string): string[] {
const tokens = new Set<string>();
for (const token of collectMatches(text, CONTEXTUAL_ACCOUNT_PATTERN, true)) {
if (isKnownAccount(token)) {
tokens.add(token);
}
}
ACCOUNT_PAIR_PATTERN.lastIndex = 0;
for (const match of text.matchAll(ACCOUNT_PAIR_PATTERN)) {
const left = normalizeAnchorToken(match[1] ?? "");
const right = normalizeAnchorToken(match[2] ?? "");
if (left && isKnownAccount(left)) {
tokens.add(left);
}
if (right && isKnownAccount(right)) {
tokens.add(right);
}
}
return Array.from(tokens).slice(0, 24);
}
function collectDocumentTypeAnchors(text: string): string[] {
return uniqueStrings(
DOCUMENT_TYPE_PATTERNS.filter((entry) => entry.pattern.test(text)).map((entry) => entry.name),
12
);
}
function flattenAnchors(input: Omit<CompanyAnchorSet, "all">): string[] {
return uniqueStrings(
[
...input.contract_numbers,
...input.document_numbers,
...input.dates,
...input.amounts,
...input.accounts.map((item) => `account:${item}`),
...input.periods.map((item) => `period:${item}`),
...input.document_types.map((item) => `doc_type:${item}`)
],
64
);
}
export function resolveCompanyAnchors(input: string): CompanyAnchorSet {
const text = String(input ?? "");
const contractNumbers = collectMatches(text, CONTRACT_PATTERN, true).map((item) => `\u0434\u043e\u0433\u043e\u0432\u043e\u0440 № ${item}`);
const documentNumbers = collectMatches(text, DOCUMENT_NUMBER_PATTERN, true).map((item) => `\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442 № ${item}`);
const dates = collectMatches(text, DATE_PATTERN, false);
const amounts = collectMatches(text, AMOUNT_PATTERN, false);
const accounts = collectAccountAnchors(text);
const periods = collectMatches(text, PERIOD_PATTERN, false);
const documentTypes = collectDocumentTypeAnchors(text);
const resultBase: Omit<CompanyAnchorSet, "all"> = {
contract_numbers: uniqueStrings(contractNumbers, 12),
document_numbers: uniqueStrings(documentNumbers, 16),
dates: uniqueStrings(dates, 16),
amounts: uniqueStrings(amounts, 16),
accounts: uniqueStrings(accounts, 24),
periods: uniqueStrings(periods, 12),
document_types: documentTypes
};
return {
...resultBase,
all: flattenAnchors(resultBase)
};
}