From 466b3b66e5b9d79c11ca4629e50b1a30c3af49ed Mon Sep 17 00:00:00 2001 From: dctouch Date: Tue, 5 May 2026 13:09:42 +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=BF=D1=80=D0=BE=D1=84=D0=B8=D0=BB=D1=8C?= =?UTF-8?q?=20=D0=B4=D0=BE=D0=BA=D1=83=D0=BC=D0=B5=D0=BD=D1=82=D0=BE=D0=B2?= =?UTF-8?q?=20=D0=B2=20=D0=B1=D0=B8=D0=B7=D0=BD=D0=B5=D1=81-=D0=BE=D0=B1?= =?UTF-8?q?=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 | 25 +++ .../11 - architecture_turnaround/README.md | 5 +- .../assistantMcpDiscoveryAnswerAdapter.js | 45 ++++- .../assistantMcpDiscoveryPilotExecutor.js | 154 ++++++++++++++- .../assistantMcpDiscoveryAnswerAdapter.ts | 47 ++++- .../assistantMcpDiscoveryPilotExecutor.ts | 180 +++++++++++++++++- ...assistantMcpDiscoveryAnswerAdapter.test.ts | 13 ++ ...assistantMcpDiscoveryPilotExecutor.test.ts | 31 ++- 9 files changed, 479 insertions(+), 26 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 73ba119..04c8299 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 @@ -36,8 +36,9 @@ If another document says `78%`, `87%`, `92%`, or `85%` for a module that is now - Completed active slice: `Business Overview Debt Due-Date Boundary Bridge`: organization-level overdue debt, debt quality, debt aging, due-date, and credit-risk wording now routes to `business_overview`, while explicit buyer/debtor lists stay in exact receivables routes with a due-date proof boundary. - 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. - 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: `~96% (Open-World Bounded Autonomy Breadth)`. +- Active module progress: `~97% (Open-World Bounded Autonomy Breadth)`. ## Reporting Rule @@ -74,7 +75,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, 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, 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 231ffdd..89f5752 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 @@ -646,3 +646,28 @@ Local validation is accepted for this slice: - `npm.cmd run build`: passed. Graphify rebuild after Slice 22 code/doc sync: `6062 nodes`, `13207 edges`, `136 communities`. + +## Slice 23 - Business Overview Document/Account Activity Profile Bridge + +This slice adds a low-risk operational-mix fact family to broad company analysis. + +The runtime already has a reviewed `document_type_and_account_section_profile` recipe. Before this slice, that evidence existed as a separate exact profile but was not visible inside `business_overview`, so a broad company-analysis answer could discuss money flow, tax/debt/stock proxies, and concentration while still missing a basic activity-shape question: which document types and accounting sections dominate the visible 1C activity. + +Implemented now: + +- `business_overview` selects and executes the reviewed `document_type_and_account_section_profile` recipe as an optional current-turn profile probe; +- the pilot derives `document_activity_profile` with top document types, top account sections, totals, shares, matched rows, and the stable basis `document_type_and_account_section_profile_confirmed_1c_rows`; +- evidence and answer drafting can surface the leading document type and leading account section as confirmed operational activity mix; +- risk synthesis and headline wording can mention this activity mix when present; +- `must_not_claim` explicitly forbids treating the profile as process quality, accounting correctness, or complete 1C activity coverage; +- missing exact accounting profit, due-date debt aging, vendor-risk/procurement quality, and reserve/liquidation inventory evidence remain pending reviewed-route work. + +This is not a process-audit engine. It is a bounded management context bridge: "what kind of confirmed 1C activity is visible" without pretending to prove whether the business processes or accounting are healthy. + +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 23 code/doc sync: `6066 nodes`, `13222 edges`, `136 communities`. diff --git a/docs/ARCH/11 - architecture_turnaround/README.md b/docs/ARCH/11 - architecture_turnaround/README.md index 3a294a0..cfc8c55 100644 --- a/docs/ARCH/11 - architecture_turnaround/README.md +++ b/docs/ARCH/11 - architecture_turnaround/README.md @@ -71,6 +71,7 @@ Status canon for planning: - The current completed breadth slice is `Business Overview Debt Due-Date Boundary Bridge`: organization-level overdue debt, debt quality, debt aging, due-date, and credit-risk wording now reaches `business_overview`, while explicit buyer/debtor lists stay in exact receivables routes with a due-date proof boundary. - 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 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). @@ -136,11 +137,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: `~96%`, 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, 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: `~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 - 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: `6052 nodes`, `13187 edges`, `138 communities` +- graph snapshot after latest rebuild: `6066 nodes`, `13222 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; diff --git a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryAnswerAdapter.js b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryAnswerAdapter.js index ebc443c..d5a049c 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryAnswerAdapter.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryAnswerAdapter.js @@ -399,6 +399,9 @@ function headlineFor(mode, pilot) { if (overview.activity_period) { families.push("активность"); } + if (overview.document_activity_profile) { + families.push("профиль типов документов и разделов учета"); + } if (overview.tax_position) { families.push("НДС-позиция"); } @@ -621,6 +624,7 @@ function buildMustNotClaim(pilot) { claims.push("Do not present business overview yearly operating-flow breakdown as profit, financial result, or a complete annual P&L."); 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 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."); @@ -977,6 +981,23 @@ function derivedBusinessOverviewConfirmedLines(pilot) { if (overview.activity_period) { lines.push(`Окно подтвержденной активности в 1С: ${overview.activity_period.first_activity_date} — ${overview.activity_period.latest_activity_date}; ориентировочно ${overview.activity_period.duration_human_ru}.`); } + if (overview.document_activity_profile) { + const profile = overview.document_activity_profile; + const topDocument = profile.top_document_types[0]; + const topSection = profile.top_account_sections[0]; + const parts = []; + if (topDocument) { + const shareText = topDocument.share_pct === null ? "" : ` (${topDocument.share_pct}%)`; + parts.push(`ведущий тип документов ${topDocument.document_type} — ${topDocument.count} документов${shareText}`); + } + if (topSection) { + const shareText = topSection.share_pct === null ? "" : ` (${topSection.share_pct}%)`; + parts.push(`ведущий раздел учета ${topSection.account_section} — ${topSection.operation_count} операций${shareText}`); + } + if (parts.length > 0) { + lines.push(`Профиль операционной активности${organization}${period}: ${parts.join("; ")}. Это activity mix по найденным строкам 1С, а не аудит качества учета или полноты процессов.`); + } + } if (overview.tax_position) { const taxDirection = overview.tax_position.net_vat_direction === "vat_to_pay" ? "к уплате" @@ -1138,6 +1159,17 @@ function businessOverviewRiskSynthesisLine(overview) { if (overview.debt_staleness_risk_proxy) { signals.push(`staleness risk proxy открытых расчетов: ${debtStalenessRiskBandRu(overview.debt_staleness_risk_proxy.risk_band)}, возраст ${overview.debt_staleness_risk_proxy.max_contract_age_days} дн., концентрация старейшего крупного договора ${overview.debt_staleness_risk_proxy.top_contract_share_pct}%`); } + if (overview.document_activity_profile) { + const topDocument = overview.document_activity_profile.top_document_types[0]; + const topSection = overview.document_activity_profile.top_account_sections[0]; + const parts = [ + topDocument ? `ведущий тип документов ${topDocument.document_type}` : null, + topSection ? `ведущий раздел учета ${topSection.account_section}` : null + ].filter((item) => Boolean(item)); + if (parts.length > 0) { + signals.push(`операционный activity mix: ${parts.join(", ")}`); + } + } 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) { @@ -1159,7 +1191,7 @@ function businessOverviewRiskSynthesisLine(overview) { } function businessOverviewExecutiveVerdictLine(overview) { const hasCash = overview.incoming_customer_revenue.rows_with_amount > 0 || overview.outgoing_supplier_payout.rows_with_amount > 0; - const hasExtraSignals = Boolean(overview.tax_position || + const hasTaxDebtInventorySignals = Boolean(overview.tax_position || overview.trading_margin_proxy || overview.debt_position || overview.debt_open_settlement_quality || @@ -1167,6 +1199,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; if (!hasCash && !hasExtraSignals) { return null; } @@ -1175,9 +1209,11 @@ function businessOverviewExecutiveVerdictLine(overview) { : overview.net_direction === "net_outgoing" ? "операционно исходящий поток сильнее входящего, это зона внимания к расходам/закупкам" : "операционный поток выглядит сбалансированным"; - const evidenceTone = hasExtraSignals + const evidenceTone = hasTaxDebtInventorySignals ? "часть налоговых, долговых или складских контуров уже отдельно проверена" - : "налоги, долги и склад еще не дают проверенного управленческого контекста"; + : hasDocumentActivitySignal + ? "операционный activity mix по типам документов и разделам учета уже отдельно проверен" + : "налоги, долги и склад еще не дают проверенного управленческого контекста"; return `Сводный LLM-аудит по подтвержденному: ${cashTone}; ${evidenceTone}. Это полезный управленческий срез по найденным строкам 1С, но не финальный вывод о прибыльности, марже или здоровье компании.`; } function derivedBusinessOverviewInferenceLines(pilot) { @@ -1251,6 +1287,9 @@ function buildAssistantMcpDiscoveryAnswerDraft(pilot) { if (pilot.derived_business_overview?.yearly_breakdown?.length) { pushReason(reasonCodes, "answer_contains_business_overview_yearly_operating_breakdown"); } + if (pilot.derived_business_overview?.document_activity_profile) { + pushReason(reasonCodes, "answer_contains_business_overview_document_activity_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 fec2fb1..952908b 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPilotExecutor.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPilotExecutor.js @@ -160,6 +160,17 @@ function buildValueFlowFilters(planner) { sort: "period_asc" }; } +function buildBusinessOverviewDocumentActivityFilters(planner) { + const meaning = planner.discovery_plan.turn_meaning_ref; + const organization = toNonEmptyString(meaning?.explicit_organization_scope); + const dateScope = toNonEmptyString(meaning?.explicit_date_scope); + return { + ...dateScopeToFilters(dateScope), + ...(organization ? { organization } : {}), + limit: planner.discovery_plan.execution_budget.max_rows_per_probe, + sort: "period_asc" + }; +} function buildBusinessOverviewTaxFilters(planner) { const meaning = planner.discovery_plan.turn_meaning_ref; const organization = toNonEmptyString(meaning?.explicit_organization_scope); @@ -1836,6 +1847,75 @@ function deriveBusinessOverviewYearlyBreakdown(input) { }; }); } +function normalizeBusinessOverviewActivityMarker(row) { + const marker = toNonEmptyString(rowDocumentValue(row)); + return marker ? marker.trim().toUpperCase() : null; +} +function normalizeBusinessOverviewAccountSection(value) { + const normalized = String(value ?? "").replace(/\s+/g, "").trim(); + if (!normalized) { + return null; + } + const sectionMatch = normalized.match(/^(\d{2})(?:[.\-_/]|$)/); + return sectionMatch?.[1] ?? normalized; +} +function deriveBusinessOverviewDocumentActivityProfile(result, periodScope) { + if (!result || result.error || result.matched_rows <= 0) { + return null; + } + const documentTypeBuckets = new Map(); + const accountSectionBuckets = new Map(); + for (const row of result.rows) { + const marker = normalizeBusinessOverviewActivityMarker(row); + const count = rowAmountValue(row); + if (!marker || count === null || count <= 0) { + continue; + } + if (marker === "DOC_TYPE_DOCS") { + const documentType = rowDebitAccountValue(row) ?? rowAccountValue(row); + if (documentType) { + documentTypeBuckets.set(documentType, (documentTypeBuckets.get(documentType) ?? 0) + count); + } + continue; + } + if (marker === "SECTION_DT_OPS" || marker === "SECTION_KT_OPS") { + const section = normalizeBusinessOverviewAccountSection(rowDebitAccountValue(row) ?? rowAccountValue(row)); + if (section) { + accountSectionBuckets.set(section, (accountSectionBuckets.get(section) ?? 0) + count); + } + } + } + const totalDocumentTypeCount = Array.from(documentTypeBuckets.values()).reduce((sum, count) => sum + count, 0); + const totalAccountSectionOperations = Array.from(accountSectionBuckets.values()).reduce((sum, count) => sum + count, 0); + const topDocumentTypes = Array.from(documentTypeBuckets.entries()) + .map(([documentType, count]) => ({ + document_type: documentType, + count, + share_pct: percentageOfTotal(count, totalDocumentTypeCount) + })) + .sort((left, right) => right.count - left.count || left.document_type.localeCompare(right.document_type, "ru")) + .slice(0, 5); + const topAccountSections = Array.from(accountSectionBuckets.entries()) + .map(([accountSection, operationCount]) => ({ + account_section: accountSection, + operation_count: operationCount, + share_pct: percentageOfTotal(operationCount, totalAccountSectionOperations) + })) + .sort((left, right) => right.operation_count - left.operation_count || left.account_section.localeCompare(right.account_section, "ru")) + .slice(0, 5); + if (topDocumentTypes.length <= 0 && topAccountSections.length <= 0) { + return null; + } + return { + period_scope: periodScope, + rows_matched: result.matched_rows, + total_document_type_count: totalDocumentTypeCount, + total_account_section_operations: totalAccountSectionOperations, + top_document_types: topDocumentTypes, + top_account_sections: topAccountSections, + inference_basis: "document_type_and_account_section_profile_confirmed_1c_rows" + }; +} function deriveValueFlow(result, counterparty, periodScope, direction, aggregationAxis) { if (!result || result.error || result.matched_rows <= 0) { return null; @@ -2633,6 +2713,7 @@ function deriveBusinessOverview(input) { openContractsResult: input.openContractsResult, debtAsOfDate: input.debtAsOfDate }); + const documentActivityProfile = deriveBusinessOverviewDocumentActivityProfile(input.documentActivityProfileResult, input.periodScope); const debtStalenessRiskProxy = deriveBusinessOverviewDebtStalenessRiskProxy(debtOpenSettlementQuality); const inventoryPosition = deriveBusinessOverviewInventoryPosition({ inventoryOnHandResult: input.inventoryOnHandResult, @@ -2656,6 +2737,7 @@ function deriveBusinessOverview(input) { Boolean(debtPosition), Boolean(debtOpenSettlementQuality), Boolean(debtStalenessRiskProxy), + Boolean(documentActivityProfile), Boolean(inventoryPosition), Boolean(inventoryTurnoverProxy), Boolean(inventoryStalenessRiskProxy) @@ -2684,6 +2766,7 @@ function deriveBusinessOverview(input) { inventory_position: inventoryPosition, inventory_turnover_proxy: inventoryTurnoverProxy, inventory_staleness_risk_proxy: inventoryStalenessRiskProxy, + document_activity_profile: documentActivityProfile, coverage_limited_by_probe_limit: incoming.coverage_limited_by_probe_limit || outgoing.coverage_limited_by_probe_limit, checked_signal_count: checkedSignalCount, missing_signal_families: [ @@ -2700,17 +2783,19 @@ function deriveBusinessOverview(input) { : "inventory_position", inventoryPosition?.aging_signal ? null : "inventory_aging_quality" ].filter((item) => Boolean(item)), - inference_basis: inventoryPosition + inference_basis: documentActivityProfile ? "business_overview_from_confirmed_1c_multi_family_rows" - : debtOpenSettlementQuality + : inventoryPosition ? "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" + : 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" }; } function summarizeBusinessOverviewRows(input) { @@ -2739,6 +2824,9 @@ function summarizeBusinessOverviewRows(input) { if (input.openContractsResult && !input.openContractsResult.error) { parts.push(`${input.openContractsResult.fetched_rows} open-contract rows fetched, ${input.openContractsResult.matched_rows} matched`); } + 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.inventoryOnHandResult && !input.inventoryOnHandResult.error) { parts.push(`${input.inventoryOnHandResult.fetched_rows} inventory on-hand rows fetched, ${input.inventoryOnHandResult.matched_rows} matched`); } @@ -2774,6 +2862,23 @@ function buildBusinessOverviewConfirmedFacts(derived) { if (derived.activity_period) { facts.push(`Подтвержденное окно активности в 1С: ${derived.activity_period.first_activity_date} — ${derived.activity_period.latest_activity_date}.`); } + if (derived.document_activity_profile) { + const profile = derived.document_activity_profile; + const topDocument = profile.top_document_types[0]; + const topSection = profile.top_account_sections[0]; + const parts = []; + if (topDocument) { + const shareText = topDocument.share_pct === null ? "" : ` (${topDocument.share_pct}%)`; + parts.push(`ведущий тип документов ${topDocument.document_type} — ${topDocument.count} документов${shareText}`); + } + if (topSection) { + const shareText = topSection.share_pct === null ? "" : ` (${topSection.share_pct}%)`; + parts.push(`ведущий раздел учета ${topSection.account_section} — ${topSection.operation_count} операций${shareText}`); + } + if (parts.length > 0) { + facts.push(`Профиль операционной активности${organization}${period} подтвержден по типам документов и разделам учета 1С: ${parts.join("; ")}. Это activity mix, а не аудит качества учета или полноты бизнес-процессов.`); + } + } if (derived.tax_position) { const taxDirection = derived.tax_position.net_vat_direction === "vat_to_pay" ? "к уплате" @@ -3537,10 +3642,12 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) { let receivablesResult = null; let payablesResult = null; let openContractsResult = null; + let documentActivityProfileResult = null; let inventoryOnHandResult = null; let inventoryAgingResult = null; const valueFilters = buildValueFlowFilters(planner); const lifecycleFilters = buildLifecycleFilters(planner); + const documentActivityFilters = buildBusinessOverviewDocumentActivityFilters(planner); const taxFilters = buildBusinessOverviewTaxFilters(planner); const tradingMarginFilters = buildBusinessOverviewTradingMarginFilters(planner); const debtFilters = buildBusinessOverviewDebtFilters(planner); @@ -3550,6 +3657,7 @@ 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 taxSelection = taxFilters ? (0, addressRecipeCatalog_1.selectAddressRecipe)("vat_liability_confirmed_for_tax_period", taxFilters) : null; @@ -3601,6 +3709,13 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) { }; } pushReason(reasonCodes, "pilot_business_overview_recipes_selected"); + if (documentActivitySelection.selected_recipe) { + pushReason(reasonCodes, "pilot_business_overview_document_activity_profile_recipe_selected"); + } + else { + 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 (taxSelection?.selected_recipe) { pushReason(reasonCodes, "pilot_business_overview_tax_recipe_selected"); } @@ -3824,6 +3939,15 @@ 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); + documentActivityProfileResult = await runtimeDeps.executeAddressMcpQuery({ + query: documentActivityPlan.query, + limit: documentActivityPlan.limit, + account_scope: documentActivityPlan.account_scope + }); + probeResults.push(queryResultToProbeResult(step.primitive_id, documentActivityProfileResult)); + } if (lifecycleResult.error) { pushUnique(queryLimitations, lifecycleResult.error); pushReason(reasonCodes, "pilot_business_overview_query_documents_mcp_error"); @@ -3838,6 +3962,13 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) { else if (tradingMarginResult) { pushReason(reasonCodes, "pilot_business_overview_trading_margin_query_mcp_executed"); } + if (documentActivityProfileResult?.error) { + pushUnique(queryLimitations, documentActivityProfileResult.error); + pushReason(reasonCodes, "pilot_business_overview_document_activity_profile_query_mcp_error"); + } + else if (documentActivityProfileResult) { + pushReason(reasonCodes, "pilot_business_overview_document_activity_profile_query_mcp_executed"); + } continue; } skippedPrimitives.push(step.primitive_id); @@ -3852,6 +3983,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) { receivablesResult, payablesResult, openContractsResult, + documentActivityProfileResult, debtAsOfDate, inventoryOnHandResult, inventoryAgingResult, @@ -3873,6 +4005,9 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) { if (derivedBusinessOverview.activity_period) { pushReason(reasonCodes, "pilot_derived_business_overview_activity_window_from_confirmed_rows"); } + if (derivedBusinessOverview.document_activity_profile) { + pushReason(reasonCodes, "pilot_derived_business_overview_document_activity_profile_from_confirmed_rows"); + } if (derivedBusinessOverview.tax_position) { pushReason(reasonCodes, "pilot_derived_business_overview_tax_position_from_confirmed_rows"); } @@ -3910,6 +4045,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) { receivablesResult, payablesResult, openContractsResult, + documentActivityProfileResult, inventoryOnHandResult, inventoryAgingResult }); diff --git a/llm_normalizer/backend/src/services/assistantMcpDiscoveryAnswerAdapter.ts b/llm_normalizer/backend/src/services/assistantMcpDiscoveryAnswerAdapter.ts index a002725..f32e559 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryAnswerAdapter.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryAnswerAdapter.ts @@ -498,6 +498,9 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD if (overview.activity_period) { families.push("активность"); } + if (overview.document_activity_profile) { + families.push("профиль типов документов и разделов учета"); + } if (overview.tax_position) { families.push("НДС-позиция"); } @@ -730,6 +733,7 @@ function buildMustNotClaim(pilot: AssistantMcpDiscoveryPilotExecutionContract): claims.push("Do not present business overview yearly operating-flow breakdown as profit, financial result, or a complete annual P&L."); 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 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."); @@ -1141,6 +1145,25 @@ function derivedBusinessOverviewConfirmedLines(pilot: AssistantMcpDiscoveryPilot `Окно подтвержденной активности в 1С: ${overview.activity_period.first_activity_date} — ${overview.activity_period.latest_activity_date}; ориентировочно ${overview.activity_period.duration_human_ru}.` ); } + if (overview.document_activity_profile) { + const profile = overview.document_activity_profile; + const topDocument = profile.top_document_types[0]; + const topSection = profile.top_account_sections[0]; + const parts: string[] = []; + if (topDocument) { + const shareText = topDocument.share_pct === null ? "" : ` (${topDocument.share_pct}%)`; + parts.push(`ведущий тип документов ${topDocument.document_type} — ${topDocument.count} документов${shareText}`); + } + if (topSection) { + const shareText = topSection.share_pct === null ? "" : ` (${topSection.share_pct}%)`; + parts.push(`ведущий раздел учета ${topSection.account_section} — ${topSection.operation_count} операций${shareText}`); + } + if (parts.length > 0) { + lines.push( + `Профиль операционной активности${organization}${period}: ${parts.join("; ")}. Это activity mix по найденным строкам 1С, а не аудит качества учета или полноты процессов.` + ); + } + } if (overview.tax_position) { const taxDirection = overview.tax_position.net_vat_direction === "vat_to_pay" @@ -1333,6 +1356,17 @@ function businessOverviewRiskSynthesisLine(overview: BusinessOverview): string | `staleness risk proxy открытых расчетов: ${debtStalenessRiskBandRu(overview.debt_staleness_risk_proxy.risk_band)}, возраст ${overview.debt_staleness_risk_proxy.max_contract_age_days} дн., концентрация старейшего крупного договора ${overview.debt_staleness_risk_proxy.top_contract_share_pct}%` ); } + if (overview.document_activity_profile) { + const topDocument = overview.document_activity_profile.top_document_types[0]; + const topSection = overview.document_activity_profile.top_account_sections[0]; + const parts = [ + topDocument ? `ведущий тип документов ${topDocument.document_type}` : null, + topSection ? `ведущий раздел учета ${topSection.account_section}` : null + ].filter((item): item is string => Boolean(item)); + if (parts.length > 0) { + signals.push(`операционный activity mix: ${parts.join(", ")}`); + } + } 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) { @@ -1357,7 +1391,7 @@ function businessOverviewRiskSynthesisLine(overview: BusinessOverview): string | function businessOverviewExecutiveVerdictLine(overview: BusinessOverview): string | null { const hasCash = overview.incoming_customer_revenue.rows_with_amount > 0 || overview.outgoing_supplier_payout.rows_with_amount > 0; - const hasExtraSignals = Boolean( + const hasTaxDebtInventorySignals = Boolean( overview.tax_position || overview.trading_margin_proxy || overview.debt_position || @@ -1367,6 +1401,8 @@ 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; if (!hasCash && !hasExtraSignals) { return null; } @@ -1376,9 +1412,11 @@ function businessOverviewExecutiveVerdictLine(overview: BusinessOverview): strin : overview.net_direction === "net_outgoing" ? "операционно исходящий поток сильнее входящего, это зона внимания к расходам/закупкам" : "операционный поток выглядит сбалансированным"; - const evidenceTone = hasExtraSignals + const evidenceTone = hasTaxDebtInventorySignals ? "часть налоговых, долговых или складских контуров уже отдельно проверена" - : "налоги, долги и склад еще не дают проверенного управленческого контекста"; + : hasDocumentActivitySignal + ? "операционный activity mix по типам документов и разделам учета уже отдельно проверен" + : "налоги, долги и склад еще не дают проверенного управленческого контекста"; return `Сводный LLM-аудит по подтвержденному: ${cashTone}; ${evidenceTone}. Это полезный управленческий срез по найденным строкам 1С, но не финальный вывод о прибыльности, марже или здоровье компании.`; } @@ -1460,6 +1498,9 @@ export function buildAssistantMcpDiscoveryAnswerDraft( if (pilot.derived_business_overview?.yearly_breakdown?.length) { pushReason(reasonCodes, "answer_contains_business_overview_yearly_operating_breakdown"); } + if (pilot.derived_business_overview?.document_activity_profile) { + pushReason(reasonCodes, "answer_contains_business_overview_document_activity_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 999f9b4..eba8720 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryPilotExecutor.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryPilotExecutor.ts @@ -138,6 +138,28 @@ export interface AssistantMcpDiscoveryBusinessOverviewYearBucket { net_direction: AssistantMcpDiscoveryNetDirection; } +export interface AssistantMcpDiscoveryBusinessOverviewDocumentTypeBucket { + document_type: string; + count: number; + share_pct: number | null; +} + +export interface AssistantMcpDiscoveryBusinessOverviewAccountSectionBucket { + account_section: string; + operation_count: number; + share_pct: number | null; +} + +export interface AssistantMcpDiscoveryDerivedBusinessOverviewDocumentActivityProfile { + period_scope: string | null; + rows_matched: number; + total_document_type_count: number; + total_account_section_operations: number; + top_document_types: AssistantMcpDiscoveryBusinessOverviewDocumentTypeBucket[]; + top_account_sections: AssistantMcpDiscoveryBusinessOverviewAccountSectionBucket[]; + inference_basis: "document_type_and_account_section_profile_confirmed_1c_rows"; +} + export interface AssistantMcpDiscoveryDerivedBidirectionalValueFlow { counterparty: string | null; period_scope: string | null; @@ -174,6 +196,7 @@ export interface AssistantMcpDiscoveryDerivedBusinessOverview { inventory_position: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryPosition | null; inventory_turnover_proxy: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryTurnoverProxy | null; inventory_staleness_risk_proxy: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryStalenessRiskProxy | null; + document_activity_profile: AssistantMcpDiscoveryDerivedBusinessOverviewDocumentActivityProfile | null; coverage_limited_by_probe_limit: boolean; checked_signal_count: number; missing_signal_families: string[]; @@ -615,6 +638,18 @@ function buildValueFlowFilters(planner: AssistantMcpDiscoveryPlannerContract): A }; } +function buildBusinessOverviewDocumentActivityFilters(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); + return { + ...dateScopeToFilters(dateScope), + ...(organization ? { organization } : {}), + limit: planner.discovery_plan.execution_budget.max_rows_per_probe, + sort: "period_asc" + }; +} + function buildBusinessOverviewTaxFilters(planner: AssistantMcpDiscoveryPlannerContract): AddressFilterSet | null { const meaning = planner.discovery_plan.turn_meaning_ref; const organization = toNonEmptyString(meaning?.explicit_organization_scope); @@ -2603,6 +2638,85 @@ function deriveBusinessOverviewYearlyBreakdown(input: { }); } +function normalizeBusinessOverviewActivityMarker(row: Record): string | null { + const marker = toNonEmptyString(rowDocumentValue(row)); + return marker ? marker.trim().toUpperCase() : null; +} + +function normalizeBusinessOverviewAccountSection(value: string | null): string | null { + const normalized = String(value ?? "").replace(/\s+/g, "").trim(); + if (!normalized) { + return null; + } + const sectionMatch = normalized.match(/^(\d{2})(?:[.\-_/]|$)/); + return sectionMatch?.[1] ?? normalized; +} + +function deriveBusinessOverviewDocumentActivityProfile( + result: AddressMcpQueryExecutorResult | null, + periodScope: string | null +): AssistantMcpDiscoveryDerivedBusinessOverviewDocumentActivityProfile | null { + if (!result || result.error || result.matched_rows <= 0) { + return null; + } + + const documentTypeBuckets = new Map(); + const accountSectionBuckets = new Map(); + for (const row of result.rows) { + const marker = normalizeBusinessOverviewActivityMarker(row); + const count = rowAmountValue(row); + if (!marker || count === null || count <= 0) { + continue; + } + if (marker === "DOC_TYPE_DOCS") { + const documentType = rowDebitAccountValue(row) ?? rowAccountValue(row); + if (documentType) { + documentTypeBuckets.set(documentType, (documentTypeBuckets.get(documentType) ?? 0) + count); + } + continue; + } + if (marker === "SECTION_DT_OPS" || marker === "SECTION_KT_OPS") { + const section = normalizeBusinessOverviewAccountSection(rowDebitAccountValue(row) ?? rowAccountValue(row)); + if (section) { + accountSectionBuckets.set(section, (accountSectionBuckets.get(section) ?? 0) + count); + } + } + } + + const totalDocumentTypeCount = Array.from(documentTypeBuckets.values()).reduce((sum, count) => sum + count, 0); + const totalAccountSectionOperations = Array.from(accountSectionBuckets.values()).reduce((sum, count) => sum + count, 0); + const topDocumentTypes = Array.from(documentTypeBuckets.entries()) + .map(([documentType, count]) => ({ + document_type: documentType, + count, + share_pct: percentageOfTotal(count, totalDocumentTypeCount) + })) + .sort((left, right) => right.count - left.count || left.document_type.localeCompare(right.document_type, "ru")) + .slice(0, 5); + const topAccountSections = Array.from(accountSectionBuckets.entries()) + .map(([accountSection, operationCount]) => ({ + account_section: accountSection, + operation_count: operationCount, + share_pct: percentageOfTotal(operationCount, totalAccountSectionOperations) + })) + .sort((left, right) => right.operation_count - left.operation_count || left.account_section.localeCompare(right.account_section, "ru")) + .slice(0, 5); + + if (topDocumentTypes.length <= 0 && topAccountSections.length <= 0) { + return null; + } + + return { + period_scope: periodScope, + rows_matched: result.matched_rows, + total_document_type_count: totalDocumentTypeCount, + total_account_section_operations: totalAccountSectionOperations, + top_document_types: topDocumentTypes, + top_account_sections: topAccountSections, + inference_basis: "document_type_and_account_section_profile_confirmed_1c_rows" + }; +} + function deriveValueFlow( result: AssistantMcpDiscoveryCoverageAwareQueryResult | null, counterparty: string | null, @@ -3521,6 +3635,7 @@ function deriveBusinessOverview(input: { receivablesResult: AddressMcpQueryExecutorResult | null; payablesResult: AddressMcpQueryExecutorResult | null; openContractsResult: AddressMcpQueryExecutorResult | null; + documentActivityProfileResult: AddressMcpQueryExecutorResult | null; debtAsOfDate: string | null; inventoryOnHandResult: AddressMcpQueryExecutorResult | null; inventoryAgingResult: AddressMcpQueryExecutorResult | null; @@ -3558,6 +3673,10 @@ function deriveBusinessOverview(input: { openContractsResult: input.openContractsResult, debtAsOfDate: input.debtAsOfDate }); + const documentActivityProfile = deriveBusinessOverviewDocumentActivityProfile( + input.documentActivityProfileResult, + input.periodScope + ); const debtStalenessRiskProxy = deriveBusinessOverviewDebtStalenessRiskProxy(debtOpenSettlementQuality); const inventoryPosition = deriveBusinessOverviewInventoryPosition({ inventoryOnHandResult: input.inventoryOnHandResult, @@ -3581,6 +3700,7 @@ function deriveBusinessOverview(input: { Boolean(debtPosition), Boolean(debtOpenSettlementQuality), Boolean(debtStalenessRiskProxy), + Boolean(documentActivityProfile), Boolean(inventoryPosition), Boolean(inventoryTurnoverProxy), Boolean(inventoryStalenessRiskProxy) @@ -3610,6 +3730,7 @@ function deriveBusinessOverview(input: { inventory_position: inventoryPosition, inventory_turnover_proxy: inventoryTurnoverProxy, inventory_staleness_risk_proxy: inventoryStalenessRiskProxy, + document_activity_profile: documentActivityProfile, coverage_limited_by_probe_limit: incoming.coverage_limited_by_probe_limit || outgoing.coverage_limited_by_probe_limit, checked_signal_count: checkedSignalCount, @@ -3628,7 +3749,9 @@ function deriveBusinessOverview(input: { inventoryPosition?.aging_signal ? null : "inventory_aging_quality" ].filter((item): item is string => Boolean(item)), inference_basis: - inventoryPosition + documentActivityProfile + ? "business_overview_from_confirmed_1c_multi_family_rows" + : inventoryPosition ? "business_overview_from_confirmed_1c_multi_family_rows" : debtOpenSettlementQuality ? "business_overview_from_confirmed_1c_multi_family_rows" @@ -3651,6 +3774,7 @@ function summarizeBusinessOverviewRows(input: { receivablesResult: AddressMcpQueryExecutorResult | null; payablesResult: AddressMcpQueryExecutorResult | null; openContractsResult: AddressMcpQueryExecutorResult | null; + documentActivityProfileResult: AddressMcpQueryExecutorResult | null; inventoryOnHandResult: AddressMcpQueryExecutorResult | null; inventoryAgingResult: AddressMcpQueryExecutorResult | null; }): string | null { @@ -3679,6 +3803,9 @@ function summarizeBusinessOverviewRows(input: { if (input.openContractsResult && !input.openContractsResult.error) { parts.push(`${input.openContractsResult.fetched_rows} open-contract rows fetched, ${input.openContractsResult.matched_rows} matched`); } + 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.inventoryOnHandResult && !input.inventoryOnHandResult.error) { parts.push(`${input.inventoryOnHandResult.fetched_rows} inventory on-hand rows fetched, ${input.inventoryOnHandResult.matched_rows} matched`); } @@ -3727,6 +3854,25 @@ function buildBusinessOverviewConfirmedFacts(derived: AssistantMcpDiscoveryDeriv `Подтвержденное окно активности в 1С: ${derived.activity_period.first_activity_date} — ${derived.activity_period.latest_activity_date}.` ); } + if (derived.document_activity_profile) { + const profile = derived.document_activity_profile; + const topDocument = profile.top_document_types[0]; + const topSection = profile.top_account_sections[0]; + const parts: string[] = []; + if (topDocument) { + const shareText = topDocument.share_pct === null ? "" : ` (${topDocument.share_pct}%)`; + parts.push(`ведущий тип документов ${topDocument.document_type} — ${topDocument.count} документов${shareText}`); + } + if (topSection) { + const shareText = topSection.share_pct === null ? "" : ` (${topSection.share_pct}%)`; + parts.push(`ведущий раздел учета ${topSection.account_section} — ${topSection.operation_count} операций${shareText}`); + } + if (parts.length > 0) { + facts.push( + `Профиль операционной активности${organization}${period} подтвержден по типам документов и разделам учета 1С: ${parts.join("; ")}. Это activity mix, а не аудит качества учета или полноты бизнес-процессов.` + ); + } + } if (derived.tax_position) { const taxDirection = derived.tax_position.net_vat_direction === "vat_to_pay" @@ -4641,10 +4787,12 @@ export async function executeAssistantMcpDiscoveryPilot( let receivablesResult: AddressMcpQueryExecutorResult | null = null; let payablesResult: AddressMcpQueryExecutorResult | null = null; let openContractsResult: AddressMcpQueryExecutorResult | null = null; + let documentActivityProfileResult: 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 taxFilters = buildBusinessOverviewTaxFilters(planner); const tradingMarginFilters = buildBusinessOverviewTradingMarginFilters(planner); const debtFilters = buildBusinessOverviewDebtFilters(planner); @@ -4654,6 +4802,7 @@ 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 taxSelection = taxFilters ? selectAddressRecipe("vat_liability_confirmed_for_tax_period", taxFilters) : null; @@ -4707,6 +4856,12 @@ export async function executeAssistantMcpDiscoveryPilot( } pushReason(reasonCodes, "pilot_business_overview_recipes_selected"); + if (documentActivitySelection.selected_recipe) { + pushReason(reasonCodes, "pilot_business_overview_document_activity_profile_recipe_selected"); + } else { + 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 (taxSelection?.selected_recipe) { pushReason(reasonCodes, "pilot_business_overview_tax_recipe_selected"); } else if (!taxFilters) { @@ -4919,6 +5074,18 @@ export async function executeAssistantMcpDiscoveryPilot( }); probeResults.push(queryResultToProbeResult(step.primitive_id, tradingMarginResult)); } + if (documentActivitySelection.selected_recipe) { + const documentActivityPlan = buildAddressRecipePlan( + documentActivitySelection.selected_recipe, + documentActivityFilters + ); + documentActivityProfileResult = await runtimeDeps.executeAddressMcpQuery({ + query: documentActivityPlan.query, + limit: documentActivityPlan.limit, + account_scope: documentActivityPlan.account_scope + }); + probeResults.push(queryResultToProbeResult(step.primitive_id, documentActivityProfileResult)); + } if (lifecycleResult.error) { pushUnique(queryLimitations, lifecycleResult.error); pushReason(reasonCodes, "pilot_business_overview_query_documents_mcp_error"); @@ -4931,6 +5098,12 @@ export async function executeAssistantMcpDiscoveryPilot( } else if (tradingMarginResult) { pushReason(reasonCodes, "pilot_business_overview_trading_margin_query_mcp_executed"); } + if (documentActivityProfileResult?.error) { + pushUnique(queryLimitations, documentActivityProfileResult.error); + pushReason(reasonCodes, "pilot_business_overview_document_activity_profile_query_mcp_error"); + } else if (documentActivityProfileResult) { + pushReason(reasonCodes, "pilot_business_overview_document_activity_profile_query_mcp_executed"); + } continue; } @@ -4947,6 +5120,7 @@ export async function executeAssistantMcpDiscoveryPilot( receivablesResult, payablesResult, openContractsResult, + documentActivityProfileResult, debtAsOfDate, inventoryOnHandResult, inventoryAgingResult, @@ -4968,6 +5142,9 @@ export async function executeAssistantMcpDiscoveryPilot( if (derivedBusinessOverview.activity_period) { pushReason(reasonCodes, "pilot_derived_business_overview_activity_window_from_confirmed_rows"); } + if (derivedBusinessOverview.document_activity_profile) { + pushReason(reasonCodes, "pilot_derived_business_overview_document_activity_profile_from_confirmed_rows"); + } if (derivedBusinessOverview.tax_position) { pushReason(reasonCodes, "pilot_derived_business_overview_tax_position_from_confirmed_rows"); } @@ -5005,6 +5182,7 @@ export async function executeAssistantMcpDiscoveryPilot( receivablesResult, payablesResult, openContractsResult, + documentActivityProfileResult, inventoryOnHandResult, inventoryAgingResult }); diff --git a/llm_normalizer/backend/tests/assistantMcpDiscoveryAnswerAdapter.test.ts b/llm_normalizer/backend/tests/assistantMcpDiscoveryAnswerAdapter.test.ts index 085d005..138ce29 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryAnswerAdapter.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryAnswerAdapter.test.ts @@ -233,6 +233,14 @@ describe("assistant MCP discovery answer adapter", () => { { Период: "2020-01-15T00:00:00", Регистратор: "Поступление 1" }, { Период: "2020-12-15T00:00:00", Регистратор: "Поступление 2" } ] + }, + { + rows: [ + { Period: "2020-01-01T00:00:00", Registrator: "DOC_TYPE_DOCS", AccountDt: "Списание с расчетного счета", Amount: 12 }, + { Period: "2020-01-01T00:00:00", Registrator: "DOC_TYPE_DOCS", AccountDt: "Поступление на расчетный счет", Amount: 8 }, + { 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 } + ] } ]) ); @@ -245,6 +253,9 @@ 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("Годовая раскладка операционного денежного потока"); + expect(draft.confirmed_lines.join("\n")).toContain("Профиль операционной активности"); + expect(draft.confirmed_lines.join("\n")).toContain("ведущий тип документов Списание с расчетного счета"); + expect(draft.confirmed_lines.join("\n")).toContain("ведущий раздел учета 60"); expect(draft.inference_lines.join("\n")).toContain("Аналитический вывод по оборотам"); expect(draft.inference_lines.join("\n")).toContain("Концентрация входящего потока"); expect(draft.inference_lines.join("\n")).toContain("Концентрация исходящего потока"); @@ -258,9 +269,11 @@ describe("assistant MCP discovery answer adapter", () => { 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 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.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_analyst_synthesis"); }); diff --git a/llm_normalizer/backend/tests/assistantMcpDiscoveryPilotExecutor.test.ts b/llm_normalizer/backend/tests/assistantMcpDiscoveryPilotExecutor.test.ts index e80b771..91c7988 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryPilotExecutor.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryPilotExecutor.test.ts @@ -156,8 +156,10 @@ describe("assistant MCP discovery pilot executor", () => { }, { rows: [ - { Period: "2020-03-01T00:00:00", Amount: 200000, Item: "Товар А", Counterparty: "Клиент А", СчетКт: "41.01" }, - { Period: "2020-02-01T00:00:00", Amount: 120000, Item: "Товар А", Counterparty: "Поставщик А", СчетДт: "41.01" } + { Period: "2020-01-01T00:00:00", Registrator: "DOC_TYPE_DOCS", AccountDt: "Списание с расчетного счета", Amount: 12 }, + { Period: "2020-01-01T00:00:00", Registrator: "DOC_TYPE_DOCS", AccountDt: "Поступление на расчетный счет", Amount: 8 }, + { 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 } ] } ]); @@ -210,9 +212,25 @@ describe("assistant MCP discovery pilot executor", () => { } ]); expect(result.derived_business_overview?.activity_period?.duration_total_months).toBe(11); + expect(result.derived_business_overview?.document_activity_profile).toMatchObject({ + rows_matched: 4, + total_document_type_count: 20, + total_account_section_operations: 15 + }); + expect(result.derived_business_overview?.document_activity_profile?.top_document_types[0]).toMatchObject({ + document_type: "Списание с расчетного счета", + count: 12, + share_pct: 60 + }); + expect(result.derived_business_overview?.document_activity_profile?.top_account_sections[0]).toMatchObject({ + account_section: "60", + operation_count: 9, + share_pct: 60 + }); 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.inferred_facts.join("\n")).toContain("procurement concentration proxy"); expect(result.evidence.inferred_facts.join("\n")).toContain("Самый сильный год по подтвержденным входящим поступлениям: 2021"); expect(result.evidence.unknown_facts).toContain( @@ -221,7 +239,8 @@ describe("assistant MCP discovery pilot executor", () => { 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(result.reason_codes).toContain("pilot_derived_business_overview_yearly_operating_breakdown_from_confirmed_rows"); - expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(3); + expect(result.reason_codes).toContain("pilot_derived_business_overview_document_activity_profile_from_confirmed_rows"); + expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(4); }); it("adds a checked VAT/tax family to business overview only when an explicit period is available", async () => { @@ -324,7 +343,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(10); + expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(11); const taxCall = deps.executeAddressMcpQuery.mock.calls[2]?.[0]; const tradingMarginCall = deps.executeAddressMcpQuery.mock.calls[9]?.[0]; expect(String(taxCall?.query ?? "")).toContain("НДСЗаписиКнигиПродаж"); @@ -524,7 +543,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(10); + expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(11); const receivablesCall = deps.executeAddressMcpQuery.mock.calls[3]?.[0]; const payablesCall = deps.executeAddressMcpQuery.mock.calls[4]?.[0]; const openContractsCall = deps.executeAddressMcpQuery.mock.calls[5]?.[0]; @@ -651,7 +670,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(10); + expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(11); const inventoryCall = deps.executeAddressMcpQuery.mock.calls[6]?.[0]; expect(inventoryCall?.account_scope).toContain("41.01"); });