From cd22164f3364fd538f45b348ed191e9caaaca3c9 Mon Sep 17 00:00:00 2001 From: dctouch Date: Mon, 4 May 2026 11:08:20 +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=20debt=20staleness=20risk=20proxy=20=D0=B2?= =?UTF-8?q?=20=D0=B1=D0=B8=D0=B7=D0=BD=D0=B5=D1=81-=D0=BE=D0=B1=D0=B7?= =?UTF-8?q?=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 | 30 +++++- .../11 - architecture_turnaround/README.md | 6 +- .../assistantMcpDiscoveryAnswerAdapter.js | 34 ++++++- .../assistantMcpDiscoveryPilotExecutor.js | 66 +++++++++++++ .../assistantMcpDiscoveryAnswerAdapter.ts | 43 +++++++- .../assistantMcpDiscoveryPilotExecutor.ts | 97 +++++++++++++++++++ ...assistantMcpDiscoveryAnswerAdapter.test.ts | 8 ++ ...assistantMcpDiscoveryPilotExecutor.test.ts | 11 +++ 9 files changed, 293 insertions(+), 7 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 bc66086..29be450 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 @@ -28,8 +28,9 @@ If another document says `78%`, `87%`, `92%`, or `85%` for a module that is now - 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. - Completed active slice: `Business Overview Inventory Staleness Risk Proxy Bridge`: when current-turn stock aging and sales-to-stock evidence are both present, business overview can include a bounded warehouse staleness-risk proxy while confirmed obsolete stock, reserves, write-offs, and liquidation value remain unclaimed. - Completed active slice: `Business Overview Gap-Specific Headline And Next-Step Precision`: broad company-analysis answers now name the remaining unchecked families from `missing_signal_families` instead of using stale generic profit/debt/VAT/warehouse wording after partial proxies are proven. +- Completed active slice: `Business Overview Debt Staleness Risk Proxy Bridge`: when current-turn open-settlement concentration and contract-date age are both present, business overview can include a bounded debt staleness-risk proxy while contractual delinquency, credit risk, and due-date aging remain unclaimed. - 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: `~84% (Open-World Bounded Autonomy Breadth)`. +- Active module progress: `~86% (Open-World Bounded Autonomy Breadth)`. ## Reporting Rule @@ -66,7 +67,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, sales-to-stock inventory proxy, and warehouse staleness-risk proxy into separately proven exact accounting profit/margin, due-date debt aging/overdue, and confirmed reserve/write-off/liquidation inventory evidence families; +- extend `business_overview` beyond money-flow/activity, explicit-period VAT/tax, as-of-date debt position, open-settlement concentration, contract-date debt age, debt staleness-risk proxy, as-of-date inventory position, trading-margin proxy, sales-to-stock inventory proxy, and warehouse staleness-risk proxy into separately proven exact accounting profit/margin, due-date debt aging/overdue, and confirmed reserve/write-off/liquidation inventory evidence families; - broader dynamic schema traversal for unfamiliar 1C asks; - more primitive descriptors where live evidence proves a real gap; - more replay-backed domain packs that start from user business meaning, not from route convenience; diff --git a/docs/ARCH/11 - architecture_turnaround/22 - open_world_bounded_autonomy_breadth_2026-05-01.md b/docs/ARCH/11 - architecture_turnaround/22 - open_world_bounded_autonomy_breadth_2026-05-01.md index 3960559..d924e86 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 @@ -276,7 +276,7 @@ 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 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; +- upgrade debt evidence beyond as-of-date position, open-settlement concentration, contract-date age, and debt staleness risk proxy 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. @@ -376,6 +376,8 @@ Business-overview inventory sales velocity proxy validation: - `npm.cmd test -- addressQueryRuntimeM23.test.ts`: passed `412`. - `npm.cmd run build`: passed. +Graphify rebuild after Slice 15 code/doc sync: `6040 nodes`, `13158 edges`, `135 communities`. + Graphify rebuild after Slice 12 code/doc sync: `6030 nodes`, `13136 edges`, `137 communities`. ## Slice 12 - Business Overview Inventory Sales Velocity Proxy Bridge @@ -452,3 +454,29 @@ Local validation is accepted for this slice: - `npm.cmd run build`: passed. Graphify rebuild after Slice 14 code/doc sync: `6036 nodes`, `13149 edges`, `134 communities`. + +## Slice 15 - Business Overview Debt Staleness Risk Proxy Bridge + +This slice adds a bounded management risk signal for open settlements without crossing into contractual delinquency. + +It uses only already reviewed current-turn evidence: + +- open-contract balance rows on 60/62/76; +- contract-date age extracted from those open-settlement rows; +- concentration of the oldest large open contract inside gross open balances. + +Implemented now: + +- the pilot derives `debt_staleness_risk_proxy` only when open-settlement quality and contract-date age are both present in the current business-overview turn; +- the proxy reports as-of date, gross open amount, oldest contract start date, max contract age, oldest large contract, its counterparty, amount, share of gross open balances, and a bounded risk band: `lower_visible_risk`, `watch`, `elevated`, or `high`; +- the answer adapter surfaces this as `staleness risk proxy открытых расчетов`, explicitly not as confirmed overdue debt, contractual delinquency, credit risk, or due-date aging; +- the headline narrows the remaining debt gap to `договорные сроки оплаты/due-date просрочка` when this proxy exists; +- M23 stayed green, including open-contract, stale-contract, VAT-after-contract, and follow-up carryover tests. + +This is the debt-side counterpart to the inventory staleness proxy: it gives a useful analyst warning when old contract-date signals and concentration are visible, but it still leaves true due-date/payment-term aging as pending reviewed route work. + +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. diff --git a/docs/ARCH/11 - architecture_turnaround/README.md b/docs/ARCH/11 - architecture_turnaround/README.md index 9266324..bdf0404 100644 --- a/docs/ARCH/11 - architecture_turnaround/README.md +++ b/docs/ARCH/11 - architecture_turnaround/README.md @@ -63,6 +63,7 @@ Status canon for planning: - 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 current completed breadth slice is `Business Overview Inventory Staleness Risk Proxy Bridge`: when current-turn stock aging and sales-to-stock evidence are both present, company analysis can include a bounded staleness-risk proxy while confirmed obsolete stock, reserves, write-offs, and liquidation value remain unclaimed. - The current completed breadth slice is `Business Overview Gap-Specific Headline And Next-Step Precision`: business-overview answers now name remaining unchecked families from `missing_signal_families` instead of falling back to stale generic gap wording. +- The current completed breadth slice is `Business Overview Debt Staleness Risk Proxy Bridge`: when current-turn open-settlement concentration and contract-date age are both present, company analysis can include a bounded debt staleness-risk proxy while confirmed overdue debt, contractual delinquency, credit risk, and due-date aging remain unclaimed. - The 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). @@ -128,11 +129,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: `~84%`, 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, inventory staleness-risk proxy bridged locally, and gap-specific answer shaping bridged locally; exact accounting profit/margin, true due-date debt aging/overdue, and confirmed reserve/write-off/liquidation inventory evidence are still pending +- active Open-World Bounded Autonomy Breadth progress: `~86%`, with business-overview evidence fusion, the reviewed `business_overview` catalog/data-need/planner route-fabric slice, the fresh multi-probe runtime bridge, the explicit-period VAT/tax fact-family bridge, the explicit-period debt-position bridge, the explicit-date inventory-position bridge, the open-settlement quality bridge accepted by live semantic replay, selected-item profitability bridged by local semantic/runtime regression tests, contract-date debt age bridged locally, debt staleness-risk proxy bridged locally, analyst synthesis added to business-overview answer drafting, company-period trading margin proxy bridged locally, inventory sales-to-stock proxy bridged locally, inventory staleness-risk proxy bridged locally, and gap-specific answer shaping bridged locally; exact accounting profit/margin, true due-date debt aging/overdue, and confirmed reserve/write-off/liquidation inventory evidence are still pending - 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: `6036 nodes`, `13149 edges`, `134 communities` +- graph snapshot after latest rebuild: `6040 nodes`, `13158 edges`, `135 communities` - current regression-gate breakpoint: - the validated hot paths are no longer structurally broken; - flagship continuity collapse is no longer the primary risk; @@ -183,6 +184,7 @@ Latest live proof now includes: - 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 - business-overview gap-specific answer shaping accepted locally: answer-adapter slice passed `34/34` with `1` skipped; build passed; graphify rebuilt to `6036 nodes`, `13149 edges`, `134 communities`; headline and next-step wording now follow `missing_signal_families` instead of stale generic gap labels +- business-overview debt staleness-risk proxy accepted locally: targeted executor/answer-adapter slice passed `66/66` with `1` skipped; M23 route/runtime regression passed `412/412`; build passed; graphify rebuilt to `6040 nodes`, `13158 edges`, `135 communities`; the proxy combines contract-date age and open-balance concentration while confirmed overdue debt, contractual delinquency, credit risk, and due-date aging remain unclaimed - 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 1b42418..d6ff596 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryAnswerAdapter.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryAnswerAdapter.js @@ -411,6 +411,9 @@ function headlineFor(mode, pilot) { families.push("возрастной сигнал открытых расчетов"); } } + if (overview.debt_staleness_risk_proxy) { + families.push("staleness risk proxy открытых расчетов"); + } if (overview.inventory_position) { families.push("складской срез на дату"); } @@ -427,7 +430,11 @@ function headlineFor(mode, pilot) { if (!overview.debt_position) { unknownFamilies.push("долговой срез"); } - unknownFamilies.push(overview.debt_open_settlement_quality ? "due-date просрочка" : "качество открытых расчетов"); + unknownFamilies.push(overview.debt_staleness_risk_proxy + ? "договорные сроки оплаты/due-date просрочка" + : overview.debt_open_settlement_quality + ? "due-date просрочка" + : "качество открытых расчетов"); unknownFamilies.push(businessOverviewInventoryUnknownLabel(overview)); return `По данным 1С собран ограниченный бизнес-обзор: ${families.join(", ")} подтверждены найденными строками; ${unknownFamilies.join(", ")} остаются отдельными непроверенными областями.`; } @@ -612,6 +619,7 @@ function buildMustNotClaim(pilot) { claims.push("Do not claim debt quality, VAT position, inventory health, or company health unless those contours were separately checked."); claims.push("Do not present a debt-position snapshot as debt aging, overdue debt, or credit-quality analysis."); claims.push("Do not present open-settlement concentration as contractual due-date aging or confirmed overdue debt."); + claims.push("Do not present business overview debt staleness risk proxy as confirmed overdue debt, contractual delinquency, credit risk, or due-date aging."); 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."); @@ -912,6 +920,18 @@ function inventoryStalenessRiskBandRu(riskBand) { } return "низкий видимый риск"; } +function debtStalenessRiskBandRu(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) { @@ -968,6 +988,11 @@ function derivedBusinessOverviewConfirmedLines(pilot) { lines.push(`Возрастной сигнал открытых расчетов: самая ранняя найденная дата договора ${quality.age_signal.oldest_start_date}${ageText}. Это не просрочка и не due-date анализ.`); } } + if (overview.debt_staleness_risk_proxy) { + const proxy = overview.debt_staleness_risk_proxy; + const counterpartyText = proxy.top_contract_counterparty ? ` / ${proxy.top_contract_counterparty}` : ""; + lines.push(`Staleness risk proxy открытых расчетов на ${proxy.as_of_date}: самый старый договорный сигнал ${proxy.oldest_contract_start_date}, возраст ${proxy.max_contract_age_days} дн.; старейший крупный договор ${proxy.top_contract}${counterpartyText} держит ${proxy.top_contract_amount_human_ru} (${proxy.top_contract_share_pct}% брутто открытых остатков); оценка ${debtStalenessRiskBandRu(proxy.risk_band)}. Это не подтвержденная просрочка, не кредитный риск и не due-date aging.`); + } if (overview.inventory_position) { const leader = overview.inventory_position.top_items[0]; const leaderText = leader @@ -1050,6 +1075,9 @@ function businessOverviewRiskSynthesisLine(overview) { if (overview.debt_open_settlement_quality?.age_signal?.max_age_days !== null && overview.debt_open_settlement_quality?.age_signal?.max_age_days !== undefined) { signals.push(`самый старый договорный возрастной сигнал ${overview.debt_open_settlement_quality.age_signal.max_age_days} дн.`); } + 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.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) { @@ -1075,6 +1103,7 @@ function businessOverviewExecutiveVerdictLine(overview) { overview.trading_margin_proxy || overview.debt_position || overview.debt_open_settlement_quality || + overview.debt_staleness_risk_proxy || overview.inventory_position || overview.inventory_turnover_proxy || overview.inventory_staleness_risk_proxy); @@ -1163,6 +1192,9 @@ function buildAssistantMcpDiscoveryAnswerDraft(pilot) { pushReason(reasonCodes, "answer_contains_business_overview_debt_age_signal"); } } + if (pilot.derived_business_overview?.debt_staleness_risk_proxy) { + pushReason(reasonCodes, "answer_contains_business_overview_debt_staleness_risk_proxy"); + } if (pilot.derived_business_overview?.inventory_position) { pushReason(reasonCodes, "answer_contains_business_overview_inventory_position"); } diff --git a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPilotExecutor.js b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPilotExecutor.js index 465cd68..6cfc2e4 100644 --- a/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPilotExecutor.js +++ b/llm_normalizer/backend/dist/services/assistantMcpDiscoveryPilotExecutor.js @@ -2298,6 +2298,61 @@ function deriveBusinessOverviewDebtOpenSettlementQuality(input) { inference_basis: "open_contracts_confirmed_1c_balance_rows" }; } +function debtStalenessRiskBand(input) { + if (input.maxContractAgeDays >= 365 && input.topContractSharePct >= 50) { + return "high"; + } + if (input.maxContractAgeDays >= 365 || input.topContractSharePct >= 50) { + return "elevated"; + } + if (input.maxContractAgeDays >= 180 || input.topContractSharePct >= 35) { + return "watch"; + } + return "lower_visible_risk"; +} +function deriveBusinessOverviewDebtStalenessRiskProxy(quality) { + const ageSignal = quality?.age_signal; + const topAgedContract = ageSignal?.top_aged_contracts[0]; + const topContractSharePct = topAgedContract?.share_of_gross_open_amount_pct; + if (!quality || + !ageSignal?.oldest_start_date || + ageSignal.max_age_days === null || + ageSignal.max_age_days === undefined || + !topAgedContract || + topContractSharePct === null || + topContractSharePct === undefined) { + return null; + } + return { + as_of_date: quality.as_of_date, + gross_open_amount: quality.gross_open_amount, + gross_open_amount_human_ru: quality.gross_open_amount_human_ru, + oldest_contract_start_date: ageSignal.oldest_start_date, + max_contract_age_days: ageSignal.max_age_days, + top_contract: topAgedContract.contract, + top_contract_counterparty: topAgedContract.counterparty, + top_contract_amount: topAgedContract.total_amount, + top_contract_amount_human_ru: topAgedContract.total_amount_human_ru, + top_contract_share_pct: topContractSharePct, + risk_band: debtStalenessRiskBand({ + maxContractAgeDays: ageSignal.max_age_days, + topContractSharePct + }), + inference_basis: "contract_date_age_and_open_balance_concentration_confirmed_1c_rows" + }; +} +function debtStalenessRiskBandRu(riskBand) { + if (riskBand === "high") { + return "высокая зона внимания"; + } + if (riskBand === "elevated") { + return "повышенная зона внимания"; + } + if (riskBand === "watch") { + return "зона наблюдения"; + } + return "низкий видимый риск"; +} function daysBetweenIsoDates(leftIsoDate, rightIsoDate) { const leftMatch = leftIsoDate.match(/^(\d{4})-(\d{2})-(\d{2})$/); const rightMatch = rightIsoDate.match(/^(\d{4})-(\d{2})-(\d{2})$/); @@ -2493,6 +2548,7 @@ function deriveBusinessOverview(input) { openContractsResult: input.openContractsResult, debtAsOfDate: input.debtAsOfDate }); + const debtStalenessRiskProxy = deriveBusinessOverviewDebtStalenessRiskProxy(debtOpenSettlementQuality); const inventoryPosition = deriveBusinessOverviewInventoryPosition({ inventoryOnHandResult: input.inventoryOnHandResult, inventoryAgingResult: input.inventoryAgingResult, @@ -2514,6 +2570,7 @@ function deriveBusinessOverview(input) { Boolean(tradingMarginProxy), Boolean(debtPosition), Boolean(debtOpenSettlementQuality), + Boolean(debtStalenessRiskProxy), Boolean(inventoryPosition), Boolean(inventoryTurnoverProxy), Boolean(inventoryStalenessRiskProxy) @@ -2536,6 +2593,7 @@ function deriveBusinessOverview(input) { trading_margin_proxy: tradingMarginProxy, debt_position: debtPosition, debt_open_settlement_quality: debtOpenSettlementQuality, + debt_staleness_risk_proxy: debtStalenessRiskProxy, inventory_position: inventoryPosition, inventory_turnover_proxy: inventoryTurnoverProxy, inventory_staleness_risk_proxy: inventoryStalenessRiskProxy, @@ -2660,6 +2718,11 @@ function buildBusinessOverviewConfirmedFacts(derived) { facts.push(`Возрастной сигнал открытых расчетов подтвержден по датам договоров: самая ранняя дата договора ${quality.age_signal.oldest_start_date}${ageText}. Это не договорная просрочка и не due-date анализ.`); } } + if (derived.debt_staleness_risk_proxy) { + const proxy = derived.debt_staleness_risk_proxy; + const counterpartyText = proxy.top_contract_counterparty ? ` / ${proxy.top_contract_counterparty}` : ""; + facts.push(`Staleness risk proxy открытых расчетов на ${proxy.as_of_date}: самый старый договорный сигнал ${proxy.oldest_contract_start_date}, возраст ${proxy.max_contract_age_days} дн.; старейший крупный договор ${proxy.top_contract}${counterpartyText} держит ${proxy.top_contract_amount_human_ru} (${proxy.top_contract_share_pct}% брутто открытых остатков); оценка ${debtStalenessRiskBandRu(proxy.risk_band)}. Это не подтвержденная просрочка, не кредитный риск и не due-date aging.`); + } if (derived.inventory_position) { const leader = derived.inventory_position.top_items[0]; const leaderText = leader @@ -3704,6 +3767,9 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) { pushReason(reasonCodes, "pilot_derived_business_overview_debt_age_signal_from_contract_dates"); } } + if (derivedBusinessOverview.debt_staleness_risk_proxy) { + pushReason(reasonCodes, "pilot_derived_business_overview_debt_staleness_risk_proxy_from_confirmed_rows"); + } if (derivedBusinessOverview.inventory_position) { pushReason(reasonCodes, "pilot_derived_business_overview_inventory_position_from_confirmed_rows"); } diff --git a/llm_normalizer/backend/src/services/assistantMcpDiscoveryAnswerAdapter.ts b/llm_normalizer/backend/src/services/assistantMcpDiscoveryAnswerAdapter.ts index 5ad8100..0c2ad75 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryAnswerAdapter.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryAnswerAdapter.ts @@ -510,6 +510,9 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD families.push("возрастной сигнал открытых расчетов"); } } + if (overview.debt_staleness_risk_proxy) { + families.push("staleness risk proxy открытых расчетов"); + } if (overview.inventory_position) { families.push("складской срез на дату"); } @@ -526,7 +529,13 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD if (!overview.debt_position) { unknownFamilies.push("долговой срез"); } - unknownFamilies.push(overview.debt_open_settlement_quality ? "due-date просрочка" : "качество открытых расчетов"); + unknownFamilies.push( + overview.debt_staleness_risk_proxy + ? "договорные сроки оплаты/due-date просрочка" + : overview.debt_open_settlement_quality + ? "due-date просрочка" + : "качество открытых расчетов" + ); unknownFamilies.push(businessOverviewInventoryUnknownLabel(overview)); return `По данным 1С собран ограниченный бизнес-обзор: ${families.join(", ")} подтверждены найденными строками; ${unknownFamilies.join(", ")} остаются отдельными непроверенными областями.`; } @@ -719,6 +728,7 @@ function buildMustNotClaim(pilot: AssistantMcpDiscoveryPilotExecutionContract): claims.push("Do not claim debt quality, VAT position, inventory health, or company health unless those contours were separately checked."); claims.push("Do not present a debt-position snapshot as debt aging, overdue debt, or credit-quality analysis."); claims.push("Do not present open-settlement concentration as contractual due-date aging or confirmed overdue debt."); + claims.push("Do not present business overview debt staleness risk proxy as confirmed overdue debt, contractual delinquency, credit risk, or due-date aging."); 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."); @@ -1059,6 +1069,21 @@ function inventoryStalenessRiskBandRu( return "низкий видимый риск"; } +function debtStalenessRiskBandRu( + 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) { @@ -1133,6 +1158,13 @@ function derivedBusinessOverviewConfirmedLines(pilot: AssistantMcpDiscoveryPilot ); } } + if (overview.debt_staleness_risk_proxy) { + const proxy = overview.debt_staleness_risk_proxy; + const counterpartyText = proxy.top_contract_counterparty ? ` / ${proxy.top_contract_counterparty}` : ""; + lines.push( + `Staleness risk proxy открытых расчетов на ${proxy.as_of_date}: самый старый договорный сигнал ${proxy.oldest_contract_start_date}, возраст ${proxy.max_contract_age_days} дн.; старейший крупный договор ${proxy.top_contract}${counterpartyText} держит ${proxy.top_contract_amount_human_ru} (${proxy.top_contract_share_pct}% брутто открытых остатков); оценка ${debtStalenessRiskBandRu(proxy.risk_band)}. Это не подтвержденная просрочка, не кредитный риск и не due-date aging.` + ); + } if (overview.inventory_position) { const leader = overview.inventory_position.top_items[0]; const leaderText = leader @@ -1228,6 +1260,11 @@ function businessOverviewRiskSynthesisLine(overview: BusinessOverview): string | if (overview.debt_open_settlement_quality?.age_signal?.max_age_days !== null && overview.debt_open_settlement_quality?.age_signal?.max_age_days !== undefined) { signals.push(`самый старый договорный возрастной сигнал ${overview.debt_open_settlement_quality.age_signal.max_age_days} дн.`); } + 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.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) { @@ -1257,6 +1294,7 @@ function businessOverviewExecutiveVerdictLine(overview: BusinessOverview): strin overview.trading_margin_proxy || overview.debt_position || overview.debt_open_settlement_quality || + overview.debt_staleness_risk_proxy || overview.inventory_position || overview.inventory_turnover_proxy || overview.inventory_staleness_risk_proxy @@ -1355,6 +1393,9 @@ export function buildAssistantMcpDiscoveryAnswerDraft( pushReason(reasonCodes, "answer_contains_business_overview_debt_age_signal"); } } + if (pilot.derived_business_overview?.debt_staleness_risk_proxy) { + pushReason(reasonCodes, "answer_contains_business_overview_debt_staleness_risk_proxy"); + } if (pilot.derived_business_overview?.inventory_position) { pushReason(reasonCodes, "answer_contains_business_overview_inventory_position"); } diff --git a/llm_normalizer/backend/src/services/assistantMcpDiscoveryPilotExecutor.ts b/llm_normalizer/backend/src/services/assistantMcpDiscoveryPilotExecutor.ts index 19c1b5c..c0977f5 100644 --- a/llm_normalizer/backend/src/services/assistantMcpDiscoveryPilotExecutor.ts +++ b/llm_normalizer/backend/src/services/assistantMcpDiscoveryPilotExecutor.ts @@ -155,6 +155,7 @@ export interface AssistantMcpDiscoveryDerivedBusinessOverview { trading_margin_proxy: AssistantMcpDiscoveryDerivedBusinessOverviewTradingMarginProxy | null; debt_position: AssistantMcpDiscoveryDerivedBusinessOverviewDebtPosition | null; debt_open_settlement_quality: AssistantMcpDiscoveryDerivedBusinessOverviewDebtOpenSettlementQuality | null; + debt_staleness_risk_proxy: AssistantMcpDiscoveryDerivedBusinessOverviewDebtStalenessRiskProxy | null; inventory_position: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryPosition | null; inventory_turnover_proxy: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryTurnoverProxy | null; inventory_staleness_risk_proxy: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryStalenessRiskProxy | null; @@ -277,6 +278,21 @@ export interface AssistantMcpDiscoveryDerivedBusinessOverviewDebtOpenSettlementQ inference_basis: "open_contracts_confirmed_1c_balance_rows"; } +export interface AssistantMcpDiscoveryDerivedBusinessOverviewDebtStalenessRiskProxy { + as_of_date: string; + gross_open_amount: number; + gross_open_amount_human_ru: string; + oldest_contract_start_date: string; + max_contract_age_days: number; + top_contract: string; + top_contract_counterparty: string | null; + top_contract_amount: number; + top_contract_amount_human_ru: string; + top_contract_share_pct: number; + risk_band: "lower_visible_risk" | "watch" | "elevated" | "high"; + inference_basis: "contract_date_age_and_open_balance_concentration_confirmed_1c_rows"; +} + export interface AssistantMcpDiscoveryBusinessOverviewInventoryItemBucket { item: string; rows_with_amount: number; @@ -3111,6 +3127,74 @@ function deriveBusinessOverviewDebtOpenSettlementQuality(input: { }; } +function debtStalenessRiskBand(input: { + maxContractAgeDays: number; + topContractSharePct: number; +}): AssistantMcpDiscoveryDerivedBusinessOverviewDebtStalenessRiskProxy["risk_band"] { + if (input.maxContractAgeDays >= 365 && input.topContractSharePct >= 50) { + return "high"; + } + if (input.maxContractAgeDays >= 365 || input.topContractSharePct >= 50) { + return "elevated"; + } + if (input.maxContractAgeDays >= 180 || input.topContractSharePct >= 35) { + return "watch"; + } + return "lower_visible_risk"; +} + +function deriveBusinessOverviewDebtStalenessRiskProxy( + quality: AssistantMcpDiscoveryDerivedBusinessOverviewDebtOpenSettlementQuality | null +): AssistantMcpDiscoveryDerivedBusinessOverviewDebtStalenessRiskProxy | null { + const ageSignal = quality?.age_signal; + const topAgedContract = ageSignal?.top_aged_contracts[0]; + const topContractSharePct = topAgedContract?.share_of_gross_open_amount_pct; + if ( + !quality || + !ageSignal?.oldest_start_date || + ageSignal.max_age_days === null || + ageSignal.max_age_days === undefined || + !topAgedContract || + topContractSharePct === null || + topContractSharePct === undefined + ) { + return null; + } + + return { + as_of_date: quality.as_of_date, + gross_open_amount: quality.gross_open_amount, + gross_open_amount_human_ru: quality.gross_open_amount_human_ru, + oldest_contract_start_date: ageSignal.oldest_start_date, + max_contract_age_days: ageSignal.max_age_days, + top_contract: topAgedContract.contract, + top_contract_counterparty: topAgedContract.counterparty, + top_contract_amount: topAgedContract.total_amount, + top_contract_amount_human_ru: topAgedContract.total_amount_human_ru, + top_contract_share_pct: topContractSharePct, + risk_band: debtStalenessRiskBand({ + maxContractAgeDays: ageSignal.max_age_days, + topContractSharePct + }), + inference_basis: "contract_date_age_and_open_balance_concentration_confirmed_1c_rows" + }; +} + +function debtStalenessRiskBandRu( + riskBand: AssistantMcpDiscoveryDerivedBusinessOverviewDebtStalenessRiskProxy["risk_band"] +): string { + if (riskBand === "high") { + return "высокая зона внимания"; + } + if (riskBand === "elevated") { + return "повышенная зона внимания"; + } + if (riskBand === "watch") { + return "зона наблюдения"; + } + return "низкий видимый риск"; +} + function daysBetweenIsoDates(leftIsoDate: string, rightIsoDate: string): number | null { const leftMatch = leftIsoDate.match(/^(\d{4})-(\d{2})-(\d{2})$/); const rightMatch = rightIsoDate.match(/^(\d{4})-(\d{2})-(\d{2})$/); @@ -3362,6 +3446,7 @@ function deriveBusinessOverview(input: { openContractsResult: input.openContractsResult, debtAsOfDate: input.debtAsOfDate }); + const debtStalenessRiskProxy = deriveBusinessOverviewDebtStalenessRiskProxy(debtOpenSettlementQuality); const inventoryPosition = deriveBusinessOverviewInventoryPosition({ inventoryOnHandResult: input.inventoryOnHandResult, inventoryAgingResult: input.inventoryAgingResult, @@ -3383,6 +3468,7 @@ function deriveBusinessOverview(input: { Boolean(tradingMarginProxy), Boolean(debtPosition), Boolean(debtOpenSettlementQuality), + Boolean(debtStalenessRiskProxy), Boolean(inventoryPosition), Boolean(inventoryTurnoverProxy), Boolean(inventoryStalenessRiskProxy) @@ -3406,6 +3492,7 @@ function deriveBusinessOverview(input: { trading_margin_proxy: tradingMarginProxy, debt_position: debtPosition, debt_open_settlement_quality: debtOpenSettlementQuality, + debt_staleness_risk_proxy: debtStalenessRiskProxy, inventory_position: inventoryPosition, inventory_turnover_proxy: inventoryTurnoverProxy, inventory_staleness_risk_proxy: inventoryStalenessRiskProxy, @@ -3565,6 +3652,13 @@ function buildBusinessOverviewConfirmedFacts(derived: AssistantMcpDiscoveryDeriv ); } } + if (derived.debt_staleness_risk_proxy) { + const proxy = derived.debt_staleness_risk_proxy; + const counterpartyText = proxy.top_contract_counterparty ? ` / ${proxy.top_contract_counterparty}` : ""; + facts.push( + `Staleness risk proxy открытых расчетов на ${proxy.as_of_date}: самый старый договорный сигнал ${proxy.oldest_contract_start_date}, возраст ${proxy.max_contract_age_days} дн.; старейший крупный договор ${proxy.top_contract}${counterpartyText} держит ${proxy.top_contract_amount_human_ru} (${proxy.top_contract_share_pct}% брутто открытых остатков); оценка ${debtStalenessRiskBandRu(proxy.risk_band)}. Это не подтвержденная просрочка, не кредитный риск и не due-date aging.` + ); + } if (derived.inventory_position) { const leader = derived.inventory_position.top_items[0]; const leaderText = leader @@ -4737,6 +4831,9 @@ export async function executeAssistantMcpDiscoveryPilot( pushReason(reasonCodes, "pilot_derived_business_overview_debt_age_signal_from_contract_dates"); } } + if (derivedBusinessOverview.debt_staleness_risk_proxy) { + pushReason(reasonCodes, "pilot_derived_business_overview_debt_staleness_risk_proxy_from_confirmed_rows"); + } if (derivedBusinessOverview.inventory_position) { pushReason(reasonCodes, "pilot_derived_business_overview_inventory_position_from_confirmed_rows"); } diff --git a/llm_normalizer/backend/tests/assistantMcpDiscoveryAnswerAdapter.test.ts b/llm_normalizer/backend/tests/assistantMcpDiscoveryAnswerAdapter.test.ts index 73693a9..ce1a115 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryAnswerAdapter.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryAnswerAdapter.test.ts @@ -404,21 +404,29 @@ describe("assistant MCP discovery answer adapter", () => { expect(draft.headline).toContain("долговой срез"); expect(draft.headline).toContain("качество открытых расчетов"); expect(draft.headline).toContain("возрастной сигнал открытых расчетов"); + expect(draft.headline).toContain("staleness risk proxy открытых расчетов"); + expect(draft.headline).toContain("договорные сроки оплаты/due-date просрочка"); expect(draft.confirmed_lines.join("\n")).toContain("Долговой срез на 2020-12-31"); expect(draft.confirmed_lines.join("\n")).toContain("Качество открытых расчетов на 2020-12-31"); expect(draft.confirmed_lines.join("\n")).toContain("Возрастной сигнал открытых расчетов"); expect(draft.confirmed_lines.join("\n")).toContain("не due-date анализ"); + expect(draft.confirmed_lines.join("\n")).toContain("Staleness risk proxy открытых расчетов"); + expect(draft.confirmed_lines.join("\n")).toContain("высокая зона внимания"); + expect(draft.confirmed_lines.join("\n")).toContain("не due-date aging"); expect(draft.confirmed_lines.join("\n")).toContain("нетто"); expect(draft.inference_lines.join("\n")).toContain("Риски и контуры внимания"); expect(draft.inference_lines.join("\n")).toContain("самый старый договорный возрастной сигнал"); + expect(draft.inference_lines.join("\n")).toContain("staleness risk proxy открытых расчетов"); expect(draft.inference_lines.join("\n")).toContain("Сводный LLM-аудит"); expect(draft.unknown_lines.join("\n")).toContain("due-date"); expect(draft.reason_codes).toContain("answer_contains_business_overview_debt_position"); expect(draft.reason_codes).toContain("answer_contains_business_overview_open_settlement_quality"); expect(draft.reason_codes).toContain("answer_contains_business_overview_debt_age_signal"); + expect(draft.reason_codes).toContain("answer_contains_business_overview_debt_staleness_risk_proxy"); expect(draft.reason_codes).toContain("answer_contains_business_overview_analyst_synthesis"); expect(draft.must_not_claim).toContain("Do not present a debt-position snapshot as debt aging, overdue debt, or credit-quality analysis."); expect(draft.must_not_claim).toContain("Do not present open-settlement concentration as contractual due-date aging or confirmed overdue debt."); + expect(draft.must_not_claim).toContain("Do not present business overview debt staleness risk proxy as confirmed overdue debt, contractual delinquency, credit risk, or due-date aging."); }); it("surfaces checked inventory-position snapshot in business overview without treating it as warehouse liquidity", async () => { diff --git a/llm_normalizer/backend/tests/assistantMcpDiscoveryPilotExecutor.test.ts b/llm_normalizer/backend/tests/assistantMcpDiscoveryPilotExecutor.test.ts index a41d1bb..63a5bf7 100644 --- a/llm_normalizer/backend/tests/assistantMcpDiscoveryPilotExecutor.test.ts +++ b/llm_normalizer/backend/tests/assistantMcpDiscoveryPilotExecutor.test.ts @@ -468,6 +468,14 @@ describe("assistant MCP discovery pilot executor", () => { latest_start_date: "2020-03-15", max_age_days: 690 }); + expect(result.derived_business_overview?.debt_staleness_risk_proxy).toMatchObject({ + as_of_date: "2020-12-31", + oldest_contract_start_date: "2019-02-10", + max_contract_age_days: 690, + top_contract_amount: 90000, + top_contract_share_pct: 75, + risk_band: "high" + }); expect(result.derived_business_overview?.debt_open_settlement_quality?.age_signal?.top_aged_contracts[0]).toMatchObject({ contract: "Договор А от 10.02.2019", start_date: "2019-02-10", @@ -480,6 +488,8 @@ describe("assistant MCP discovery pilot executor", () => { expect(result.evidence.confirmed_facts.join("\n")).toContain("Долговая позиция на 2020-12-31"); expect(result.evidence.confirmed_facts.join("\n")).toContain("Качество открытых расчетов на 2020-12-31"); expect(result.evidence.confirmed_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.confirmed_facts.join("\n")).toContain("не due-date анализ"); expect(result.evidence.unknown_facts.join("\n")).toContain("due-date"); expect(result.reason_codes).toContain("pilot_business_overview_debt_query_mcp_executed"); @@ -487,6 +497,7 @@ describe("assistant MCP discovery pilot executor", () => { expect(result.reason_codes).toContain("pilot_business_overview_open_contracts_query_mcp_executed"); expect(result.reason_codes).toContain("pilot_derived_business_overview_open_settlement_quality_from_confirmed_rows"); expect(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); const receivablesCall = deps.executeAddressMcpQuery.mock.calls[3]?.[0]; const payablesCall = deps.executeAddressMcpQuery.mock.calls[4]?.[0];