From 027e9b373e87212ab1b26263719356c746a1411a Mon Sep 17 00:00:00 2001 From: dctouch Date: Mon, 4 May 2026 11:36:33 +0300 Subject: [PATCH] =?UTF-8?q?Open-World:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D0=BA=D0=BE=D0=BD=D1=86=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=D1=80=D0=B0=D1=86=D0=B8=D1=8E=20=D0=BF=D0=BE=D1=81=D1=82=D0=B0?= =?UTF-8?q?=D0=B2=D1=89=D0=B8=D0=BA=D0=BE=D0=B2=20=D0=B2=20=D0=B1=D0=B8?= =?UTF-8?q?=D0=B7=D0=BD=D0=B5=D1=81-=D0=BE=D0=B1=D0=B7=D0=BE=D1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../21 - current_status_canon_2026-05-01.md | 5 ++-- ...rld_bounded_autonomy_breadth_2026-05-01.md | 28 +++++++++++++++++-- .../11 - architecture_turnaround/README.md | 6 ++-- .../assistantMcpDiscoveryAnswerAdapter.js | 19 +++++++++++++ .../assistantMcpDiscoveryPilotExecutor.js | 25 ++++++++++++++++- .../assistantMcpDiscoveryAnswerAdapter.ts | 22 +++++++++++++++ .../assistantMcpDiscoveryPilotExecutor.ts | 28 ++++++++++++++++++- ...assistantMcpDiscoveryAnswerAdapter.test.ts | 4 +++ ...assistantMcpDiscoveryPilotExecutor.test.ts | 7 +++++ 9 files changed, 136 insertions(+), 8 deletions(-) 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 29be450..febbd18 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 @@ -29,8 +29,9 @@ If another document says `78%`, `87%`, `92%`, or `85%` for a module that is now - Completed active slice: `Business Overview Inventory Staleness Risk Proxy Bridge`: when current-turn stock aging and sales-to-stock evidence are both present, business overview can include a bounded warehouse staleness-risk proxy while confirmed obsolete stock, reserves, write-offs, and liquidation value remain unclaimed. - Completed active slice: `Business Overview Gap-Specific Headline And Next-Step Precision`: broad company-analysis answers now name the remaining unchecked families from `missing_signal_families` instead of using stale generic profit/debt/VAT/warehouse wording after partial proxies are proven. - Completed active slice: `Business Overview Debt Staleness Risk Proxy Bridge`: when current-turn open-settlement concentration and contract-date age are both present, business overview can include a bounded debt staleness-risk proxy while contractual delinquency, credit risk, and due-date aging remain unclaimed. +- Completed active slice: `Business Overview Supplier Concentration Proxy Bridge`: business overview now derives top suppliers/recipients from confirmed outgoing payment rows and surfaces procurement concentration without claiming vendor risk, procurement quality, or full expense structure. - 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: `~86% (Open-World Bounded Autonomy Breadth)`. +- Active module progress: `~88% (Open-World Bounded Autonomy Breadth)`. ## Reporting Rule @@ -67,7 +68,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, 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, and confirmed reserve/write-off/liquidation inventory evidence families; +- extend `business_overview` beyond money-flow/activity, customer and supplier concentration, 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, 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 d924e86..f8c71d6 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 @@ -376,8 +376,6 @@ Business-overview inventory sales velocity proxy validation: - `npm.cmd test -- addressQueryRuntimeM23.test.ts`: passed `412`. - `npm.cmd run build`: passed. -Graphify rebuild after Slice 15 code/doc sync: `6040 nodes`, `13158 edges`, `135 communities`. - Graphify rebuild after Slice 12 code/doc sync: `6030 nodes`, `13136 edges`, `137 communities`. ## Slice 12 - Business Overview Inventory Sales Velocity Proxy Bridge @@ -480,3 +478,29 @@ 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 `412`. - `npm.cmd run build`: passed. + +Graphify rebuild after Slice 15 code/doc sync: `6040 nodes`, `13158 edges`, `135 communities`. + +## Slice 16 - Business Overview Supplier Concentration Proxy Bridge + +This slice fills a low-risk but important analyst gap in the broad company overview. + +Before this slice, `business_overview` could name the largest confirmed customer from incoming payment rows, but the outgoing side only showed total supplier payout. That made company analysis asymmetrical: the assistant could discuss customer concentration but could not say whether outgoing payments were concentrated on one supplier/recipient. + +Implemented now: + +- the pilot reuses the reviewed `deriveRankedValueFlow` aggregation on the outgoing supplier-payout result; +- `derived_business_overview.top_suppliers` now carries the top confirmed outgoing counterparties from the current-turn rows; +- evidence and answer drafting surface the largest confirmed supplier/recipient and the share of checked outgoing payment flow; +- the wording is explicitly bounded as `procurement concentration proxy`, not vendor risk, procurement quality, or full expense structure; +- reason codes now expose both pilot-side and answer-side supplier concentration signals for replay review. + +This is intentionally not a new due-diligence domain. It is a management concentration signal derived from rows already fetched by the existing business-overview runtime bridge. + +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 `412`. +- `npm.cmd run build`: passed. + +Graphify rebuild after Slice 16 code/doc sync: `6041 nodes`, `13162 edges`, `136 communities`. diff --git a/docs/ARCH/11 - architecture_turnaround/README.md b/docs/ARCH/11 - architecture_turnaround/README.md index bdf0404..4150c91 100644 --- a/docs/ARCH/11 - architecture_turnaround/README.md +++ b/docs/ARCH/11 - architecture_turnaround/README.md @@ -64,6 +64,7 @@ Status canon for planning: - The current completed breadth slice is `Business Overview Inventory Staleness Risk Proxy Bridge`: when current-turn stock aging and sales-to-stock evidence are both present, company analysis can include a bounded staleness-risk proxy while confirmed obsolete stock, reserves, write-offs, and liquidation value remain unclaimed. - The current completed breadth slice is `Business Overview Gap-Specific Headline And Next-Step Precision`: business-overview answers now name remaining unchecked families from `missing_signal_families` instead of falling back to stale generic gap wording. - The current completed breadth slice is `Business Overview Debt Staleness Risk Proxy Bridge`: when current-turn open-settlement concentration and contract-date age are both present, company analysis can include a bounded debt staleness-risk proxy while confirmed overdue debt, contractual delinquency, credit risk, and due-date aging remain unclaimed. +- The current completed breadth slice is `Business Overview Supplier Concentration Proxy Bridge`: company analysis now ranks confirmed outgoing payment counterparties and surfaces supplier/procurement concentration as a bounded proxy, not as vendor risk or full expense structure. - 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). @@ -129,11 +130,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: `~86%`, 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, 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, and confirmed reserve/write-off/liquidation inventory evidence are still pending +- active Open-World Bounded Autonomy Breadth progress: `~88%`, 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, supplier concentration proxy 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: `6040 nodes`, `13158 edges`, `135 communities` +- graph snapshot after latest rebuild: `6041 nodes`, `13162 edges`, `136 communities` - current regression-gate breakpoint: - the validated hot paths are no longer structurally broken; - flagship continuity collapse is no longer the primary risk; @@ -185,6 +186,7 @@ Latest live proof now includes: - business-overview inventory staleness-risk proxy accepted locally: targeted executor/answer-adapter slice passed `66/66` with `1` skipped; M23 route/runtime regression passed `412/412`; build passed; graphify rebuilt to `6034 nodes`, `13145 edges`, `136 communities`; the proxy combines stock aging and sales-to-stock ratio while confirmed obsolete stock, reserves, write-offs, and liquidation value remain unclaimed - business-overview gap-specific answer shaping accepted locally: answer-adapter slice passed `34/34` with `1` skipped; build passed; graphify rebuilt to `6036 nodes`, `13149 edges`, `134 communities`; headline and next-step wording now follow `missing_signal_families` instead of stale generic gap labels - business-overview debt staleness-risk proxy accepted locally: targeted executor/answer-adapter slice passed `66/66` with `1` skipped; M23 route/runtime regression passed `412/412`; build passed; graphify rebuilt to `6040 nodes`, `13158 edges`, `135 communities`; the proxy combines contract-date age and open-balance concentration while confirmed overdue debt, contractual delinquency, credit risk, and due-date aging remain unclaimed +- business-overview supplier concentration proxy accepted locally: targeted executor/answer-adapter slice passed `66/66` with `1` skipped; M23 route/runtime regression passed `412/412`; build passed; graphify rebuilt to `6041 nodes`, `13162 edges`, `136 communities`; the proxy ranks confirmed outgoing payment counterparties while vendor risk, procurement quality, and full expense structure remain unclaimed - inventory template lift accepted locally: catalog/data-need/planner/turn-input slice passed `139/139` with `6` skipped; full MCP-discovery slice passed `276/276` with `9` skipped; build passed; graphify stayed at `5912 nodes`, `12833 edges`, `138 communities` - inventory runtime-boundary hardening accepted locally: runtime-bridge/answer-adapter/pilot-executor slice passed `68/68` with `1` skipped; full MCP-discovery slice passed `277/277` with `9` skipped; build passed; graphify rebuilt to `5913 nodes`, `12837 edges`, `138 communities` - inventory exact-runtime bridge accepted locally: runtime-bridge/answer-adapter/pilot-executor slice passed `70/70` with `1` skipped; full MCP-discovery slice passed `279/279` with `9` skipped; build passed; graphify rebuilt to `5930 nodes`, `12884 edges`, `135 communities` diff --git a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryAnswerAdapter.js b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryAnswerAdapter.js index d6ff596..38f302d 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryAnswerAdapter.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryAnswerAdapter.js @@ -616,6 +616,7 @@ function buildMustNotClaim(pilot) { if (isBusinessOverviewPilot(pilot)) { claims.push("Do not present business overview cash-flow spread as profit or margin."); 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 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."); @@ -950,6 +951,10 @@ function derivedBusinessOverviewConfirmedLines(pilot) { if (leader) { lines.push(`Самый крупный подтвержденный клиент в проверенном срезе: ${leader.axis_value} — ${leader.total_amount_human_ru}.`); } + const supplierLeader = overview.top_suppliers?.[0]; + if (supplierLeader) { + lines.push(`Самый крупный подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${supplierLeader.axis_value} — ${supplierLeader.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}.`); } @@ -1044,6 +1049,16 @@ function businessOverviewCustomerConcentrationLine(overview) { ? `Концентрация входящего потока: крупнейший подтвержденный клиент ${leader.axis_value} дает около ${share} проверенных входящих поступлений (${leader.total_amount_human_ru}). Это сигнал зависимости от клиента, а не полный customer-risk аудит.` : `Крупнейший подтвержденный клиент в проверенном срезе: ${leader.axis_value} — ${leader.total_amount_human_ru}.`; } +function businessOverviewSupplierConcentrationLine(overview) { + const leader = overview.top_suppliers?.[0]; + if (!leader || overview.outgoing_supplier_payout.total_amount <= 0) { + return null; + } + const share = percentText(leader.total_amount, overview.outgoing_supplier_payout.total_amount); + return share + ? `Концентрация исходящего потока: крупнейший подтвержденный поставщик/получатель исходящих платежей ${leader.axis_value} держит около ${share} проверенных исходящих платежей (${leader.total_amount_human_ru}). Это сигнал procurement concentration по найденным строкам, а не полный vendor-risk аудит или структура всех расходов.` + : `Крупнейший подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${leader.axis_value} — ${leader.total_amount_human_ru}.`; +} function businessOverviewRiskSynthesisLine(overview) { const signals = []; if (overview.tax_position) { @@ -1128,6 +1143,7 @@ function derivedBusinessOverviewInferenceLines(pilot) { return [ businessOverviewCashSynthesisLine(overview), businessOverviewCustomerConcentrationLine(overview), + businessOverviewSupplierConcentrationLine(overview), businessOverviewRiskSynthesisLine(overview), businessOverviewExecutiveVerdictLine(overview), "Это аналитическая интерпретация подтвержденных строк, а не прибыль и не маржа: для финального управленческого вывода нужны отдельные расходы, себестоимость, закрывающие документы, долги, налоги и складская оборачиваемость." @@ -1183,6 +1199,9 @@ function buildAssistantMcpDiscoveryAnswerDraft(pilot) { if (pilot.derived_business_overview?.trading_margin_proxy) { pushReason(reasonCodes, "answer_contains_business_overview_trading_margin_proxy"); } + if (pilot.derived_business_overview?.top_suppliers?.length) { + pushReason(reasonCodes, "answer_contains_business_overview_supplier_concentration"); + } 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 6cfc2e4..13410c1 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPilotExecutor.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPilotExecutor.js @@ -2536,6 +2536,12 @@ function deriveBusinessOverview(input) { direction: "incoming_customer_revenue", rankingNeed: "top_desc" }); + const rankedOutgoing = deriveRankedValueFlow(input.outgoingResult, { + organizationScope: input.organizationScope, + periodScope: input.periodScope, + direction: "outgoing_supplier_payout", + rankingNeed: "top_desc" + }); const activityPeriod = deriveActivityPeriod(input.lifecycleResult); const taxPosition = deriveBusinessOverviewTaxPosition(input.taxResult, input.periodScope); const tradingMarginProxy = deriveBusinessOverviewTradingMarginProxy(input.tradingMarginResult, input.periodScope); @@ -2588,6 +2594,7 @@ function deriveBusinessOverview(input) { net_amount_human_ru: formatAmountHumanRu(Math.abs(netAmount)), net_direction: netDirectionFromAmount(netAmount), top_customers: rankedIncoming?.ranked_values ?? [], + top_suppliers: rankedOutgoing?.ranked_values ?? [], activity_period: activityPeriod, tax_position: taxPosition, trading_margin_proxy: tradingMarginProxy, @@ -2677,6 +2684,10 @@ function buildBusinessOverviewConfirmedFacts(derived) { const leader = derived.top_customers[0]; facts.push(`Самый крупный подтвержденный клиент в проверенном срезе: ${leader.axis_value} — ${leader.total_amount_human_ru}.`); } + if (derived.top_suppliers.length > 0) { + const leader = derived.top_suppliers[0]; + facts.push(`Самый крупный подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${leader.axis_value} — ${leader.total_amount_human_ru}.`); + } if (derived.activity_period) { facts.push(`Подтвержденное окно активности в 1С: ${derived.activity_period.first_activity_date} — ${derived.activity_period.latest_activity_date}.`); } @@ -2765,10 +2776,19 @@ function buildBusinessOverviewInferredFacts(derived) { : derived.net_direction === "net_outgoing" ? "денежный поток в проверенном срезе больше исходящий, чем входящий" : "входящий и исходящий денежный поток в проверенном срезе примерно сбалансированы"; + const supplierLeader = derived.top_suppliers[0]; + const supplierSharePct = supplierLeader + ? percentageOfTotal(supplierLeader.total_amount, derived.outgoing_supplier_payout.total_amount) + : null; return [ `Расчетное нетто по найденным строкам: ${derived.net_amount_human_ru}; ${direction}.`, + supplierLeader + ? supplierSharePct !== null + ? `Крупнейший подтвержденный поставщик/получатель исходящих платежей ${supplierLeader.axis_value} держит около ${supplierSharePct}% проверенного исходящего потока (${supplierLeader.total_amount_human_ru}). Это procurement concentration proxy по найденным строкам, а не полный vendor-risk аудит.` + : `Крупнейший подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${supplierLeader.axis_value} — ${supplierLeader.total_amount_human_ru}.` + : null, "Это операционный денежный сигнал по найденным строкам 1С, а не прибыль, маржа или бухгалтерское заключение о здоровье бизнеса." - ]; + ].filter((fact) => Boolean(fact)); } function buildBusinessOverviewUnknownFacts(derived) { const missing = new Set(derived?.missing_signal_families ?? [ @@ -3749,6 +3769,9 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) { if (derivedBusinessOverview.top_customers.length > 0) { pushReason(reasonCodes, "pilot_derived_business_overview_top_customers_from_confirmed_rows"); } + if (derivedBusinessOverview.top_suppliers.length > 0) { + pushReason(reasonCodes, "pilot_derived_business_overview_top_suppliers_from_confirmed_rows"); + } if (derivedBusinessOverview.activity_period) { pushReason(reasonCodes, "pilot_derived_business_overview_activity_window_from_confirmed_rows"); } diff --git a/llm_normalizer/backend/src/services/assistantMcpDiscoveryAnswerAdapter.ts b/llm_normalizer/backend/src/services/assistantMcpDiscoveryAnswerAdapter.ts index 0c2ad75..789119b 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryAnswerAdapter.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryAnswerAdapter.ts @@ -725,6 +725,7 @@ function buildMustNotClaim(pilot: AssistantMcpDiscoveryPilotExecutionContract): if (isBusinessOverviewPilot(pilot)) { claims.push("Do not present business overview cash-flow spread as profit or margin."); 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 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."); @@ -1106,6 +1107,12 @@ function derivedBusinessOverviewConfirmedLines(pilot: AssistantMcpDiscoveryPilot if (leader) { lines.push(`Самый крупный подтвержденный клиент в проверенном срезе: ${leader.axis_value} — ${leader.total_amount_human_ru}.`); } + const supplierLeader = overview.top_suppliers?.[0]; + if (supplierLeader) { + lines.push( + `Самый крупный подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${supplierLeader.axis_value} — ${supplierLeader.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}.` @@ -1227,6 +1234,17 @@ function businessOverviewCustomerConcentrationLine(overview: BusinessOverview): : `Крупнейший подтвержденный клиент в проверенном срезе: ${leader.axis_value} — ${leader.total_amount_human_ru}.`; } +function businessOverviewSupplierConcentrationLine(overview: BusinessOverview): string | null { + const leader = overview.top_suppliers?.[0]; + if (!leader || overview.outgoing_supplier_payout.total_amount <= 0) { + return null; + } + const share = percentText(leader.total_amount, overview.outgoing_supplier_payout.total_amount); + return share + ? `Концентрация исходящего потока: крупнейший подтвержденный поставщик/получатель исходящих платежей ${leader.axis_value} держит около ${share} проверенных исходящих платежей (${leader.total_amount_human_ru}). Это сигнал procurement concentration по найденным строкам, а не полный vendor-risk аудит или структура всех расходов.` + : `Крупнейший подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${leader.axis_value} — ${leader.total_amount_human_ru}.`; +} + function businessOverviewRiskSynthesisLine(overview: BusinessOverview): string | null { const signals: string[] = []; if (overview.tax_position) { @@ -1322,6 +1340,7 @@ function derivedBusinessOverviewInferenceLines(pilot: AssistantMcpDiscoveryPilot return [ businessOverviewCashSynthesisLine(overview), businessOverviewCustomerConcentrationLine(overview), + businessOverviewSupplierConcentrationLine(overview), businessOverviewRiskSynthesisLine(overview), businessOverviewExecutiveVerdictLine(overview), "Это аналитическая интерпретация подтвержденных строк, а не прибыль и не маржа: для финального управленческого вывода нужны отдельные расходы, себестоимость, закрывающие документы, долги, налоги и складская оборачиваемость." @@ -1384,6 +1403,9 @@ export function buildAssistantMcpDiscoveryAnswerDraft( if (pilot.derived_business_overview?.trading_margin_proxy) { pushReason(reasonCodes, "answer_contains_business_overview_trading_margin_proxy"); } + if (pilot.derived_business_overview?.top_suppliers?.length) { + pushReason(reasonCodes, "answer_contains_business_overview_supplier_concentration"); + } 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 c0977f5..cd77779 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryPilotExecutor.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryPilotExecutor.ts @@ -150,6 +150,7 @@ export interface AssistantMcpDiscoveryDerivedBusinessOverview { net_amount_human_ru: string; net_direction: AssistantMcpDiscoveryNetDirection; top_customers: AssistantMcpDiscoveryRankedValueFlowBucket[]; + top_suppliers: AssistantMcpDiscoveryRankedValueFlowBucket[]; activity_period: AssistantMcpDiscoveryDerivedActivityPeriod | null; tax_position: AssistantMcpDiscoveryDerivedBusinessOverviewTaxPosition | null; trading_margin_proxy: AssistantMcpDiscoveryDerivedBusinessOverviewTradingMarginProxy | null; @@ -3434,6 +3435,12 @@ function deriveBusinessOverview(input: { direction: "incoming_customer_revenue", rankingNeed: "top_desc" }); + const rankedOutgoing = deriveRankedValueFlow(input.outgoingResult, { + organizationScope: input.organizationScope, + periodScope: input.periodScope, + direction: "outgoing_supplier_payout", + rankingNeed: "top_desc" + }); const activityPeriod = deriveActivityPeriod(input.lifecycleResult); const taxPosition = deriveBusinessOverviewTaxPosition(input.taxResult, input.periodScope); const tradingMarginProxy = deriveBusinessOverviewTradingMarginProxy(input.tradingMarginResult, input.periodScope); @@ -3487,6 +3494,7 @@ function deriveBusinessOverview(input: { net_amount_human_ru: formatAmountHumanRu(Math.abs(netAmount)), net_direction: netDirectionFromAmount(netAmount), top_customers: rankedIncoming?.ranked_values ?? [], + top_suppliers: rankedOutgoing?.ranked_values ?? [], activity_period: activityPeriod, tax_position: taxPosition, trading_margin_proxy: tradingMarginProxy, @@ -3597,6 +3605,12 @@ function buildBusinessOverviewConfirmedFacts(derived: AssistantMcpDiscoveryDeriv `Самый крупный подтвержденный клиент в проверенном срезе: ${leader.axis_value} — ${leader.total_amount_human_ru}.` ); } + if (derived.top_suppliers.length > 0) { + const leader = derived.top_suppliers[0]; + facts.push( + `Самый крупный подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${leader.axis_value} — ${leader.total_amount_human_ru}.` + ); + } if (derived.activity_period) { facts.push( `Подтвержденное окно активности в 1С: ${derived.activity_period.first_activity_date} — ${derived.activity_period.latest_activity_date}.` @@ -3713,10 +3727,19 @@ function buildBusinessOverviewInferredFacts(derived: AssistantMcpDiscoveryDerive : derived.net_direction === "net_outgoing" ? "денежный поток в проверенном срезе больше исходящий, чем входящий" : "входящий и исходящий денежный поток в проверенном срезе примерно сбалансированы"; + const supplierLeader = derived.top_suppliers[0]; + const supplierSharePct = supplierLeader + ? percentageOfTotal(supplierLeader.total_amount, derived.outgoing_supplier_payout.total_amount) + : null; return [ `Расчетное нетто по найденным строкам: ${derived.net_amount_human_ru}; ${direction}.`, + supplierLeader + ? supplierSharePct !== null + ? `Крупнейший подтвержденный поставщик/получатель исходящих платежей ${supplierLeader.axis_value} держит около ${supplierSharePct}% проверенного исходящего потока (${supplierLeader.total_amount_human_ru}). Это procurement concentration proxy по найденным строкам, а не полный vendor-risk аудит.` + : `Крупнейший подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${supplierLeader.axis_value} — ${supplierLeader.total_amount_human_ru}.` + : null, "Это операционный денежный сигнал по найденным строкам 1С, а не прибыль, маржа или бухгалтерское заключение о здоровье бизнеса." - ]; + ].filter((fact): fact is string => Boolean(fact)); } function buildBusinessOverviewUnknownFacts(derived: AssistantMcpDiscoveryDerivedBusinessOverview | null): string[] { @@ -4813,6 +4836,9 @@ export async function executeAssistantMcpDiscoveryPilot( if (derivedBusinessOverview.top_customers.length > 0) { pushReason(reasonCodes, "pilot_derived_business_overview_top_customers_from_confirmed_rows"); } + if (derivedBusinessOverview.top_suppliers.length > 0) { + pushReason(reasonCodes, "pilot_derived_business_overview_top_suppliers_from_confirmed_rows"); + } if (derivedBusinessOverview.activity_period) { pushReason(reasonCodes, "pilot_derived_business_overview_activity_window_from_confirmed_rows"); } diff --git a/llm_normalizer/backend/tests/assistantMcpDiscoveryAnswerAdapter.test.ts b/llm_normalizer/backend/tests/assistantMcpDiscoveryAnswerAdapter.test.ts index ce1a115..5998dd6 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryAnswerAdapter.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryAnswerAdapter.test.ts @@ -239,14 +239,18 @@ describe("assistant MCP discovery answer adapter", () => { expect(draft.headline).toContain("бизнес-обзор"); expect(draft.confirmed_lines.join("\n")).toContain("Входящие поступления"); expect(draft.confirmed_lines.join("\n")).toContain("Самый крупный подтвержденный клиент"); + expect(draft.confirmed_lines.join("\n")).toContain("Самый крупный подтвержденный поставщик"); expect(draft.inference_lines.join("\n")).toContain("Аналитический вывод по оборотам"); expect(draft.inference_lines.join("\n")).toContain("Концентрация входящего потока"); + expect(draft.inference_lines.join("\n")).toContain("Концентрация исходящего потока"); expect(draft.inference_lines.join("\n")).toContain("Сводный LLM-аудит"); 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.must_not_claim).toContain("Do not present business overview supplier concentration as vendor-risk audit, procurement quality, or full expense structure."); 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_analyst_synthesis"); }); diff --git a/llm_normalizer/backend/tests/assistantMcpDiscoveryPilotExecutor.test.ts b/llm_normalizer/backend/tests/assistantMcpDiscoveryPilotExecutor.test.ts index 63a5bf7..3fb6f45 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryPilotExecutor.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryPilotExecutor.test.ts @@ -189,12 +189,19 @@ describe("assistant MCP discovery pilot executor", () => { axis_value: "Клиент А", total_amount: 120000 }); + expect(result.derived_business_overview?.top_suppliers[0]).toMatchObject({ + axis_value: "Поставщик А", + total_amount: 150000 + }); expect(result.derived_business_overview?.activity_period?.duration_total_months).toBe(11); expect(result.evidence.confirmed_facts.join("\n")).toContain("В 1С подтверждены входящие поступления"); + expect(result.evidence.confirmed_facts.join("\n")).toContain("Самый крупный подтвержденный поставщик"); + expect(result.evidence.inferred_facts.join("\n")).toContain("procurement concentration proxy"); expect(result.evidence.unknown_facts).toContain( "Прибыль и маржа этим бизнес-обзором не подтверждены: нужны себестоимость, расходы и закрывающие документы." ); expect(result.reason_codes).toContain("pilot_derived_business_overview_from_confirmed_rows"); + expect(result.reason_codes).toContain("pilot_derived_business_overview_top_suppliers_from_confirmed_rows"); expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(3); });