Закрепить ответы ассистента по бизнес-зависимостям

This commit is contained in:
2026-05-26 00:52:02 +03:00
parent 92473c894f
commit e3e17ac7db
27 changed files with 663 additions and 28 deletions
@@ -1869,6 +1869,10 @@ function resolveUnicodeAddressIntentBridge(text) {
/(?:хвост|задержк|проблем|систематическ)/iu.test(normalized)) {
return unicodeBridgeResolution("list_payables_counterparties", "high", "supplier_tail_risk_signal_detected");
}
if (/(?:поставщик|закуп|исходящ|плат[её]ж|supplier|vendor|procurement|outgoing|payout)/iu.test(normalized) &&
/(?:зависим|завязан|концентрац|ключев|основн|критич|dependency|concentration|key|critical)/iu.test(normalized)) {
return unicodeBridgeResolution("unknown", "high", "unicode_business_overview_supplier_quality_deferred_to_discovery");
}
if (hasDocumentCue && (hasCounterpartyCue || hasLooseCounterpartyByAnchor) && !hasContractCue) {
return unicodeBridgeResolution("list_documents_by_counterparty", "high", "unicode_documents_by_counterparty_bridge_signal_detected");
}
@@ -3202,8 +3202,14 @@ class AddressQueryService {
shadowRouteAudit
});
}
const rawUserMessageForReply = typeof options.rawUserMessage === "string" && options.rawUserMessage.trim().length > 0
? options.rawUserMessage
: undefined;
const composeOptionsFromFilters = (filterSet, options = {}) => ({
userMessage,
rawUserMessage: typeof options.rawUserMessage === "string" && options.rawUserMessage.trim().length > 0
? options.rawUserMessage
: rawUserMessageForReply,
itemHint: typeof filterSet.item === "string" ? filterSet.item : undefined,
counterpartyHint: typeof options.counterpartyHint === "string"
? options.counterpartyHint
@@ -406,7 +406,11 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
const focus = deps.detectValueRankingFocus(options.userMessage);
const limit = deps.detectRankingLimit(options.userMessage, 20);
const minOpsForAvgCheck = deps.detectMinOpsForAvgCheck(options.userMessage);
const normalizedQuestion = deps.normalizeQuestionText(options.userMessage);
const questionSignalText = [options.rawUserMessage, options.userMessage]
.map((value) => String(value ?? "").trim())
.filter(Boolean)
.join(" ");
const normalizedQuestion = deps.normalizeQuestionText(questionSignalText || options.userMessage);
const asksSingleBestCounterparty = focus === "top_by_total" &&
/(?:какой|кто|which|who|какой|кто)/iu.test(normalizedQuestion) &&
/(?:больше\s+всего|сам(?:ый|ая|ое|ые)|наибольш|прин[её]с|highest|most|больше\s+всего|сам(?:ый|ая|РѕРµ|ые)|наибол|РїСЂРёРЅ[её]СЃ)/iu.test(normalizedQuestion) &&
@@ -418,6 +422,7 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
const effectiveLimit = asksSingleBestCounterparty || semanticSingleBestCounterparty ? 1 : limit;
const cashflowBoundaryLine = buildCashflowBoundaryLine(isSupplier);
const cashflowNextStepLine = buildCashflowNextStepLine(isSupplier, normalizedQuestion);
const dependencyCue = /(?:зависим|завязан|концентрац|ключев|основн|критич|dependency|concentration|key|critical)/iu.test(normalizedQuestion);
const byCounterparty = new Map();
const byYear = new Map();
const deals = [];
@@ -653,13 +658,20 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
? `В выбранном срезе найден один поставщик: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг.`
: `В выбранном срезе найден один клиент: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг.`
: isSupplier
? `Крупнейший поставщик по подтвержденным выплатам ${rankingPeriodLabel}: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).`
: `Самый доходный клиент ${rankingPeriodLabel} по подтвержденным поступлениям: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).`;
? dependencyCue
? `Главный сигнал зависимости по исходящим платежам ${rankingPeriodLabel}: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).`
: `Крупнейший поставщик по подтвержденным выплатам ${rankingPeriodLabel}: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).`
: dependencyCue
? `Главный сигнал зависимости по входящим деньгам ${rankingPeriodLabel}: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).`
: `Самый доходный клиент ${rankingPeriodLabel} по подтвержденным поступлениям: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).`;
lines.unshift(directAnswerLine);
if (dependencyCue) {
lines.splice(1, 0, "Это сигнал концентрации по найденным денежным строкам 1С, а не подтверждение договорной зависимости, надежности контрагента или чистой прибыли.");
}
if (!isSupplier) {
lines.splice(1, 0, cashflowBoundaryLine);
lines.splice(dependencyCue ? 2 : 1, 0, cashflowBoundaryLine);
if (cashflowNextStepLine) {
lines.splice(2, 0, cashflowNextStepLine);
lines.splice(dependencyCue ? 3 : 2, 0, cashflowNextStepLine);
}
}
}
@@ -98,6 +98,7 @@ async function runAssistantAddressAttemptRuntime(input) {
};
const runAddressLaneAttempt = async (messageUsed, carryMeta, analysisDateHint, llmSemanticHints = null) => runAddressLaneAttemptRuntimeSafe((0, assistantAddressLaneAttemptInputBuilder_1.buildAssistantAddressLaneAttemptRuntimeInput)({
messageUsed,
rawUserMessage: input.userMessage,
carryMeta,
analysisDateHint,
llmSemanticHints,
@@ -4,6 +4,7 @@ exports.buildAssistantAddressLaneAttemptRuntimeInput = buildAssistantAddressLane
function buildAssistantAddressLaneAttemptRuntimeInput(input) {
return {
messageUsed: input.messageUsed,
rawUserMessage: input.rawUserMessage,
carryMeta: input.carryMeta,
analysisDateHint: input.analysisDateHint,
llmSemanticHints: input.llmSemanticHints,
@@ -9,7 +9,8 @@ function resolveAssistantAddressLaneAttemptFollowupContext(carryMeta) {
}
function buildAssistantAddressLaneAttemptQueryOptions(input) {
const base = {
analysisDateHint: input.analysisDateHint
analysisDateHint: input.analysisDateHint,
rawUserMessage: input.rawUserMessage ?? undefined
};
if (input.scopedFollowupContext) {
base.followupContext = input.scopedFollowupContext;
@@ -10,6 +10,7 @@ async function runAssistantAddressLaneAttemptRuntime(input) {
scopedFollowupContext,
llmSemanticHints: input.llmSemanticHints ?? null,
activeOrganization: input.activeOrganization,
knownOrganizations: input.knownOrganizations
knownOrganizations: input.knownOrganizations,
rawUserMessage: input.rawUserMessage ?? input.messageUsed
}));
}
@@ -69,6 +69,10 @@ function requestsCounterpartyLeaderSurface(turnMeaning, graph) {
}
return /(?:(?:кто|как\p{L}*|покаж\p{L}*|назов\p{L}*|раскро\p{L}*)[\s\S]{0,100}(?:главн\p{L}*|крупнейш\p{L}*|основн\p{L}*|ведущ\p{L}*|топ)[\s\S]{0,80}(?:клиент|заказчик|поставщик|получател)|(?:главн\p{L}*|крупнейш\p{L}*|основн\p{L}*|ведущ\p{L}*)[\s\S]{0,80}(?:клиент|заказчик|поставщик|получател)|(?:топ[-\s]*(?:клиент|заказчик|поставщик|получател))|(?:клиент|поставщик)[\s\S]{0,80}(?:главн|крупнейш|основн|ведущ|топ))/iu.test(text);
}
function requestsBusinessOverviewNextChecks(turnMeaning, graph) {
const text = normalizedTurnAndGraphText(turnMeaning, graph);
return /(?:что\s+(?:дальше\s+)?провер\w*|что\s+провер\w*\s+дальше|следующ\w*\s+шаг|в\s+первую\s+очередь\s+провер\w*|next\s+checks?|next\s+steps?)/iu.test(text);
}
function requestsCompactCashflowAnswer(turnMeaning, graph) {
const text = normalizeQuestionText([
turnMeaning?.raw_message,
@@ -429,6 +433,13 @@ function businessOverviewPeriodText(overview) {
const period = toNonEmptyString(overview.period_scope);
return period ? `за ${period}` : "за все доступное проверенное окно";
}
function businessOverviewPeriodPhrase(value) {
const normalized = toNonEmptyString(value);
if (!normalized) {
return "за все доступное проверенное окно";
}
return /^за\s+/iu.test(normalized) ? normalized : `за ${normalized}`;
}
function strongestIncomingYear(overview) {
const years = Array.isArray(overview.yearly_breakdown) ? overview.yearly_breakdown : [];
const sorted = years
@@ -857,6 +868,7 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
const compactCashflowRequested = directMoneyOnlyAnswer && requestsCompactCashflowAnswer(turnMeaning, graph);
const cashflowPolarityRequested = compactCashflowRequested && requestsCashflowPolarityAnswer(turnMeaning, graph);
const directAccountingProfitRequested = requestsDirectAccountingProfitAnswer(turnMeaning, graph);
const nextChecksRequested = requestsBusinessOverviewNextChecks(turnMeaning, graph);
const rawMessage = toNonEmptyString(turnMeaning?.raw_message) ?? toNonEmptyString(turnMeaning?.effective_message);
const earningsAmountRequested = /(?:\u0437\u0430\u0440\u0430\u0431\u043e\u0442|\u0434\u043e\u0445\u043e\u0434\p{L}*)/iu.test(normalizeQuestionText(rawMessage ?? ""));
const accountingFinancialResultForEarnings = toRecordObject(overview.accounting_financial_result);
@@ -1039,9 +1051,12 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
? `${vendorProcurementQuality.top_non_financial_supplier_share_pct}%`
: null;
const periodScope = toNonEmptyString(vendorProcurementQuality.period_scope) ?? period;
const totalText = totalOutgoing ? `; всего исходящих платежей в проверенном срезе ${totalOutgoing}` : "";
const periodPhrase = businessOverviewPeriodPhrase(periodScope);
const totalText = totalOutgoing
? `; всего исходящих платежей в проверенном срезе ${sentenceAmount(totalOutgoing)}`
: "";
if (status === "financial_institution_leads_outgoing_cash") {
lines.push(`Коротко: проверка концентрации закупок/исходящих платежей за ${periodScope} не подтверждает зависимость от обычного поставщика: крупнейший получатель исходящих денег ${topOutgoingName ?? "не распознан"}${topOutgoingShare ? ` держит около ${topOutgoingShare}` : ""}${topOutgoingAmount ? ` (${topOutgoingAmount})` : ""}, но по названию это банк/финансовая организация${totalText}.`);
lines.push(`Коротко: проверка концентрации закупок/исходящих платежей ${periodPhrase} не подтверждает зависимость от обычного поставщика: крупнейший получатель исходящих денег ${topOutgoingName ?? "не распознан"}${topOutgoingShare ? ` держит около ${topOutgoingShare}` : ""}${topOutgoingAmount ? ` (${topOutgoingAmount})` : ""}, но по названию это банк/финансовая организация${totalText}.`);
const financialHintText = financialFlowHintTextRuFromRecord(topOutgoingRecord);
if (financialHintText) {
lines.push(financialHintText);
@@ -1051,17 +1066,17 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
}
}
else if (status === "reviewed_procurement_concentration") {
lines.push(`Коротко: точный риск зависимости от одного поставщика не подтвержден полностью; проверка концентрации закупок/исходящих платежей за ${periodScope} нашла крупнейшего получателя исходящего потока: ${topOutgoingName ?? nonFinancialName ?? "получатель не распознан"}${topOutgoingShare ? ` держит около ${topOutgoingShare}` : nonFinancialShare ? ` держит около ${nonFinancialShare}` : ""}${topOutgoingAmount ? ` (${topOutgoingAmount})` : nonFinancialAmount ? ` (${nonFinancialAmount})` : ""}${totalText}.`);
lines.push(`Коротко: точный риск зависимости от одного поставщика не подтвержден полностью; проверка концентрации закупок/исходящих платежей ${periodPhrase} нашла крупнейшего получателя исходящего потока: ${topOutgoingName ?? nonFinancialName ?? "получатель не распознан"}${topOutgoingShare ? ` держит около ${topOutgoingShare}` : nonFinancialShare ? ` держит около ${nonFinancialShare}` : ""}${topOutgoingAmount ? ` (${topOutgoingAmount})` : nonFinancialAmount ? ` (${nonFinancialAmount})` : ""}${totalText}.`);
}
else {
lines.push(`Коротко: проверка концентрации закупок/исходящих платежей за ${periodScope} выполнена, но надежной небанковской концентрации поставщика по найденным исходящим платежам не хватает${totalText}.`);
lines.push(`Коротко: проверка концентрации закупок/исходящих платежей ${periodPhrase} выполнена, но надежной небанковской концентрации поставщика по найденным исходящим платежам не хватает${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}`);
lines.push(`Это не аудит поставщиков: срез не доказывает надежность поставщика, качество поставок, договорные условия, назначение каждого платежа и полную структуру всех расходов.${contractText}`);
return joinBusinessReplyLines(lines);
}
const supplierBasis = topSupplier
@@ -1102,6 +1117,19 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
lines.push("Проверить нужно отдельно: складской срез на дату, учетную политику резервов, списания и ликвидационную стоимость; косвенные признаки нельзя выдавать за доказанный факт резерва.");
return joinBusinessReplyLines(lines);
}
if (!separateSubject &&
!crossScopeExecutiveSummary &&
!counterpartyLeaderSurfaceRequested &&
!rankingNeed &&
nextChecksRequested) {
const subject = organizationScope ? `по компании ${organizationScope}` : "по компании";
lines.push(`В первую очередь ${subject} надо проверить чистую прибыль через закрытие 90/91/99, зависимость от топ-3 клиентов и поставщиков, долги/зависшие расчеты, складские остатки и НДС-позицию.`);
lines.push("Смысл порядка: сначала отделить бухгалтерский финрезультат от денежного нетто, затем проверить концентрацию потоков и только после этого углубляться в склад, долги и налоговую позицию.");
if (limitLine) {
lines.push(limitLine);
}
return joinBusinessReplyLines(lines);
}
if (!separateSubject &&
!crossScopeExecutiveSummary &&
!counterpartyLeaderSurfaceRequested &&
@@ -676,6 +676,15 @@ function hasDebtDueDateBoundaryFollowupSignal(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 hasSupplierDependencyFollowupSignal(text) {
if (!text) {
return false;
}
const normalized = compactLower(text);
const hasSupplierOrOutgoingCue = /(?:\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u0437\u0430\u043a\u0443\u043f|\u0438\u0441\u0445\u043e\u0434\u044f\u0449|\u043f\u043b\u0430\u0442[\u0435\u0451]\u0436|\u0437\u0430\u043f\u043b\u0430\u0442|supplier|vendor|procurement|outgoing|payout)/iu.test(normalized);
const hasDependencyCue = /(?:\u0437\u0430\u0432\u0438\u0441\u0438\u043c|\u0437\u0430\u0432\u044f\u0437\u0430\u043d|\u043a\u043e\u043d\u0446\u0435\u043d\u0442\u0440\u0430\u0446|\u043a\u043b\u044e\u0447\u0435\u0432|\u043e\u0441\u043d\u043e\u0432\u043d|\u043a\u0440\u0438\u0442\u0438\u0447|dependency|concentration|key|critical)/iu.test(normalized);
return hasSupplierOrOutgoingCue && hasDependencyCue;
}
function hasOrganizationLevelDebtDueDateOverviewSignal(text) {
if (!text) {
return false;
@@ -771,7 +780,7 @@ function hasBusinessOverviewContinuationSignal(text) {
}
const normalized = compactLower(text);
const hasEvidenceContinuationCue = /(?:по\s+этим\s+данн|на\s+базе\s+этих\s+данн|из\s+этого|по\s+итогам|что\s+подтвержден|чего\s+не\s+хватает|что\s+пока\s+нельзя|что\s+можно\s+сказать|какой\s+вывод|итогов|резюм|вывод)/iu.test(normalized);
const hasAnalystContinuationCue = /(?:можно\s+ли|если\s+нет|proxy|прокси|аудит|оцен|что\s+думаешь|нормальн\p{L}*\s+прибыл|прибыл|марж|рентаб|ликвидн|просроч|качество\s+долг|риск|налогов\p{L}*\s+вывод)/iu.test(normalized);
const hasAnalystContinuationCue = /(?:можно\s+ли|если\s+нет|proxy|прокси|аудит|оцен|что\s+думаешь|нормальн\p{L}*\s+прибыл|прибыл|марж|рентаб|ликвидн|просроч|качество\s+долг|риск|зависим|завязан|концентрац|налогов\p{L}*\s+вывод)/iu.test(normalized);
const hasTaxContinuationCue = /(?:ндс|vat)[\s\S]{0,120}(?:позици|период|основан|не\s+хватает|налогов\p{L}*\s+вывод)|(?:позици|налогов\p{L}*\s+вывод)[\s\S]{0,80}(?:ндс|vat)/iu.test(normalized);
const hasFinalSummaryCue = /(?:\u0447\u0442\u043e\s+\u043c\u044b\s+\u0437\u043d\u0430\u0435\u043c|\u0447\u0442\u043e\s+\u043f\u043e\u043d\u044f\u0442\u043d\u043e|\u0447\u0442\u043e\s+\u043f\u0440\u043e\u0432\u0435\u0440\w*\s+\u0434\u0430\u043b\u044c\u0448\u0435|\u0441\u043b\u0435\u0434\u0443\u044e\u0449\w*\s+\u0448\u0430\u0433|\u0438\u0442\u043e\u0433\w*\s+\u0432\u044b\u0432\u043e\u0434|\u043a\u0430\u043a\u043e\u0439\s+\u0432\u044b\u0432\u043e\u0434|\u0447\u0442\u043e\s+\u0441\s+\u044d\u0442\u0438\u043c\s+\u0434\u0435\u043b\u0430\u0442\u044c|what\s+do\s+we\s+know|what\s+is\s+missing|next\s+step|final\s+summary)/iu.test(normalized);
const hasMoneyBreakdownCue = /(?:\u0440\u0430\u0441\u043a\u0440\u043e\p{L}*\s+\u0434\u0435\u043d\p{L}*|\u0441\u043a\u043e\u043b\u044c\u043a\u043e\s+\u0432\u0441\u0435\u0433\u043e\s+\u043f\u043e\u043b\u0443\u0447|\u0441\u043a\u043e\u043b\u044c\u043a\u043e\s+(?:\u0432\u0441\u0435\u0433\u043e\s+)?\u0437\u0430\u043f\u043b\u0430\u0442|\u0447\u0438\u0441\u0442\p{L}*\s+\u0434\u0435\u043d\u0435\u0436\u043d\p{L}*\s+\u043f\u043e\u0442\u043e\u043a|\u0433\u043b\u0430\u0432\u043d\p{L}*\s+(?:\u043a\u043b\u0438\u0435\u043d\u0442|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a)|top\s+(?:customer|supplier)|cash\s+breakdown)/iu.test(normalized) &&
@@ -1339,7 +1348,9 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
const debtDueDateBusinessOverviewSignal = ((rawBusinessOverviewSignal || seededBusinessOverviewSignal) &&
hasOrganizationLevelDebtDueDateOverviewSignal(rawText)) ||
debtDueDateFollowupBusinessOverviewSignal;
const supplierQualityBusinessOverviewSignal = (rawBusinessOverviewSignal || seededBusinessOverviewSignal) && hasOrganizationLevelSupplierQualityOverviewSignal(rawText);
const supplierQualityBusinessOverviewSignal = ((rawBusinessOverviewSignal || seededBusinessOverviewSignal) &&
(hasOrganizationLevelSupplierQualityOverviewSignal(rawText) || hasSupplierDependencyFollowupSignal(rawText))) ||
(businessOverviewContinuationSignal && hasSupplierDependencyFollowupSignal(rawText));
const profitMarginFollowupBusinessOverviewSignal = businessOverviewContinuationSignal && hasProfitMarginBoundaryFollowupSignal(rawText);
const profitMarginBusinessOverviewSignal = ((rawBusinessOverviewSignal || seededBusinessOverviewSignal) &&
hasOrganizationLevelProfitMarginBoundaryOverviewSignal(rawText)) ||
@@ -2389,6 +2400,9 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
if (debtDueDateFollowupBusinessOverviewSignal) {
pushReason(reasonCodes, "mcp_discovery_business_overview_debt_due_date_followup_boundary");
}
if (supplierQualityBusinessOverviewSignal && hasSupplierDependencyFollowupSignal(rawText)) {
pushReason(reasonCodes, "mcp_discovery_business_overview_supplier_dependency_followup_boundary");
}
if (explicitVatSuppressesBusinessOverviewContinuation) {
pushReason(reasonCodes, "mcp_discovery_business_overview_continuation_suppressed_by_explicit_vat_question");
}
@@ -309,6 +309,17 @@ function detectBroadBusinessEvaluation(text) {
family: "broad_business_evaluation"
};
}
if (/(?:что\s+(?:дальше\s+)?провер\w*|что\s+провер\w*\s+дальше|следующ\w*\s+шаг|в\s+первую\s+очередь\s+провер\w*|next\s+checks?|next\s+steps?)/iu.test(normalized)) {
return {
family: "broad_business_evaluation"
};
}
if (/(?:поставщик|закуп|исходящ|плат[её]ж|supplier|vendor|procurement|outgoing|payout)/iu.test(normalized) &&
/(?:зависим|завязан|концентрац|ключев|основн|критич|dependency|concentration|key|critical)/iu.test(normalized)) {
return {
family: "broad_business_evaluation"
};
}
if (hasOrganizationLevelEarningsOverviewSignal(normalized)) {
return {
family: "broad_business_evaluation"