Open-World: расширить бизнес-обзор факт-семействами

This commit is contained in:
2026-05-04 08:20:46 +03:00
parent e65b7fdaed
commit edab736a6d
23 changed files with 4408 additions and 148 deletions
@@ -117,6 +117,9 @@ function isValueFlowPilot(pilot) {
pilot.pilot_scope === "counterparty_supplier_payout_query_movements_v1" ||
pilot.pilot_scope === "counterparty_bidirectional_value_flow_query_movements_v1");
}
function isBusinessOverviewPilot(pilot) {
return pilot.pilot_scope === "business_overview_route_template_v1";
}
function isDocumentPilot(pilot) {
return pilot.pilot_scope === "counterparty_document_evidence_query_documents_v1";
}
@@ -333,6 +336,42 @@ function headlineFor(mode, pilot) {
}
return "Инвентарный route-template уже выбран, но live-исполнение этого generic MCP контура еще не подключено; складской/товарный факт не подтвержден.";
}
if (isBusinessOverviewPilot(pilot) && pilot.derived_business_overview && mode === "confirmed_with_bounded_inference") {
const overview = pilot.derived_business_overview;
const families = [];
if (overview.incoming_customer_revenue.rows_with_amount > 0 ||
overview.outgoing_supplier_payout.rows_with_amount > 0) {
families.push("денежный поток");
}
if (overview.activity_period) {
families.push("активность");
}
if (overview.tax_position) {
families.push("НДС-позиция");
}
if (overview.debt_position) {
families.push("долговой срез на дату");
}
if (overview.debt_open_settlement_quality) {
families.push("качество открытых расчетов");
}
if (overview.inventory_position) {
families.push("складской срез на дату");
}
const unknownFamilies = ["прибыль/маржа"];
if (!overview.tax_position) {
unknownFamilies.push("НДС");
}
if (!overview.debt_position) {
unknownFamilies.push("долговой срез");
}
unknownFamilies.push(overview.debt_open_settlement_quality ? "due-date просрочка" : "качество открытых расчетов");
unknownFamilies.push(overview.inventory_position ? "полноценная складская ликвидность" : "склад");
return `По данным 1С собран ограниченный бизнес-обзор: ${families.join(", ")} подтверждены найденными строками; ${unknownFamilies.join(", ")} остаются отдельными непроверенными областями.`;
}
if (isBusinessOverviewPilot(pilot) && mode === "checked_sources_only") {
return "Бизнес-обзор был запущен, но подтвержденные денежные или activity-сигналы в найденных строках не получены.";
}
if (isEntityResolutionPilot(pilot) && mode === "needs_clarification") {
return "По каталогу 1С нашлось несколько похожих контрагентов, и без уточнения нельзя честно выбрать правильную сущность.";
}
@@ -469,6 +508,9 @@ function nextStepFor(mode, pilot) {
}
return "Следующий шаг - связать inventory route-template с exact inventory runtime и затем проверить live-прогоном.";
}
if (mode === "confirmed_with_bounded_inference" && isBusinessOverviewPilot(pilot)) {
return "Если нужен уже управленческий вывод, следующим шагом стоит отдельно проверить прибыль/маржу, долги, НДС и складскую ликвидность, а затем собрать полный бизнес-аудит.";
}
if (mode === "confirmed_with_bounded_inference" && pilot.derived_metadata_surface) {
const surface = pilot.derived_metadata_surface;
if (surface.ambiguity_detected && surface.ambiguity_entity_sets.length > 0) {
@@ -500,6 +542,14 @@ function buildMustNotClaim(pilot) {
claims.push("Do not claim full all-time turnover unless the checked period and coverage prove it.");
claims.push("Do not present a derived sum as a legal/accounting final total outside the checked 1C rows.");
}
if (isBusinessOverviewPilot(pilot)) {
claims.push("Do not present business overview cash-flow spread as profit or margin.");
claims.push("Do not claim debt quality, VAT position, inventory health, or company health unless those contours were separately checked.");
claims.push("Do not present a debt-position snapshot as debt aging, overdue debt, or credit-quality analysis.");
claims.push("Do not present open-settlement concentration as contractual due-date aging or confirmed overdue debt.");
claims.push("Do not present an inventory snapshot or purchase-date aging signal as turnover, obsolescence, liquidation value, or full inventory health.");
claims.push("Do not expose business_overview_route_template_v1 or MCP primitive names in the user answer.");
}
if (pilot.derived_ranked_value_flow) {
claims.push("Do not present a bounded ranking as a complete all-time ranking outside the checked period and organization.");
claims.push("Do not imply the top-ranked counterparty is globally final when probe-limit or scope boundaries still exist.");
@@ -760,6 +810,95 @@ function derivedBidirectionalValueFlowMonthlyLines(pilot) {
}
return flow.monthly_breakdown.map((bucket) => `Помесячно: ${monthLabelRu(bucket.month_bucket)} — получили ${bucket.incoming_total_amount_human_ru}, заплатили ${bucket.outgoing_total_amount_human_ru}, ${netLabelRu(bucket.net_direction)} ${bucket.net_amount_human_ru}`);
}
function businessOverviewNetDirectionRu(direction) {
if (direction === "net_incoming") {
return "операционный денежный поток в проверенном срезе больше входящий, чем исходящий";
}
if (direction === "net_outgoing") {
return "операционный денежный поток в проверенном срезе больше исходящий, чем входящий";
}
return "входящий и исходящий денежный поток в проверенном срезе примерно сбалансированы";
}
function derivedBusinessOverviewConfirmedLines(pilot) {
const overview = pilot.derived_business_overview;
if (!overview) {
return [];
}
const organization = overview.organization_scope ? ` по организации ${overview.organization_scope}` : "";
const period = overview.period_scope ? ` за ${overview.period_scope}` : " за все доступное проверенное окно";
const lines = [];
if (overview.incoming_customer_revenue.rows_with_amount > 0) {
lines.push(`Входящие поступления${organization}${period}: ${overview.incoming_customer_revenue.total_amount_human_ru} по ${overview.incoming_customer_revenue.rows_with_amount} строкам с суммой.`);
}
if (overview.outgoing_supplier_payout.rows_with_amount > 0) {
lines.push(`Исходящие платежи/списания${organization}${period}: ${overview.outgoing_supplier_payout.total_amount_human_ru} по ${overview.outgoing_supplier_payout.rows_with_amount} строкам с суммой.`);
}
const leader = overview.top_customers[0];
if (leader) {
lines.push(`Самый крупный подтвержденный клиент в проверенном срезе: ${leader.axis_value}${leader.total_amount_human_ru}.`);
}
if (overview.activity_period) {
lines.push(`Окно подтвержденной активности в 1С: ${overview.activity_period.first_activity_date}${overview.activity_period.latest_activity_date}; ориентировочно ${overview.activity_period.duration_human_ru}.`);
}
if (overview.tax_position) {
const taxDirection = overview.tax_position.net_vat_direction === "vat_to_pay"
? "к уплате"
: overview.tax_position.net_vat_direction === "vat_to_recover_or_offset"
? "к вычету/зачету"
: "сбалансирован";
lines.push(`НДС-позиция за ${overview.tax_position.period_scope}: книга продаж ${overview.tax_position.sales_vat_amount_human_ru}, книга покупок/вычеты ${overview.tax_position.purchase_vat_amount_human_ru}, нетто ${taxDirection} ${overview.tax_position.net_vat_amount_human_ru}.`);
}
if (overview.debt_position) {
const debtDirection = overview.debt_position.net_debt_position_direction === "net_receivable"
? "в пользу дебиторки"
: overview.debt_position.net_debt_position_direction === "net_payable"
? "в сторону кредиторки"
: "сбалансировано";
lines.push(`Долговой срез на ${overview.debt_position.as_of_date}: дебиторка ${overview.debt_position.receivables.total_amount_human_ru}, кредиторка ${overview.debt_position.payables.total_amount_human_ru}, нетто ${debtDirection} ${overview.debt_position.net_debt_position_amount_human_ru}.`);
}
if (overview.debt_open_settlement_quality) {
const quality = overview.debt_open_settlement_quality;
const topContract = quality.top_contracts[0];
const topContractText = topContract
? ` Крупнейший открытый договор: ${topContract.contract}${topContract.counterparty ? ` / ${topContract.counterparty}` : ""}${topContract.total_amount_human_ru}${topContract.share_of_gross_open_amount_pct === null ? "" : ` (${topContract.share_of_gross_open_amount_pct}%)`}.`
: "";
lines.push(`Качество открытых расчетов на ${quality.as_of_date}: брутто открытых договорных остатков ${quality.gross_open_amount_human_ru}, договоров ${quality.unique_contracts}, контрагентов ${quality.unique_counterparties}.${topContractText}`);
}
if (overview.inventory_position) {
const leader = overview.inventory_position.top_items[0];
const leaderText = leader
? ` Крупнейшая подтвержденная позиция: ${leader.item}${leader.total_amount_human_ru}.`
: "";
lines.push(`Складской срез на ${overview.inventory_position.as_of_date}: остаток ${overview.inventory_position.total_amount_human_ru} по ${overview.inventory_position.rows_with_amount} строкам с суммой и ${overview.inventory_position.rows_with_quantity} строкам с количеством.${leaderText}`);
if (overview.inventory_position.aging_signal?.oldest_purchase_date) {
const ageText = overview.inventory_position.aging_signal.max_age_days === null
? ""
: `, максимальный возраст сигнала ${overview.inventory_position.aging_signal.max_age_days} дн.`;
lines.push(`Возрастной сигнал склада: самая ранняя найденная дата закупки ${overview.inventory_position.aging_signal.oldest_purchase_date}${ageText}.`);
}
}
return lines;
}
function derivedBusinessOverviewInferenceLine(pilot) {
const overview = pilot.derived_business_overview;
if (!overview) {
return null;
}
if (overview.incoming_customer_revenue.rows_with_amount <= 0 &&
overview.outgoing_supplier_payout.rows_with_amount <= 0) {
return null;
}
return [
`Расчетное нетто по найденным строкам: ${overview.net_amount_human_ru}; ${businessOverviewNetDirectionRu(overview.net_direction)}.`,
"Это нормальный операционный сигнал, но не прибыль и не маржа: для управленческого вывода нужны отдельные расходы, себестоимость, долги, налоги и склад."
].join(" ");
}
function businessOverviewUnknownLines(pilot) {
if (!pilot.derived_business_overview) {
return userFacingUnknowns(pilot.evidence.unknown_facts);
}
return userFacingUnknowns(pilot.evidence.unknown_facts);
}
function buildAssistantMcpDiscoveryAnswerDraft(pilot) {
const mode = modeFor(pilot);
const reasonCodes = [...pilot.reason_codes, ...pilot.evidence.reason_codes];
@@ -770,7 +909,8 @@ function buildAssistantMcpDiscoveryAnswerDraft(pilot) {
if (pilot.evidence.inferred_facts.length > 0) {
pushReason(reasonCodes, "answer_contains_bounded_inference");
}
const derivedInferenceLine = derivedActivityInferenceLine(pilot) ??
const derivedInferenceLine = derivedBusinessOverviewInferenceLine(pilot) ??
derivedActivityInferenceLine(pilot) ??
derivedMetadataInferenceLine(pilot) ??
derivedRankedValueFlowInferenceLine(pilot) ??
derivedEntityResolutionInferenceLine(pilot);
@@ -785,23 +925,43 @@ function buildAssistantMcpDiscoveryAnswerDraft(pilot) {
const monthlyConfirmedLines = derivedBidirectionalValueFlowMonthlyLines(pilot).length > 0
? derivedBidirectionalValueFlowMonthlyLines(pilot)
: derivedValueFlowMonthlyLines(pilot);
const businessOverviewLines = derivedBusinessOverviewConfirmedLines(pilot);
if (monthlyConfirmedLines.length > 0) {
pushReason(reasonCodes, "answer_contains_monthly_breakdown");
}
const confirmedLines = pilot.derived_ranked_value_flow && derivedValueLine
? [derivedValueLine]
: derivedValueLine
? [...pilot.evidence.confirmed_facts, derivedValueLine, ...monthlyConfirmedLines]
: derivedEntityResolutionLine
? [...pilot.evidence.confirmed_facts, derivedEntityResolutionLine]
: derivedMetadataLine
? [derivedMetadataLine]
: pilot.evidence.confirmed_facts;
const unknownLines = pilot.derived_metadata_surface
? pilot.derived_metadata_surface.available_fields.length > 0
? userFacingUnknowns(pilot.evidence.unknown_facts)
: ["Детальный список полей этих объектов этим шагом не получен."]
: rankedValueFlowUnknownLines(pilot);
if (businessOverviewLines.length > 0) {
pushReason(reasonCodes, "answer_contains_business_overview");
}
if (pilot.derived_business_overview?.tax_position) {
pushReason(reasonCodes, "answer_contains_business_overview_tax_position");
}
if (pilot.derived_business_overview?.debt_position) {
pushReason(reasonCodes, "answer_contains_business_overview_debt_position");
}
if (pilot.derived_business_overview?.debt_open_settlement_quality) {
pushReason(reasonCodes, "answer_contains_business_overview_open_settlement_quality");
}
if (pilot.derived_business_overview?.inventory_position) {
pushReason(reasonCodes, "answer_contains_business_overview_inventory_position");
}
const confirmedLines = businessOverviewLines.length > 0
? businessOverviewLines
: pilot.derived_ranked_value_flow && derivedValueLine
? [derivedValueLine]
: derivedValueLine
? [...pilot.evidence.confirmed_facts, derivedValueLine, ...monthlyConfirmedLines]
: derivedEntityResolutionLine
? [...pilot.evidence.confirmed_facts, derivedEntityResolutionLine]
: derivedMetadataLine
? [derivedMetadataLine]
: pilot.evidence.confirmed_facts;
const unknownLines = pilot.derived_business_overview
? businessOverviewUnknownLines(pilot)
: pilot.derived_metadata_surface
? pilot.derived_metadata_surface.available_fields.length > 0
? userFacingUnknowns(pilot.evidence.unknown_facts)
: ["Детальный список полей этих объектов этим шагом не получен."]
: rankedValueFlowUnknownLines(pilot);
return {
schema_version: exports.ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryAnswerAdapter",
File diff suppressed because it is too large Load Diff
@@ -511,6 +511,9 @@ function metadataAmbiguityCollapsesToMovementLane(values) {
function hasLifecycleSignal(text) {
return /(?:сколько\s+лет|как\s+давно|давно\s+ли|возраст|перв(?:ая|ый)\s+актив|когда\s+начал|когда\s+появ|lifecycle|activity\s+duration|business\s+age|how\s+long)/iu.test(text);
}
function hasBusinessOverviewSignal(text) {
return /(?:\u0431\u0438\u0437\u043d\u0435\u0441[-\s]?\u043e\u0431\u0437\u043e\u0440|\u0431\u0438\u0437\u043d\u0435\u0441[-\s]?\u0430\u0443\u0434\u0438\u0442|\u043f\u043e\u043b\u043d\w*\s+\u0430\u043d\u0430\u043b\u0438\u0437\s+(?:\u043a\u043e\u043c\u043f\u0430\u043d|\u0431\u0438\u0437\u043d\u0435\u0441|\u0434\u0435\u044f\u0442\u0435\u043b)|\u0441\u0432\u043e\u0434\u043d\w*\s+\u0430\u043d\u0430\u043b\u0438\u0437\s+(?:\u043a\u043e\u043c\u043f\u0430\u043d|\u0431\u0438\u0437\u043d\u0435\u0441|\u0434\u0435\u044f\u0442\u0435\u043b)|\u043a\u0430\u043a\s+\u0442\u044b\s+\u043e\u0446\u0435\u043d(?:\u0438\u0448\u044c|\u0438)\s+\u0434\u0435\u044f\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442|\u043a\u043e\u043c\u043f\u0430\u043d(?:\u0438\u0438|\u0438\u044e|\u0438\u044f)\s+\u0432\s+\u0446\u0435\u043b\u043e\u043c|company\s+(?:analysis|overview)|business\s+(?:overview|audit)|llm[-\s]?audit|бизнес[-\s]?обзор|бизнес[-\s]?аудит)/iu.test(text);
}
function hasValueFlowSignal(text) {
return /(?:оборот|выручк|оплат|плат[её]ж|заплат|перечисл|списан|расход|исходящ|входящ|получ(?:ил|ено|ен)|поступил|поступлен|денежн[а-яёa-z0-9_-]*\s+поток|(?<!\p{L})заработ(?:ал|али|ало|аем|ает|ать|ано|ок)(?!\p{L})|supplier|value[-\s]?flow|turnover|revenue|payment|payout|outflow|cash\s+flow|\bearn(?:ed|ing|ings)?\b)/iu.test(text);
}
@@ -543,13 +546,15 @@ function extractOrganizationScopeFromRawText(value) {
if (!match?.[1]) {
return null;
}
return toNonEmptyString(match[1]);
return toNonEmptyString(match[1]
.replace(/\s+(?:\u043f\u043e\s+\u0434\u0430\u043d\u043d\u044b\u043c\s+1\u0441|\u0432\s+\u0446\u0435\u043b\u043e\u043c|\u0437\u0430\s+(?:\d{4}-\d{2}-\d{2}|\d{4}-\d{2}|(?:19|20)\d{2})(?:\s+\u0433(?:\u043e\u0434|\.)?)?|\u043d\u0430\s+(?:\d{4}-\d{2}-\d{2}|\d{4}-\d{2}|(?:19|20)\d{2})|\u0437\u0430\s+\u0432\u0441[\u0435\u0451]\s+(?:\u0434\u043e\u0441\u0442\u0443\u043f\p{L}+\s+)?\u0432\u0440\u0435\u043c\u044f|\u0437\u0430\s+\u0432\u0435\u0441\u044c\s+(?:\u0434\u043e\u0441\u0442\u0443\u043f\p{L}+\s+)?\u043f\u0435\u0440\u0438\u043e\u0434|\u0437\u0430\s+\u0432\u0441\u044e\s+\u0438\u0441\u0442\u043e\u0440\u0438(?:\u044e|\u0438)|\u0434\u0430\u0439\s+\u0431\u0438\u0437\u043d\u0435\u0441-?\u043e\u0431\u0437\u043e\u0440|\u0431\u0438\u0437\u043d\u0435\u0441-?\u043e\u0431\u0437\u043e\u0440|\u043d\u043e\s+\u043d\u0435).*$/iu, "")
.trim());
}
function hasMonthlyAggregationSignal(text) {
return /(?:\u043f\u043e\s+\u043c\u0435\u0441\u044f\u0446\u0430\u043c|\u043f\u043e\u043c\u0435\u0441\u044f\u0447\u043d\u043e|\u0435\u0436\u0435\u043c\u0435\u0441\u044f\u0447\u043d\u043e|month\s+by\s+month|by\s+month|monthly)/iu.test(text);
}
function hasAllTimeScopeHint(text) {
return /(?:\u0437\u0430\s+\u0432\u0441[\u0435\u0451]\s+\u0432\u0440\u0435\u043c\u044f|\u0437\u0430\s+\u0432\u0435\u0441\u044c\s+\u043f\u0435\u0440\u0438\u043e\u0434|\u0437\u0430\s+\u0432\u0441\u044e\s+\u0438\u0441\u0442\u043e\u0440\u0438(?:\u044e|\u0438)|\u0437\u0430\s+\u043b\u044e\u0431\u043e\u0439\s+\u043f\u0435\u0440\u0438\u043e\u0434|for\s+all\s+time|all\s+time|entire\s+period|full\s+history|any\s+period)/iu.test(text);
return /(?:\u0437\u0430\s+\u0432\u0441[\u0435\u0451]\s+(?:\u0434\u043e\u0441\u0442\u0443\u043f\p{L}+\s+)?\u0432\u0440\u0435\u043c\u044f|\u0437\u0430\s+\u0432\u0435\u0441\u044c\s+(?:\u0434\u043e\u0441\u0442\u0443\u043f\p{L}+\s+)?\u043f\u0435\u0440\u0438\u043e\u0434|\u0437\u0430\s+\u0432\u0441\u044e\s+\u0438\u0441\u0442\u043e\u0440\u0438(?:\u044e|\u0438)|\u0437\u0430\s+\u043b\u044e\u0431\u043e\u0439\s+\u043f\u0435\u0440\u0438\u043e\u0434|for\s+all\s+time|all\s+time|entire\s+period|full\s+history|any\s+period)/iu.test(text);
}
function hasMetadataSignal(text) {
if (/(?:\u043c\u0435\u0442\u0430\u0434\u0430\u043d|schema|catalog|metadata\s+surface|\u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440[\u0430\u044b]\s+1\u0441|\u0441\u0445\u0435\u043c[\u0430\u044b]\s+1\u0441)/iu.test(text)) {
@@ -742,6 +747,15 @@ function metadataScopeHintFromRawText(text) {
function hasExplicitDateScopeLiteral(text) {
return /(?:\b(?:19|20)\d{2}\b|\b\d{4}-\d{2}-\d{2}\b|\b\d{4}-\d{2}\b)/iu.test(text);
}
function stripNegatedTaxDateScopeClauses(text) {
const dateScopeLiteral = String.raw `\b(?:\d{4}-\d{2}-\d{2}|\d{4}-\d{2}|(?:19|20)\d{2})\b`;
const taxToken = String.raw `(?:\u043d\u0434\u0441|vat)`;
const scopeToken = String.raw `(?:\u0437\u0430|\u043d\u0430|\u043f\u043e|for|in)`;
const negatedVerb = String.raw `(?:\u0442\u0430\u0449\u0438(?:\u0442\u044c)?|\u0431\u0435\u0440\u0438|\u0431\u0440\u0430\u0442\u044c|\u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439(?:\u0442\u0435)?|\u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c|\u0441\u0447\u0438\u0442\u0430\u0439(?:\u0442\u0435)?|\u0441\u0447\u0438\u0442\u0430\u0442\u044c|\u043f\u043e\u0434\u0442\u044f\u0433\u0438\u0432\u0430\u0439(?:\u0442\u0435)?|\u043f\u043e\u0434\u0442\u044f\u0433\u0438\u0432\u0430\u0442\u044c|\u043f\u0435\u0440\u0435\u043d\u043e\u0441\u0438|\u043f\u0435\u0440\u0435\u043d\u043e\u0441\u0438\u0442\u044c|pull|reuse|use|carry)`;
const direct = new RegExp(String.raw `(?:^|[\s,;:])(?:\u043d\u0435\s+${negatedVerb}\s+${taxToken}\s+${scopeToken}\s+${dateScopeLiteral}|\u0431\u0435\u0437\s+${taxToken}\s+${scopeToken}\s+${dateScopeLiteral}|do\s+not\s+(?:${negatedVerb}\s+)?${taxToken}\s+${scopeToken}\s+${dateScopeLiteral})`, "giu");
const reversed = new RegExp(String.raw `${taxToken}\s+${scopeToken}\s+${dateScopeLiteral}\s+(?:\u043d\u0435\s+${negatedVerb}|do\s+not\s+(?:${negatedVerb})?)`, "giu");
return text.replace(direct, " ").replace(reversed, " ");
}
function collectDateScopeFromRawText(text) {
const isoDate = text.match(/\b(\d{4}-\d{2}-\d{2})\b/u);
if (isoDate?.[1]) {
@@ -768,6 +782,9 @@ function isImplicitCurrentDateScope(value) {
}
function semanticNeedFor(input) {
const combined = compactLower(`${input.domain ?? ""} ${input.action ?? ""} ${input.unsupported ?? ""}`);
if (/(?:broad_business_evaluation|broad_evaluation|business_summary|business_overview|company analysis|business audit)/iu.test(combined)) {
return "business overview evidence with bounded analyst interpretation";
}
if (input.metadataSignal || /(?:metadata|schema|catalog|inspect_(?:catalog|documents|registers|fields))/iu.test(combined)) {
return "1C metadata evidence";
}
@@ -841,9 +858,11 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
const rawEntitySourceText = repairedUserText ?? rawUserText ?? repairedEffectiveText ?? rawEffectiveText ?? rawSignalSourceText;
const rawText = compactLower(rawSignalSourceText);
const rawReferentialDocumentExclusionSignal = hasReferentialDocumentExclusionFollowupSignal(repairedUserText ?? rawUserText ?? "");
const rawLifecycleSignal = hasLifecycleSignal(rawText);
const rawBidirectionalValueFlowSignal = !rawLifecycleSignal && hasBidirectionalValueFlowSignal(rawText);
const rawValueFlowSignal = !rawLifecycleSignal &&
const rawBusinessOverviewSignal = hasBusinessOverviewSignal(rawText);
const rawLifecycleSignal = !rawBusinessOverviewSignal && hasLifecycleSignal(rawText);
const rawBidirectionalValueFlowSignal = !rawBusinessOverviewSignal && !rawLifecycleSignal && hasBidirectionalValueFlowSignal(rawText);
const rawValueFlowSignal = !rawBusinessOverviewSignal &&
!rawLifecycleSignal &&
(hasValueFlowSignal(rawText) || hasValueRankingSignal(rawText) || rawBidirectionalValueFlowSignal);
const rawMetadataSignal = !rawLifecycleSignal &&
!rawValueFlowSignal &&
@@ -854,9 +873,13 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
const rawValueFlowAggregateQuestionSignal = rawValueFlowSignal && hasValueFlowAggregateQuestionSignal(rawText);
const monthlyAggregationSignal = hasMonthlyAggregationSignal(rawText);
const rawAllTimeScopeSignal = hasAllTimeScopeHint(rawText);
const explicitDateScopeLiteralDetected = hasExplicitDateScopeLiteral(rawText);
const dateScopeSignalText = stripNegatedTaxDateScopeClauses(rawText);
const negatedTaxDateScopeOnlySignal = dateScopeSignalText !== rawText &&
hasExplicitDateScopeLiteral(rawText) &&
!hasExplicitDateScopeLiteral(dateScopeSignalText);
const explicitDateScopeLiteralDetected = hasExplicitDateScopeLiteral(dateScopeSignalText);
const relativeCurrentDateHintDetected = hasRelativeCurrentDateHint(rawText);
const rawDateScope = collectDateScopeFromRawText(rawText);
const rawDateScope = collectDateScopeFromRawText(dateScopeSignalText);
const rawMetadataScopeHint = rawMetadataSignal ? metadataScopeHintFromRawText(rawText) : null;
const rawEntityCandidate = rawEntityResolutionSignal ? rawEntityResolutionCandidate(rawEntitySourceText) : null;
const entityResolutionClarificationCandidate = followupSeed.pilotScope === "entity_resolution_search_v1" &&
@@ -870,6 +893,11 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
const rawAggregationAxis = toNonEmptyString(assistantTurnMeaning?.asked_aggregation_axis);
const unsupported = toNonEmptyString(assistantTurnMeaning?.unsupported_but_understood_family);
const broadBusinessEvaluationUnsupported = unsupported === "broad_business_evaluation";
const businessOverviewSignal = rawBusinessOverviewSignal ||
broadBusinessEvaluationUnsupported ||
rawDomain === "business_summary" ||
rawDomain === "business_overview" ||
rawAction === "broad_evaluation";
const explicitIntentCandidate = toNonEmptyString(assistantTurnMeaning?.explicit_intent_candidate);
const currentTurnDocumentLaneSignal = rawAction === "list_documents";
const currentTurnMovementLaneSignal = rawAction === "list_movements";
@@ -1182,10 +1210,12 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
: followupDiscoverySeedApplicable || effectiveMetadataFollowupSeedApplicable
? followupSeed.unsupported
: null;
const lifecycleSignal = rawLifecycleSignal || seededDomain === "counterparty_lifecycle";
const bidirectionalValueFlowSignal = !lifecycleSignal &&
const lifecycleSignal = !businessOverviewSignal && (rawLifecycleSignal || seededDomain === "counterparty_lifecycle");
const bidirectionalValueFlowSignal = !businessOverviewSignal &&
!lifecycleSignal &&
(rawBidirectionalValueFlowSignal || seededAction === "net_value_flow");
const valueFlowSignal = !lifecycleSignal &&
const valueFlowSignal = !businessOverviewSignal &&
!lifecycleSignal &&
!metadataGroundedMovementLaneApplicable &&
(rawValueFlowSignal || seededDomain === "counterparty_value");
const payoutSignal = valueFlowSignal &&
@@ -1194,9 +1224,9 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
const semanticDataNeed = metadataAmbiguityLaneClarificationApplicable
? "metadata lane clarification"
: semanticNeedFor({
domain: rawDomain ?? seededDomain,
action: rawAction ?? seededAction,
unsupported: broadBusinessEvaluationUnsupported ? seededUnsupported : unsupported ?? seededUnsupported,
domain: businessOverviewSignal ? "business_overview" : rawDomain ?? seededDomain,
action: businessOverviewSignal ? "broad_evaluation" : rawAction ?? seededAction,
unsupported: businessOverviewSignal ? "broad_business_evaluation" : unsupported ?? seededUnsupported,
lifecycleSignal,
valueFlowSignal,
metadataSignal: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable,
@@ -1315,23 +1345,37 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
}
}
const clarificationLoopStillNeedsPeriod = Boolean(followupSeed.loopStatus === "awaiting_clarification" && followupSeed.loopPendingAxes.includes("period"));
const businessOverviewRawWithoutDateScope = Boolean(businessOverviewSignal &&
!rawAllTimeScopeSignal &&
!explicitDateScopeLiteralDetected &&
!rawDateScope &&
!relativeCurrentDateHintDetected);
const predecomposeDateScopeCountsAsCurrentTurnPeriod = Boolean(predecomposeDateScope &&
!isImplicitCurrentDateScope(predecomposeDateScope) &&
!businessOverviewRawWithoutDateScope);
const currentTurnCarriesExplicitPeriod = Boolean(explicitDateScopeLiteralDetected ||
rawDateScope ||
relativeCurrentDateHintDetected ||
(predecomposeDateScope && !isImplicitCurrentDateScope(predecomposeDateScope)));
predecomposeDateScopeCountsAsCurrentTurnPeriod);
const suppressImplicitCurrentDateScope = Boolean(!currentTurnCarriesExplicitPeriod &&
(clarificationLoopStillNeedsPeriod ||
businessOverviewSignal ||
openScopeValueFlowWithoutResolvedCounterparty ||
(valueFlowOrganizationStaysScope && (Boolean(followupSeed.rankingNeed) || bidirectionalValueFlowSignal))));
const suppressNegatedTaxOnlyDateScope = Boolean(businessOverviewSignal && negatedTaxDateScopeOnlySignal);
const normalizedPredecomposeDateScope = (rawEntitySearchOverridesStaleScope && !currentTurnCarriesExplicitPeriod) ||
suppressNegatedTaxOnlyDateScope ||
businessOverviewRawWithoutDateScope ||
(suppressImplicitCurrentDateScope && isImplicitCurrentDateScope(predecomposeDateScope))
? null
: predecomposeDateScope;
const normalizedAssistantTurnMeaningDateScope = rawEntitySearchOverridesStaleScope ||
suppressNegatedTaxOnlyDateScope ||
(suppressImplicitCurrentDateScope && isImplicitCurrentDateScope(assistantTurnMeaningDateScope))
? null
: assistantTurnMeaningDateScope;
const normalizedFollowupDateScope = rawEntitySearchOverridesStaleScope ||
suppressNegatedTaxOnlyDateScope ||
(suppressImplicitCurrentDateScope && isImplicitCurrentDateScope(followupSeed.dateScope))
? null
: followupSeed.dateScope;
@@ -1348,41 +1392,45 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
normalizedFollowupDateScope);
const clarificationLoopSeedApplied = Boolean(followupSeed.loopStatus === "awaiting_clarification" && followupSeed.loopSelectedChainId);
const turnMeaning = {
asked_domain_family: lifecycleSignal
? "counterparty_lifecycle"
: valueFlowSignal
? "counterparty_value"
: metadataGroundedMovementLaneApplicable
? "movements"
: metadataGroundedDocumentLaneApplicable
? "documents"
: entityResolutionSignal
? "entity_resolution"
: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
? "metadata"
: rawDomain ?? seededDomain,
asked_action_family: lifecycleSignal
? "activity_duration"
: valueFlowSignal
? bidirectionalValueFlowSignal
? "net_value_flow"
: payoutSignal
? "payout"
: rawAction ?? seededAction ?? "turnover"
: metadataGroundedMovementLaneApplicable
? "list_movements"
: metadataGroundedDocumentLaneApplicable
? "list_documents"
: entityResolutionSignal
? "search_business_entity"
: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
? metadataActionFromRawText(rawText) ?? seededAction
: rawAction ?? seededAction,
asked_domain_family: businessOverviewSignal
? "business_overview"
: lifecycleSignal
? "counterparty_lifecycle"
: valueFlowSignal
? "counterparty_value"
: metadataGroundedMovementLaneApplicable
? "movements"
: metadataGroundedDocumentLaneApplicable
? "documents"
: entityResolutionSignal
? "entity_resolution"
: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
? "metadata"
: rawDomain ?? seededDomain,
asked_action_family: businessOverviewSignal
? "broad_evaluation"
: lifecycleSignal
? "activity_duration"
: valueFlowSignal
? bidirectionalValueFlowSignal
? "net_value_flow"
: payoutSignal
? "payout"
: rawAction ?? seededAction ?? "turnover"
: metadataGroundedMovementLaneApplicable
? "list_movements"
: metadataGroundedDocumentLaneApplicable
? "list_documents"
: entityResolutionSignal
? "search_business_entity"
: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
? metadataActionFromRawText(rawText) ?? seededAction
: rawAction ?? seededAction,
asked_aggregation_axis: monthlyAggregationSignal ? "month" : rawAggregationAxis,
seeded_ranking_need: valueFlowSignal && followupSeed.rankingNeed && !rawEntitySearchOverridesStaleScope
? followupSeed.rankingNeed
: undefined,
explicit_entity_candidates: entityCandidates,
explicit_entity_candidates: businessOverviewSignal ? [] : entityCandidates,
metadata_ambiguity_entity_sets: metadataAmbiguityLaneClarificationApplicable && followupSeed.metadataAmbiguityEntitySets.length > 0
? followupSeed.metadataAmbiguityEntitySets
: undefined,
@@ -1390,29 +1438,32 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
explicit_organization_scope: explicitOrganizationScope,
explicit_date_scope: explicitDateScope,
subject_resolution_optional: metadataScopedLaneWithoutSubject || undefined,
unsupported_but_understood_family: unsupported ??
(lifecycleSignal
? "counterparty_lifecycle"
: valueFlowSignal
? bidirectionalValueFlowSignal
? "counterparty_bidirectional_value_flow_or_netting"
: payoutSignal
? "counterparty_payouts_or_outflow"
: seededUnsupported ?? "counterparty_value_or_turnover"
: metadataGroundedMovementLaneApplicable
? "movement_evidence"
: metadataGroundedDocumentLaneApplicable
? "document_evidence"
: metadataAmbiguityLaneClarificationApplicable
? "metadata_lane_choice_clarification"
: entityResolutionSignal
? "entity_resolution"
: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
? "1c_metadata_surface"
: followupDiscoverySeedApplicable
? seededUnsupported
: null),
unsupported_but_understood_family: businessOverviewSignal
? "broad_business_evaluation"
: unsupported ??
(lifecycleSignal
? "counterparty_lifecycle"
: valueFlowSignal
? bidirectionalValueFlowSignal
? "counterparty_bidirectional_value_flow_or_netting"
: payoutSignal
? "counterparty_payouts_or_outflow"
: seededUnsupported ?? "counterparty_value_or_turnover"
: metadataGroundedMovementLaneApplicable
? "movement_evidence"
: metadataGroundedDocumentLaneApplicable
? "document_evidence"
: metadataAmbiguityLaneClarificationApplicable
? "metadata_lane_choice_clarification"
: entityResolutionSignal
? "entity_resolution"
: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
? "1c_metadata_surface"
: followupDiscoverySeedApplicable
? seededUnsupported
: null),
stale_replay_forbidden: Boolean(assistantTurnMeaning?.stale_replay_forbidden ||
businessOverviewSignal ||
unsupported ||
lifecycleSignal ||
valueFlowSignal ||
@@ -1467,7 +1518,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
semanticDataNeed &&
(entityCandidates.length > 0 || explicitOrganizationScope || openScopeValueFlowWithoutResolvedCounterparty));
const runDiscovery = shouldRunDiscovery({
unsupported: broadBusinessEvaluationUnsupported ? seededUnsupported : unsupported ?? seededUnsupported,
unsupported: businessOverviewSignal ? "broad_business_evaluation" : unsupported ?? seededUnsupported,
lifecycleSignal,
valueFlowSignal,
metadataSignal: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable,
@@ -1481,7 +1532,8 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
metadataGroundedMovementLaneApplicable ||
metadataGroundedDocumentLaneApplicable ||
groundedValueFlowFollowupApplicable,
forceDiscoveryOverExplicitIntent: Boolean(entityResolutionClarificationCandidate) ||
forceDiscoveryOverExplicitIntent: businessOverviewSignal ||
Boolean(entityResolutionClarificationCandidate) ||
organizationClarificationFollowupApplicable ||
periodClarificationFollowupApplicable ||
metadataAmbiguityLaneClarificationApplicable ||
@@ -1554,6 +1606,9 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
if (rawAllTimeScopeSignal) {
pushReason(reasonCodes, "mcp_discovery_all_time_scope_signal_detected");
}
if (suppressNegatedTaxOnlyDateScope) {
pushReason(reasonCodes, "mcp_discovery_negated_tax_period_scope_suppressed");
}
if (followupDiscoverySeedApplicable) {
pushReason(reasonCodes, "mcp_discovery_seeded_from_followup_context");
}
@@ -1632,8 +1687,8 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
if (unsupported) {
pushReason(reasonCodes, "mcp_discovery_unsupported_but_understood_turn");
}
if (broadBusinessEvaluationUnsupported) {
pushReason(reasonCodes, "mcp_discovery_broad_business_evaluation_kept_in_living_chat");
if (businessOverviewSignal) {
pushReason(reasonCodes, "mcp_discovery_broad_business_evaluation_route_candidate");
}
if (!(valueFlowOrganizationStaysScope && normalizedPredecomposeCounterparty === explicitOrganizationScope) &&
normalizedPredecomposeCounterparty) {
@@ -117,6 +117,11 @@ function detectBroadBusinessEvaluation(text) {
if (!normalized) {
return null;
}
if (/(?:\u0431\u0438\u0437\u043d\u0435\u0441[-\s]?\u043e\u0431\u0437\u043e\u0440|\u0431\u0438\u0437\u043d\u0435\u0441[-\s]?\u0430\u0443\u0434\u0438\u0442|\u043f\u043e\u043b\u043d\w*\s+\u0430\u043d\u0430\u043b\u0438\u0437\s+(?:\u043a\u043e\u043c\u043f\u0430\u043d|\u0431\u0438\u0437\u043d\u0435\u0441|\u0434\u0435\u044f\u0442\u0435\u043b)|\u0441\u0432\u043e\u0434\u043d\w*\s+\u0430\u043d\u0430\u043b\u0438\u0437\s+(?:\u043a\u043e\u043c\u043f\u0430\u043d|\u0431\u0438\u0437\u043d\u0435\u0441|\u0434\u0435\u044f\u0442\u0435\u043b)|\u043a\u0430\u043a\s+\u0442\u044b\s+\u043e\u0446\u0435\u043d(?:\u0438\u0448\u044c|\u0438)\s+\u0434\u0435\u044f\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442|\u043a\u043e\u043c\u043f\u0430\u043d(?:\u0438\u0438|\u0438\u044e|\u0438\u044f)\s+\u0432\s+\u0446\u0435\u043b\u043e\u043c|company\s+(?:analysis|overview)|business\s+(?:overview|audit)|llm[-\s]?audit|бизнес[-\s]?обзор|бизнес[-\s]?аудит)/iu.test(normalized)) {
return {
family: "broad_business_evaluation"
};
}
if (/(?:как\s+ты\s+оценишь\s+деятельност[ьи]\s+компан|оценк[аи]?\s+деятельност[ьи]\s+компан|оцени\s+(?:компан|бизнес|деятельност)|(?:полный|сводный|нормальн\w*|взросл\w*)\s+анализ\s+(?:компан|бизнес|деятельност)|проанализируй\s+(?:компан|бизнес|деятельност)|(?:что\s+думаешь|какое\s+мнение)\s+(?:о|по)\s+(?:компан|бизнес)|(?:llm[-\s]?)?аудит\s+(?:компан|бизнес)|что\s+у\s+нас\s+вообще\s+происход|где\s+главн(?:ые|ый)\s+риски|как\s+у\s+нас\s+дела\s+по\s+компан)/iu.test(normalized)) {
return {
family: "broad_business_evaluation"
@@ -214,7 +219,7 @@ function createAssistantTurnMeaningPolicy(deps = {}) {
asked_domain_family: askedDomainFamily,
asked_action_family: askedActionFamily,
explicit_intent_candidate: explicitIntentCandidate,
explicit_entity_candidates: buildEntityCandidates(counterpartyTurnover),
explicit_entity_candidates: broadBusinessEvaluation?.family ? [] : buildEntityCandidates(counterpartyTurnover),
meaning_confidence: broadBusinessEvaluation?.family
? "medium"
: supportedIntent?.confidence ?? (counterpartyTurnover?.family ? "medium" : "low"),
@@ -158,6 +158,10 @@ function isValueFlowPilot(pilot: AssistantMcpDiscoveryPilotExecutionContract): b
);
}
function isBusinessOverviewPilot(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
return pilot.pilot_scope === "business_overview_route_template_v1";
}
function isDocumentPilot(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
return pilot.pilot_scope === "counterparty_document_evidence_query_documents_v1";
}
@@ -431,6 +435,44 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
}
return "Инвентарный route-template уже выбран, но live-исполнение этого generic MCP контура еще не подключено; складской/товарный факт не подтвержден.";
}
if (isBusinessOverviewPilot(pilot) && pilot.derived_business_overview && mode === "confirmed_with_bounded_inference") {
const overview = pilot.derived_business_overview;
const families: string[] = [];
if (
overview.incoming_customer_revenue.rows_with_amount > 0 ||
overview.outgoing_supplier_payout.rows_with_amount > 0
) {
families.push("денежный поток");
}
if (overview.activity_period) {
families.push("активность");
}
if (overview.tax_position) {
families.push("НДС-позиция");
}
if (overview.debt_position) {
families.push("долговой срез на дату");
}
if (overview.debt_open_settlement_quality) {
families.push("качество открытых расчетов");
}
if (overview.inventory_position) {
families.push("складской срез на дату");
}
const unknownFamilies = ["прибыль/маржа"];
if (!overview.tax_position) {
unknownFamilies.push("НДС");
}
if (!overview.debt_position) {
unknownFamilies.push("долговой срез");
}
unknownFamilies.push(overview.debt_open_settlement_quality ? "due-date просрочка" : "качество открытых расчетов");
unknownFamilies.push(overview.inventory_position ? "полноценная складская ликвидность" : "склад");
return `По данным 1С собран ограниченный бизнес-обзор: ${families.join(", ")} подтверждены найденными строками; ${unknownFamilies.join(", ")} остаются отдельными непроверенными областями.`;
}
if (isBusinessOverviewPilot(pilot) && mode === "checked_sources_only") {
return "Бизнес-обзор был запущен, но подтвержденные денежные или activity-сигналы в найденных строках не получены.";
}
if (isEntityResolutionPilot(pilot) && mode === "needs_clarification") {
return "По каталогу 1С нашлось несколько похожих контрагентов, и без уточнения нельзя честно выбрать правильную сущность.";
}
@@ -574,6 +616,9 @@ function nextStepFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
}
return "Следующий шаг - связать inventory route-template с exact inventory runtime и затем проверить live-прогоном.";
}
if (mode === "confirmed_with_bounded_inference" && isBusinessOverviewPilot(pilot)) {
return "Если нужен уже управленческий вывод, следующим шагом стоит отдельно проверить прибыль/маржу, долги, НДС и складскую ликвидность, а затем собрать полный бизнес-аудит.";
}
if (mode === "confirmed_with_bounded_inference" && pilot.derived_metadata_surface) {
const surface = pilot.derived_metadata_surface;
if (surface.ambiguity_detected && surface.ambiguity_entity_sets.length > 0) {
@@ -606,6 +651,14 @@ function buildMustNotClaim(pilot: AssistantMcpDiscoveryPilotExecutionContract):
claims.push("Do not claim full all-time turnover unless the checked period and coverage prove it.");
claims.push("Do not present a derived sum as a legal/accounting final total outside the checked 1C rows.");
}
if (isBusinessOverviewPilot(pilot)) {
claims.push("Do not present business overview cash-flow spread as profit or margin.");
claims.push("Do not claim debt quality, VAT position, inventory health, or company health unless those contours were separately checked.");
claims.push("Do not present a debt-position snapshot as debt aging, overdue debt, or credit-quality analysis.");
claims.push("Do not present open-settlement concentration as contractual due-date aging or confirmed overdue debt.");
claims.push("Do not present an inventory snapshot or purchase-date aging signal as turnover, obsolescence, liquidation value, or full inventory health.");
claims.push("Do not expose business_overview_route_template_v1 or MCP primitive names in the user answer.");
}
if (pilot.derived_ranked_value_flow) {
claims.push("Do not present a bounded ranking as a complete all-time ranking outside the checked period and organization.");
claims.push("Do not imply the top-ranked counterparty is globally final when probe-limit or scope boundaries still exist.");
@@ -899,6 +952,119 @@ function derivedBidirectionalValueFlowMonthlyLines(pilot: AssistantMcpDiscoveryP
);
}
function businessOverviewNetDirectionRu(direction: "net_incoming" | "net_outgoing" | "balanced"): string {
if (direction === "net_incoming") {
return "операционный денежный поток в проверенном срезе больше входящий, чем исходящий";
}
if (direction === "net_outgoing") {
return "операционный денежный поток в проверенном срезе больше исходящий, чем входящий";
}
return "входящий и исходящий денежный поток в проверенном срезе примерно сбалансированы";
}
function derivedBusinessOverviewConfirmedLines(pilot: AssistantMcpDiscoveryPilotExecutionContract): string[] {
const overview = pilot.derived_business_overview;
if (!overview) {
return [];
}
const organization = overview.organization_scope ? ` по организации ${overview.organization_scope}` : "";
const period = overview.period_scope ? ` за ${overview.period_scope}` : " за все доступное проверенное окно";
const lines: string[] = [];
if (overview.incoming_customer_revenue.rows_with_amount > 0) {
lines.push(
`Входящие поступления${organization}${period}: ${overview.incoming_customer_revenue.total_amount_human_ru} по ${overview.incoming_customer_revenue.rows_with_amount} строкам с суммой.`
);
}
if (overview.outgoing_supplier_payout.rows_with_amount > 0) {
lines.push(
`Исходящие платежи/списания${organization}${period}: ${overview.outgoing_supplier_payout.total_amount_human_ru} по ${overview.outgoing_supplier_payout.rows_with_amount} строкам с суммой.`
);
}
const leader = overview.top_customers[0];
if (leader) {
lines.push(`Самый крупный подтвержденный клиент в проверенном срезе: ${leader.axis_value}${leader.total_amount_human_ru}.`);
}
if (overview.activity_period) {
lines.push(
`Окно подтвержденной активности в 1С: ${overview.activity_period.first_activity_date}${overview.activity_period.latest_activity_date}; ориентировочно ${overview.activity_period.duration_human_ru}.`
);
}
if (overview.tax_position) {
const taxDirection =
overview.tax_position.net_vat_direction === "vat_to_pay"
? "к уплате"
: overview.tax_position.net_vat_direction === "vat_to_recover_or_offset"
? "к вычету/зачету"
: "сбалансирован";
lines.push(
`НДС-позиция за ${overview.tax_position.period_scope}: книга продаж ${overview.tax_position.sales_vat_amount_human_ru}, книга покупок/вычеты ${overview.tax_position.purchase_vat_amount_human_ru}, нетто ${taxDirection} ${overview.tax_position.net_vat_amount_human_ru}.`
);
}
if (overview.debt_position) {
const debtDirection =
overview.debt_position.net_debt_position_direction === "net_receivable"
? "в пользу дебиторки"
: overview.debt_position.net_debt_position_direction === "net_payable"
? "в сторону кредиторки"
: "сбалансировано";
lines.push(
`Долговой срез на ${overview.debt_position.as_of_date}: дебиторка ${overview.debt_position.receivables.total_amount_human_ru}, кредиторка ${overview.debt_position.payables.total_amount_human_ru}, нетто ${debtDirection} ${overview.debt_position.net_debt_position_amount_human_ru}.`
);
}
if (overview.debt_open_settlement_quality) {
const quality = overview.debt_open_settlement_quality;
const topContract = quality.top_contracts[0];
const topContractText = topContract
? ` Крупнейший открытый договор: ${topContract.contract}${topContract.counterparty ? ` / ${topContract.counterparty}` : ""}${topContract.total_amount_human_ru}${topContract.share_of_gross_open_amount_pct === null ? "" : ` (${topContract.share_of_gross_open_amount_pct}%)`}.`
: "";
lines.push(
`Качество открытых расчетов на ${quality.as_of_date}: брутто открытых договорных остатков ${quality.gross_open_amount_human_ru}, договоров ${quality.unique_contracts}, контрагентов ${quality.unique_counterparties}.${topContractText}`
);
}
if (overview.inventory_position) {
const leader = overview.inventory_position.top_items[0];
const leaderText = leader
? ` Крупнейшая подтвержденная позиция: ${leader.item}${leader.total_amount_human_ru}.`
: "";
lines.push(
`Складской срез на ${overview.inventory_position.as_of_date}: остаток ${overview.inventory_position.total_amount_human_ru} по ${overview.inventory_position.rows_with_amount} строкам с суммой и ${overview.inventory_position.rows_with_quantity} строкам с количеством.${leaderText}`
);
if (overview.inventory_position.aging_signal?.oldest_purchase_date) {
const ageText = overview.inventory_position.aging_signal.max_age_days === null
? ""
: `, максимальный возраст сигнала ${overview.inventory_position.aging_signal.max_age_days} дн.`;
lines.push(
`Возрастной сигнал склада: самая ранняя найденная дата закупки ${overview.inventory_position.aging_signal.oldest_purchase_date}${ageText}.`
);
}
}
return lines;
}
function derivedBusinessOverviewInferenceLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
const overview = pilot.derived_business_overview;
if (!overview) {
return null;
}
if (
overview.incoming_customer_revenue.rows_with_amount <= 0 &&
overview.outgoing_supplier_payout.rows_with_amount <= 0
) {
return null;
}
return [
`Расчетное нетто по найденным строкам: ${overview.net_amount_human_ru}; ${businessOverviewNetDirectionRu(overview.net_direction)}.`,
"Это нормальный операционный сигнал, но не прибыль и не маржа: для управленческого вывода нужны отдельные расходы, себестоимость, долги, налоги и склад."
].join(" ");
}
function businessOverviewUnknownLines(pilot: AssistantMcpDiscoveryPilotExecutionContract): string[] {
if (!pilot.derived_business_overview) {
return userFacingUnknowns(pilot.evidence.unknown_facts);
}
return userFacingUnknowns(pilot.evidence.unknown_facts);
}
export function buildAssistantMcpDiscoveryAnswerDraft(
pilot: AssistantMcpDiscoveryPilotExecutionContract
): AssistantMcpDiscoveryAnswerDraftContract {
@@ -912,6 +1078,7 @@ export function buildAssistantMcpDiscoveryAnswerDraft(
pushReason(reasonCodes, "answer_contains_bounded_inference");
}
const derivedInferenceLine =
derivedBusinessOverviewInferenceLine(pilot) ??
derivedActivityInferenceLine(pilot) ??
derivedMetadataInferenceLine(pilot) ??
derivedRankedValueFlowInferenceLine(pilot) ??
@@ -929,10 +1096,28 @@ export function buildAssistantMcpDiscoveryAnswerDraft(
derivedBidirectionalValueFlowMonthlyLines(pilot).length > 0
? derivedBidirectionalValueFlowMonthlyLines(pilot)
: derivedValueFlowMonthlyLines(pilot);
const businessOverviewLines = derivedBusinessOverviewConfirmedLines(pilot);
if (monthlyConfirmedLines.length > 0) {
pushReason(reasonCodes, "answer_contains_monthly_breakdown");
}
const confirmedLines = pilot.derived_ranked_value_flow && derivedValueLine
if (businessOverviewLines.length > 0) {
pushReason(reasonCodes, "answer_contains_business_overview");
}
if (pilot.derived_business_overview?.tax_position) {
pushReason(reasonCodes, "answer_contains_business_overview_tax_position");
}
if (pilot.derived_business_overview?.debt_position) {
pushReason(reasonCodes, "answer_contains_business_overview_debt_position");
}
if (pilot.derived_business_overview?.debt_open_settlement_quality) {
pushReason(reasonCodes, "answer_contains_business_overview_open_settlement_quality");
}
if (pilot.derived_business_overview?.inventory_position) {
pushReason(reasonCodes, "answer_contains_business_overview_inventory_position");
}
const confirmedLines = businessOverviewLines.length > 0
? businessOverviewLines
: pilot.derived_ranked_value_flow && derivedValueLine
? [derivedValueLine]
: derivedValueLine
? [...pilot.evidence.confirmed_facts, derivedValueLine, ...monthlyConfirmedLines]
@@ -941,7 +1126,9 @@ export function buildAssistantMcpDiscoveryAnswerDraft(
: derivedMetadataLine
? [derivedMetadataLine]
: pilot.evidence.confirmed_facts;
const unknownLines = pilot.derived_metadata_surface
const unknownLines = pilot.derived_business_overview
? businessOverviewUnknownLines(pilot)
: pilot.derived_metadata_surface
? pilot.derived_metadata_surface.available_fields.length > 0
? userFacingUnknowns(pilot.evidence.unknown_facts)
: ["Детальный список полей этих объектов этим шагом не получен."]
File diff suppressed because it is too large Load Diff
@@ -682,6 +682,12 @@ function hasLifecycleSignal(text: string): boolean {
);
}
function hasBusinessOverviewSignal(text: string): boolean {
return /(?:\u0431\u0438\u0437\u043d\u0435\u0441[-\s]?\u043e\u0431\u0437\u043e\u0440|\u0431\u0438\u0437\u043d\u0435\u0441[-\s]?\u0430\u0443\u0434\u0438\u0442|\u043f\u043e\u043b\u043d\w*\s+\u0430\u043d\u0430\u043b\u0438\u0437\s+(?:\u043a\u043e\u043c\u043f\u0430\u043d|\u0431\u0438\u0437\u043d\u0435\u0441|\u0434\u0435\u044f\u0442\u0435\u043b)|\u0441\u0432\u043e\u0434\u043d\w*\s+\u0430\u043d\u0430\u043b\u0438\u0437\s+(?:\u043a\u043e\u043c\u043f\u0430\u043d|\u0431\u0438\u0437\u043d\u0435\u0441|\u0434\u0435\u044f\u0442\u0435\u043b)|\u043a\u0430\u043a\s+\u0442\u044b\s+\u043e\u0446\u0435\u043d(?:\u0438\u0448\u044c|\u0438)\s+\u0434\u0435\u044f\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442|\u043a\u043e\u043c\u043f\u0430\u043d(?:\u0438\u0438|\u0438\u044e|\u0438\u044f)\s+\u0432\s+\u0446\u0435\u043b\u043e\u043c|company\s+(?:analysis|overview)|business\s+(?:overview|audit)|llm[-\s]?audit|бизнес[-\s]?РѕР±Р·РѕСЂ|бизнес[-\s]?аудиС)/iu.test(
text
);
}
function hasValueFlowSignal(text: string): boolean {
return /(?:оборот|выручк|оплат|плат[её]ж|заплат|перечисл|списан|расход|исходящ|входящ|получ(?:ил|ено|ен)|поступил|поступлен|денежн[а-яёa-z0-9_-]*\s+поток|(?<!\p{L})заработ(?:ал|али|ало|аем|ает|ать|ано|ок)(?!\p{L})|supplier|value[-\s]?flow|turnover|revenue|payment|payout|outflow|cash\s+flow|\bearn(?:ed|ing|ings)?\b)/iu.test(
text
@@ -737,7 +743,14 @@ function extractOrganizationScopeFromRawText(value: unknown): string | null {
if (!match?.[1]) {
return null;
}
return toNonEmptyString(match[1]);
return toNonEmptyString(
match[1]
.replace(
/\s+(?:\u043f\u043e\s+\u0434\u0430\u043d\u043d\u044b\u043c\s+1\u0441|\u0432\s+\u0446\u0435\u043b\u043e\u043c|\u0437\u0430\s+(?:\d{4}-\d{2}-\d{2}|\d{4}-\d{2}|(?:19|20)\d{2})(?:\s+\u0433(?:\u043e\u0434|\.)?)?|\u043d\u0430\s+(?:\d{4}-\d{2}-\d{2}|\d{4}-\d{2}|(?:19|20)\d{2})|\u0437\u0430\s+\u0432\u0441[\u0435\u0451]\s+(?:\u0434\u043e\u0441\u0442\u0443\u043f\p{L}+\s+)?\u0432\u0440\u0435\u043c\u044f|\u0437\u0430\s+\u0432\u0435\u0441\u044c\s+(?:\u0434\u043e\u0441\u0442\u0443\u043f\p{L}+\s+)?\u043f\u0435\u0440\u0438\u043e\u0434|\u0437\u0430\s+\u0432\u0441\u044e\s+\u0438\u0441\u0442\u043e\u0440\u0438(?:\u044e|\u0438)|\u0434\u0430\u0439\s+\u0431\u0438\u0437\u043d\u0435\u0441-?\u043e\u0431\u0437\u043e\u0440|\u0431\u0438\u0437\u043d\u0435\u0441-?\u043e\u0431\u0437\u043e\u0440|\u043d\u043e\s+\u043d\u0435).*$/iu,
""
)
.trim()
);
}
function hasMonthlyAggregationSignal(text: string): boolean {
@@ -747,7 +760,7 @@ function hasMonthlyAggregationSignal(text: string): boolean {
}
function hasAllTimeScopeHint(text: string): boolean {
return /(?:\u0437\u0430\s+\u0432\u0441[\u0435\u0451]\s+\u0432\u0440\u0435\u043c\u044f|\u0437\u0430\s+\u0432\u0435\u0441\u044c\s+\u043f\u0435\u0440\u0438\u043e\u0434|\u0437\u0430\s+\u0432\u0441\u044e\s+\u0438\u0441\u0442\u043e\u0440\u0438(?:\u044e|\u0438)|\u0437\u0430\s+\u043b\u044e\u0431\u043e\u0439\s+\u043f\u0435\u0440\u0438\u043e\u0434|for\s+all\s+time|all\s+time|entire\s+period|full\s+history|any\s+period)/iu.test(
return /(?:\u0437\u0430\s+\u0432\u0441[\u0435\u0451]\s+(?:\u0434\u043e\u0441\u0442\u0443\u043f\p{L}+\s+)?\u0432\u0440\u0435\u043c\u044f|\u0437\u0430\s+\u0432\u0435\u0441\u044c\s+(?:\u0434\u043e\u0441\u0442\u0443\u043f\p{L}+\s+)?\u043f\u0435\u0440\u0438\u043e\u0434|\u0437\u0430\s+\u0432\u0441\u044e\s+\u0438\u0441\u0442\u043e\u0440\u0438(?:\u044e|\u0438)|\u0437\u0430\s+\u043b\u044e\u0431\u043e\u0439\s+\u043f\u0435\u0440\u0438\u043e\u0434|for\s+all\s+time|all\s+time|entire\s+period|full\s+history|any\s+period)/iu.test(
text
);
}
@@ -1000,6 +1013,22 @@ function hasExplicitDateScopeLiteral(text: string): boolean {
return /(?:\b(?:19|20)\d{2}\b|\b\d{4}-\d{2}-\d{2}\b|\b\d{4}-\d{2}\b)/iu.test(text);
}
function stripNegatedTaxDateScopeClauses(text: string): string {
const dateScopeLiteral = String.raw`\b(?:\d{4}-\d{2}-\d{2}|\d{4}-\d{2}|(?:19|20)\d{2})\b`;
const taxToken = String.raw`(?:\u043d\u0434\u0441|vat)`;
const scopeToken = String.raw`(?:\u0437\u0430|\u043d\u0430|\u043f\u043e|for|in)`;
const negatedVerb = String.raw`(?:\u0442\u0430\u0449\u0438(?:\u0442\u044c)?|\u0431\u0435\u0440\u0438|\u0431\u0440\u0430\u0442\u044c|\u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439(?:\u0442\u0435)?|\u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c|\u0441\u0447\u0438\u0442\u0430\u0439(?:\u0442\u0435)?|\u0441\u0447\u0438\u0442\u0430\u0442\u044c|\u043f\u043e\u0434\u0442\u044f\u0433\u0438\u0432\u0430\u0439(?:\u0442\u0435)?|\u043f\u043e\u0434\u0442\u044f\u0433\u0438\u0432\u0430\u0442\u044c|\u043f\u0435\u0440\u0435\u043d\u043e\u0441\u0438|\u043f\u0435\u0440\u0435\u043d\u043e\u0441\u0438\u0442\u044c|pull|reuse|use|carry)`;
const direct = new RegExp(
String.raw`(?:^|[\s,;:])(?:\u043d\u0435\s+${negatedVerb}\s+${taxToken}\s+${scopeToken}\s+${dateScopeLiteral}|\u0431\u0435\u0437\s+${taxToken}\s+${scopeToken}\s+${dateScopeLiteral}|do\s+not\s+(?:${negatedVerb}\s+)?${taxToken}\s+${scopeToken}\s+${dateScopeLiteral})`,
"giu"
);
const reversed = new RegExp(
String.raw`${taxToken}\s+${scopeToken}\s+${dateScopeLiteral}\s+(?:\u043d\u0435\s+${negatedVerb}|do\s+not\s+(?:${negatedVerb})?)`,
"giu"
);
return text.replace(direct, " ").replace(reversed, " ");
}
function collectDateScopeFromRawText(text: string): string | null {
const isoDate = text.match(/\b(\d{4}-\d{2}-\d{2})\b/u);
if (isoDate?.[1]) {
@@ -1040,6 +1069,9 @@ function semanticNeedFor(input: {
entityResolutionSignal: boolean;
}): string | null {
const combined = compactLower(`${input.domain ?? ""} ${input.action ?? ""} ${input.unsupported ?? ""}`);
if (/(?:broad_business_evaluation|broad_evaluation|business_summary|business_overview|company analysis|business audit)/iu.test(combined)) {
return "business overview evidence with bounded analyst interpretation";
}
if (input.metadataSignal || /(?:metadata|schema|catalog|inspect_(?:catalog|documents|registers|fields))/iu.test(combined)) {
return "1C metadata evidence";
}
@@ -1133,9 +1165,12 @@ export function buildAssistantMcpDiscoveryTurnInput(
const rawReferentialDocumentExclusionSignal = hasReferentialDocumentExclusionFollowupSignal(
repairedUserText ?? rawUserText ?? ""
);
const rawLifecycleSignal = hasLifecycleSignal(rawText);
const rawBidirectionalValueFlowSignal = !rawLifecycleSignal && hasBidirectionalValueFlowSignal(rawText);
const rawBusinessOverviewSignal = hasBusinessOverviewSignal(rawText);
const rawLifecycleSignal = !rawBusinessOverviewSignal && hasLifecycleSignal(rawText);
const rawBidirectionalValueFlowSignal =
!rawBusinessOverviewSignal && !rawLifecycleSignal && hasBidirectionalValueFlowSignal(rawText);
const rawValueFlowSignal =
!rawBusinessOverviewSignal &&
!rawLifecycleSignal &&
(hasValueFlowSignal(rawText) || hasValueRankingSignal(rawText) || rawBidirectionalValueFlowSignal);
const rawMetadataSignal =
@@ -1150,9 +1185,14 @@ export function buildAssistantMcpDiscoveryTurnInput(
rawValueFlowSignal && hasValueFlowAggregateQuestionSignal(rawText);
const monthlyAggregationSignal = hasMonthlyAggregationSignal(rawText);
const rawAllTimeScopeSignal = hasAllTimeScopeHint(rawText);
const explicitDateScopeLiteralDetected = hasExplicitDateScopeLiteral(rawText);
const dateScopeSignalText = stripNegatedTaxDateScopeClauses(rawText);
const negatedTaxDateScopeOnlySignal =
dateScopeSignalText !== rawText &&
hasExplicitDateScopeLiteral(rawText) &&
!hasExplicitDateScopeLiteral(dateScopeSignalText);
const explicitDateScopeLiteralDetected = hasExplicitDateScopeLiteral(dateScopeSignalText);
const relativeCurrentDateHintDetected = hasRelativeCurrentDateHint(rawText);
const rawDateScope = collectDateScopeFromRawText(rawText);
const rawDateScope = collectDateScopeFromRawText(dateScopeSignalText);
const rawMetadataScopeHint = rawMetadataSignal ? metadataScopeHintFromRawText(rawText) : null;
const rawEntityCandidate = rawEntityResolutionSignal ? rawEntityResolutionCandidate(rawEntitySourceText) : null;
const entityResolutionClarificationCandidate =
@@ -1171,6 +1211,12 @@ export function buildAssistantMcpDiscoveryTurnInput(
const rawAggregationAxis = toNonEmptyString(assistantTurnMeaning?.asked_aggregation_axis);
const unsupported = toNonEmptyString(assistantTurnMeaning?.unsupported_but_understood_family);
const broadBusinessEvaluationUnsupported = unsupported === "broad_business_evaluation";
const businessOverviewSignal =
rawBusinessOverviewSignal ||
broadBusinessEvaluationUnsupported ||
rawDomain === "business_summary" ||
rawDomain === "business_overview" ||
rawAction === "broad_evaluation";
const explicitIntentCandidate = toNonEmptyString(assistantTurnMeaning?.explicit_intent_candidate);
const currentTurnDocumentLaneSignal = rawAction === "list_documents";
const currentTurnMovementLaneSignal = rawAction === "list_movements";
@@ -1553,11 +1599,13 @@ export function buildAssistantMcpDiscoveryTurnInput(
? followupSeed.unsupported
: null;
const lifecycleSignal =
rawLifecycleSignal || seededDomain === "counterparty_lifecycle";
!businessOverviewSignal && (rawLifecycleSignal || seededDomain === "counterparty_lifecycle");
const bidirectionalValueFlowSignal =
!businessOverviewSignal &&
!lifecycleSignal &&
(rawBidirectionalValueFlowSignal || seededAction === "net_value_flow");
const valueFlowSignal =
!businessOverviewSignal &&
!lifecycleSignal &&
!metadataGroundedMovementLaneApplicable &&
(rawValueFlowSignal || seededDomain === "counterparty_value");
@@ -1568,9 +1616,9 @@ export function buildAssistantMcpDiscoveryTurnInput(
const semanticDataNeed = metadataAmbiguityLaneClarificationApplicable
? "metadata lane clarification"
: semanticNeedFor({
domain: rawDomain ?? seededDomain,
action: rawAction ?? seededAction,
unsupported: broadBusinessEvaluationUnsupported ? seededUnsupported : unsupported ?? seededUnsupported,
domain: businessOverviewSignal ? "business_overview" : rawDomain ?? seededDomain,
action: businessOverviewSignal ? "broad_evaluation" : rawAction ?? seededAction,
unsupported: businessOverviewSignal ? "broad_business_evaluation" : unsupported ?? seededUnsupported,
lifecycleSignal,
valueFlowSignal,
metadataSignal: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable,
@@ -1710,30 +1758,48 @@ export function buildAssistantMcpDiscoveryTurnInput(
const clarificationLoopStillNeedsPeriod = Boolean(
followupSeed.loopStatus === "awaiting_clarification" && followupSeed.loopPendingAxes.includes("period")
);
const businessOverviewRawWithoutDateScope = Boolean(
businessOverviewSignal &&
!rawAllTimeScopeSignal &&
!explicitDateScopeLiteralDetected &&
!rawDateScope &&
!relativeCurrentDateHintDetected
);
const predecomposeDateScopeCountsAsCurrentTurnPeriod = Boolean(
predecomposeDateScope &&
!isImplicitCurrentDateScope(predecomposeDateScope) &&
!businessOverviewRawWithoutDateScope
);
const currentTurnCarriesExplicitPeriod = Boolean(
explicitDateScopeLiteralDetected ||
rawDateScope ||
relativeCurrentDateHintDetected ||
(predecomposeDateScope && !isImplicitCurrentDateScope(predecomposeDateScope))
predecomposeDateScopeCountsAsCurrentTurnPeriod
);
const suppressImplicitCurrentDateScope = Boolean(
!currentTurnCarriesExplicitPeriod &&
(clarificationLoopStillNeedsPeriod ||
businessOverviewSignal ||
openScopeValueFlowWithoutResolvedCounterparty ||
(valueFlowOrganizationStaysScope && (Boolean(followupSeed.rankingNeed) || bidirectionalValueFlowSignal)))
);
const suppressNegatedTaxOnlyDateScope = Boolean(businessOverviewSignal && negatedTaxDateScopeOnlySignal);
const normalizedPredecomposeDateScope =
(rawEntitySearchOverridesStaleScope && !currentTurnCarriesExplicitPeriod) ||
suppressNegatedTaxOnlyDateScope ||
businessOverviewRawWithoutDateScope ||
(suppressImplicitCurrentDateScope && isImplicitCurrentDateScope(predecomposeDateScope))
? null
: predecomposeDateScope;
const normalizedAssistantTurnMeaningDateScope =
rawEntitySearchOverridesStaleScope ||
suppressNegatedTaxOnlyDateScope ||
(suppressImplicitCurrentDateScope && isImplicitCurrentDateScope(assistantTurnMeaningDateScope))
? null
: assistantTurnMeaningDateScope;
const normalizedFollowupDateScope =
rawEntitySearchOverridesStaleScope ||
suppressNegatedTaxOnlyDateScope ||
(suppressImplicitCurrentDateScope && isImplicitCurrentDateScope(followupSeed.dateScope))
? null
: followupSeed.dateScope;
@@ -1757,7 +1823,9 @@ export function buildAssistantMcpDiscoveryTurnInput(
const turnMeaning: AssistantMcpDiscoveryTurnMeaningRef = {
asked_domain_family:
lifecycleSignal
businessOverviewSignal
? "business_overview"
: lifecycleSignal
? "counterparty_lifecycle"
: valueFlowSignal
? "counterparty_value"
@@ -1770,9 +1838,11 @@ export function buildAssistantMcpDiscoveryTurnInput(
: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
? "metadata"
: rawDomain ?? seededDomain,
asked_action_family: lifecycleSignal
? "activity_duration"
: valueFlowSignal
asked_action_family: businessOverviewSignal
? "broad_evaluation"
: lifecycleSignal
? "activity_duration"
: valueFlowSignal
? bidirectionalValueFlowSignal
? "net_value_flow"
: payoutSignal
@@ -1792,7 +1862,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
valueFlowSignal && followupSeed.rankingNeed && !rawEntitySearchOverridesStaleScope
? followupSeed.rankingNeed
: undefined,
explicit_entity_candidates: entityCandidates,
explicit_entity_candidates: businessOverviewSignal ? [] : entityCandidates,
metadata_ambiguity_entity_sets:
metadataAmbiguityLaneClarificationApplicable && followupSeed.metadataAmbiguityEntitySets.length > 0
? followupSeed.metadataAmbiguityEntitySets
@@ -1802,7 +1872,9 @@ export function buildAssistantMcpDiscoveryTurnInput(
explicit_date_scope: explicitDateScope,
subject_resolution_optional: metadataScopedLaneWithoutSubject || undefined,
unsupported_but_understood_family:
unsupported ??
businessOverviewSignal
? "broad_business_evaluation"
: unsupported ??
(lifecycleSignal
? "counterparty_lifecycle"
: valueFlowSignal
@@ -1826,6 +1898,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
: null),
stale_replay_forbidden: Boolean(
assistantTurnMeaning?.stale_replay_forbidden ||
businessOverviewSignal ||
unsupported ||
lifecycleSignal ||
valueFlowSignal ||
@@ -1886,7 +1959,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
);
const runDiscovery = shouldRunDiscovery({
unsupported: broadBusinessEvaluationUnsupported ? seededUnsupported : unsupported ?? seededUnsupported,
unsupported: businessOverviewSignal ? "broad_business_evaluation" : unsupported ?? seededUnsupported,
lifecycleSignal,
valueFlowSignal,
metadataSignal: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable,
@@ -1902,6 +1975,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
metadataGroundedDocumentLaneApplicable ||
groundedValueFlowFollowupApplicable,
forceDiscoveryOverExplicitIntent:
businessOverviewSignal ||
Boolean(entityResolutionClarificationCandidate) ||
organizationClarificationFollowupApplicable ||
periodClarificationFollowupApplicable ||
@@ -1976,6 +2050,9 @@ export function buildAssistantMcpDiscoveryTurnInput(
if (rawAllTimeScopeSignal) {
pushReason(reasonCodes, "mcp_discovery_all_time_scope_signal_detected");
}
if (suppressNegatedTaxOnlyDateScope) {
pushReason(reasonCodes, "mcp_discovery_negated_tax_period_scope_suppressed");
}
if (followupDiscoverySeedApplicable) {
pushReason(reasonCodes, "mcp_discovery_seeded_from_followup_context");
}
@@ -2054,8 +2131,8 @@ export function buildAssistantMcpDiscoveryTurnInput(
if (unsupported) {
pushReason(reasonCodes, "mcp_discovery_unsupported_but_understood_turn");
}
if (broadBusinessEvaluationUnsupported) {
pushReason(reasonCodes, "mcp_discovery_broad_business_evaluation_kept_in_living_chat");
if (businessOverviewSignal) {
pushReason(reasonCodes, "mcp_discovery_broad_business_evaluation_route_candidate");
}
if (
!(valueFlowOrganizationStaysScope && normalizedPredecomposeCounterparty === explicitOrganizationScope) &&
@@ -122,6 +122,13 @@ function detectBroadBusinessEvaluation(text) {
if (!normalized) {
return null;
}
if (
/(?:\u0431\u0438\u0437\u043d\u0435\u0441[-\s]?\u043e\u0431\u0437\u043e\u0440|\u0431\u0438\u0437\u043d\u0435\u0441[-\s]?\u0430\u0443\u0434\u0438\u0442|\u043f\u043e\u043b\u043d\w*\s+\u0430\u043d\u0430\u043b\u0438\u0437\s+(?:\u043a\u043e\u043c\u043f\u0430\u043d|\u0431\u0438\u0437\u043d\u0435\u0441|\u0434\u0435\u044f\u0442\u0435\u043b)|\u0441\u0432\u043e\u0434\u043d\w*\s+\u0430\u043d\u0430\u043b\u0438\u0437\s+(?:\u043a\u043e\u043c\u043f\u0430\u043d|\u0431\u0438\u0437\u043d\u0435\u0441|\u0434\u0435\u044f\u0442\u0435\u043b)|\u043a\u0430\u043a\s+\u0442\u044b\s+\u043e\u0446\u0435\u043d(?:\u0438\u0448\u044c|\u0438)\s+\u0434\u0435\u044f\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442|\u043a\u043e\u043c\u043f\u0430\u043d(?:\u0438\u0438|\u0438\u044e|\u0438\u044f)\s+\u0432\s+\u0446\u0435\u043b\u043e\u043c|company\s+(?:analysis|overview)|business\s+(?:overview|audit)|llm[-\s]?audit|бизнес[-\s]?РѕР±Р·РѕСЂ|бизнес[-\s]?аудиС)/iu.test(normalized)
) {
return {
family: "broad_business_evaluation"
};
}
if (
/(?:как\s+ты\s+оценишь\s+деятельност[ьи]\s+компан|оценк[аи]?\s+деятельност[ьи]\s+компан|оцени\s+(?:компан|бизнес|деятельност)|(?:полный|сводный|нормальн\w*|взросл\w*)\s+анализ\s+(?:компан|бизнес|деятельност)|проанализируй\s+(?:компан|бизнес|деятельност)|(?:что\s+думаешь|какое\s+мнение)\s+(?:о|по)\s+(?:компан|бизнес)|(?:llm[-\s]?)?аудит\s+(?:компан|бизнес)|что\s+у\s+нас\s+вообще\s+происход|где\s+главн(?:ые|ый)\s+риски|как\s+у\s+нас\s+дела\s+по\s+компан)/iu.test(
normalized
@@ -232,7 +239,7 @@ export function createAssistantTurnMeaningPolicy(deps = {}) {
asked_domain_family: askedDomainFamily,
asked_action_family: askedActionFamily,
explicit_intent_candidate: explicitIntentCandidate,
explicit_entity_candidates: buildEntityCandidates(counterpartyTurnover),
explicit_entity_candidates: broadBusinessEvaluation?.family ? [] : buildEntityCandidates(counterpartyTurnover),
meaning_confidence: broadBusinessEvaluation?.family
? "medium"
: supportedIntent?.confidence ?? (counterpartyTurnover?.family ? "medium" : "low"),
@@ -180,6 +180,283 @@ describe("assistant MCP discovery answer adapter", () => {
expect(draft.must_not_claim).toContain("Do not present the confirmed movement rows as a complete movement universe.");
});
it("turns business overview multi-probe evidence into an analyst-safe draft", async () => {
const planner = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
business_fact_family: "business_overview",
action_family: "broad_evaluation",
aggregation_need: null,
time_scope_need: "all_time_scope",
comparison_need: null,
ranking_need: null,
proof_expectation: "bounded_inference",
clarification_gaps: [],
decomposition_candidates: [
"collect_scoped_movements",
"aggregate_checked_amounts",
"aggregate_ranked_axis_values",
"fetch_supporting_documents",
"probe_coverage",
"explain_evidence_basis"
],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_profit_or_margin_claim_without_evidence"],
reason_codes: ["data_need_graph_built", "data_need_graph_family_business_overview"]
},
turnMeaning: {
asked_domain_family: "business_summary",
asked_action_family: "broad_evaluation",
explicit_organization_scope: "ООО Альтернатива Плюс",
unsupported_but_understood_family: "broad_business_evaluation"
}
});
const pilot = await executeAssistantMcpDiscoveryPilot(
planner,
buildSequentialDeps([
{
rows: [
{ Period: "2020-01-15T00:00:00", Amount: 120000, Counterparty: "Клиент А" },
{ Period: "2020-02-15T00:00:00", Amount: 80000, Counterparty: "Клиент Б" }
]
},
{
rows: [{ Period: "2020-01-20T00:00:00", Amount: 150000, Counterparty: "Поставщик А" }]
},
{
rows: [
{ Период: "2020-01-15T00:00:00", Регистратор: "Поступление 1" },
{ Период: "2020-12-15T00:00:00", Регистратор: "Поступление 2" }
]
}
])
);
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
expect(draft.answer_mode).toBe("confirmed_with_bounded_inference");
expect(draft.headline).toContain("бизнес-обзор");
expect(draft.confirmed_lines.join("\n")).toContain("Входящие поступления");
expect(draft.confirmed_lines.join("\n")).toContain("Самый крупный подтвержденный клиент");
expect(draft.inference_lines.join("\n")).toContain("не прибыль и не маржа");
expect(draft.unknown_lines.join("\n")).toContain("Прибыль и маржа");
expect(draft.unknown_lines.join("\n")).toContain("Налоговая/VAT-позиция");
expect(draft.must_not_claim).toContain("Do not present business overview cash-flow spread as profit or margin.");
expect(draft.reason_codes).toContain("answer_contains_business_overview");
});
it("surfaces checked VAT/tax position in business overview without treating it as profit", async () => {
const planner = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
business_fact_family: "business_overview",
action_family: "broad_evaluation",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: null,
ranking_need: null,
proof_expectation: "bounded_inference",
clarification_gaps: [],
decomposition_candidates: [
"collect_scoped_movements",
"aggregate_checked_amounts",
"aggregate_ranked_axis_values",
"fetch_supporting_documents",
"probe_coverage",
"explain_evidence_basis"
],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_profit_or_margin_claim_without_evidence"],
reason_codes: ["data_need_graph_built", "data_need_graph_family_business_overview"]
},
turnMeaning: {
asked_domain_family: "business_summary",
asked_action_family: "broad_evaluation",
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020",
unsupported_but_understood_family: "broad_business_evaluation"
}
});
const pilot = await executeAssistantMcpDiscoveryPilot(
planner,
buildSequentialDeps([
{ rows: [{ Period: "2020-01-15T00:00:00", Amount: 120000, Counterparty: "Клиент А" }] },
{ rows: [{ Period: "2020-01-20T00:00:00", Amount: 50000, Counterparty: "Поставщик А" }] },
{
rows: [
{ Регистратор: "VAT_BOOK_SALES", СчетДт: "68.02", Сумма: 40000 },
{ Регистратор: "VAT_BOOK_PURCHASES", СчетДт: "19", Сумма: 12000 }
]
},
{
rows: [
{ Период: "2020-01-15T00:00:00", Регистратор: "Поступление 1" },
{ Период: "2020-12-15T00:00:00", Регистратор: "Поступление 2" }
]
}
])
);
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
expect(draft.headline).toContain("НДС-позиция");
expect(draft.confirmed_lines.join("\n")).toContain("НДС-позиция за 2020");
expect(draft.confirmed_lines.join("\n")).toContain("нетто к уплате 28 000 руб.");
expect(draft.inference_lines.join("\n")).toContain("не прибыль и не маржа");
expect(draft.unknown_lines.join("\n")).not.toContain("Налоговая/VAT-позиция");
expect(draft.reason_codes).toContain("answer_contains_business_overview_tax_position");
expect(draft.must_not_claim).toContain("Do not present business overview cash-flow spread as profit or margin.");
});
it("surfaces checked debt-position and open-settlement quality without treating them as overdue debt", async () => {
const planner = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
business_fact_family: "business_overview",
action_family: "broad_evaluation",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: null,
ranking_need: null,
proof_expectation: "bounded_inference",
clarification_gaps: [],
decomposition_candidates: [
"collect_scoped_movements",
"aggregate_checked_amounts",
"aggregate_ranked_axis_values",
"fetch_supporting_documents",
"probe_coverage",
"explain_evidence_basis"
],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_profit_or_margin_claim_without_evidence"],
reason_codes: ["data_need_graph_built", "data_need_graph_family_business_overview"]
},
turnMeaning: {
asked_domain_family: "business_summary",
asked_action_family: "broad_evaluation",
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020",
unsupported_but_understood_family: "broad_business_evaluation"
}
});
const pilot = await executeAssistantMcpDiscoveryPilot(
planner,
buildSequentialDeps([
{ rows: [{ Period: "2020-01-15T00:00:00", Amount: 120000, Counterparty: "Клиент А" }] },
{ rows: [{ Period: "2020-01-20T00:00:00", Amount: 50000, Counterparty: "Поставщик А" }] },
{ rows: [] },
{
rows: [
{ Period: "2020-12-31T00:00:00", Amount: 100000, Counterparty: "Клиент А" }
]
},
{
rows: [
{ Period: "2020-12-31T00:00:00", Amount: 40000, Counterparty: "Поставщик А" }
]
},
{
rows: [
{ Period: "2020-12-31T00:00:00", Amount: 100000, Counterparty: "Клиент А", Contract: "Договор А" },
{ Period: "2020-12-31T00:00:00", Amount: 50000, Counterparty: "Поставщик А", Contract: "Договор Б" }
]
},
{ rows: [] },
{ rows: [] },
{
rows: [
{ Period: "2020-01-15T00:00:00", Registrator: "Поступление 1" },
{ Period: "2020-12-15T00:00:00", Registrator: "Поступление 2" }
]
}
])
);
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
expect(draft.headline).toContain("долговой срез");
expect(draft.headline).toContain("качество открытых расчетов");
expect(draft.confirmed_lines.join("\n")).toContain("Долговой срез на 2020-12-31");
expect(draft.confirmed_lines.join("\n")).toContain("Качество открытых расчетов на 2020-12-31");
expect(draft.confirmed_lines.join("\n")).toContain("нетто");
expect(draft.unknown_lines.join("\n")).toContain("due-date");
expect(draft.reason_codes).toContain("answer_contains_business_overview_debt_position");
expect(draft.reason_codes).toContain("answer_contains_business_overview_open_settlement_quality");
expect(draft.must_not_claim).toContain("Do not present a debt-position snapshot as debt aging, overdue debt, or credit-quality analysis.");
expect(draft.must_not_claim).toContain("Do not present open-settlement concentration as contractual due-date aging or confirmed overdue debt.");
});
it("surfaces checked inventory-position snapshot in business overview without treating it as warehouse liquidity", async () => {
const planner = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
business_fact_family: "business_overview",
action_family: "broad_evaluation",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: null,
ranking_need: null,
proof_expectation: "bounded_inference",
clarification_gaps: [],
decomposition_candidates: [
"collect_scoped_movements",
"aggregate_checked_amounts",
"aggregate_ranked_axis_values",
"fetch_supporting_documents",
"probe_coverage",
"explain_evidence_basis"
],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_profit_or_margin_claim_without_evidence"],
reason_codes: ["data_need_graph_built", "data_need_graph_family_business_overview"]
},
turnMeaning: {
asked_domain_family: "business_summary",
asked_action_family: "broad_evaluation",
explicit_organization_scope: "ООО Тест",
explicit_date_scope: "2020",
unsupported_but_understood_family: "broad_business_evaluation"
}
});
const pilot = await executeAssistantMcpDiscoveryPilot(
planner,
buildSequentialDeps([
{ rows: [{ Period: "2020-01-15T00:00:00", Amount: 120000, Counterparty: "Клиент А" }] },
{ rows: [{ Period: "2020-01-20T00:00:00", Amount: 50000, Counterparty: "Поставщик А" }] },
{ rows: [] },
{ rows: [] },
{ rows: [] },
{ rows: [] },
{
rows: [
{ Period: "2020-12-31T00:00:00", Amount: 250000, Quantity: 10, Item: "Товар А" },
{ Period: "2020-12-31T00:00:00", Amount: 50000, Quantity: 5, Item: "Товар Б" }
]
},
{
rows: [
{ Period: "2020-01-10T00:00:00", Amount: 200000, Quantity: 8, Item: "Товар А" }
]
},
{ rows: [{ Period: "2020-01-15T00:00:00", Registrator: "Поступление 1" }] }
])
);
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
expect(draft.headline).toContain("складской срез");
expect(draft.confirmed_lines.join("\n")).toContain("Складской срез на 2020-12-31");
expect(draft.confirmed_lines.join("\n")).toContain("Товар А");
expect(draft.unknown_lines.join("\n")).toContain("оборачиваемость");
expect(draft.reason_codes).toContain("answer_contains_business_overview_inventory_position");
expect(draft.must_not_claim).toContain("Do not present an inventory snapshot or purchase-date aging signal as turnover, obsolescence, liquidation value, or full inventory health.");
});
it("renders metadata-scoped movement all-time follow-up as an all-time bounded answer", async () => {
const planner = planAssistantMcpDiscovery({
dataNeedGraph: {
@@ -103,7 +103,7 @@ describe("assistant MCP discovery pilot executor", () => {
expect(deps.executeAddressMcpQuery).not.toHaveBeenCalled();
});
it("keeps business overview as an explicit unsupported runtime scope until the fresh multi-probe bridge exists", async () => {
it("executes business overview as a bounded multi-probe bridge over money flow and activity evidence", async () => {
const planner = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
@@ -134,24 +134,350 @@ describe("assistant MCP discovery pilot executor", () => {
explicit_organization_scope: "ООО Альтернатива Плюс"
}
});
const deps = buildDeps([]);
const deps = buildSequentialDeps([
{
rows: [
{ Period: "2020-01-15T00:00:00", Amount: 120000, Counterparty: "Клиент А" },
{ Period: "2020-02-15T00:00:00", Amount: 80000, Counterparty: "Клиент Б" }
]
},
{
rows: [
{ Period: "2020-01-20T00:00:00", Amount: 150000, Counterparty: "Поставщик А" }
]
},
{
rows: [
{ Период: "2020-01-15T00:00:00", Регистратор: "Поступление 1" },
{ Период: "2020-12-15T00:00:00", Регистратор: "Поступление 2" }
]
}
]);
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
expect(planner.planner_status).toBe("ready_for_execution");
expect(result.pilot_status).toBe("unsupported");
expect(result.pilot_status).toBe("executed");
expect(result.pilot_scope).toBe("business_overview_route_template_v1");
expect(result.mcp_execution_performed).toBe(false);
expect(result.executed_primitives).toEqual([]);
expect(result.mcp_execution_performed).toBe(true);
expect(result.executed_primitives).toEqual(["query_movements", "query_documents"]);
expect(result.skipped_primitives).toEqual([
"query_movements",
"aggregate_by_axis",
"query_documents",
"probe_coverage",
"explain_evidence_basis"
]);
expect(result.reason_codes).toContain("pilot_scope_unsupported_for_live_execution");
expect(deps.executeAddressMcpQuery).not.toHaveBeenCalled();
expect(result.derived_business_overview).toMatchObject({
organization_scope: "ООО Альтернатива Плюс",
incoming_customer_revenue: {
total_amount: 200000,
rows_with_amount: 2
},
outgoing_supplier_payout: {
total_amount: 150000,
rows_with_amount: 1
},
net_amount: 50000,
net_direction: "net_incoming"
});
expect(result.derived_business_overview?.top_customers[0]).toMatchObject({
axis_value: "Клиент А",
total_amount: 120000
});
expect(result.derived_business_overview?.activity_period?.duration_total_months).toBe(11);
expect(result.evidence.confirmed_facts.join("\n")).toContain("В 1С подтверждены входящие поступления");
expect(result.evidence.unknown_facts).toContain(
"Прибыль и маржа этим бизнес-обзором не подтверждены: нужны себестоимость, расходы и закрывающие документы."
);
expect(result.reason_codes).toContain("pilot_derived_business_overview_from_confirmed_rows");
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(3);
});
it("adds a checked VAT/tax family to business overview only when an explicit period is available", async () => {
const planner = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
business_fact_family: "business_overview",
action_family: "broad_evaluation",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: null,
ranking_need: null,
proof_expectation: "bounded_inference",
clarification_gaps: [],
decomposition_candidates: [
"collect_scoped_movements",
"aggregate_checked_amounts",
"aggregate_ranked_axis_values",
"fetch_supporting_documents",
"probe_coverage",
"explain_evidence_basis"
],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_profit_or_margin_claim_without_evidence"],
reason_codes: ["data_need_graph_built", "data_need_graph_family_business_overview"]
},
turnMeaning: {
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation",
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020"
}
});
const deps = buildSequentialDeps([
{
rows: [
{ Period: "2020-01-15T00:00:00", Amount: 120000, Counterparty: "Клиент А" }
]
},
{
rows: [
{ Period: "2020-01-20T00:00:00", Amount: 50000, Counterparty: "Поставщик А" }
]
},
{
rows: [
{ Регистратор: "VAT_BOOK_SALES", СчетДт: "68.02", Сумма: 40000 },
{ Регистратор: "VAT_BOOK_PURCHASES", СчетДт: "19", Сумма: 12000 }
]
},
{ rows: [] },
{ rows: [] },
{ rows: [] },
{ rows: [] },
{ rows: [] },
{
rows: [
{ Период: "2020-01-15T00:00:00", Регистратор: "Поступление 1" },
{ Период: "2020-12-15T00:00:00", Регистратор: "Поступление 2" }
]
}
]);
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
expect(result.pilot_status).toBe("executed");
expect(result.derived_business_overview?.tax_position).toMatchObject({
period_scope: "2020",
rows_matched: 2,
rows_with_amount: 2,
sales_vat_amount: 40000,
purchase_vat_amount: 12000,
net_vat_amount: 28000,
net_vat_direction: "vat_to_pay"
});
expect(result.derived_business_overview?.missing_signal_families).not.toContain("tax_position");
expect(result.evidence.confirmed_facts.join("\n")).toContain("НДС-позиция за 2020 подтверждена");
expect(result.evidence.unknown_facts.join("\n")).not.toContain("Налоговая/VAT-позиция этим бизнес-обзором не подтверждена");
expect(result.reason_codes).toContain("pilot_business_overview_tax_query_mcp_executed");
expect(result.reason_codes).toContain("pilot_derived_business_overview_tax_position_from_confirmed_rows");
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(9);
const taxCall = deps.executeAddressMcpQuery.mock.calls[2]?.[0];
expect(String(taxCall?.query ?? "")).toContain("НДСЗаписиКнигиПродаж");
expect(String(taxCall?.query ?? "")).toContain("НДСЗаписиКнигиПокупок");
});
it("adds a checked debt-position family to business overview only as an as-of-date snapshot", async () => {
const planner = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
business_fact_family: "business_overview",
action_family: "broad_evaluation",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: null,
ranking_need: null,
proof_expectation: "bounded_inference",
clarification_gaps: [],
decomposition_candidates: [
"collect_scoped_movements",
"aggregate_checked_amounts",
"aggregate_ranked_axis_values",
"fetch_supporting_documents",
"probe_coverage",
"explain_evidence_basis"
],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_profit_or_margin_claim_without_evidence"],
reason_codes: ["data_need_graph_built", "data_need_graph_family_business_overview"]
},
turnMeaning: {
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation",
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020"
}
});
const deps = buildSequentialDeps([
{ rows: [{ Period: "2020-01-15T00:00:00", Amount: 120000, Counterparty: "Клиент А" }] },
{ rows: [{ Period: "2020-01-20T00:00:00", Amount: 50000, Counterparty: "Поставщик А" }] },
{ rows: [] },
{
rows: [
{ Period: "2020-12-31T00:00:00", Amount: 70000, Counterparty: "Клиент А" },
{ Period: "2020-12-31T00:00:00", Amount: 30000, Counterparty: "Клиент Б" }
]
},
{
rows: [
{ Period: "2020-12-31T00:00:00", Amount: 40000, Counterparty: "Поставщик А" }
]
},
{
rows: [
{ Period: "2020-12-31T00:00:00", Amount: 90000, Counterparty: "Клиент А", Contract: "Договор А" },
{ Period: "2020-12-31T00:00:00", Amount: 30000, Counterparty: "Клиент Б", Contract: "Договор Б" }
]
},
{ rows: [] },
{ rows: [] },
{
rows: [
{ Period: "2020-01-15T00:00:00", Registrator: "Поступление 1" },
{ Period: "2020-12-15T00:00:00", Registrator: "Поступление 2" }
]
}
]);
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
expect(result.pilot_status).toBe("executed");
expect(result.derived_business_overview?.debt_position).toMatchObject({
as_of_date: "2020-12-31",
receivables: {
total_amount: 100000,
rows_with_amount: 2
},
payables: {
total_amount: 40000,
rows_with_amount: 1
},
net_debt_position_amount: 60000,
net_debt_position_direction: "net_receivable"
});
expect(result.derived_business_overview?.debt_position?.receivables.top_counterparties[0]).toMatchObject({
axis_value: "Клиент А",
total_amount: 70000
});
expect(result.derived_business_overview?.debt_open_settlement_quality).toMatchObject({
as_of_date: "2020-12-31",
rows_with_amount: 2,
gross_open_amount: 120000,
unique_counterparties: 2,
unique_contracts: 2,
concentration_top_contract_pct: 75
});
expect(result.derived_business_overview?.missing_signal_families).not.toContain("debt_position");
expect(result.derived_business_overview?.missing_signal_families).not.toContain("debt_open_settlement_quality");
expect(result.derived_business_overview?.missing_signal_families).toContain("debt_due_date_aging_quality");
expect(result.evidence.confirmed_facts.join("\n")).toContain("Долговая позиция на 2020-12-31");
expect(result.evidence.confirmed_facts.join("\n")).toContain("Качество открытых расчетов на 2020-12-31");
expect(result.evidence.unknown_facts.join("\n")).toContain("due-date");
expect(result.reason_codes).toContain("pilot_business_overview_debt_query_mcp_executed");
expect(result.reason_codes).toContain("pilot_derived_business_overview_debt_position_from_confirmed_rows");
expect(result.reason_codes).toContain("pilot_business_overview_open_contracts_query_mcp_executed");
expect(result.reason_codes).toContain("pilot_derived_business_overview_open_settlement_quality_from_confirmed_rows");
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(9);
const receivablesCall = deps.executeAddressMcpQuery.mock.calls[3]?.[0];
const payablesCall = deps.executeAddressMcpQuery.mock.calls[4]?.[0];
const openContractsCall = deps.executeAddressMcpQuery.mock.calls[5]?.[0];
expect(String(receivablesCall?.query ?? "")).toContain("62");
expect(String(payablesCall?.query ?? "")).toContain("60");
expect(String(openContractsCall?.query ?? "")).toContain("СуммаРазвернутыйОстатокКт");
});
it("adds a checked inventory-position family to business overview only as an as-of-date snapshot", async () => {
const planner = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
business_fact_family: "business_overview",
action_family: "broad_evaluation",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: null,
ranking_need: null,
proof_expectation: "bounded_inference",
clarification_gaps: [],
decomposition_candidates: [
"collect_scoped_movements",
"aggregate_checked_amounts",
"aggregate_ranked_axis_values",
"fetch_supporting_documents",
"probe_coverage",
"explain_evidence_basis"
],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_profit_or_margin_claim_without_evidence"],
reason_codes: ["data_need_graph_built", "data_need_graph_family_business_overview"]
},
turnMeaning: {
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation",
explicit_organization_scope: "ООО Тест",
explicit_date_scope: "2020"
}
});
const deps = buildSequentialDeps([
{ rows: [{ Period: "2020-01-15T00:00:00", Amount: 120000, Counterparty: "Клиент А" }] },
{ rows: [{ Period: "2020-01-20T00:00:00", Amount: 50000, Counterparty: "Поставщик А" }] },
{ rows: [] },
{ rows: [] },
{ rows: [] },
{ rows: [] },
{
rows: [
{ Period: "2020-12-31T00:00:00", Amount: 250000, Quantity: 10, Item: "Товар А" },
{ Period: "2020-12-31T00:00:00", Amount: 50000, Quantity: 5, Item: "Товар Б" }
]
},
{
rows: [
{ Period: "2020-01-10T00:00:00", Amount: 200000, Quantity: 8, Item: "Товар А" },
{ Period: "2020-11-01T00:00:00", Amount: 50000, Quantity: 2, Item: "Товар Б" }
]
},
{
rows: [
{ Period: "2020-01-15T00:00:00", Registrator: "Поступление 1" },
{ Period: "2020-12-15T00:00:00", Registrator: "Поступление 2" }
]
}
]);
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
expect(result.pilot_status).toBe("executed");
expect(result.derived_business_overview?.inventory_position).toMatchObject({
as_of_date: "2020-12-31",
rows_matched: 2,
rows_with_amount: 2,
rows_with_quantity: 2,
total_amount: 300000,
total_quantity: 15,
aging_signal: {
rows_matched: 2,
rows_with_purchase_date: 2,
oldest_purchase_date: "2020-01-10",
latest_purchase_date: "2020-11-01",
max_age_days: 356
}
});
expect(result.derived_business_overview?.inventory_position?.top_items[0]).toMatchObject({
item: "Товар А",
total_amount: 250000,
total_quantity: 10
});
expect(result.derived_business_overview?.missing_signal_families).not.toContain("inventory_position");
expect(result.derived_business_overview?.missing_signal_families).toContain("inventory_turnover_quality");
expect(result.evidence.confirmed_facts.join("\n")).toContain("Складской срез на 2020-12-31");
expect(result.evidence.unknown_facts.join("\n")).toContain("оборачиваемость");
expect(result.reason_codes).toContain("pilot_business_overview_inventory_query_mcp_executed");
expect(result.reason_codes).toContain("pilot_derived_business_overview_inventory_position_from_confirmed_rows");
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(9);
const inventoryCall = deps.executeAddressMcpQuery.mock.calls[6]?.[0];
expect(inventoryCall?.account_scope).toContain("41.01");
});
it("uses the explicit selected chain id when choosing the movement pilot scope", async () => {
@@ -221,7 +221,7 @@ describe("assistant MCP discovery planner", () => {
}
});
it("builds a catalog-compatible business overview plan without pretending the fresh runtime probe exists yet", () => {
it("builds a catalog-compatible business overview plan for the fresh multi-probe runtime bridge", () => {
const result = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
@@ -737,4 +737,67 @@ describe("assistant MCP discovery response policy", () => {
"mcp_discovery_response_policy_keep_broad_business_summary_over_clarification_candidate"
);
});
it("replaces deterministic broad business evaluation summary with a grounded business overview candidate", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: "legacy broad summary",
currentReplySource: "deterministic_broad_business_evaluation_contract",
livingChatSource: "deterministic_broad_business_evaluation_contract",
modeDecisionReason: "unsupported_current_turn_meaning_boundary",
addressRuntimeMeta: {
assistant_mcp_discovery_entry_point_v1: entryPoint({
turn_input: {
adapter_status: "ready",
should_run_discovery: true,
turn_meaning_ref: {
asked_domain_family: "business_summary",
asked_action_family: "broad_evaluation",
unsupported_but_understood_family: "broad_business_evaluation",
stale_replay_forbidden: true
},
data_need_graph: {
business_fact_family: "business_overview",
clarification_gaps: []
}
},
bridge: {
bridge_status: "answer_draft_ready",
user_facing_response_allowed: true,
business_fact_answer_allowed: true,
requires_user_clarification: false,
answer_draft: {
answer_mode: "confirmed_with_bounded_inference",
headline: "Business overview was assembled from confirmed 1C money flow and activity rows.",
confirmed_lines: [
"Incoming customer money flow: 200000.00 RUB.",
"Outgoing supplier payouts: 150000.00 RUB.",
"Top confirmed customer by incoming money flow: Client A - 120000.00 RUB."
],
inference_lines: [
"Net confirmed cash-flow spread is +50000.00 RUB; this is not profit or margin."
],
unknown_lines: [
"Profit and margin are not confirmed by this overview.",
"VAT/tax position is not confirmed by this overview."
],
limitation_lines: ["Business overview is limited to checked 1C rows."],
next_step_line: "Check profit/margin, debt quality, VAT/tax position, and inventory liquidity."
}
}
})
}
});
expect(result.applied).toBe(true);
expect(result.decision).toBe("apply_candidate");
expect(result.reply_source).toBe("mcp_discovery_response_candidate_guarded");
expect(result.reply_text).toContain("Incoming customer money flow");
expect(result.reply_text).toContain("this is not profit or margin");
expect(result.reply_text).toContain("VAT/tax position is not confirmed");
expect(result.reply_text).not.toContain("query_movements");
expect(result.reason_codes).toContain("mcp_discovery_response_policy_candidate_applied");
expect(result.reason_codes).not.toContain(
"mcp_discovery_response_policy_keep_broad_business_summary_over_clarification_candidate"
);
});
});
@@ -37,6 +37,22 @@ function buildBidirectionalDeps(
};
}
function buildSequentialDeps(results: Array<{ rows: Array<Record<string, unknown>>; error?: string | null }>) {
const executeAddressMcpQuery = vi.fn(async () => {
const next = results.shift() ?? { rows: [] };
const rows = next.rows;
const error = next.error ?? null;
return {
fetched_rows: rows.length,
matched_rows: error ? 0 : rows.length,
raw_rows: rows,
rows: error ? [] : rows,
error
};
});
return { executeAddressMcpQuery };
}
function buildMetadataDeps(rows: Array<Record<string, unknown>>, error: string | null = null) {
return {
executeAddressMcpMetadata: vi.fn(async () => ({
@@ -114,6 +130,56 @@ describe("assistant MCP discovery runtime entry point", () => {
expect(result.reason_codes).toContain("mcp_discovery_unsupported_but_understood_turn");
});
it("runs the business overview bridge from broad evaluation turn meaning through multi-probe evidence", async () => {
const deps = buildSequentialDeps([
{
rows: [
{ Period: "2020-01-15T00:00:00", Amount: 120000, Counterparty: "Client A" },
{ Period: "2020-02-15T00:00:00", Amount: 80000, Counterparty: "Client B" }
]
},
{
rows: [{ Period: "2020-01-20T00:00:00", Amount: 150000, Counterparty: "Supplier A" }]
},
{
rows: [
{ Period: "2020-01-15T00:00:00", Registrar: "Customer payment 1" },
{ Period: "2020-12-15T00:00:00", Registrar: "Customer payment 2" }
]
}
]);
const result = await runAssistantMcpDiscoveryRuntimeEntryPoint({
assistantTurnMeaning: {
asked_domain_family: "business_summary",
asked_action_family: "broad_evaluation",
explicit_organization_scope: "Alternative Plus",
unsupported_but_understood_family: "broad_business_evaluation",
stale_replay_forbidden: true
},
deps
});
expect(result.entry_status).toBe("bridge_executed");
expect(result.discovery_attempted).toBe(true);
expect(result.turn_input.semantic_data_need).toBe("business overview evidence with bounded analyst interpretation");
expect(result.turn_input.data_need_graph?.business_fact_family).toBe("business_overview");
expect(result.turn_input.data_need_graph?.clarification_gaps).toEqual([]);
expect(result.bridge?.bridge_status).toBe("answer_draft_ready");
expect(result.bridge?.pilot.pilot_scope).toBe("business_overview_route_template_v1");
expect(result.bridge?.pilot.derived_business_overview).toMatchObject({
organization_scope: "Alternative Plus",
net_amount: 50000,
net_direction: "net_incoming"
});
expect(result.bridge?.answer_draft.answer_mode).toBe("confirmed_with_bounded_inference");
expect(result.bridge?.answer_draft.confirmed_lines.join("\n")).toContain("Client A");
expect(result.bridge?.answer_draft.inference_lines.join("\n")).toContain("\u043d\u0435 \u043f\u0440\u0438\u0431\u044b\u043b\u044c");
expect(result.bridge?.answer_draft.unknown_lines.join("\n")).toContain("VAT");
expect(result.reason_codes).toContain("pilot_derived_business_overview_from_confirmed_rows");
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(3);
});
it("runs the bridge for raw metadata wording without an exact route owner", async () => {
const result = await runAssistantMcpDiscoveryRuntimeEntryPoint({
userMessage: "какие документы и поля есть в 1С по НДС?",
@@ -1409,7 +1409,7 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.reason_codes).toContain("mcp_discovery_not_applicable_for_supported_exact_turn");
});
it("does not replace broad business evaluation with metadata discovery", () => {
it("routes broad business evaluation into business overview discovery without metadata drift", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage:
"\u041a\u0430\u043a \u0442\u044b \u043e\u0446\u0435\u043d\u0438\u0448\u044c \u0434\u0435\u044f\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u044c \u043a\u043e\u043c\u043f\u0430\u043d\u0438\u0438?",
@@ -1422,11 +1422,117 @@ describe("assistant MCP discovery turn input adapter", () => {
}
});
expect(result.adapter_status).toBe("not_applicable");
expect(result.should_run_discovery).toBe(false);
expect(result.turn_meaning_ref).toBeNull();
expect(result.reason_codes).toContain("mcp_discovery_broad_business_evaluation_kept_in_living_chat");
expect(result.reason_codes).toContain("mcp_discovery_not_applicable_for_supported_exact_turn");
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.semantic_data_need).toBe("business overview evidence with bounded analyst interpretation");
expect(result.data_need_graph?.business_fact_family).toBe("business_overview");
expect(result.data_need_graph?.clarification_gaps).toContain("organization");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation",
explicit_date_scope: "2026-05-01",
unsupported_but_understood_family: "broad_business_evaluation",
stale_replay_forbidden: true
});
expect(result.reason_codes).toContain("mcp_discovery_broad_business_evaluation_route_candidate");
expect(result.reason_codes).toContain("mcp_discovery_data_need_graph_built");
expect(result.reason_codes).not.toContain("mcp_discovery_metadata_signal_detected");
expect(result.reason_codes).not.toContain("mcp_discovery_not_applicable_for_supported_exact_turn");
});
it("lets raw business-overview wording override stale exact turnover meaning", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage:
"\u0414\u0430\u0439 \u0431\u0438\u0437\u043d\u0435\u0441-\u043e\u0431\u0437\u043e\u0440 \u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441 \u043f\u043e \u0434\u0430\u043d\u043d\u044b\u043c 1\u0421: \u043e\u0431\u043e\u0440\u043e\u0442\u044b, \u043d\u0435\u0442\u0442\u043e, \u0430\u043a\u0442\u0438\u0432\u043d\u043e\u0441\u0442\u044c.",
assistantTurnMeaning: {
asked_domain_family: "counterparty",
asked_action_family: "counterparty_value_or_turnover",
explicit_intent_candidate: "customer_revenue_and_payments",
explicit_entity_candidates: [{ value: "\u043d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u043e." }],
stale_replay_forbidden: false
},
predecomposeContract: {
entities: { organization: "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441" },
period: { period_to: "2026-05-03" }
}
});
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.semantic_data_need).toBe("business overview evidence with bounded analyst interpretation");
expect(result.data_need_graph?.business_fact_family).toBe("business_overview");
expect(result.data_need_graph?.time_scope_need).toBe("all_time_scope");
expect(result.data_need_graph?.clarification_gaps).toEqual([]);
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation",
explicit_organization_scope: "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441",
unsupported_but_understood_family: "broad_business_evaluation",
stale_replay_forbidden: true
});
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.reason_codes).toContain("mcp_discovery_broad_business_evaluation_route_candidate");
expect(result.reason_codes).not.toContain("mcp_discovery_value_flow_signal_detected");
});
it("keeps explicit year out of the organization scope for raw business overview wording", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage:
"Дай бизнес-обзор ООО Альтернатива Плюс за 2020 год по данным 1С: деньги, нетто, активность, дебиторка и кредиторка на дату.",
assistantTurnMeaning: {
asked_domain_family: "business_summary",
asked_action_family: "broad_evaluation",
unsupported_but_understood_family: "broad_business_evaluation",
stale_replay_forbidden: true
},
predecomposeContract: {
entities: { organization: "ООО Альтернатива Плюс" },
period: { period_from: "2020-01-01", period_to: "2020-12-31" }
}
});
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.data_need_graph?.business_fact_family).toBe("business_overview");
expect(result.data_need_graph?.time_scope_need).toBe("explicit_period");
expect(result.turn_meaning_ref).toMatchObject({
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020"
});
});
it("keeps all-time business overview from reusing a negated VAT period as active scope", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage:
"\u0422\u0435\u043f\u0435\u0440\u044c \u043f\u043e \u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441 \u0437\u0430 \u0432\u0441\u0435 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e\u0435 \u0432\u0440\u0435\u043c\u044f \u0434\u0430\u0439 \u0431\u0438\u0437\u043d\u0435\u0441-\u043e\u0431\u0437\u043e\u0440 \u0432 \u0446\u0435\u043b\u043e\u043c, \u043d\u043e \u043d\u0435 \u0442\u0430\u0449\u0438 \u041d\u0414\u0421 \u0437\u0430 2020 \u043a\u0430\u043a \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d\u043d\u0443\u044e \u043e\u0431\u0449\u0443\u044e \u043d\u0430\u043b\u043e\u0433\u043e\u0432\u0443\u044e \u043f\u043e\u0437\u0438\u0446\u0438\u044e.",
assistantTurnMeaning: {
asked_domain_family: "business_summary",
asked_action_family: "broad_evaluation",
explicit_date_scope: "2020-12-31",
unsupported_but_understood_family: "broad_business_evaluation",
stale_replay_forbidden: true
},
predecomposeContract: {
entities: { organization: "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441" },
period: { period_to: "2020-12-31" }
}
});
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.data_need_graph?.business_fact_family).toBe("business_overview");
expect(result.data_need_graph?.time_scope_need).toBe("all_time_scope");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "business_overview",
asked_action_family: "broad_evaluation",
explicit_organization_scope: "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441",
unsupported_but_understood_family: "broad_business_evaluation",
stale_replay_forbidden: true
});
expect(result.turn_meaning_ref?.explicit_date_scope).toBeUndefined();
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.reason_codes).toContain("mcp_discovery_all_time_scope_signal_detected");
expect(result.reason_codes).toContain("mcp_discovery_negated_tax_period_scope_suppressed");
});
it("does not bootstrap metadata discovery from a referential document exclusion follow-up over exact document context", () => {
@@ -126,4 +126,23 @@ describe("assistantTurnMeaningPolicy", () => {
expect(meaning.unsupported_but_understood_family).toBe("broad_business_evaluation");
expect(meaning.reason_codes).toContain("broad_business_evaluation_current_turn_signal");
});
it("lets explicit business overview wording beat turnover and net-flow cues", () => {
const policy = buildPolicy({
resolveAddressIntent: () => ({ intent: "customer_revenue_and_payments", confidence: "high" })
});
const meaning = policy.resolveAssistantTurnMeaning({
rawUserMessage:
"\u0414\u0430\u0439 \u0431\u0438\u0437\u043d\u0435\u0441-\u043e\u0431\u0437\u043e\u0440 \u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441: \u043e\u0431\u043e\u0440\u043e\u0442\u044b, \u043d\u0435\u0442\u0442\u043e, \u0430\u043a\u0442\u0438\u0432\u043d\u043e\u0441\u0442\u044c."
});
expect(meaning.explicit_intent_candidate).toBeNull();
expect(meaning.asked_domain_family).toBe("business_summary");
expect(meaning.asked_action_family).toBe("broad_evaluation");
expect(meaning.explicit_entity_candidates).toEqual([]);
expect(meaning.unsupported_but_understood_family).toBe("broad_business_evaluation");
expect(meaning.stale_replay_forbidden).toBe(true);
expect(meaning.reason_codes).toContain("broad_business_evaluation_current_turn_signal");
});
});