diff --git a/docs/ARCH/11 - architecture_turnaround/21 - current_status_canon_2026-05-01.md b/docs/ARCH/11 - architecture_turnaround/21 - current_status_canon_2026-05-01.md index 04c8299..c53f6ff 100644 --- a/docs/ARCH/11 - architecture_turnaround/21 - current_status_canon_2026-05-01.md +++ b/docs/ARCH/11 - architecture_turnaround/21 - current_status_canon_2026-05-01.md @@ -37,8 +37,9 @@ If another document says `78%`, `87%`, `92%`, or `85%` for a module that is now - Completed active slice: `Business Overview Inventory Reserve/Liquidation Boundary Bridge`: organization-level inventory reserve, write-off, obsolete-stock, and liquidation-value wording now routes to `business_overview`, while explicit item/stock lists stay in exact inventory routes with a reserve/liquidation proof boundary. - Completed active slice: `Business Overview Supplier/Procurement Quality Boundary Bridge`: organization-level supplier concentration, vendor-risk, dependency, and procurement-quality wording now routes to `business_overview`, while supplier payment/open-settlement/doc questions stay in exact supplier/payables routes with a vendor-risk proof boundary. - Completed active slice: `Business Overview Document/Account Activity Profile Bridge`: business overview now executes the reviewed `document_type_and_account_section_profile` recipe and surfaces confirmed operational activity mix without claiming process quality, accounting correctness, or complete 1C activity coverage. +- Completed active slice: `Business Overview Counterparty/Contract Profile Bridge`: business overview now executes reviewed `counterparty_population_and_roles` and `contract_usage_overview` recipes, surfacing active counterparty role split and contract usage without claiming CRM quality, counterparty due diligence, legal completeness, or contract-risk. - Next active slice: continue breadth into exact company-wide accounting profit/margin, real due-date debt aging, confirmed inventory reserve/write-off/liquidation evidence, and broader unfamiliar 1C route families only where reviewed evidence routes exist. -- Active module progress: `~97% (Open-World Bounded Autonomy Breadth)`. +- Active module progress: `~98% (Open-World Bounded Autonomy Breadth)`. ## Reporting Rule @@ -75,7 +76,7 @@ The project is not yet a universal arbitrary-1C agent. Remaining work belongs to the next breadth module: -- extend `business_overview` beyond money-flow/activity, customer and supplier concentration, document/account-section activity mix, yearly operating-flow dynamics, explicit profit/margin wording boundaries, explicit debt due-date wording boundaries, explicit inventory reserve/liquidation wording boundaries, explicit supplier/procurement-quality wording boundaries, explicit-period VAT/tax, as-of-date debt position, open-settlement concentration, contract-date debt age, debt staleness-risk proxy, as-of-date inventory position, trading-margin proxy, sales-to-stock inventory proxy, and warehouse staleness-risk proxy into separately proven exact accounting profit/margin, due-date debt aging/overdue, confirmed vendor-risk/procurement-quality analysis, and confirmed reserve/write-off/liquidation inventory evidence families; +- extend `business_overview` beyond money-flow/activity, customer and supplier concentration, document/account-section activity mix, counterparty role split, contract usage, yearly operating-flow dynamics, explicit profit/margin wording boundaries, explicit debt due-date wording boundaries, explicit inventory reserve/liquidation wording boundaries, explicit supplier/procurement-quality wording boundaries, explicit-period VAT/tax, as-of-date debt position, open-settlement concentration, contract-date debt age, debt staleness-risk proxy, as-of-date inventory position, trading-margin proxy, sales-to-stock inventory proxy, and warehouse staleness-risk proxy into separately proven exact accounting profit/margin, due-date debt aging/overdue, confirmed vendor-risk/procurement-quality analysis, and confirmed reserve/write-off/liquidation inventory evidence families; - broader dynamic schema traversal for unfamiliar 1C asks; - more primitive descriptors where live evidence proves a real gap; - more replay-backed domain packs that start from user business meaning, not from route convenience; diff --git a/docs/ARCH/11 - architecture_turnaround/22 - open_world_bounded_autonomy_breadth_2026-05-01.md b/docs/ARCH/11 - architecture_turnaround/22 - open_world_bounded_autonomy_breadth_2026-05-01.md index 89f5752..fa897ad 100644 --- a/docs/ARCH/11 - architecture_turnaround/22 - open_world_bounded_autonomy_breadth_2026-05-01.md +++ b/docs/ARCH/11 - architecture_turnaround/22 - open_world_bounded_autonomy_breadth_2026-05-01.md @@ -671,3 +671,29 @@ Local validation is accepted for this slice: - `npm.cmd run build`: passed. Graphify rebuild after Slice 23 code/doc sync: `6066 nodes`, `13222 edges`, `136 communities`. + +## Slice 24 - Business Overview Counterparty/Contract Profile Bridge + +This slice adds two more already-reviewed profile families to broad company analysis. + +After Slice 23, `business_overview` could describe operational activity mix by document type and account section. It still could not use two existing safe aggregate profiles that are important for a mature analyst answer: the size/role split of the counterparty base and basic contract usage. Both facts are useful business context, but neither should be inflated into CRM quality, due diligence, legal completeness, or contract-risk. + +Implemented now: + +- `business_overview` selects and executes the reviewed `counterparty_population_and_roles` recipe as an optional current-turn profile probe; +- the pilot derives `counterparty_profile` with total counterparties when available, active counterparties, customer-only count, supplier-only count, mixed-role count, and inactive/other count; +- `business_overview` selects and executes the reviewed `contract_usage_overview` recipe as an optional current-turn profile probe; +- the pilot derives `contract_usage_profile` with total contracts, used contracts, unused contracts, and used-contract share; +- evidence and answer drafting surface both profiles as confirmed management context; +- risk synthesis and headline wording can mention counterparty-base and contract-usage profiles when present; +- `must_not_claim` explicitly forbids treating these profiles as CRM quality, counterparty due diligence, contract-risk audit, or legal completeness. + +This is still not exact accounting profit, payment-term overdue analysis, vendor-risk due diligence, or inventory reserve/liquidation evidence. It is a safe broadening of company analysis through existing profile aggregates. + +Local validation is accepted for this slice: + +- `npm.cmd test -- assistantMcpDiscoveryPilotExecutor.test.ts assistantMcpDiscoveryAnswerAdapter.test.ts`: passed `66` with `1` skipped. +- `npm.cmd test -- addressQueryRuntimeM23.test.ts`: passed `416`. +- `npm.cmd run build`: passed. + +Graphify rebuild after Slice 24 code/doc sync: `6069 nodes`, `13230 edges`, `140 communities`. diff --git a/docs/ARCH/11 - architecture_turnaround/README.md b/docs/ARCH/11 - architecture_turnaround/README.md index cfc8c55..592e5d4 100644 --- a/docs/ARCH/11 - architecture_turnaround/README.md +++ b/docs/ARCH/11 - architecture_turnaround/README.md @@ -72,6 +72,7 @@ Status canon for planning: - The current completed breadth slice is `Business Overview Inventory Reserve/Liquidation Boundary Bridge`: organization-level inventory reserve, write-off, obsolete-stock, and liquidation-value wording now reaches `business_overview`, while explicit item/stock lists stay in exact inventory routes with a reserve/liquidation proof boundary. - The current completed breadth slice is `Business Overview Supplier/Procurement Quality Boundary Bridge`: organization-level supplier concentration, vendor-risk, dependency, and procurement-quality wording now reaches `business_overview`, while supplier payment/open-settlement/doc questions stay in exact supplier/payables routes with a vendor-risk proof boundary. - The current completed breadth slice is `Business Overview Document/Account Activity Profile Bridge`: business overview now executes the reviewed document-type/account-section profile and can surface confirmed operational activity mix without claiming process quality, accounting correctness, or full 1C coverage. +- The current completed breadth slice is `Business Overview Counterparty/Contract Profile Bridge`: business overview now executes reviewed counterparty population/roles and contract usage profiles, while CRM quality, counterparty due diligence, legal completeness, and contract-risk remain unclaimed. - The next active breadth slice continues breadth into exact company-wide accounting profit/margin, real due-date debt aging, confirmed reserve/write-off/liquidation inventory evidence, and broader unfamiliar 1C route families without relaxing truth boundaries. - The short source of truth for status wording is [21 - current_status_canon_2026-05-01.md](./21%20-%20current_status_canon_2026-05-01.md). @@ -137,11 +138,11 @@ Current honest status: - pre-multidomain readiness: `~90%` - bounded-autonomy foundation readiness: `~89%` - open-world bounded-autonomy readiness: `~87%` -- active Open-World Bounded Autonomy Breadth progress: `~97%`, with business-overview evidence fusion, the reviewed `business_overview` catalog/data-need/planner route-fabric slice, the fresh multi-probe runtime bridge, the explicit-period VAT/tax fact-family bridge, the explicit-period debt-position bridge, the explicit-date inventory-position bridge, the open-settlement quality bridge accepted by live semantic replay, selected-item profitability bridged by local semantic/runtime regression tests, contract-date debt age bridged locally, debt staleness-risk proxy bridged locally, debt due-date boundary arbitration bridged locally, inventory reserve/liquidation boundary arbitration bridged locally, supplier/procurement-quality boundary arbitration bridged locally, supplier concentration proxy bridged locally, document/account-section activity profile bridged locally, yearly operating-flow proxy bridged locally, earnings/best-year wording arbitration bridged locally, profit/margin wording boundary arbitration bridged locally, analyst synthesis added to business-overview answer drafting, company-period trading margin proxy bridged locally, inventory sales-to-stock proxy bridged locally, inventory staleness-risk proxy bridged locally, and gap-specific answer shaping bridged locally; exact accounting profit/margin, true due-date debt aging/overdue, confirmed vendor-risk/procurement-quality analysis, and confirmed reserve/write-off/liquidation inventory evidence are still pending +- active Open-World Bounded Autonomy Breadth progress: `~98%`, with business-overview evidence fusion, the reviewed `business_overview` catalog/data-need/planner route-fabric slice, the fresh multi-probe runtime bridge, the explicit-period VAT/tax fact-family bridge, the explicit-period debt-position bridge, the explicit-date inventory-position bridge, the open-settlement quality bridge accepted by live semantic replay, selected-item profitability bridged by local semantic/runtime regression tests, contract-date debt age bridged locally, debt staleness-risk proxy bridged locally, debt due-date boundary arbitration bridged locally, inventory reserve/liquidation boundary arbitration bridged locally, supplier/procurement-quality boundary arbitration bridged locally, supplier concentration proxy bridged locally, document/account-section activity profile bridged locally, counterparty population/roles and contract usage profiles bridged locally, yearly operating-flow proxy bridged locally, earnings/best-year wording arbitration bridged locally, profit/margin wording boundary arbitration bridged locally, analyst synthesis added to business-overview answer drafting, company-period trading margin proxy bridged locally, inventory sales-to-stock proxy bridged locally, inventory staleness-risk proxy bridged locally, and gap-specific answer shaping bridged locally; exact accounting profit/margin, true due-date debt aging/overdue, confirmed vendor-risk/procurement-quality analysis, and confirmed reserve/write-off/liquidation inventory evidence are still pending - Post-F semantic integrity module progress: `~99%` operationally closed, with remaining risk now treated as next-slice discovery rather than an open blocker inside the closed slice - active inventory-stock breadth slice progress: `100%` for the declared scenario pack, not for arbitrary inventory questions - Planner Autonomy Consolidation progress: `100%` for the declared module, with catalog-fabric, value-flow arbitration, lifecycle bounded inference, broad-evaluation bridge, inventory catalog templates, inventory runtime-boundary honesty, exact inventory recipe bridging, unambiguous metadata-surface lane inference, catalog chain-template scoring, structured chain-match contract exposure, runtime/debug propagation, subject-aware bidirectional comparison arbitration, structured catalog-alignment verdicts, representative alignment regression guard, catalog-alignment reason-code telemetry, explicit `alignment_status` propagation, truth-harness/acceptance-matrix surfacing, soft divergence warning, `catalog_alignment_ok` acceptance invariant, step-level expected catalog-alignment assertions, phase66 and phase32 spec alignment expectations, AGENT source-catalog surfacing, generated phase83 mixed planner-brain replay spec, checked-source user-facing error sanitation, surface-grounded catalog promotion, and guarded live phase83 acceptance validated. Broader unfamiliar 1C asks are now next-module breadth work rather than an open blocker inside this declared slice -- graph snapshot after latest rebuild: `6066 nodes`, `13222 edges`, `136 communities` +- graph snapshot after latest rebuild: `6069 nodes`, `13230 edges`, `140 communities` - current regression-gate breakpoint: - the validated hot paths are no longer structurally broken; - flagship continuity collapse is no longer the primary risk; diff --git a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryAnswerAdapter.js b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryAnswerAdapter.js index d5a049c..6fee3c7 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryAnswerAdapter.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryAnswerAdapter.js @@ -402,6 +402,12 @@ function headlineFor(mode, pilot) { if (overview.document_activity_profile) { families.push("профиль типов документов и разделов учета"); } + if (overview.counterparty_profile) { + families.push("профиль контрагентской базы"); + } + if (overview.contract_usage_profile) { + families.push("договорной профиль"); + } if (overview.tax_position) { families.push("НДС-позиция"); } @@ -625,6 +631,7 @@ function buildMustNotClaim(pilot) { claims.push("Do not present business overview trading-margin proxy as clean profit, accounting financial result, or exact cost-of-sales margin."); claims.push("Do not present business overview supplier concentration as vendor-risk audit, procurement quality, or full expense structure."); claims.push("Do not present business overview document/account-section activity profile as process quality, accounting correctness, or completeness of all 1C activity."); + claims.push("Do not present business overview counterparty or contract profile as CRM quality, counterparty due diligence, contract-risk audit, or legal completeness."); 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."); @@ -998,6 +1005,21 @@ function derivedBusinessOverviewConfirmedLines(pilot) { lines.push(`Профиль операционной активности${organization}${period}: ${parts.join("; ")}. Это activity mix по найденным строкам 1С, а не аудит качества учета или полноты процессов.`); } } + if (overview.counterparty_profile) { + const profile = overview.counterparty_profile; + const totalText = profile.total_counterparties === null + ? `${profile.active_counterparties} активных контрагентов` + : `${profile.total_counterparties} контрагентов в базе, ${profile.active_counterparties} активных по документальной активности`; + lines.push(`Профиль контрагентской базы${organization}${period}: ${totalText}; заказчики ${profile.customer_only_count}, поставщики ${profile.supplier_only_count}, смешанная роль ${profile.mixed_role_count}. Это не CRM-аудит, не юридическая проверка контрагентов и не оценка качества клиентской базы.`); + } + if (overview.contract_usage_profile) { + const profile = overview.contract_usage_profile; + const totalText = profile.total_contracts === null + ? `${profile.used_contracts} договоров с подтвержденной связью с операциями` + : `${profile.used_contracts} из ${profile.total_contracts} договоров используются${profile.used_contract_share_pct === null ? "" : ` (${profile.used_contract_share_pct}%)`}`; + const unusedText = profile.unused_contracts === null ? "" : `, неиспользуемых ${profile.unused_contracts}`; + lines.push(`Договорной профиль${organization}${period}: ${totalText}${unusedText}. Это не contract-risk аудит, не проверка актуальности условий и не доказательство качества договорной базы.`); + } if (overview.tax_position) { const taxDirection = overview.tax_position.net_vat_direction === "vat_to_pay" ? "к уплате" @@ -1170,6 +1192,19 @@ function businessOverviewRiskSynthesisLine(overview) { signals.push(`операционный activity mix: ${parts.join(", ")}`); } } + if (overview.counterparty_profile) { + const totalText = overview.counterparty_profile.total_counterparties === null + ? `${overview.counterparty_profile.active_counterparties} активных контрагентов` + : `${overview.counterparty_profile.total_counterparties} контрагентов, активных ${overview.counterparty_profile.active_counterparties}`; + signals.push(`контрагентская база: ${totalText}`); + } + if (overview.contract_usage_profile) { + const profile = overview.contract_usage_profile; + const totalText = profile.total_contracts === null + ? `${profile.used_contracts} используемых договоров` + : `${profile.used_contracts}/${profile.total_contracts} договоров используются${profile.used_contract_share_pct === null ? "" : ` (${profile.used_contract_share_pct}%)`}`; + signals.push(`договорной профиль: ${totalText}`); + } if (overview.inventory_position) { signals.push(`складской остаток на дату ${overview.inventory_position.total_amount_human_ru}`); if (overview.inventory_position.aging_signal?.max_age_days !== null && overview.inventory_position.aging_signal?.max_age_days !== undefined) { @@ -1199,8 +1234,8 @@ function businessOverviewExecutiveVerdictLine(overview) { overview.inventory_position || overview.inventory_turnover_proxy || overview.inventory_staleness_risk_proxy); - const hasDocumentActivitySignal = Boolean(overview.document_activity_profile); - const hasExtraSignals = hasTaxDebtInventorySignals || hasDocumentActivitySignal; + const hasOperationalProfileSignal = Boolean(overview.document_activity_profile || overview.counterparty_profile || overview.contract_usage_profile); + const hasExtraSignals = hasTaxDebtInventorySignals || hasOperationalProfileSignal; if (!hasCash && !hasExtraSignals) { return null; } @@ -1211,8 +1246,8 @@ function businessOverviewExecutiveVerdictLine(overview) { : "операционный поток выглядит сбалансированным"; const evidenceTone = hasTaxDebtInventorySignals ? "часть налоговых, долговых или складских контуров уже отдельно проверена" - : hasDocumentActivitySignal - ? "операционный activity mix по типам документов и разделам учета уже отдельно проверен" + : hasOperationalProfileSignal + ? "операционные профили по документам, контрагентам или договорам уже отдельно проверены" : "налоги, долги и склад еще не дают проверенного управленческого контекста"; return `Сводный LLM-аудит по подтвержденному: ${cashTone}; ${evidenceTone}. Это полезный управленческий срез по найденным строкам 1С, но не финальный вывод о прибыльности, марже или здоровье компании.`; } @@ -1290,6 +1325,12 @@ function buildAssistantMcpDiscoveryAnswerDraft(pilot) { if (pilot.derived_business_overview?.document_activity_profile) { pushReason(reasonCodes, "answer_contains_business_overview_document_activity_profile"); } + if (pilot.derived_business_overview?.counterparty_profile) { + pushReason(reasonCodes, "answer_contains_business_overview_counterparty_profile"); + } + if (pilot.derived_business_overview?.contract_usage_profile) { + pushReason(reasonCodes, "answer_contains_business_overview_contract_usage_profile"); + } if (pilot.derived_business_overview?.debt_position) { pushReason(reasonCodes, "answer_contains_business_overview_debt_position"); } diff --git a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPilotExecutor.js b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPilotExecutor.js index 952908b..62a9999 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPilotExecutor.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPilotExecutor.js @@ -160,7 +160,7 @@ function buildValueFlowFilters(planner) { sort: "period_asc" }; } -function buildBusinessOverviewDocumentActivityFilters(planner) { +function buildBusinessOverviewProfileFilters(planner) { const meaning = planner.discovery_plan.turn_meaning_ref; const organization = toNonEmptyString(meaning?.explicit_organization_scope); const dateScope = toNonEmptyString(meaning?.explicit_date_scope); @@ -1916,6 +1916,64 @@ function deriveBusinessOverviewDocumentActivityProfile(result, periodScope) { inference_basis: "document_type_and_account_section_profile_confirmed_1c_rows" }; } +function sumBusinessOverviewMarker(result, marker) { + return result.rows.reduce((sum, row) => { + const currentMarker = normalizeBusinessOverviewActivityMarker(row); + if (currentMarker !== marker) { + return sum; + } + const amount = rowAmountValue(row); + return amount === null ? sum : sum + amount; + }, 0); +} +function deriveBusinessOverviewCounterpartyProfile(result, periodScope) { + if (!result || result.error || result.matched_rows <= 0) { + return null; + } + const totalCounterparties = sumBusinessOverviewMarker(result, "CP_TOTAL"); + const customerActive = sumBusinessOverviewMarker(result, "CP_CUSTOMER_ACTIVE"); + const supplierActive = sumBusinessOverviewMarker(result, "CP_SUPPLIER_ACTIVE"); + const mixedActive = sumBusinessOverviewMarker(result, "CP_MIXED_ACTIVE"); + const activeUnion = sumBusinessOverviewMarker(result, "CP_ACTIVE_UNION"); + const customerOnly = Math.max(0, customerActive - mixedActive); + const supplierOnly = Math.max(0, supplierActive - mixedActive); + const resolvedActive = customerOnly + supplierOnly + mixedActive; + const activeCounterparties = Math.max(activeUnion, resolvedActive); + if (totalCounterparties <= 0 && activeCounterparties <= 0) { + return null; + } + return { + period_scope: periodScope, + rows_matched: result.matched_rows, + total_counterparties: totalCounterparties > 0 ? totalCounterparties : null, + active_counterparties: activeCounterparties, + customer_only_count: customerOnly, + supplier_only_count: supplierOnly, + mixed_role_count: mixedActive, + other_or_inactive_count: totalCounterparties > 0 ? Math.max(0, totalCounterparties - resolvedActive) : null, + inference_basis: "counterparty_population_roles_confirmed_1c_rows" + }; +} +function deriveBusinessOverviewContractUsageProfile(result, periodScope) { + if (!result || result.error || result.matched_rows <= 0) { + return null; + } + const totalContracts = sumBusinessOverviewMarker(result, "CT_TOTAL"); + const usedContracts = sumBusinessOverviewMarker(result, "CT_USED"); + if (totalContracts <= 0 && usedContracts <= 0) { + return null; + } + const cappedUsedContracts = totalContracts > 0 ? Math.min(usedContracts, totalContracts) : usedContracts; + return { + period_scope: periodScope, + rows_matched: result.matched_rows, + total_contracts: totalContracts > 0 ? totalContracts : null, + used_contracts: usedContracts, + unused_contracts: totalContracts > 0 ? Math.max(0, totalContracts - cappedUsedContracts) : null, + used_contract_share_pct: totalContracts > 0 ? percentageOfTotal(cappedUsedContracts, totalContracts) : null, + inference_basis: "contract_usage_overview_confirmed_1c_rows" + }; +} function deriveValueFlow(result, counterparty, periodScope, direction, aggregationAxis) { if (!result || result.error || result.matched_rows <= 0) { return null; @@ -2714,6 +2772,8 @@ function deriveBusinessOverview(input) { debtAsOfDate: input.debtAsOfDate }); const documentActivityProfile = deriveBusinessOverviewDocumentActivityProfile(input.documentActivityProfileResult, input.periodScope); + const counterpartyProfile = deriveBusinessOverviewCounterpartyProfile(input.counterpartyProfileResult, input.periodScope); + const contractUsageProfile = deriveBusinessOverviewContractUsageProfile(input.contractUsageProfileResult, input.periodScope); const debtStalenessRiskProxy = deriveBusinessOverviewDebtStalenessRiskProxy(debtOpenSettlementQuality); const inventoryPosition = deriveBusinessOverviewInventoryPosition({ inventoryOnHandResult: input.inventoryOnHandResult, @@ -2738,6 +2798,8 @@ function deriveBusinessOverview(input) { Boolean(debtOpenSettlementQuality), Boolean(debtStalenessRiskProxy), Boolean(documentActivityProfile), + Boolean(counterpartyProfile), + Boolean(contractUsageProfile), Boolean(inventoryPosition), Boolean(inventoryTurnoverProxy), Boolean(inventoryStalenessRiskProxy) @@ -2746,6 +2808,7 @@ function deriveBusinessOverview(input) { return null; } const netAmount = incoming.total_amount - outgoing.total_amount; + const hasBusinessOverviewProfileSignal = Boolean(documentActivityProfile || counterpartyProfile || contractUsageProfile); return { organization_scope: input.organizationScope, period_scope: input.periodScope, @@ -2767,6 +2830,8 @@ function deriveBusinessOverview(input) { inventory_turnover_proxy: inventoryTurnoverProxy, inventory_staleness_risk_proxy: inventoryStalenessRiskProxy, document_activity_profile: documentActivityProfile, + counterparty_profile: counterpartyProfile, + contract_usage_profile: contractUsageProfile, coverage_limited_by_probe_limit: incoming.coverage_limited_by_probe_limit || outgoing.coverage_limited_by_probe_limit, checked_signal_count: checkedSignalCount, missing_signal_families: [ @@ -2783,19 +2848,17 @@ function deriveBusinessOverview(input) { : "inventory_position", inventoryPosition?.aging_signal ? null : "inventory_aging_quality" ].filter((item) => Boolean(item)), - inference_basis: documentActivityProfile + inference_basis: hasBusinessOverviewProfileSignal || inventoryPosition ? "business_overview_from_confirmed_1c_multi_family_rows" - : inventoryPosition + : debtOpenSettlementQuality ? "business_overview_from_confirmed_1c_multi_family_rows" - : debtOpenSettlementQuality - ? "business_overview_from_confirmed_1c_multi_family_rows" - : taxPosition && debtPosition - ? "business_overview_from_confirmed_1c_money_activity_tax_and_debt_rows" - : taxPosition - ? "business_overview_from_confirmed_1c_money_activity_and_tax_rows" - : debtPosition - ? "business_overview_from_confirmed_1c_money_activity_and_debt_rows" - : "business_overview_from_confirmed_1c_money_and_activity_rows" + : taxPosition && debtPosition + ? "business_overview_from_confirmed_1c_money_activity_tax_and_debt_rows" + : taxPosition + ? "business_overview_from_confirmed_1c_money_activity_and_tax_rows" + : debtPosition + ? "business_overview_from_confirmed_1c_money_activity_and_debt_rows" + : "business_overview_from_confirmed_1c_money_and_activity_rows" }; } function summarizeBusinessOverviewRows(input) { @@ -2827,6 +2890,12 @@ function summarizeBusinessOverviewRows(input) { if (input.documentActivityProfileResult && !input.documentActivityProfileResult.error) { parts.push(`${input.documentActivityProfileResult.fetched_rows} document/account-section profile rows fetched, ${input.documentActivityProfileResult.matched_rows} matched`); } + if (input.counterpartyProfileResult && !input.counterpartyProfileResult.error) { + parts.push(`${input.counterpartyProfileResult.fetched_rows} counterparty role-profile rows fetched, ${input.counterpartyProfileResult.matched_rows} matched`); + } + if (input.contractUsageProfileResult && !input.contractUsageProfileResult.error) { + parts.push(`${input.contractUsageProfileResult.fetched_rows} contract-usage profile rows fetched, ${input.contractUsageProfileResult.matched_rows} matched`); + } if (input.inventoryOnHandResult && !input.inventoryOnHandResult.error) { parts.push(`${input.inventoryOnHandResult.fetched_rows} inventory on-hand rows fetched, ${input.inventoryOnHandResult.matched_rows} matched`); } @@ -2879,6 +2948,21 @@ function buildBusinessOverviewConfirmedFacts(derived) { facts.push(`Профиль операционной активности${organization}${period} подтвержден по типам документов и разделам учета 1С: ${parts.join("; ")}. Это activity mix, а не аудит качества учета или полноты бизнес-процессов.`); } } + if (derived.counterparty_profile) { + const profile = derived.counterparty_profile; + const totalText = profile.total_counterparties === null + ? `${profile.active_counterparties} активных контрагентов` + : `${profile.total_counterparties} контрагентов в базе, ${profile.active_counterparties} активных по документальной активности`; + facts.push(`Профиль контрагентской базы${organization}${period} подтвержден по 1С: ${totalText}; заказчики ${profile.customer_only_count}, поставщики ${profile.supplier_only_count}, смешанная роль ${profile.mixed_role_count}. Это не CRM-аудит, не юридическая проверка контрагентов и не оценка качества клиентской базы.`); + } + if (derived.contract_usage_profile) { + const profile = derived.contract_usage_profile; + const totalText = profile.total_contracts === null + ? `${profile.used_contracts} договоров с подтвержденной связью с операциями` + : `${profile.used_contracts} из ${profile.total_contracts} договоров используются${profile.used_contract_share_pct === null ? "" : ` (${profile.used_contract_share_pct}%)`}`; + const unusedText = profile.unused_contracts === null ? "" : `, неиспользуемых ${profile.unused_contracts}`; + facts.push(`Договорной профиль${organization}${period} подтвержден по 1С: ${totalText}${unusedText}. Это не contract-risk аудит, не проверка актуальности условий и не доказательство качества договорной базы.`); + } if (derived.tax_position) { const taxDirection = derived.tax_position.net_vat_direction === "vat_to_pay" ? "к уплате" @@ -3643,11 +3727,13 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) { let payablesResult = null; let openContractsResult = null; let documentActivityProfileResult = null; + let counterpartyProfileResult = null; + let contractUsageProfileResult = null; let inventoryOnHandResult = null; let inventoryAgingResult = null; const valueFilters = buildValueFlowFilters(planner); const lifecycleFilters = buildLifecycleFilters(planner); - const documentActivityFilters = buildBusinessOverviewDocumentActivityFilters(planner); + const profileFilters = buildBusinessOverviewProfileFilters(planner); const taxFilters = buildBusinessOverviewTaxFilters(planner); const tradingMarginFilters = buildBusinessOverviewTradingMarginFilters(planner); const debtFilters = buildBusinessOverviewDebtFilters(planner); @@ -3657,7 +3743,9 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) { const incomingSelection = (0, addressRecipeCatalog_1.selectAddressRecipe)("customer_revenue_and_payments", valueFilters); const outgoingSelection = (0, addressRecipeCatalog_1.selectAddressRecipe)("supplier_payouts_profile", valueFilters); const lifecycleSelection = (0, addressRecipeCatalog_1.selectAddressRecipe)("counterparty_activity_lifecycle", lifecycleFilters); - const documentActivitySelection = (0, addressRecipeCatalog_1.selectAddressRecipe)("document_type_and_account_section_profile", documentActivityFilters); + const documentActivitySelection = (0, addressRecipeCatalog_1.selectAddressRecipe)("document_type_and_account_section_profile", profileFilters); + const counterpartyProfileSelection = (0, addressRecipeCatalog_1.selectAddressRecipe)("counterparty_population_and_roles", profileFilters); + const contractUsageSelection = (0, addressRecipeCatalog_1.selectAddressRecipe)("contract_usage_overview", profileFilters); const taxSelection = taxFilters ? (0, addressRecipeCatalog_1.selectAddressRecipe)("vat_liability_confirmed_for_tax_period", taxFilters) : null; @@ -3716,6 +3804,20 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) { pushReason(reasonCodes, "pilot_business_overview_document_activity_profile_recipe_not_available"); pushUnique(queryLimitations, "Business overview document/account-section profile requires an executable document-section profile recipe"); } + if (counterpartyProfileSelection.selected_recipe) { + pushReason(reasonCodes, "pilot_business_overview_counterparty_profile_recipe_selected"); + } + else { + pushReason(reasonCodes, "pilot_business_overview_counterparty_profile_recipe_not_available"); + pushUnique(queryLimitations, "Business overview counterparty profile requires an executable counterparty population/roles recipe"); + } + if (contractUsageSelection.selected_recipe) { + pushReason(reasonCodes, "pilot_business_overview_contract_usage_profile_recipe_selected"); + } + else { + pushReason(reasonCodes, "pilot_business_overview_contract_usage_profile_recipe_not_available"); + pushUnique(queryLimitations, "Business overview contract usage profile requires an executable contract usage recipe"); + } if (taxSelection?.selected_recipe) { pushReason(reasonCodes, "pilot_business_overview_tax_recipe_selected"); } @@ -3940,7 +4042,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) { probeResults.push(queryResultToProbeResult(step.primitive_id, tradingMarginResult)); } if (documentActivitySelection.selected_recipe) { - const documentActivityPlan = (0, addressRecipeCatalog_1.buildAddressRecipePlan)(documentActivitySelection.selected_recipe, documentActivityFilters); + const documentActivityPlan = (0, addressRecipeCatalog_1.buildAddressRecipePlan)(documentActivitySelection.selected_recipe, profileFilters); documentActivityProfileResult = await runtimeDeps.executeAddressMcpQuery({ query: documentActivityPlan.query, limit: documentActivityPlan.limit, @@ -3948,6 +4050,24 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) { }); probeResults.push(queryResultToProbeResult(step.primitive_id, documentActivityProfileResult)); } + if (counterpartyProfileSelection.selected_recipe) { + const counterpartyProfilePlan = (0, addressRecipeCatalog_1.buildAddressRecipePlan)(counterpartyProfileSelection.selected_recipe, profileFilters); + counterpartyProfileResult = await runtimeDeps.executeAddressMcpQuery({ + query: counterpartyProfilePlan.query, + limit: counterpartyProfilePlan.limit, + account_scope: counterpartyProfilePlan.account_scope + }); + probeResults.push(queryResultToProbeResult(step.primitive_id, counterpartyProfileResult)); + } + if (contractUsageSelection.selected_recipe) { + const contractUsagePlan = (0, addressRecipeCatalog_1.buildAddressRecipePlan)(contractUsageSelection.selected_recipe, profileFilters); + contractUsageProfileResult = await runtimeDeps.executeAddressMcpQuery({ + query: contractUsagePlan.query, + limit: contractUsagePlan.limit, + account_scope: contractUsagePlan.account_scope + }); + probeResults.push(queryResultToProbeResult(step.primitive_id, contractUsageProfileResult)); + } if (lifecycleResult.error) { pushUnique(queryLimitations, lifecycleResult.error); pushReason(reasonCodes, "pilot_business_overview_query_documents_mcp_error"); @@ -3969,6 +4089,20 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) { else if (documentActivityProfileResult) { pushReason(reasonCodes, "pilot_business_overview_document_activity_profile_query_mcp_executed"); } + if (counterpartyProfileResult?.error) { + pushUnique(queryLimitations, counterpartyProfileResult.error); + pushReason(reasonCodes, "pilot_business_overview_counterparty_profile_query_mcp_error"); + } + else if (counterpartyProfileResult) { + pushReason(reasonCodes, "pilot_business_overview_counterparty_profile_query_mcp_executed"); + } + if (contractUsageProfileResult?.error) { + pushUnique(queryLimitations, contractUsageProfileResult.error); + pushReason(reasonCodes, "pilot_business_overview_contract_usage_profile_query_mcp_error"); + } + else if (contractUsageProfileResult) { + pushReason(reasonCodes, "pilot_business_overview_contract_usage_profile_query_mcp_executed"); + } continue; } skippedPrimitives.push(step.primitive_id); @@ -3984,6 +4118,8 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) { payablesResult, openContractsResult, documentActivityProfileResult, + counterpartyProfileResult, + contractUsageProfileResult, debtAsOfDate, inventoryOnHandResult, inventoryAgingResult, @@ -4008,6 +4144,12 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) { if (derivedBusinessOverview.document_activity_profile) { pushReason(reasonCodes, "pilot_derived_business_overview_document_activity_profile_from_confirmed_rows"); } + if (derivedBusinessOverview.counterparty_profile) { + pushReason(reasonCodes, "pilot_derived_business_overview_counterparty_profile_from_confirmed_rows"); + } + if (derivedBusinessOverview.contract_usage_profile) { + pushReason(reasonCodes, "pilot_derived_business_overview_contract_usage_profile_from_confirmed_rows"); + } if (derivedBusinessOverview.tax_position) { pushReason(reasonCodes, "pilot_derived_business_overview_tax_position_from_confirmed_rows"); } @@ -4046,6 +4188,8 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) { payablesResult, openContractsResult, documentActivityProfileResult, + counterpartyProfileResult, + contractUsageProfileResult, inventoryOnHandResult, inventoryAgingResult }); diff --git a/llm_normalizer/backend/src/services/assistantMcpDiscoveryAnswerAdapter.ts b/llm_normalizer/backend/src/services/assistantMcpDiscoveryAnswerAdapter.ts index f32e559..26eff98 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryAnswerAdapter.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryAnswerAdapter.ts @@ -501,6 +501,12 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD if (overview.document_activity_profile) { families.push("профиль типов документов и разделов учета"); } + if (overview.counterparty_profile) { + families.push("профиль контрагентской базы"); + } + if (overview.contract_usage_profile) { + families.push("договорной профиль"); + } if (overview.tax_position) { families.push("НДС-позиция"); } @@ -734,6 +740,7 @@ function buildMustNotClaim(pilot: AssistantMcpDiscoveryPilotExecutionContract): claims.push("Do not present business overview trading-margin proxy as clean profit, accounting financial result, or exact cost-of-sales margin."); claims.push("Do not present business overview supplier concentration as vendor-risk audit, procurement quality, or full expense structure."); claims.push("Do not present business overview document/account-section activity profile as process quality, accounting correctness, or completeness of all 1C activity."); + claims.push("Do not present business overview counterparty or contract profile as CRM quality, counterparty due diligence, contract-risk audit, or legal completeness."); 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."); @@ -1164,6 +1171,25 @@ function derivedBusinessOverviewConfirmedLines(pilot: AssistantMcpDiscoveryPilot ); } } + if (overview.counterparty_profile) { + const profile = overview.counterparty_profile; + const totalText = profile.total_counterparties === null + ? `${profile.active_counterparties} активных контрагентов` + : `${profile.total_counterparties} контрагентов в базе, ${profile.active_counterparties} активных по документальной активности`; + lines.push( + `Профиль контрагентской базы${organization}${period}: ${totalText}; заказчики ${profile.customer_only_count}, поставщики ${profile.supplier_only_count}, смешанная роль ${profile.mixed_role_count}. Это не CRM-аудит, не юридическая проверка контрагентов и не оценка качества клиентской базы.` + ); + } + if (overview.contract_usage_profile) { + const profile = overview.contract_usage_profile; + const totalText = profile.total_contracts === null + ? `${profile.used_contracts} договоров с подтвержденной связью с операциями` + : `${profile.used_contracts} из ${profile.total_contracts} договоров используются${profile.used_contract_share_pct === null ? "" : ` (${profile.used_contract_share_pct}%)`}`; + const unusedText = profile.unused_contracts === null ? "" : `, неиспользуемых ${profile.unused_contracts}`; + lines.push( + `Договорной профиль${organization}${period}: ${totalText}${unusedText}. Это не contract-risk аудит, не проверка актуальности условий и не доказательство качества договорной базы.` + ); + } if (overview.tax_position) { const taxDirection = overview.tax_position.net_vat_direction === "vat_to_pay" @@ -1367,6 +1393,19 @@ function businessOverviewRiskSynthesisLine(overview: BusinessOverview): string | signals.push(`операционный activity mix: ${parts.join(", ")}`); } } + if (overview.counterparty_profile) { + const totalText = overview.counterparty_profile.total_counterparties === null + ? `${overview.counterparty_profile.active_counterparties} активных контрагентов` + : `${overview.counterparty_profile.total_counterparties} контрагентов, активных ${overview.counterparty_profile.active_counterparties}`; + signals.push(`контрагентская база: ${totalText}`); + } + if (overview.contract_usage_profile) { + const profile = overview.contract_usage_profile; + const totalText = profile.total_contracts === null + ? `${profile.used_contracts} используемых договоров` + : `${profile.used_contracts}/${profile.total_contracts} договоров используются${profile.used_contract_share_pct === null ? "" : ` (${profile.used_contract_share_pct}%)`}`; + signals.push(`договорной профиль: ${totalText}`); + } if (overview.inventory_position) { signals.push(`складской остаток на дату ${overview.inventory_position.total_amount_human_ru}`); if (overview.inventory_position.aging_signal?.max_age_days !== null && overview.inventory_position.aging_signal?.max_age_days !== undefined) { @@ -1401,8 +1440,10 @@ function businessOverviewExecutiveVerdictLine(overview: BusinessOverview): strin overview.inventory_turnover_proxy || overview.inventory_staleness_risk_proxy ); - const hasDocumentActivitySignal = Boolean(overview.document_activity_profile); - const hasExtraSignals = hasTaxDebtInventorySignals || hasDocumentActivitySignal; + const hasOperationalProfileSignal = Boolean( + overview.document_activity_profile || overview.counterparty_profile || overview.contract_usage_profile + ); + const hasExtraSignals = hasTaxDebtInventorySignals || hasOperationalProfileSignal; if (!hasCash && !hasExtraSignals) { return null; } @@ -1414,8 +1455,8 @@ function businessOverviewExecutiveVerdictLine(overview: BusinessOverview): strin : "операционный поток выглядит сбалансированным"; const evidenceTone = hasTaxDebtInventorySignals ? "часть налоговых, долговых или складских контуров уже отдельно проверена" - : hasDocumentActivitySignal - ? "операционный activity mix по типам документов и разделам учета уже отдельно проверен" + : hasOperationalProfileSignal + ? "операционные профили по документам, контрагентам или договорам уже отдельно проверены" : "налоги, долги и склад еще не дают проверенного управленческого контекста"; return `Сводный LLM-аудит по подтвержденному: ${cashTone}; ${evidenceTone}. Это полезный управленческий срез по найденным строкам 1С, но не финальный вывод о прибыльности, марже или здоровье компании.`; } @@ -1501,6 +1542,12 @@ export function buildAssistantMcpDiscoveryAnswerDraft( if (pilot.derived_business_overview?.document_activity_profile) { pushReason(reasonCodes, "answer_contains_business_overview_document_activity_profile"); } + if (pilot.derived_business_overview?.counterparty_profile) { + pushReason(reasonCodes, "answer_contains_business_overview_counterparty_profile"); + } + if (pilot.derived_business_overview?.contract_usage_profile) { + pushReason(reasonCodes, "answer_contains_business_overview_contract_usage_profile"); + } if (pilot.derived_business_overview?.debt_position) { pushReason(reasonCodes, "answer_contains_business_overview_debt_position"); } diff --git a/llm_normalizer/backend/src/services/assistantMcpDiscoveryPilotExecutor.ts b/llm_normalizer/backend/src/services/assistantMcpDiscoveryPilotExecutor.ts index eba8720..77af3da 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryPilotExecutor.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryPilotExecutor.ts @@ -160,6 +160,28 @@ export interface AssistantMcpDiscoveryDerivedBusinessOverviewDocumentActivityPro inference_basis: "document_type_and_account_section_profile_confirmed_1c_rows"; } +export interface AssistantMcpDiscoveryDerivedBusinessOverviewCounterpartyProfile { + period_scope: string | null; + rows_matched: number; + total_counterparties: number | null; + active_counterparties: number; + customer_only_count: number; + supplier_only_count: number; + mixed_role_count: number; + other_or_inactive_count: number | null; + inference_basis: "counterparty_population_roles_confirmed_1c_rows"; +} + +export interface AssistantMcpDiscoveryDerivedBusinessOverviewContractUsageProfile { + period_scope: string | null; + rows_matched: number; + total_contracts: number | null; + used_contracts: number; + unused_contracts: number | null; + used_contract_share_pct: number | null; + inference_basis: "contract_usage_overview_confirmed_1c_rows"; +} + export interface AssistantMcpDiscoveryDerivedBidirectionalValueFlow { counterparty: string | null; period_scope: string | null; @@ -197,6 +219,8 @@ export interface AssistantMcpDiscoveryDerivedBusinessOverview { inventory_turnover_proxy: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryTurnoverProxy | null; inventory_staleness_risk_proxy: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryStalenessRiskProxy | null; document_activity_profile: AssistantMcpDiscoveryDerivedBusinessOverviewDocumentActivityProfile | null; + counterparty_profile: AssistantMcpDiscoveryDerivedBusinessOverviewCounterpartyProfile | null; + contract_usage_profile: AssistantMcpDiscoveryDerivedBusinessOverviewContractUsageProfile | null; coverage_limited_by_probe_limit: boolean; checked_signal_count: number; missing_signal_families: string[]; @@ -638,7 +662,7 @@ function buildValueFlowFilters(planner: AssistantMcpDiscoveryPlannerContract): A }; } -function buildBusinessOverviewDocumentActivityFilters(planner: AssistantMcpDiscoveryPlannerContract): AddressFilterSet { +function buildBusinessOverviewProfileFilters(planner: AssistantMcpDiscoveryPlannerContract): AddressFilterSet { const meaning = planner.discovery_plan.turn_meaning_ref; const organization = toNonEmptyString(meaning?.explicit_organization_scope); const dateScope = toNonEmptyString(meaning?.explicit_date_scope); @@ -2717,6 +2741,73 @@ function deriveBusinessOverviewDocumentActivityProfile( }; } +function sumBusinessOverviewMarker(result: AddressMcpQueryExecutorResult, marker: string): number { + return result.rows.reduce((sum, row) => { + const currentMarker = normalizeBusinessOverviewActivityMarker(row); + if (currentMarker !== marker) { + return sum; + } + const amount = rowAmountValue(row); + return amount === null ? sum : sum + amount; + }, 0); +} + +function deriveBusinessOverviewCounterpartyProfile( + result: AddressMcpQueryExecutorResult | null, + periodScope: string | null +): AssistantMcpDiscoveryDerivedBusinessOverviewCounterpartyProfile | null { + if (!result || result.error || result.matched_rows <= 0) { + return null; + } + const totalCounterparties = sumBusinessOverviewMarker(result, "CP_TOTAL"); + const customerActive = sumBusinessOverviewMarker(result, "CP_CUSTOMER_ACTIVE"); + const supplierActive = sumBusinessOverviewMarker(result, "CP_SUPPLIER_ACTIVE"); + const mixedActive = sumBusinessOverviewMarker(result, "CP_MIXED_ACTIVE"); + const activeUnion = sumBusinessOverviewMarker(result, "CP_ACTIVE_UNION"); + const customerOnly = Math.max(0, customerActive - mixedActive); + const supplierOnly = Math.max(0, supplierActive - mixedActive); + const resolvedActive = customerOnly + supplierOnly + mixedActive; + const activeCounterparties = Math.max(activeUnion, resolvedActive); + if (totalCounterparties <= 0 && activeCounterparties <= 0) { + return null; + } + return { + period_scope: periodScope, + rows_matched: result.matched_rows, + total_counterparties: totalCounterparties > 0 ? totalCounterparties : null, + active_counterparties: activeCounterparties, + customer_only_count: customerOnly, + supplier_only_count: supplierOnly, + mixed_role_count: mixedActive, + other_or_inactive_count: totalCounterparties > 0 ? Math.max(0, totalCounterparties - resolvedActive) : null, + inference_basis: "counterparty_population_roles_confirmed_1c_rows" + }; +} + +function deriveBusinessOverviewContractUsageProfile( + result: AddressMcpQueryExecutorResult | null, + periodScope: string | null +): AssistantMcpDiscoveryDerivedBusinessOverviewContractUsageProfile | null { + if (!result || result.error || result.matched_rows <= 0) { + return null; + } + const totalContracts = sumBusinessOverviewMarker(result, "CT_TOTAL"); + const usedContracts = sumBusinessOverviewMarker(result, "CT_USED"); + if (totalContracts <= 0 && usedContracts <= 0) { + return null; + } + const cappedUsedContracts = totalContracts > 0 ? Math.min(usedContracts, totalContracts) : usedContracts; + return { + period_scope: periodScope, + rows_matched: result.matched_rows, + total_contracts: totalContracts > 0 ? totalContracts : null, + used_contracts: usedContracts, + unused_contracts: totalContracts > 0 ? Math.max(0, totalContracts - cappedUsedContracts) : null, + used_contract_share_pct: totalContracts > 0 ? percentageOfTotal(cappedUsedContracts, totalContracts) : null, + inference_basis: "contract_usage_overview_confirmed_1c_rows" + }; +} + function deriveValueFlow( result: AssistantMcpDiscoveryCoverageAwareQueryResult | null, counterparty: string | null, @@ -3636,6 +3727,8 @@ function deriveBusinessOverview(input: { payablesResult: AddressMcpQueryExecutorResult | null; openContractsResult: AddressMcpQueryExecutorResult | null; documentActivityProfileResult: AddressMcpQueryExecutorResult | null; + counterpartyProfileResult: AddressMcpQueryExecutorResult | null; + contractUsageProfileResult: AddressMcpQueryExecutorResult | null; debtAsOfDate: string | null; inventoryOnHandResult: AddressMcpQueryExecutorResult | null; inventoryAgingResult: AddressMcpQueryExecutorResult | null; @@ -3677,6 +3770,14 @@ function deriveBusinessOverview(input: { input.documentActivityProfileResult, input.periodScope ); + const counterpartyProfile = deriveBusinessOverviewCounterpartyProfile( + input.counterpartyProfileResult, + input.periodScope + ); + const contractUsageProfile = deriveBusinessOverviewContractUsageProfile( + input.contractUsageProfileResult, + input.periodScope + ); const debtStalenessRiskProxy = deriveBusinessOverviewDebtStalenessRiskProxy(debtOpenSettlementQuality); const inventoryPosition = deriveBusinessOverviewInventoryPosition({ inventoryOnHandResult: input.inventoryOnHandResult, @@ -3701,6 +3802,8 @@ function deriveBusinessOverview(input: { Boolean(debtOpenSettlementQuality), Boolean(debtStalenessRiskProxy), Boolean(documentActivityProfile), + Boolean(counterpartyProfile), + Boolean(contractUsageProfile), Boolean(inventoryPosition), Boolean(inventoryTurnoverProxy), Boolean(inventoryStalenessRiskProxy) @@ -3710,6 +3813,9 @@ function deriveBusinessOverview(input: { } const netAmount = incoming.total_amount - outgoing.total_amount; + const hasBusinessOverviewProfileSignal = Boolean( + documentActivityProfile || counterpartyProfile || contractUsageProfile + ); return { organization_scope: input.organizationScope, period_scope: input.periodScope, @@ -3731,6 +3837,8 @@ function deriveBusinessOverview(input: { inventory_turnover_proxy: inventoryTurnoverProxy, inventory_staleness_risk_proxy: inventoryStalenessRiskProxy, document_activity_profile: documentActivityProfile, + counterparty_profile: counterpartyProfile, + contract_usage_profile: contractUsageProfile, coverage_limited_by_probe_limit: incoming.coverage_limited_by_probe_limit || outgoing.coverage_limited_by_probe_limit, checked_signal_count: checkedSignalCount, @@ -3749,9 +3857,7 @@ function deriveBusinessOverview(input: { inventoryPosition?.aging_signal ? null : "inventory_aging_quality" ].filter((item): item is string => Boolean(item)), inference_basis: - documentActivityProfile - ? "business_overview_from_confirmed_1c_multi_family_rows" - : inventoryPosition + hasBusinessOverviewProfileSignal || inventoryPosition ? "business_overview_from_confirmed_1c_multi_family_rows" : debtOpenSettlementQuality ? "business_overview_from_confirmed_1c_multi_family_rows" @@ -3775,6 +3881,8 @@ function summarizeBusinessOverviewRows(input: { payablesResult: AddressMcpQueryExecutorResult | null; openContractsResult: AddressMcpQueryExecutorResult | null; documentActivityProfileResult: AddressMcpQueryExecutorResult | null; + counterpartyProfileResult: AddressMcpQueryExecutorResult | null; + contractUsageProfileResult: AddressMcpQueryExecutorResult | null; inventoryOnHandResult: AddressMcpQueryExecutorResult | null; inventoryAgingResult: AddressMcpQueryExecutorResult | null; }): string | null { @@ -3806,6 +3914,12 @@ function summarizeBusinessOverviewRows(input: { if (input.documentActivityProfileResult && !input.documentActivityProfileResult.error) { parts.push(`${input.documentActivityProfileResult.fetched_rows} document/account-section profile rows fetched, ${input.documentActivityProfileResult.matched_rows} matched`); } + if (input.counterpartyProfileResult && !input.counterpartyProfileResult.error) { + parts.push(`${input.counterpartyProfileResult.fetched_rows} counterparty role-profile rows fetched, ${input.counterpartyProfileResult.matched_rows} matched`); + } + if (input.contractUsageProfileResult && !input.contractUsageProfileResult.error) { + parts.push(`${input.contractUsageProfileResult.fetched_rows} contract-usage profile rows fetched, ${input.contractUsageProfileResult.matched_rows} matched`); + } if (input.inventoryOnHandResult && !input.inventoryOnHandResult.error) { parts.push(`${input.inventoryOnHandResult.fetched_rows} inventory on-hand rows fetched, ${input.inventoryOnHandResult.matched_rows} matched`); } @@ -3873,6 +3987,25 @@ function buildBusinessOverviewConfirmedFacts(derived: AssistantMcpDiscoveryDeriv ); } } + if (derived.counterparty_profile) { + const profile = derived.counterparty_profile; + const totalText = profile.total_counterparties === null + ? `${profile.active_counterparties} активных контрагентов` + : `${profile.total_counterparties} контрагентов в базе, ${profile.active_counterparties} активных по документальной активности`; + facts.push( + `Профиль контрагентской базы${organization}${period} подтвержден по 1С: ${totalText}; заказчики ${profile.customer_only_count}, поставщики ${profile.supplier_only_count}, смешанная роль ${profile.mixed_role_count}. Это не CRM-аудит, не юридическая проверка контрагентов и не оценка качества клиентской базы.` + ); + } + if (derived.contract_usage_profile) { + const profile = derived.contract_usage_profile; + const totalText = profile.total_contracts === null + ? `${profile.used_contracts} договоров с подтвержденной связью с операциями` + : `${profile.used_contracts} из ${profile.total_contracts} договоров используются${profile.used_contract_share_pct === null ? "" : ` (${profile.used_contract_share_pct}%)`}`; + const unusedText = profile.unused_contracts === null ? "" : `, неиспользуемых ${profile.unused_contracts}`; + facts.push( + `Договорной профиль${organization}${period} подтвержден по 1С: ${totalText}${unusedText}. Это не contract-risk аудит, не проверка актуальности условий и не доказательство качества договорной базы.` + ); + } if (derived.tax_position) { const taxDirection = derived.tax_position.net_vat_direction === "vat_to_pay" @@ -4788,11 +4921,13 @@ export async function executeAssistantMcpDiscoveryPilot( let payablesResult: AddressMcpQueryExecutorResult | null = null; let openContractsResult: AddressMcpQueryExecutorResult | null = null; let documentActivityProfileResult: AddressMcpQueryExecutorResult | null = null; + let counterpartyProfileResult: AddressMcpQueryExecutorResult | null = null; + let contractUsageProfileResult: AddressMcpQueryExecutorResult | null = null; let inventoryOnHandResult: AddressMcpQueryExecutorResult | null = null; let inventoryAgingResult: AddressMcpQueryExecutorResult | null = null; const valueFilters = buildValueFlowFilters(planner); const lifecycleFilters = buildLifecycleFilters(planner); - const documentActivityFilters = buildBusinessOverviewDocumentActivityFilters(planner); + const profileFilters = buildBusinessOverviewProfileFilters(planner); const taxFilters = buildBusinessOverviewTaxFilters(planner); const tradingMarginFilters = buildBusinessOverviewTradingMarginFilters(planner); const debtFilters = buildBusinessOverviewDebtFilters(planner); @@ -4802,7 +4937,9 @@ export async function executeAssistantMcpDiscoveryPilot( const incomingSelection = selectAddressRecipe("customer_revenue_and_payments", valueFilters); const outgoingSelection = selectAddressRecipe("supplier_payouts_profile", valueFilters); const lifecycleSelection = selectAddressRecipe("counterparty_activity_lifecycle", lifecycleFilters); - const documentActivitySelection = selectAddressRecipe("document_type_and_account_section_profile", documentActivityFilters); + const documentActivitySelection = selectAddressRecipe("document_type_and_account_section_profile", profileFilters); + const counterpartyProfileSelection = selectAddressRecipe("counterparty_population_and_roles", profileFilters); + const contractUsageSelection = selectAddressRecipe("contract_usage_overview", profileFilters); const taxSelection = taxFilters ? selectAddressRecipe("vat_liability_confirmed_for_tax_period", taxFilters) : null; @@ -4862,6 +4999,18 @@ export async function executeAssistantMcpDiscoveryPilot( pushReason(reasonCodes, "pilot_business_overview_document_activity_profile_recipe_not_available"); pushUnique(queryLimitations, "Business overview document/account-section profile requires an executable document-section profile recipe"); } + if (counterpartyProfileSelection.selected_recipe) { + pushReason(reasonCodes, "pilot_business_overview_counterparty_profile_recipe_selected"); + } else { + pushReason(reasonCodes, "pilot_business_overview_counterparty_profile_recipe_not_available"); + pushUnique(queryLimitations, "Business overview counterparty profile requires an executable counterparty population/roles recipe"); + } + if (contractUsageSelection.selected_recipe) { + pushReason(reasonCodes, "pilot_business_overview_contract_usage_profile_recipe_selected"); + } else { + pushReason(reasonCodes, "pilot_business_overview_contract_usage_profile_recipe_not_available"); + pushUnique(queryLimitations, "Business overview contract usage profile requires an executable contract usage recipe"); + } if (taxSelection?.selected_recipe) { pushReason(reasonCodes, "pilot_business_overview_tax_recipe_selected"); } else if (!taxFilters) { @@ -5077,7 +5226,7 @@ export async function executeAssistantMcpDiscoveryPilot( if (documentActivitySelection.selected_recipe) { const documentActivityPlan = buildAddressRecipePlan( documentActivitySelection.selected_recipe, - documentActivityFilters + profileFilters ); documentActivityProfileResult = await runtimeDeps.executeAddressMcpQuery({ query: documentActivityPlan.query, @@ -5086,6 +5235,24 @@ export async function executeAssistantMcpDiscoveryPilot( }); probeResults.push(queryResultToProbeResult(step.primitive_id, documentActivityProfileResult)); } + if (counterpartyProfileSelection.selected_recipe) { + const counterpartyProfilePlan = buildAddressRecipePlan(counterpartyProfileSelection.selected_recipe, profileFilters); + counterpartyProfileResult = await runtimeDeps.executeAddressMcpQuery({ + query: counterpartyProfilePlan.query, + limit: counterpartyProfilePlan.limit, + account_scope: counterpartyProfilePlan.account_scope + }); + probeResults.push(queryResultToProbeResult(step.primitive_id, counterpartyProfileResult)); + } + if (contractUsageSelection.selected_recipe) { + const contractUsagePlan = buildAddressRecipePlan(contractUsageSelection.selected_recipe, profileFilters); + contractUsageProfileResult = await runtimeDeps.executeAddressMcpQuery({ + query: contractUsagePlan.query, + limit: contractUsagePlan.limit, + account_scope: contractUsagePlan.account_scope + }); + probeResults.push(queryResultToProbeResult(step.primitive_id, contractUsageProfileResult)); + } if (lifecycleResult.error) { pushUnique(queryLimitations, lifecycleResult.error); pushReason(reasonCodes, "pilot_business_overview_query_documents_mcp_error"); @@ -5104,6 +5271,18 @@ export async function executeAssistantMcpDiscoveryPilot( } else if (documentActivityProfileResult) { pushReason(reasonCodes, "pilot_business_overview_document_activity_profile_query_mcp_executed"); } + if (counterpartyProfileResult?.error) { + pushUnique(queryLimitations, counterpartyProfileResult.error); + pushReason(reasonCodes, "pilot_business_overview_counterparty_profile_query_mcp_error"); + } else if (counterpartyProfileResult) { + pushReason(reasonCodes, "pilot_business_overview_counterparty_profile_query_mcp_executed"); + } + if (contractUsageProfileResult?.error) { + pushUnique(queryLimitations, contractUsageProfileResult.error); + pushReason(reasonCodes, "pilot_business_overview_contract_usage_profile_query_mcp_error"); + } else if (contractUsageProfileResult) { + pushReason(reasonCodes, "pilot_business_overview_contract_usage_profile_query_mcp_executed"); + } continue; } @@ -5121,6 +5300,8 @@ export async function executeAssistantMcpDiscoveryPilot( payablesResult, openContractsResult, documentActivityProfileResult, + counterpartyProfileResult, + contractUsageProfileResult, debtAsOfDate, inventoryOnHandResult, inventoryAgingResult, @@ -5145,6 +5326,12 @@ export async function executeAssistantMcpDiscoveryPilot( if (derivedBusinessOverview.document_activity_profile) { pushReason(reasonCodes, "pilot_derived_business_overview_document_activity_profile_from_confirmed_rows"); } + if (derivedBusinessOverview.counterparty_profile) { + pushReason(reasonCodes, "pilot_derived_business_overview_counterparty_profile_from_confirmed_rows"); + } + if (derivedBusinessOverview.contract_usage_profile) { + pushReason(reasonCodes, "pilot_derived_business_overview_contract_usage_profile_from_confirmed_rows"); + } if (derivedBusinessOverview.tax_position) { pushReason(reasonCodes, "pilot_derived_business_overview_tax_position_from_confirmed_rows"); } @@ -5183,6 +5370,8 @@ export async function executeAssistantMcpDiscoveryPilot( payablesResult, openContractsResult, documentActivityProfileResult, + counterpartyProfileResult, + contractUsageProfileResult, inventoryOnHandResult, inventoryAgingResult }); diff --git a/llm_normalizer/backend/tests/assistantMcpDiscoveryAnswerAdapter.test.ts b/llm_normalizer/backend/tests/assistantMcpDiscoveryAnswerAdapter.test.ts index 138ce29..77198b2 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryAnswerAdapter.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryAnswerAdapter.test.ts @@ -241,6 +241,21 @@ describe("assistant MCP discovery answer adapter", () => { { Period: "2020-01-01T00:00:00", Registrator: "SECTION_DT_OPS", AccountDt: "60", Amount: 9 }, { Period: "2020-01-01T00:00:00", Registrator: "SECTION_KT_OPS", AccountDt: "62", Amount: 6 } ] + }, + { + rows: [ + { Period: "2020-01-01T00:00:00", Registrator: "CP_TOTAL", Amount: 412 }, + { Period: "2020-01-01T00:00:00", Registrator: "CP_CUSTOMER_ACTIVE", Amount: 145 }, + { Period: "2020-01-01T00:00:00", Registrator: "CP_SUPPLIER_ACTIVE", Amount: 94 }, + { Period: "2020-01-01T00:00:00", Registrator: "CP_MIXED_ACTIVE", Amount: 23 }, + { Period: "2020-01-01T00:00:00", Registrator: "CP_ACTIVE_UNION", Amount: 216 } + ] + }, + { + rows: [ + { Period: "2020-01-01T00:00:00", Registrator: "CT_TOTAL", Amount: 520 }, + { Period: "2020-01-01T00:00:00", Registrator: "CT_USED", Amount: 148 } + ] } ]) ); @@ -256,6 +271,10 @@ describe("assistant MCP discovery answer adapter", () => { expect(draft.confirmed_lines.join("\n")).toContain("Профиль операционной активности"); expect(draft.confirmed_lines.join("\n")).toContain("ведущий тип документов Списание с расчетного счета"); expect(draft.confirmed_lines.join("\n")).toContain("ведущий раздел учета 60"); + expect(draft.confirmed_lines.join("\n")).toContain("Профиль контрагентской базы"); + expect(draft.confirmed_lines.join("\n")).toContain("412 контрагентов в базе"); + expect(draft.confirmed_lines.join("\n")).toContain("Договорной профиль"); + expect(draft.confirmed_lines.join("\n")).toContain("148 из 520 договоров используются"); expect(draft.inference_lines.join("\n")).toContain("Аналитический вывод по оборотам"); expect(draft.inference_lines.join("\n")).toContain("Концентрация входящего потока"); expect(draft.inference_lines.join("\n")).toContain("Концентрация исходящего потока"); @@ -270,10 +289,13 @@ describe("assistant MCP discovery answer adapter", () => { expect(draft.must_not_claim).toContain("Do not present business overview yearly operating-flow breakdown as profit, financial result, or a complete annual P&L."); expect(draft.must_not_claim).toContain("Do not present business overview supplier concentration as vendor-risk audit, procurement quality, or full expense structure."); expect(draft.must_not_claim).toContain("Do not present business overview document/account-section activity profile as process quality, accounting correctness, or completeness of all 1C activity."); + expect(draft.must_not_claim).toContain("Do not present business overview counterparty or contract profile as CRM quality, counterparty due diligence, contract-risk audit, or legal completeness."); expect(draft.reason_codes).toContain("answer_contains_business_overview"); expect(draft.reason_codes).toContain("answer_contains_business_overview_supplier_concentration"); expect(draft.reason_codes).toContain("answer_contains_business_overview_yearly_operating_breakdown"); expect(draft.reason_codes).toContain("answer_contains_business_overview_document_activity_profile"); + expect(draft.reason_codes).toContain("answer_contains_business_overview_counterparty_profile"); + expect(draft.reason_codes).toContain("answer_contains_business_overview_contract_usage_profile"); expect(draft.reason_codes).toContain("answer_contains_business_overview_analyst_synthesis"); }); diff --git a/llm_normalizer/backend/tests/assistantMcpDiscoveryPilotExecutor.test.ts b/llm_normalizer/backend/tests/assistantMcpDiscoveryPilotExecutor.test.ts index 91c7988..8b58efd 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryPilotExecutor.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryPilotExecutor.test.ts @@ -161,6 +161,21 @@ describe("assistant MCP discovery pilot executor", () => { { Period: "2020-01-01T00:00:00", Registrator: "SECTION_DT_OPS", AccountDt: "60", Amount: 9 }, { Period: "2020-01-01T00:00:00", Registrator: "SECTION_KT_OPS", AccountDt: "62", Amount: 6 } ] + }, + { + rows: [ + { Period: "2020-01-01T00:00:00", Registrator: "CP_TOTAL", Amount: 412 }, + { Period: "2020-01-01T00:00:00", Registrator: "CP_CUSTOMER_ACTIVE", Amount: 145 }, + { Period: "2020-01-01T00:00:00", Registrator: "CP_SUPPLIER_ACTIVE", Amount: 94 }, + { Period: "2020-01-01T00:00:00", Registrator: "CP_MIXED_ACTIVE", Amount: 23 }, + { Period: "2020-01-01T00:00:00", Registrator: "CP_ACTIVE_UNION", Amount: 216 } + ] + }, + { + rows: [ + { Period: "2020-01-01T00:00:00", Registrator: "CT_TOTAL", Amount: 520 }, + { Period: "2020-01-01T00:00:00", Registrator: "CT_USED", Amount: 148 } + ] } ]); @@ -227,10 +242,28 @@ describe("assistant MCP discovery pilot executor", () => { operation_count: 9, share_pct: 60 }); + expect(result.derived_business_overview?.counterparty_profile).toMatchObject({ + rows_matched: 5, + total_counterparties: 412, + active_counterparties: 216, + customer_only_count: 122, + supplier_only_count: 71, + mixed_role_count: 23, + other_or_inactive_count: 196 + }); + expect(result.derived_business_overview?.contract_usage_profile).toMatchObject({ + rows_matched: 2, + total_contracts: 520, + used_contracts: 148, + unused_contracts: 372, + used_contract_share_pct: 28.46 + }); expect(result.evidence.confirmed_facts.join("\n")).toContain("В 1С подтверждены входящие поступления"); expect(result.evidence.confirmed_facts.join("\n")).toContain("Самый крупный подтвержденный поставщик"); expect(result.evidence.confirmed_facts.join("\n")).toContain("Годовая раскладка операционного денежного потока"); expect(result.evidence.confirmed_facts.join("\n")).toContain("Профиль операционной активности"); + expect(result.evidence.confirmed_facts.join("\n")).toContain("Профиль контрагентской базы"); + expect(result.evidence.confirmed_facts.join("\n")).toContain("Договорной профиль"); expect(result.evidence.inferred_facts.join("\n")).toContain("procurement concentration proxy"); expect(result.evidence.inferred_facts.join("\n")).toContain("Самый сильный год по подтвержденным входящим поступлениям: 2021"); expect(result.evidence.unknown_facts).toContain( @@ -240,7 +273,9 @@ describe("assistant MCP discovery pilot executor", () => { expect(result.reason_codes).toContain("pilot_derived_business_overview_top_suppliers_from_confirmed_rows"); expect(result.reason_codes).toContain("pilot_derived_business_overview_yearly_operating_breakdown_from_confirmed_rows"); expect(result.reason_codes).toContain("pilot_derived_business_overview_document_activity_profile_from_confirmed_rows"); - expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(4); + expect(result.reason_codes).toContain("pilot_derived_business_overview_counterparty_profile_from_confirmed_rows"); + expect(result.reason_codes).toContain("pilot_derived_business_overview_contract_usage_profile_from_confirmed_rows"); + expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(6); }); it("adds a checked VAT/tax family to business overview only when an explicit period is available", async () => { @@ -343,7 +378,7 @@ describe("assistant MCP discovery pilot executor", () => { expect(result.reason_codes).toContain("pilot_derived_business_overview_tax_position_from_confirmed_rows"); expect(result.reason_codes).toContain("pilot_business_overview_trading_margin_query_mcp_executed"); expect(result.reason_codes).toContain("pilot_derived_business_overview_trading_margin_proxy_from_confirmed_rows"); - expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(11); + expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(13); const taxCall = deps.executeAddressMcpQuery.mock.calls[2]?.[0]; const tradingMarginCall = deps.executeAddressMcpQuery.mock.calls[9]?.[0]; expect(String(taxCall?.query ?? "")).toContain("НДСЗаписиКнигиПродаж"); @@ -543,7 +578,7 @@ describe("assistant MCP discovery pilot executor", () => { expect(result.reason_codes).toContain("pilot_derived_business_overview_open_settlement_quality_from_confirmed_rows"); expect(result.reason_codes).toContain("pilot_derived_business_overview_debt_age_signal_from_contract_dates"); expect(result.reason_codes).toContain("pilot_derived_business_overview_debt_staleness_risk_proxy_from_confirmed_rows"); - expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(11); + expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(13); const receivablesCall = deps.executeAddressMcpQuery.mock.calls[3]?.[0]; const payablesCall = deps.executeAddressMcpQuery.mock.calls[4]?.[0]; const openContractsCall = deps.executeAddressMcpQuery.mock.calls[5]?.[0]; @@ -670,7 +705,7 @@ describe("assistant MCP discovery pilot executor", () => { expect(result.reason_codes).toContain("pilot_derived_business_overview_inventory_position_from_confirmed_rows"); expect(result.reason_codes).toContain("pilot_derived_business_overview_inventory_turnover_proxy_from_confirmed_rows"); expect(result.reason_codes).toContain("pilot_derived_business_overview_inventory_staleness_risk_proxy_from_confirmed_rows"); - expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(11); + expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(13); const inventoryCall = deps.executeAddressMcpQuery.mock.calls[6]?.[0]; expect(inventoryCall?.account_scope).toContain("41.01"); });