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 461481c..37c390e 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 @@ -26,8 +26,9 @@ If another document says `78%`, `87%`, `92%`, or `85%` for a module that is now - Completed active slice: `Business Overview Analyst Synthesis Layer`: business-overview answers now turn checked fact families into a bounded analyst note with operating scale, customer concentration, risk contours, and explicit profit/margin boundaries. - Completed active slice: `Business Overview Trading Margin Proxy Bridge`: explicit-period business overview can include a bounded товарный sales-vs-purchase document proxy for revenue, purchase-cost trace, gross spread, and margin proxy, while clean profit/accounting финрезультат remains unclaimed. - Completed active slice: `Business Overview Inventory Sales Velocity Proxy Bridge`: when explicit-period stock and товарные sales evidence are both present, business overview can include a bounded sales-to-stock proxy while full FIFO turnover/liquidity remains unclaimed. -- Next active slice: continue breadth into exact company-wide accounting profit/margin, real due-date debt aging, full inventory-liquidity/obsolescence, and broader unfamiliar 1C route families only where reviewed evidence routes exist. -- Active module progress: `~80% (Open-World Bounded Autonomy Breadth)`. +- 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. +- 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: `~83% (Open-World Bounded Autonomy Breadth)`. ## Reporting Rule @@ -64,7 +65,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, as-of-date inventory position, trading-margin proxy, and sales-to-stock inventory proxy into separately proven exact accounting profit/margin, due-date debt aging/overdue, and full inventory-liquidity/obsolescence evidence families; +- extend `business_overview` beyond money-flow/activity, explicit-period VAT/tax, as-of-date debt position, open-settlement concentration, contract-date debt age, 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 4115d03..a7efbc4 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 @@ -275,7 +275,7 @@ Local validation is accepted for this slice: Grow this bridge beyond the first confirmed signal bundle: - add separate evidence families for exact company-wide accounting profit/margin and due-date debt aging/overdue quality where reviewed closing-cost and due-date/payment-term routes exist; -- extend inventory evidence from sales-to-stock proxy into full FIFO turnover/liquidity/obsolescence only when reviewed stock aging, sales velocity, reserve, or liquidation-value evidence exists; +- extend inventory evidence beyond stock position, sales-to-stock proxy, and purchase-date staleness risk proxy into full FIFO turnover, confirmed obsolete-stock/liquidity, reserves, write-offs, or liquidation value only when reviewed evidence exists; - upgrade debt evidence from as-of-date position/open-settlement concentration/contract-date age into overdue aging only when reviewed due-date or payment-term aging evidence exists; - extend VAT/tax beyond explicit-period tax position only when the requested tax fact is provable and the period is explicit; - keep Post-F stale-scope and phase83 catalog-alignment canaries green while widening the route. @@ -402,3 +402,30 @@ 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. + +## Slice 13 - Business Overview Inventory Staleness Risk Proxy Bridge + +This slice adds one more bounded warehouse-quality signal without pretending that the assistant now knows true obsolete stock, reserves, write-offs, or liquidation value. + +It uses only evidence already materialized in the same current business-overview turn: + +- `inventory_position.aging_signal.oldest_purchase_date` and `max_age_days`; +- `inventory_turnover_proxy.sales_to_stock_amount_ratio`. + +Implemented now: + +- the pilot derives `inventory_staleness_risk_proxy` only when current-turn inventory position, purchase-date age, and sales-to-stock proxy are all present; +- the proxy reports as-of date, period scope, oldest purchase signal date, max purchase age in days, sales-to-stock ratio, and one of four bounded bands: `lower_visible_risk`, `watch`, `elevated`, `high`; +- the answer adapter surfaces this as `staleness risk proxy склада`, explicitly not as confirmed неликвидность, reserve, write-off, or liquidation value; +- when this proxy exists, the missing inventory family narrows again from generic liquidity quality toward `inventory_reserve_liquidation_quality`; +- all-time business overview still does not reuse stale stock, sales-window, or purchase-age evidence. + +This is a management risk indicator, not an accounting conclusion. It helps the broad company overview say "there is visible warehouse staleness pressure" when the evidence supports that, while keeping formal inventory quality facts out of the answer until a reviewed route proves them. + +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 13 code/doc sync: `6034 nodes`, `13145 edges`, `136 communities`. diff --git a/docs/ARCH/11 - architecture_turnaround/README.md b/docs/ARCH/11 - architecture_turnaround/README.md index d00f32a..2d91a89 100644 --- a/docs/ARCH/11 - architecture_turnaround/README.md +++ b/docs/ARCH/11 - architecture_turnaround/README.md @@ -61,7 +61,8 @@ Status canon for planning: - The current completed breadth slice is `Business Overview Analyst Synthesis Layer`: broad company-analysis answers now synthesize checked fact families into operating scale, customer concentration, risk contours, and a concise bounded LLM-audit. - The current completed breadth slice is `Business Overview Trading Margin Proxy Bridge`: explicit-period company analysis can now include товарный sales-vs-purchase document proxy for revenue, purchase-cost trace, gross spread, and margin proxy, while clean profit/accounting финрезультат remains unclaimed. - The current completed breadth slice is `Business Overview Inventory Sales Velocity Proxy Bridge`: when explicit-period stock and sales evidence are both present, company analysis can include a bounded sales-to-stock proxy while full FIFO/liquidity/obsolescence remains unclaimed. -- The next active breadth slice continues breadth into exact company-wide accounting profit/margin, real due-date debt aging, full inventory-liquidity/obsolescence, and broader unfamiliar 1C route families without relaxing truth boundaries. +- 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 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). It now documents a turnaround that is already operational in code, already materially past the acute regression breakpoint, and already moved through bounded MCP autonomy, Post-F hardening, inventory breadth proof, and the declared Planner Autonomy slice: @@ -126,11 +127,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: `~80%`, 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, analyst synthesis added to business-overview answer drafting, company-period trading margin proxy bridged locally, and inventory sales-to-stock proxy bridged locally; exact accounting profit/margin, true due-date debt aging/overdue, and full inventory-liquidity/obsolescence expansion are still pending +- active Open-World Bounded Autonomy Breadth progress: `~83%`, 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, analyst synthesis added to business-overview answer drafting, company-period trading margin proxy bridged locally, inventory sales-to-stock proxy bridged locally, and inventory staleness-risk proxy bridged locally; exact accounting profit/margin, true due-date debt aging/overdue, 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: `6030 nodes`, `13136 edges`, `137 communities` +- graph snapshot after latest rebuild: `6034 nodes`, `13145 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; @@ -179,6 +180,7 @@ Latest live proof now includes: - business-overview open-settlement quality bridge accepted live: `address_truth_harness_phase88_business_overview_open_settlement_quality_live_20260504_openquality4` accepted `2/2`, proving explicit-period open-contract concentration and all-time follow-up protection against stale open-contract/debt-quality reuse - business-overview contract-date debt age signal accepted locally: targeted executor/answer-adapter slice passed `65/65` with `1` skipped; full MCP-discovery slice passed `305/305` with `9` skipped; build passed; graphify rebuilt to `6016 nodes`, `13098 edges`, `139 communities`; contract-date age is surfaced as a bounded signal while due-date aging/overdue debt remains unclaimed - business-overview analyst synthesis accepted locally: answer-adapter slice passed `34/34` with `1` skipped; full MCP-discovery slice passed `305/305` with `9` skipped; build passed; graphify rebuilt to `6023 nodes`, `13112 edges`, `136 communities`; broad company-analysis drafts now include operating scale, customer concentration, risk contours, and bounded LLM-audit inference lines +- 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 - 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 82bda30..696c18e 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryAnswerAdapter.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryAnswerAdapter.js @@ -367,6 +367,9 @@ function headlineFor(mode, pilot) { if (overview.inventory_turnover_proxy) { families.push("оборотный proxy склада"); } + if (overview.inventory_staleness_risk_proxy) { + families.push("staleness risk proxy склада"); + } const unknownFamilies = [overview.trading_margin_proxy ? "чистая прибыль/точная маржа" : "прибыль/маржа"]; if (!overview.tax_position) { unknownFamilies.push("НДС"); @@ -559,6 +562,7 @@ function buildMustNotClaim(pilot) { claims.push("Do not present open-settlement concentration as contractual due-date aging or confirmed overdue debt."); claims.push("Do not present an inventory snapshot or purchase-date aging signal as turnover, obsolescence, liquidation value, or full inventory health."); claims.push("Do not present business overview inventory turnover proxy as full inventory liquidity, FIFO turnover, obsolescence analysis, or liquidation value."); + claims.push("Do not present business overview inventory staleness risk proxy as confirmed obsolete stock, reserve, write-off, or liquidation value."); claims.push("Do not expose business_overview_route_template_v1 or MCP primitive names in the user answer."); } if (pilot.derived_ranked_value_flow) { @@ -844,6 +848,18 @@ function percentText(part, total) { const pct = percentOfTotal(part, total); return pct === null ? null : `${pct}%`; } +function inventoryStalenessRiskBandRu(riskBand) { + if (riskBand === "high") { + return "высокая зона внимания"; + } + if (riskBand === "elevated") { + return "повышенная зона внимания"; + } + if (riskBand === "watch") { + return "зона наблюдения"; + } + return "низкий видимый риск"; +} function derivedBusinessOverviewConfirmedLines(pilot) { const overview = pilot.derived_business_overview; if (!overview) { @@ -923,6 +939,10 @@ function derivedBusinessOverviewConfirmedLines(pilot) { : `${proxy.stock_to_sales_revenue_pct}%`; lines.push(`Оборотный proxy склада за ${proxy.period_scope}: продажи ${proxy.sales_revenue_human_ru}, остаток на ${proxy.as_of_date} ${proxy.inventory_amount_human_ru}, sales-to-stock ratio ${ratioText}, остаток к продажам ${stockShareText}. Это не полноценная складская ликвидность, не FIFO-оборачиваемость и не анализ устаревания.`); } + if (overview.inventory_staleness_risk_proxy) { + const proxy = overview.inventory_staleness_risk_proxy; + lines.push(`Staleness risk proxy склада на ${proxy.as_of_date}: самая ранняя дата закупочного сигнала ${proxy.oldest_purchase_date}, возраст ${proxy.max_purchase_age_days} дн., sales-to-stock ${proxy.sales_to_stock_amount_ratio}x, оценка ${inventoryStalenessRiskBandRu(proxy.risk_band)}. Это не подтвержденная неликвидность, не резерв и не ликвидационная стоимость.`); + } return lines; } function businessOverviewCashSynthesisLine(overview) { @@ -990,6 +1010,9 @@ function businessOverviewRiskSynthesisLine(overview) { : `sales-to-stock ${overview.inventory_turnover_proxy.sales_to_stock_amount_ratio}x`; signals.push(`оборотный proxy склада: ${ratioText}`); } + if (overview.inventory_staleness_risk_proxy) { + signals.push(`staleness risk proxy склада: ${inventoryStalenessRiskBandRu(overview.inventory_staleness_risk_proxy.risk_band)}, возраст ${overview.inventory_staleness_risk_proxy.max_purchase_age_days} дн.`); + } return signals.length > 0 ? `Риски и контуры внимания по подтвержденным данным: ${signals.join("; ")}.` : null; @@ -1001,7 +1024,8 @@ function businessOverviewExecutiveVerdictLine(overview) { overview.debt_position || overview.debt_open_settlement_quality || overview.inventory_position || - overview.inventory_turnover_proxy); + overview.inventory_turnover_proxy || + overview.inventory_staleness_risk_proxy); if (!hasCash && !hasExtraSignals) { return null; } @@ -1093,6 +1117,9 @@ function buildAssistantMcpDiscoveryAnswerDraft(pilot) { if (pilot.derived_business_overview?.inventory_turnover_proxy) { pushReason(reasonCodes, "answer_contains_business_overview_inventory_turnover_proxy"); } + if (pilot.derived_business_overview?.inventory_staleness_risk_proxy) { + pushReason(reasonCodes, "answer_contains_business_overview_inventory_staleness_risk_proxy"); + } const confirmedLines = businessOverviewLines.length > 0 ? businessOverviewLines : pilot.derived_ranked_value_flow && derivedValueLine diff --git a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPilotExecutor.js b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPilotExecutor.js index d861594..465cd68 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPilotExecutor.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPilotExecutor.js @@ -2424,6 +2424,54 @@ function deriveBusinessOverviewInventoryTurnoverProxy(input) { inference_basis: "sales_document_revenue_vs_inventory_balance_confirmed_1c_rows" }; } +function inventoryStalenessRiskBand(input) { + if (input.maxPurchaseAgeDays >= 365 && input.salesToStockAmountRatio < 1) { + return "high"; + } + if (input.maxPurchaseAgeDays >= 365 || input.salesToStockAmountRatio < 1) { + return "elevated"; + } + if (input.maxPurchaseAgeDays >= 180 || input.salesToStockAmountRatio < 2) { + return "watch"; + } + return "lower_visible_risk"; +} +function deriveBusinessOverviewInventoryStalenessRiskProxy(input) { + const { inventoryPosition, inventoryTurnoverProxy } = input; + const maxPurchaseAgeDays = inventoryPosition?.aging_signal?.max_age_days; + const oldestPurchaseDate = inventoryPosition?.aging_signal?.oldest_purchase_date; + const salesToStockAmountRatio = inventoryTurnoverProxy?.sales_to_stock_amount_ratio; + if (!inventoryPosition || + !inventoryTurnoverProxy || + !oldestPurchaseDate || + maxPurchaseAgeDays === null || + maxPurchaseAgeDays === undefined || + salesToStockAmountRatio === null || + salesToStockAmountRatio === undefined) { + return null; + } + return { + as_of_date: inventoryPosition.as_of_date, + period_scope: inventoryTurnoverProxy.period_scope, + oldest_purchase_date: oldestPurchaseDate, + max_purchase_age_days: maxPurchaseAgeDays, + sales_to_stock_amount_ratio: salesToStockAmountRatio, + risk_band: inventoryStalenessRiskBand({ maxPurchaseAgeDays, salesToStockAmountRatio }), + inference_basis: "purchase_date_age_and_sales_to_stock_proxy_confirmed_1c_rows" + }; +} +function inventoryStalenessRiskBandRu(riskBand) { + if (riskBand === "high") { + return "высокая зона внимания"; + } + if (riskBand === "elevated") { + return "повышенная зона внимания"; + } + if (riskBand === "watch") { + return "зона наблюдения"; + } + return "низкий видимый риск"; +} function deriveBusinessOverview(input) { const incoming = deriveValueFlowSideSummary(input.incomingResult); const outgoing = deriveValueFlowSideSummary(input.outgoingResult); @@ -2454,6 +2502,10 @@ function deriveBusinessOverview(input) { inventoryPosition, tradingMarginProxy }); + const inventoryStalenessRiskProxy = deriveBusinessOverviewInventoryStalenessRiskProxy({ + inventoryPosition, + inventoryTurnoverProxy + }); const checkedSignalCount = [ incoming.rows_with_amount > 0, outgoing.rows_with_amount > 0, @@ -2463,7 +2515,8 @@ function deriveBusinessOverview(input) { Boolean(debtPosition), Boolean(debtOpenSettlementQuality), Boolean(inventoryPosition), - Boolean(inventoryTurnoverProxy) + Boolean(inventoryTurnoverProxy), + Boolean(inventoryStalenessRiskProxy) ].filter(Boolean).length; if (checkedSignalCount <= 0) { return null; @@ -2485,6 +2538,7 @@ function deriveBusinessOverview(input) { debt_open_settlement_quality: debtOpenSettlementQuality, inventory_position: inventoryPosition, inventory_turnover_proxy: inventoryTurnoverProxy, + inventory_staleness_risk_proxy: inventoryStalenessRiskProxy, coverage_limited_by_probe_limit: incoming.coverage_limited_by_probe_limit || outgoing.coverage_limited_by_probe_limit, checked_signal_count: checkedSignalCount, missing_signal_families: [ @@ -2492,7 +2546,13 @@ function deriveBusinessOverview(input) { debtPosition ? null : "debt_position", debtOpenSettlementQuality ? "debt_due_date_aging_quality" : "debt_open_settlement_quality", taxPosition ? null : "tax_position", - inventoryPosition ? (inventoryTurnoverProxy ? "inventory_liquidity_quality" : "inventory_turnover_quality") : "inventory_position", + inventoryPosition + ? inventoryStalenessRiskProxy + ? "inventory_reserve_liquidation_quality" + : inventoryTurnoverProxy + ? "inventory_liquidity_quality" + : "inventory_turnover_quality" + : "inventory_position", inventoryPosition?.aging_signal ? null : "inventory_aging_quality" ].filter((item) => Boolean(item)), inference_basis: inventoryPosition @@ -2623,6 +2683,10 @@ function buildBusinessOverviewConfirmedFacts(derived) { : `${proxy.stock_to_sales_revenue_pct}%`; facts.push(`Оборотный proxy склада за ${proxy.period_scope} подтвержден по продажным документам и складскому остатку: продажи ${proxy.sales_revenue_human_ru}, остаток на ${proxy.as_of_date} ${proxy.inventory_amount_human_ru}, sales-to-stock ratio ${ratioText}, остаток к продажам ${stockShareText}. Это не полноценная складская ликвидность, не FIFO-оборачиваемость и не анализ устаревания.`); } + if (derived.inventory_staleness_risk_proxy) { + const proxy = derived.inventory_staleness_risk_proxy; + facts.push(`Staleness risk proxy склада на ${proxy.as_of_date}: самая ранняя дата закупочного сигнала ${proxy.oldest_purchase_date}, возраст ${proxy.max_purchase_age_days} дн., sales-to-stock ${proxy.sales_to_stock_amount_ratio}x, оценка ${inventoryStalenessRiskBandRu(proxy.risk_band)}. Это не подтвержденная неликвидность, не резерв и не ликвидационная стоимость.`); + } return facts; } function buildBusinessOverviewInferredFacts(derived) { @@ -2689,6 +2753,9 @@ function buildBusinessOverviewUnknownFacts(derived) { : null, missing.has("inventory_liquidity_quality") ? "Полная складская ликвидность этим бизнес-обзором не подтверждена: sales-to-stock proxy показывает только соотношение продажных документов и остатка на дату, без FIFO-оборачиваемости, устаревания, резервов и ликвидационной стоимости." + : null, + missing.has("inventory_reserve_liquidation_quality") + ? "Резервы, списания, подтвержденная неликвидность и ликвидационная стоимость склада этим бизнес-обзором не подтверждены: staleness proxy показывает только возраст закупочного сигнала и sales-to-stock, без управленческого решения о запасах." : null ].filter((item) => Boolean(item)); if (derived?.coverage_limited_by_probe_limit) { @@ -3643,6 +3710,9 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) { if (derivedBusinessOverview.inventory_turnover_proxy) { pushReason(reasonCodes, "pilot_derived_business_overview_inventory_turnover_proxy_from_confirmed_rows"); } + if (derivedBusinessOverview.inventory_staleness_risk_proxy) { + pushReason(reasonCodes, "pilot_derived_business_overview_inventory_staleness_risk_proxy_from_confirmed_rows"); + } } const sourceRowsSummary = summarizeBusinessOverviewRows({ incomingResult, diff --git a/llm_normalizer/backend/src/services/assistantMcpDiscoveryAnswerAdapter.ts b/llm_normalizer/backend/src/services/assistantMcpDiscoveryAnswerAdapter.ts index 7245f49..7752a81 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryAnswerAdapter.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryAnswerAdapter.ts @@ -470,6 +470,9 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD if (overview.inventory_turnover_proxy) { families.push("оборотный proxy склада"); } + if (overview.inventory_staleness_risk_proxy) { + families.push("staleness risk proxy склада"); + } const unknownFamilies = [overview.trading_margin_proxy ? "чистая прибыль/точная маржа" : "прибыль/маржа"]; if (!overview.tax_position) { unknownFamilies.push("НДС"); @@ -670,6 +673,7 @@ function buildMustNotClaim(pilot: AssistantMcpDiscoveryPilotExecutionContract): claims.push("Do not present open-settlement concentration as contractual due-date aging or confirmed overdue debt."); claims.push("Do not present an inventory snapshot or purchase-date aging signal as turnover, obsolescence, liquidation value, or full inventory health."); claims.push("Do not present business overview inventory turnover proxy as full inventory liquidity, FIFO turnover, obsolescence analysis, or liquidation value."); + claims.push("Do not present business overview inventory staleness risk proxy as confirmed obsolete stock, reserve, write-off, or liquidation value."); claims.push("Do not expose business_overview_route_template_v1 or MCP primitive names in the user answer."); } if (pilot.derived_ranked_value_flow) { @@ -992,6 +996,21 @@ function percentText(part: number, total: number): string | null { return pct === null ? null : `${pct}%`; } +function inventoryStalenessRiskBandRu( + riskBand: NonNullable["risk_band"] +): string { + if (riskBand === "high") { + return "высокая зона внимания"; + } + if (riskBand === "elevated") { + return "повышенная зона внимания"; + } + if (riskBand === "watch") { + return "зона наблюдения"; + } + return "низкий видимый риск"; +} + function derivedBusinessOverviewConfirmedLines(pilot: AssistantMcpDiscoveryPilotExecutionContract): string[] { const overview = pilot.derived_business_overview; if (!overview) { @@ -1095,6 +1114,12 @@ function derivedBusinessOverviewConfirmedLines(pilot: AssistantMcpDiscoveryPilot `Оборотный proxy склада за ${proxy.period_scope}: продажи ${proxy.sales_revenue_human_ru}, остаток на ${proxy.as_of_date} ${proxy.inventory_amount_human_ru}, sales-to-stock ratio ${ratioText}, остаток к продажам ${stockShareText}. Это не полноценная складская ликвидность, не FIFO-оборачиваемость и не анализ устаревания.` ); } + if (overview.inventory_staleness_risk_proxy) { + const proxy = overview.inventory_staleness_risk_proxy; + lines.push( + `Staleness risk proxy склада на ${proxy.as_of_date}: самая ранняя дата закупочного сигнала ${proxy.oldest_purchase_date}, возраст ${proxy.max_purchase_age_days} дн., sales-to-stock ${proxy.sales_to_stock_amount_ratio}x, оценка ${inventoryStalenessRiskBandRu(proxy.risk_band)}. Это не подтвержденная неликвидность, не резерв и не ликвидационная стоимость.` + ); + } return lines; } @@ -1167,6 +1192,11 @@ function businessOverviewRiskSynthesisLine(overview: BusinessOverview): string | : `sales-to-stock ${overview.inventory_turnover_proxy.sales_to_stock_amount_ratio}x`; signals.push(`оборотный proxy склада: ${ratioText}`); } + if (overview.inventory_staleness_risk_proxy) { + signals.push( + `staleness risk proxy склада: ${inventoryStalenessRiskBandRu(overview.inventory_staleness_risk_proxy.risk_band)}, возраст ${overview.inventory_staleness_risk_proxy.max_purchase_age_days} дн.` + ); + } return signals.length > 0 ? `Риски и контуры внимания по подтвержденным данным: ${signals.join("; ")}.` : null; @@ -1180,7 +1210,8 @@ function businessOverviewExecutiveVerdictLine(overview: BusinessOverview): strin overview.debt_position || overview.debt_open_settlement_quality || overview.inventory_position || - overview.inventory_turnover_proxy + overview.inventory_turnover_proxy || + overview.inventory_staleness_risk_proxy ); if (!hasCash && !hasExtraSignals) { return null; @@ -1282,6 +1313,9 @@ export function buildAssistantMcpDiscoveryAnswerDraft( if (pilot.derived_business_overview?.inventory_turnover_proxy) { pushReason(reasonCodes, "answer_contains_business_overview_inventory_turnover_proxy"); } + if (pilot.derived_business_overview?.inventory_staleness_risk_proxy) { + pushReason(reasonCodes, "answer_contains_business_overview_inventory_staleness_risk_proxy"); + } const confirmedLines = businessOverviewLines.length > 0 ? businessOverviewLines : pilot.derived_ranked_value_flow && derivedValueLine diff --git a/llm_normalizer/backend/src/services/assistantMcpDiscoveryPilotExecutor.ts b/llm_normalizer/backend/src/services/assistantMcpDiscoveryPilotExecutor.ts index d8e0f5f..19c1b5c 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryPilotExecutor.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryPilotExecutor.ts @@ -157,6 +157,7 @@ export interface AssistantMcpDiscoveryDerivedBusinessOverview { debt_open_settlement_quality: AssistantMcpDiscoveryDerivedBusinessOverviewDebtOpenSettlementQuality | null; inventory_position: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryPosition | null; inventory_turnover_proxy: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryTurnoverProxy | null; + inventory_staleness_risk_proxy: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryStalenessRiskProxy | null; coverage_limited_by_probe_limit: boolean; checked_signal_count: number; missing_signal_families: string[]; @@ -319,6 +320,16 @@ export interface AssistantMcpDiscoveryDerivedBusinessOverviewInventoryTurnoverPr inference_basis: "sales_document_revenue_vs_inventory_balance_confirmed_1c_rows"; } +export interface AssistantMcpDiscoveryDerivedBusinessOverviewInventoryStalenessRiskProxy { + as_of_date: string; + period_scope: string; + oldest_purchase_date: string; + max_purchase_age_days: number; + sales_to_stock_amount_ratio: number; + risk_band: "lower_visible_risk" | "watch" | "elevated" | "high"; + inference_basis: "purchase_date_age_and_sales_to_stock_proxy_confirmed_1c_rows"; +} + export interface AssistantMcpDiscoveryDerivedMetadataSurface { metadata_scope: string | null; requested_meta_types: string[]; @@ -3253,6 +3264,68 @@ function deriveBusinessOverviewInventoryTurnoverProxy(input: { }; } +function inventoryStalenessRiskBand(input: { + maxPurchaseAgeDays: number; + salesToStockAmountRatio: number; +}): AssistantMcpDiscoveryDerivedBusinessOverviewInventoryStalenessRiskProxy["risk_band"] { + if (input.maxPurchaseAgeDays >= 365 && input.salesToStockAmountRatio < 1) { + return "high"; + } + if (input.maxPurchaseAgeDays >= 365 || input.salesToStockAmountRatio < 1) { + return "elevated"; + } + if (input.maxPurchaseAgeDays >= 180 || input.salesToStockAmountRatio < 2) { + return "watch"; + } + return "lower_visible_risk"; +} + +function deriveBusinessOverviewInventoryStalenessRiskProxy(input: { + inventoryPosition: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryPosition | null; + inventoryTurnoverProxy: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryTurnoverProxy | null; +}): AssistantMcpDiscoveryDerivedBusinessOverviewInventoryStalenessRiskProxy | null { + const { inventoryPosition, inventoryTurnoverProxy } = input; + const maxPurchaseAgeDays = inventoryPosition?.aging_signal?.max_age_days; + const oldestPurchaseDate = inventoryPosition?.aging_signal?.oldest_purchase_date; + const salesToStockAmountRatio = inventoryTurnoverProxy?.sales_to_stock_amount_ratio; + if ( + !inventoryPosition || + !inventoryTurnoverProxy || + !oldestPurchaseDate || + maxPurchaseAgeDays === null || + maxPurchaseAgeDays === undefined || + salesToStockAmountRatio === null || + salesToStockAmountRatio === undefined + ) { + return null; + } + + return { + as_of_date: inventoryPosition.as_of_date, + period_scope: inventoryTurnoverProxy.period_scope, + oldest_purchase_date: oldestPurchaseDate, + max_purchase_age_days: maxPurchaseAgeDays, + sales_to_stock_amount_ratio: salesToStockAmountRatio, + risk_band: inventoryStalenessRiskBand({ maxPurchaseAgeDays, salesToStockAmountRatio }), + inference_basis: "purchase_date_age_and_sales_to_stock_proxy_confirmed_1c_rows" + }; +} + +function inventoryStalenessRiskBandRu( + riskBand: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryStalenessRiskProxy["risk_band"] +): string { + if (riskBand === "high") { + return "высокая зона внимания"; + } + if (riskBand === "elevated") { + return "повышенная зона внимания"; + } + if (riskBand === "watch") { + return "зона наблюдения"; + } + return "низкий видимый риск"; +} + function deriveBusinessOverview(input: { incomingResult: AssistantMcpDiscoveryCoverageAwareQueryResult | null; outgoingResult: AssistantMcpDiscoveryCoverageAwareQueryResult | null; @@ -3298,6 +3371,10 @@ function deriveBusinessOverview(input: { inventoryPosition, tradingMarginProxy }); + const inventoryStalenessRiskProxy = deriveBusinessOverviewInventoryStalenessRiskProxy({ + inventoryPosition, + inventoryTurnoverProxy + }); const checkedSignalCount = [ incoming.rows_with_amount > 0, outgoing.rows_with_amount > 0, @@ -3307,7 +3384,8 @@ function deriveBusinessOverview(input: { Boolean(debtPosition), Boolean(debtOpenSettlementQuality), Boolean(inventoryPosition), - Boolean(inventoryTurnoverProxy) + Boolean(inventoryTurnoverProxy), + Boolean(inventoryStalenessRiskProxy) ].filter(Boolean).length; if (checkedSignalCount <= 0) { return null; @@ -3330,6 +3408,7 @@ function deriveBusinessOverview(input: { debt_open_settlement_quality: debtOpenSettlementQuality, inventory_position: inventoryPosition, inventory_turnover_proxy: inventoryTurnoverProxy, + inventory_staleness_risk_proxy: inventoryStalenessRiskProxy, coverage_limited_by_probe_limit: incoming.coverage_limited_by_probe_limit || outgoing.coverage_limited_by_probe_limit, checked_signal_count: checkedSignalCount, @@ -3338,7 +3417,13 @@ function deriveBusinessOverview(input: { debtPosition ? null : "debt_position", debtOpenSettlementQuality ? "debt_due_date_aging_quality" : "debt_open_settlement_quality", taxPosition ? null : "tax_position", - inventoryPosition ? (inventoryTurnoverProxy ? "inventory_liquidity_quality" : "inventory_turnover_quality") : "inventory_position", + inventoryPosition + ? inventoryStalenessRiskProxy + ? "inventory_reserve_liquidation_quality" + : inventoryTurnoverProxy + ? "inventory_liquidity_quality" + : "inventory_turnover_quality" + : "inventory_position", inventoryPosition?.aging_signal ? null : "inventory_aging_quality" ].filter((item): item is string => Boolean(item)), inference_basis: @@ -3509,6 +3594,12 @@ function buildBusinessOverviewConfirmedFacts(derived: AssistantMcpDiscoveryDeriv `Оборотный proxy склада за ${proxy.period_scope} подтвержден по продажным документам и складскому остатку: продажи ${proxy.sales_revenue_human_ru}, остаток на ${proxy.as_of_date} ${proxy.inventory_amount_human_ru}, sales-to-stock ratio ${ratioText}, остаток к продажам ${stockShareText}. Это не полноценная складская ликвидность, не FIFO-оборачиваемость и не анализ устаревания.` ); } + if (derived.inventory_staleness_risk_proxy) { + const proxy = derived.inventory_staleness_risk_proxy; + facts.push( + `Staleness risk proxy склада на ${proxy.as_of_date}: самая ранняя дата закупочного сигнала ${proxy.oldest_purchase_date}, возраст ${proxy.max_purchase_age_days} дн., sales-to-stock ${proxy.sales_to_stock_amount_ratio}x, оценка ${inventoryStalenessRiskBandRu(proxy.risk_band)}. Это не подтвержденная неликвидность, не резерв и не ликвидационная стоимость.` + ); + } return facts; } @@ -3580,6 +3671,9 @@ function buildBusinessOverviewUnknownFacts(derived: AssistantMcpDiscoveryDerived : null, missing.has("inventory_liquidity_quality") ? "Полная складская ликвидность этим бизнес-обзором не подтверждена: sales-to-stock proxy показывает только соотношение продажных документов и остатка на дату, без FIFO-оборачиваемости, устаревания, резервов и ликвидационной стоимости." + : null, + missing.has("inventory_reserve_liquidation_quality") + ? "Резервы, списания, подтвержденная неликвидность и ликвидационная стоимость склада этим бизнес-обзором не подтверждены: staleness proxy показывает только возраст закупочного сигнала и sales-to-stock, без управленческого решения о запасах." : null ].filter((item): item is string => Boolean(item)); if (derived?.coverage_limited_by_probe_limit) { @@ -4649,6 +4743,9 @@ export async function executeAssistantMcpDiscoveryPilot( if (derivedBusinessOverview.inventory_turnover_proxy) { pushReason(reasonCodes, "pilot_derived_business_overview_inventory_turnover_proxy_from_confirmed_rows"); } + if (derivedBusinessOverview.inventory_staleness_risk_proxy) { + pushReason(reasonCodes, "pilot_derived_business_overview_inventory_staleness_risk_proxy_from_confirmed_rows"); + } } const sourceRowsSummary = summarizeBusinessOverviewRows({ incomingResult, diff --git a/llm_normalizer/backend/tests/assistantMcpDiscoveryAnswerAdapter.test.ts b/llm_normalizer/backend/tests/assistantMcpDiscoveryAnswerAdapter.test.ts index 28409ae..f9c6ef9 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryAnswerAdapter.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryAnswerAdapter.test.ts @@ -488,16 +488,22 @@ describe("assistant MCP discovery answer adapter", () => { expect(draft.headline).toContain("складской срез"); expect(draft.headline).toContain("оборотный proxy склада"); + expect(draft.headline).toContain("staleness risk proxy склада"); expect(draft.confirmed_lines.join("\n")).toContain("Складской срез на 2020-12-31"); expect(draft.confirmed_lines.join("\n")).toContain("Товар А"); expect(draft.confirmed_lines.join("\n")).toContain("Оборотный proxy склада за 2020"); expect(draft.confirmed_lines.join("\n")).toContain("sales-to-stock ratio 2x"); + expect(draft.confirmed_lines.join("\n")).toContain("Staleness risk proxy склада"); + expect(draft.confirmed_lines.join("\n")).toContain("зона наблюдения"); expect(draft.inference_lines.join("\n")).toContain("оборотный proxy склада"); - expect(draft.unknown_lines.join("\n")).toContain("Полная складская ликвидность"); + expect(draft.inference_lines.join("\n")).toContain("staleness risk proxy склада"); + expect(draft.unknown_lines.join("\n")).toContain("Резервы"); expect(draft.reason_codes).toContain("answer_contains_business_overview_inventory_position"); expect(draft.reason_codes).toContain("answer_contains_business_overview_inventory_turnover_proxy"); + expect(draft.reason_codes).toContain("answer_contains_business_overview_inventory_staleness_risk_proxy"); expect(draft.must_not_claim).toContain("Do not present an inventory snapshot or purchase-date aging signal as turnover, obsolescence, liquidation value, or full inventory health."); expect(draft.must_not_claim).toContain("Do not present business overview inventory turnover proxy as full inventory liquidity, FIFO turnover, obsolescence analysis, or liquidation value."); + expect(draft.must_not_claim).toContain("Do not present business overview inventory staleness risk proxy as confirmed obsolete stock, reserve, write-off, or liquidation value."); }); it("renders metadata-scoped movement all-time follow-up as an all-time bounded answer", async () => { diff --git a/llm_normalizer/backend/tests/assistantMcpDiscoveryPilotExecutor.test.ts b/llm_normalizer/backend/tests/assistantMcpDiscoveryPilotExecutor.test.ts index fa9cfe0..a41d1bb 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryPilotExecutor.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryPilotExecutor.test.ts @@ -592,16 +592,28 @@ describe("assistant MCP discovery pilot executor", () => { sales_to_stock_amount_ratio: 2, stock_to_sales_revenue_pct: 50 }); + expect(result.derived_business_overview?.inventory_staleness_risk_proxy).toMatchObject({ + period_scope: "2020", + as_of_date: "2020-12-31", + oldest_purchase_date: "2020-01-10", + max_purchase_age_days: 356, + sales_to_stock_amount_ratio: 2, + risk_band: "watch" + }); expect(result.derived_business_overview?.missing_signal_families).not.toContain("inventory_position"); expect(result.derived_business_overview?.missing_signal_families).not.toContain("inventory_turnover_quality"); - expect(result.derived_business_overview?.missing_signal_families).toContain("inventory_liquidity_quality"); + expect(result.derived_business_overview?.missing_signal_families).not.toContain("inventory_liquidity_quality"); + expect(result.derived_business_overview?.missing_signal_families).toContain("inventory_reserve_liquidation_quality"); expect(result.evidence.confirmed_facts.join("\n")).toContain("Складской срез на 2020-12-31"); expect(result.evidence.confirmed_facts.join("\n")).toContain("Оборотный proxy склада за 2020"); expect(result.evidence.confirmed_facts.join("\n")).toContain("sales-to-stock ratio 2x"); - expect(result.evidence.unknown_facts.join("\n")).toContain("Полная складская ликвидность"); + expect(result.evidence.confirmed_facts.join("\n")).toContain("Staleness risk proxy склада"); + expect(result.evidence.confirmed_facts.join("\n")).toContain("зона наблюдения"); + expect(result.evidence.unknown_facts.join("\n")).toContain("Резервы"); expect(result.reason_codes).toContain("pilot_business_overview_inventory_query_mcp_executed"); expect(result.reason_codes).toContain("pilot_derived_business_overview_inventory_position_from_confirmed_rows"); expect(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); const inventoryCall = deps.executeAddressMcpQuery.mock.calls[6]?.[0]; expect(inventoryCall?.account_scope).toContain("41.01");