Укрепить автономные reviewed-маршруты и срезы задолженности

This commit is contained in:
dctouch 2026-05-12 23:26:20 +03:00
parent 5dedfeee86
commit b99a3be083
46 changed files with 3234 additions and 142 deletions

View File

@ -83,10 +83,11 @@ Fresh validation cut:
- Completed autonomy slice inside that loop: `Proof-Family Enablement Candidates`: exact organization-level profit/margin, overdue/due-date debt aging, inventory reserve/liquidation, and vendor-risk/procurement-quality asks remain user-safe while route candidates mark the missing reviewed proof families as `needs_route_enablement`.
- Completed autonomy slice inside that loop: `Accounting Profit-Margin Reviewed Route`: `accounting_profit_margin` is now promoted from `needs_route_enablement` into a reviewed 90/91/99 accounting-result route with accepted live replay.
- Completed autonomy slice inside that loop: `Debt Due-Date Aging Reviewed Route`: `debt_due_date_aging_quality` is now promoted from proxy-only route-candidate gap into a reviewed payment-term/open-balance route with accepted live replay.
- Current live canary: `phase94_debt_due_date_aging_reviewed_route_live4` accepted `7/7`.
- Current accepted autorun: `AGENT | Phase 94 debt due-date aging reviewed route` (`gen-ag05101319-c04f79`).
- Completed autonomy slice inside that loop: `Vendor/Procurement Quality Reviewed Route`: `vendor_risk_procurement_quality` now promotes to reviewed procurement-concentration evidence when confirmed outgoing payment, bank-like recipient segregation, non-financial recipient, counterparty-role, and contract-usage signals are reachable; phase95 live replay is accepted.
- Current live canary: `phase95_vendor_procurement_quality_reviewed_route_live2` accepted `7/7`.
- Current accepted autorun: `AGENT | Phase 95 vendor/procurement quality reviewed route` (`gen-ag05121357-9ea5d6`).
- Implementation breadth: `~99% (Open-World Bounded Autonomy Breadth through Slice 25)`.
- Next active slice: select the next phase92 proof family, likely vendor/procurement quality or inventory reserve/liquidation, and drive it through repair -> reviewed route enablement -> rerun, without treating proxy-only evidence as proof.
- Next active slice: select the remaining phase92 proof family `inventory_reserve_liquidation_quality`.
- Active module progress: `~99% (Agentic Semantic Development Loop, accepted dogfood loop + autorun hygiene; manual GUI confirmation still required)`.
## Reporting Rule
@ -98,7 +99,7 @@ Use these labels when reporting progress:
- `Прогресс модуля: 99% (Open-World Bounded Autonomy Breadth, active slice: Semantic Control Gate)` while discussing current module closure after the EHMO-derived critical subset accepted live again with W5/W7 hardening.
- `Прогресс модуля: 99% (Agentic Semantic Development Loop, accepted dogfood loop + autorun hygiene; manual GUI confirmation still required)` when discussing the current development-loop operating layer.
- `Прогресс модуля: 100% (Open-World Route Candidate Promotion, declared phase90 slice accepted)` when discussing the route-candidate handoff slice itself.
- `Прогресс модуля: 84% (Route-Candidate-Driven Enablement Loop, active slice: second reviewed proof-family route accepted)` when discussing the current candidate-driven enablement loop.
- `Прогресс модуля: 92% (Route-Candidate-Driven Enablement Loop, active slice: third reviewed proof-family route accepted)` when discussing the current candidate-driven enablement loop.
- `Open-World Business Overview implementation breadth: ~99%, Semantic Control Gate critical subset accepted, fat GUI pack still pending` when discussing only the already wired Slice 25 breadth.
- `Прогресс модуля: X% (Open-World Bounded Autonomy Breadth, active slice: <name>)` for later breadth work after the Semantic Control Gate is accepted.
@ -134,7 +135,7 @@ Remaining work belongs to the next breadth module:
- confirm the latest autorun Cyrillic hygiene cut in the GUI after backend refresh and inspect frontend/API payloads if old replacement characters remain visible;
- continue dogfooding the `Agentic Semantic Development Loop` on real stage packs, especially generated-question quality, semantic business audit, repair handoff, and rerun acceptance;
- finish closure of the `Open-World Semantic Control Gate` opened by `assistant-stage1-EHMOy3lNFt`; the EHMO-derived critical subset is accepted live after W5/W7 hardening, but the fat GUI pack and residual answer-shape roughness still need final review;
- extend `business_overview` beyond money-flow/activity, customer and supplier concentration, document/account-section activity mix, counterparty role split, contract usage, yearly operating-flow dynamics, explicit profit/margin wording boundaries, explicit debt due-date wording boundaries, explicit inventory reserve/liquidation wording boundaries, explicit supplier/procurement-quality wording boundaries, explicit-period VAT/tax, as-of-date debt position, open-settlement concentration, contract-date debt age, debt staleness-risk proxy, as-of-date inventory position, trading-margin proxy, sales-to-stock inventory proxy, warehouse staleness-risk proxy, the missing-proof ledger, the reviewed accounting profit/margin route, and the reviewed debt due-date aging route into separately proven vendor-risk/procurement-quality analysis and confirmed reserve/write-off/liquidation inventory evidence families;
- extend `business_overview` beyond money-flow/activity, customer and supplier concentration, document/account-section activity mix, counterparty role split, contract usage, yearly operating-flow dynamics, explicit profit/margin wording boundaries, explicit debt due-date wording boundaries, explicit inventory reserve/liquidation wording boundaries, explicit supplier/procurement-quality wording boundaries, explicit-period VAT/tax, as-of-date debt position, open-settlement concentration, contract-date debt age, debt staleness-risk proxy, as-of-date inventory position, trading-margin proxy, sales-to-stock inventory proxy, warehouse staleness-risk proxy, the missing-proof ledger, the reviewed accounting profit/margin route, the reviewed debt due-date aging route, and the reviewed vendor/procurement concentration route into 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;

View File

@ -50,6 +50,8 @@ Live semantic replay:
- accepted user-runnable autorun for that route replay: `AGENT | Phase 93 accounting profit-margin reviewed route` (`gen-ag05101213-596d99`).
- second reviewed proof-family route replay: `artifacts/domain_runs/phase94_debt_due_date_aging_reviewed_route_live4`, `7/7` passed, `0` warnings, `0` failures.
- accepted user-runnable autorun for that route replay: `AGENT | Phase 94 debt due-date aging reviewed route` (`gen-ag05101319-c04f79`).
- third proof-family route replay: `artifacts/domain_runs/phase95_vendor_procurement_quality_reviewed_route_live2`, `7/7` passed, `0` warnings, `0` failures.
- accepted user-runnable autorun for that route replay: `AGENT | Phase 95 vendor/procurement quality reviewed route` (`gen-ag05121357-9ea5d6`).
The replay proves the user-facing route-candidate canary remains healthy while the development tooling starts treating route candidates as repair-loop input:
@ -61,28 +63,30 @@ The replay proves the user-facing route-candidate canary remains healthy while t
- exact P&L, due-date debt aging, vendor-risk/procurement-quality, and reserve/liquidation wording now surface as concrete `needs_route_enablement` proof-family candidates when the reviewed route is missing.
- exact accounting profit/margin wording now has the first reviewed route implementation and can move to `ready_for_reviewed_execution` through confirmed 90/91/99 accounting rows.
- exact due-date debt aging wording now has the second reviewed route implementation and can move to `ready_for_reviewed_execution` through confirmed open-balance/payment-term evidence, while absent payment terms produce an honest checked-negative boundary answer.
- exact vendor-risk/procurement wording now has a live-accepted reviewed procurement-concentration route that can move to `ready_for_reviewed_execution` when confirmed outgoing payment, bank-like recipient segregation, non-financial recipient, counterparty-role, and contract-usage evidence are reachable. It still does not prove supplier reliability, delivery quality, payment purpose, contract-term compliance, or full expense structure.
## Status
Current module wording:
`Route-Candidate-Driven Enablement Loop, active slice: second reviewed proof-family route accepted`
`Route-Candidate-Driven Enablement Loop, active slice: third reviewed proof-family route accepted`
Progress: `84%`.
Progress: `92%`.
The first cut proved the handoff mechanics and live canary. The second cut proved real proof-family candidates and a saved accepted AGENT pack. The third cut proved the intended promotion loop on `accounting_profit_margin`. The fourth cut proves the same loop on `debt_due_date_aging_quality`, including short boundary follow-up continuity and saved accepted autorun hygiene. The module is still not complete because vendor/procurement quality and inventory reserve/liquidation need the same treatment or an explicit bounded non-implementation decision.
The first cut proved the handoff mechanics and live canary. The second cut proved real proof-family candidates and a saved accepted AGENT pack. The third cut proved the intended promotion loop on `accounting_profit_margin`. The fourth cut proved the same loop on `debt_due_date_aging_quality`, including short boundary follow-up continuity and saved accepted autorun hygiene. The fifth cut proves `vendor_risk_procurement_quality` as reviewed procurement-concentration evidence with accepted phase95 replay. The module is still not complete because the inventory reserve/liquidation proof family remains open.
## Next Work
Next slices:
1. Pick the next high-value proof family from phase92: `vendor_risk_procurement_quality` or `inventory_reserve_liquidation_quality`.
2. Implement the smallest reviewed route/capability for that proof family only if reliable 1C evidence is reachable.
3. Rerun phase94 as a canary plus a focused route-specific pack until the selected candidate changes from `needs_route_enablement` to `ready_for_reviewed_execution` or a clearly bounded non-implementation decision.
4. Save the accepted pack into autoruns only after the live replay and semantic review pass.
1. Pick the final phase92 proof family: `inventory_reserve_liquidation_quality`.
2. Implement or explicitly bound that final family only if reliable 1C evidence is reachable.
3. Rerun phase95 as a canary plus the focused inventory route-specific pack.
4. Save the accepted pack into autoruns only after live replay and semantic review pass.
See also:
- [27 - proof_family_enablement_candidates_2026-05-10.md](./27%20-%20proof_family_enablement_candidates_2026-05-10.md)
- [28 - accounting_profit_margin_reviewed_route_2026-05-10.md](./28%20-%20accounting_profit_margin_reviewed_route_2026-05-10.md)
- [29 - debt_due_date_aging_reviewed_route_2026-05-10.md](./29%20-%20debt_due_date_aging_reviewed_route_2026-05-10.md)
- [30 - vendor_procurement_quality_reviewed_route_2026-05-12.md](./30%20-%20vendor_procurement_quality_reviewed_route_2026-05-12.md)

View File

@ -84,27 +84,40 @@ The second candidate from this slice has now been promoted:
This means `debt_due_date_aging_quality` is no longer only a proxy-only proof-family candidate. It is now a reviewed executable route that can produce checked negative answers when payment terms are not configured.
## Phase95 Follow-Up
The third candidate from this slice has now been promoted:
- proof family: `vendor_risk_procurement_quality`;
- reviewed evidence boundary: procurement/outgoing-payment concentration inside `business_overview`;
- evidence basis: confirmed outgoing payment rows, top outgoing recipient, top non-financial recipient, counterparty role profile, and contract usage profile;
- local validation: executor/runtime bridge/answer/candidate tests passed `118/118` with `1` skipped; `npm.cmd run build` passed;
- accepted run: `artifacts/domain_runs/phase95_vendor_procurement_quality_reviewed_route_live2`, `7/7` passed;
- accepted autorun: `AGENT | Phase 95 vendor/procurement quality reviewed route` (`gen-ag05121357-9ea5d6`).
This means `vendor_risk_procurement_quality` is no longer only a missing-proof candidate when reviewed procurement-concentration evidence is reachable. It is still not a full vendor-risk due-diligence engine: supplier reliability, delivery quality, payment purpose, contract-term compliance, and complete expense structure remain unproved.
## Status
Current module wording:
`Route-Candidate-Driven Enablement Loop, active slice: second reviewed proof-family route accepted`
`Route-Candidate-Driven Enablement Loop, active slice: third reviewed proof-family route accepted`
Progress: `84%`.
Progress: `92%`.
This cut proved the missing-proof candidate surface and accepted user-runnable AGENT canary. Phase93 then implemented the first exact reviewed route for the accounting profit/margin family. Phase94 implemented the second reviewed route for due-date debt aging and verified short boundary follow-up continuity. The remaining closure work is to repeat that discipline for vendor/procurement quality and inventory reserve/liquidation, or explicitly bound them if exact evidence is not reachable.
This cut proved the missing-proof candidate surface and accepted user-runnable AGENT canary. Phase93 then implemented the first exact reviewed route for the accounting profit/margin family. Phase94 implemented the second reviewed route for due-date debt aging and verified short boundary follow-up continuity. Phase95 now promotes vendor/procurement quality through reviewed procurement-concentration evidence and accepted live replay. The remaining closure work is inventory reserve/liquidation enablement or an explicit bounded non-implementation decision.
## Next Work
Next slices:
1. Pick the next high-value proof family from the accepted phase92 candidate set: vendor/procurement quality or inventory reserve/liquidation.
2. Wire the smallest reviewed exact route/capability for that family, starting with the data that is already reachable through MCP/1C.
3. Keep the existing proxy answer as a fallback boundary, not as the final proof.
4. Rerun the phase94 canary plus a focused route-specific pack until the selected candidate moves from `needs_route_enablement` to `ready_for_reviewed_execution` or an explicit non-implementation boundary.
5. Save the new AGENT autorun only after the live replay and semantic review pass.
1. Wire or explicitly bound the final remaining family: `inventory_reserve_liquidation_quality`.
2. Keep proxy-only inventory reserve/liquidation wording bounded until reviewed evidence exists.
3. Rerun phase95 as a canary plus the focused inventory route-specific pack.
4. Save the next AGENT autorun only after live replay and semantic review pass.
See also:
- [28 - accounting_profit_margin_reviewed_route_2026-05-10.md](./28%20-%20accounting_profit_margin_reviewed_route_2026-05-10.md)
- [29 - debt_due_date_aging_reviewed_route_2026-05-10.md](./29%20-%20debt_due_date_aging_reviewed_route_2026-05-10.md)
- [30 - vendor_procurement_quality_reviewed_route_2026-05-12.md](./30%20-%20vendor_procurement_quality_reviewed_route_2026-05-12.md)

View File

@ -81,18 +81,16 @@ This keeps the phase93 accounting route as a canary while proving the candidate-
Current module wording:
`Route-Candidate-Driven Enablement Loop, active slice: second reviewed proof-family route accepted`
`Route-Candidate-Driven Enablement Loop, active slice: third reviewed proof-family route accepted`
Progress: `84%`.
Progress: `92%`.
This was the first proof that the loop can turn a route candidate into an executable reviewed route. Phase94 has now repeated the pattern for due-date debt aging. The module is not yet complete because vendor/procurement quality and inventory reserve/liquidation still need the same treatment or an explicit bounded non-implementation decision.
This was the first proof that the loop can turn a route candidate into an executable reviewed route. Phase94 repeated the pattern for due-date debt aging, and phase95 accepted vendor/procurement quality through reviewed procurement-concentration evidence. The module is not yet complete because inventory reserve/liquidation still needs the same treatment or an explicit bounded non-implementation decision.
## Next Work
Next slices:
1. Pick the next phase92 proof family: `vendor_risk_procurement_quality` or `inventory_reserve_liquidation_quality`.
2. Identify reachable 1C evidence for the selected family, or an honest fallback if the contour cannot prove it exactly.
3. Wire the smallest reviewed route/capability only if the evidence is reliable enough.
4. Rerun the phase94 canary plus a focused route-specific pack until the new route is either `ready_for_reviewed_execution` or explicitly bounded.
5. Save the next AGENT autorun only after live replay and semantic review pass.
1. Select the remaining proof family: `inventory_reserve_liquidation_quality`.
2. Wire only the smallest reliable reviewed route, not a broad heuristic.
3. Keep proxy-only reserve/liquidation wording bounded until the route is accepted.

View File

@ -77,20 +77,17 @@ The accepted replay proves:
Current module wording:
`Route-Candidate-Driven Enablement Loop, active slice: second reviewed proof-family route accepted`
`Route-Candidate-Driven Enablement Loop, active slice: third reviewed proof-family route accepted`
Progress: `84%`.
Progress: `92%`.
This is the second proof that the loop can turn a route candidate into an executable reviewed route. The module is not yet complete because vendor/procurement quality and inventory reserve/liquidation still need either reviewed exact routes or explicit bounded non-implementation decisions.
This is the second live-accepted proof that the loop can turn a route candidate into an executable reviewed route. Phase95 now accepts `vendor_risk_procurement_quality` through reviewed procurement-concentration evidence. The module is not yet complete because inventory reserve/liquidation still needs either reviewed exact route enablement or an explicit bounded non-implementation decision.
## Next Work
Next slices:
1. Pick the next phase92 proof family: either `vendor_risk_procurement_quality` or `inventory_reserve_liquidation_quality`.
2. Identify whether reliable 1C evidence exists for the chosen family.
3. Wire the smallest reviewed route only if it can prove the business claim without overreach.
4. Keep proxy-only evidence bounded if exact proof is not reachable.
5. Rerun phase94 as a canary plus the focused next route-specific pack.
6. Save the next AGENT autorun only after accepted live replay and semantic review.
1. Identify whether reliable 1C evidence exists for `inventory_reserve_liquidation_quality`.
2. Wire the smallest reviewed route only if it can prove the inventory business claim without overreach.
3. Keep proxy-only evidence bounded if exact proof is not reachable.
4. Rerun phase95 as a canary plus the focused inventory route-specific pack.

View File

@ -0,0 +1,90 @@
# 30 - Vendor/Procurement Quality Reviewed Route (2026-05-12)
## Purpose
This note records the third proof-family promotion inside the `Route-Candidate-Driven Enablement Loop`.
The selected proof family is `vendor_risk_procurement_quality`.
The implementation deliberately does not pretend to be a full vendor-risk due-diligence engine. The reachable reviewed evidence is procurement/outgoing-payment concentration, supported by counterparty-role and contract-usage profiles.
The intended answer is:
- direct enough for supplier/procurement dependency questions;
- honest about bank-like counterparties such as `СБЕРБАНК, ПАО`;
- explicit that supplier reliability, delivery quality, payment purpose, contract terms, and full expense structure are not proven by this route.
## Implementation Cut
Implemented locally:
- `business_overview` now derives `vendor_procurement_quality` from confirmed outgoing payment rows, ranked outgoing counterparties, counterparty role profile, and contract usage profile;
- the derivation records:
- total outgoing amount and row count;
- top outgoing recipient and share;
- top non-financial recipient and share;
- whether a bank/financial institution leads the outgoing cash concentration;
- supplier-only/mixed-role counts and contract usage counters when available;
- evidence status: `reviewed_procurement_concentration`, `financial_institution_leads_outgoing_cash`, or `insufficient_supplier_concentration_basis`;
- `vendor_risk_procurement_quality` is no longer left in `missing_proof_families` when this reviewed procurement-concentration evidence exists;
- route-candidate status can now move to `ready_for_reviewed_execution` for `vendor_risk_procurement_boundary` instead of staying in `needs_route_enablement`;
- user-facing response candidates and answer drafts prefer a direct procurement-concentration explanation over the old generic proxy wording;
- bank-like outgoing leaders are separated from ordinary supplier dependency rather than being presented as normal suppliers.
## Boundaries
Still not claimed:
- supplier reliability;
- delivery quality;
- payment purpose for each payment;
- contract-term compliance;
- SLA/procurement due diligence;
- full expense structure.
This slice proves a reviewed procurement concentration boundary, not a complete vendor-risk audit.
## Validation
Local validation:
- `npm.cmd test -- assistantMcpDiscoveryPilotExecutor.test.ts assistantMcpDiscoveryRuntimeBridge.test.ts assistantMcpDiscoveryAnswerAdapter.test.ts assistantMcpDiscoveryResponseCandidate.test.ts` passed `118/118` with `1` skipped;
- `npm.cmd run build` passed.
Semantic/live replay:
- spec: `docs/orchestration/address_truth_harness_phase95_vendor_procurement_quality_reviewed_route.json`;
- accepted run: `artifacts/domain_runs/phase95_vendor_procurement_quality_reviewed_route_live2`;
- final status: `accepted`, `7/7` passed, `0` warnings, `0` failures;
- accepted autorun: `AGENT | Phase 95 vendor/procurement quality reviewed route` (`gen-ag05121357-9ea5d6`).
## Status
Current module wording:
`Route-Candidate-Driven Enablement Loop, active slice: third reviewed proof-family route accepted`
Progress: `92%`.
The loop has now promoted three phase92 proof families from candidate gaps into reviewed execution or locally reviewed execution:
- `accounting_profit_margin` accepted live by phase93;
- `debt_due_date_aging_quality` accepted live by phase94;
- `vendor_risk_procurement_quality` accepted live through procurement-concentration evidence by phase95.
The module is still not complete because the remaining `inventory_reserve_liquidation_quality` family still needs either reviewed-route enablement or an explicit bounded non-implementation decision.
## Next Work
Next slices:
1. Select the final remaining phase92 family: `inventory_reserve_liquidation_quality`.
2. Determine whether reachable 1C evidence can prove reserve/write-off/liquidation quality without overreach.
3. If yes, wire the smallest reviewed route and run a focused phase96 semantic replay.
4. If no, record an explicit bounded non-implementation decision and keep the proxy answer honest.
See also:
- [26 - route_candidate_driven_enablement_loop_2026-05-10.md](./26%20-%20route_candidate_driven_enablement_loop_2026-05-10.md)
- [27 - proof_family_enablement_candidates_2026-05-10.md](./27%20-%20proof_family_enablement_candidates_2026-05-10.md)
- [29 - debt_due_date_aging_reviewed_route_2026-05-10.md](./29%20-%20debt_due_date_aging_reviewed_route_2026-05-10.md)

View File

@ -47,6 +47,7 @@ This package answers the next question:
27. [27 - proof_family_enablement_candidates_2026-05-10.md](./27%20-%20proof_family_enablement_candidates_2026-05-10.md)
28. [28 - accounting_profit_margin_reviewed_route_2026-05-10.md](./28%20-%20accounting_profit_margin_reviewed_route_2026-05-10.md)
29. [29 - debt_due_date_aging_reviewed_route_2026-05-10.md](./29%20-%20debt_due_date_aging_reviewed_route_2026-05-10.md)
30. [30 - vendor_procurement_quality_reviewed_route_2026-05-12.md](./30%20-%20vendor_procurement_quality_reviewed_route_2026-05-12.md)
## Current Status Snapshot (2026-05-10)
@ -100,6 +101,8 @@ Status canon for planning:
- The accepted user-runnable autorun for that slice is `AGENT | Phase 93 accounting profit-margin reviewed route` (`gen-ag05101213-596d99`).
- The second proof-family route is now implemented and accepted: `debt_due_date_aging_quality` moved from proxy-only route-candidate gap to reviewed payment-term/open-balance execution; `phase94_debt_due_date_aging_reviewed_route_live4` passed `7/7`.
- The accepted user-runnable autorun for that slice is `AGENT | Phase 94 debt due-date aging reviewed route` (`gen-ag05101319-c04f79`).
- The third proof-family route is now implemented and accepted: `vendor_risk_procurement_quality` moves from missing proof-family gap into reviewed procurement-concentration evidence when outgoing payment, bank-like recipient, non-financial recipient, counterparty-role, and contract-usage signals are reachable; `phase95_vendor_procurement_quality_reviewed_route_live2` passed `7/7`.
- The accepted user-runnable autorun for that slice is `AGENT | Phase 95 vendor/procurement quality reviewed route` (`gen-ag05121357-9ea5d6`).
- The phase94 replay spec was repaired to real UTF-8 Russian before autorun persistence, so the saved user-runnable pack does not repeat the earlier GUI mojibake/card-text regression.
- 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).
- The current execution spine after EHMO is [23 - current_execution_spine_and_semantic_control_gate_2026-05-05.md](./23%20-%20current_execution_spine_and_semantic_control_gate_2026-05-05.md).
@ -109,6 +112,7 @@ Status canon for planning:
- The current proof-family enablement-candidate slice is [27 - proof_family_enablement_candidates_2026-05-10.md](./27%20-%20proof_family_enablement_candidates_2026-05-10.md).
- The current first reviewed proof-family route slice is [28 - accounting_profit_margin_reviewed_route_2026-05-10.md](./28%20-%20accounting_profit_margin_reviewed_route_2026-05-10.md).
- The current second reviewed proof-family route slice is [29 - debt_due_date_aging_reviewed_route_2026-05-10.md](./29%20-%20debt_due_date_aging_reviewed_route_2026-05-10.md).
- The current third reviewed proof-family route slice is [30 - vendor_procurement_quality_reviewed_route_2026-05-12.md](./30%20-%20vendor_procurement_quality_reviewed_route_2026-05-12.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:
@ -172,13 +176,13 @@ Current honest status:
- pre-multidomain readiness: `~90%`
- bounded-autonomy foundation readiness: `~89%`
- open-world bounded-autonomy readiness: `~87%`
- active Open-World Bounded Autonomy Breadth implementation breadth: `~99%`, with business-overview evidence fusion, the reviewed `business_overview` catalog/data-need/planner route-fabric slice, the fresh multi-probe runtime bridge, the explicit-period VAT/tax fact-family bridge, the explicit-period debt-position bridge, the explicit-date inventory-position bridge, the open-settlement quality bridge accepted by live semantic replay, selected-item profitability bridged by local semantic/runtime regression tests, contract-date debt age bridged locally, debt staleness-risk proxy bridged locally, debt due-date boundary arbitration bridged locally, inventory reserve/liquidation boundary arbitration bridged locally, supplier/procurement-quality boundary arbitration bridged locally, supplier concentration proxy bridged locally, document/account-section activity profile bridged locally, counterparty population/roles and contract usage profiles bridged locally, yearly operating-flow proxy bridged locally, earnings/best-year wording arbitration bridged locally, profit/margin wording boundary arbitration bridged locally, analyst synthesis added to business-overview answer drafting, company-period trading margin proxy bridged locally, inventory sales-to-stock proxy bridged locally, inventory staleness-risk proxy bridged locally, gap-specific answer shaping bridged locally, missing proof families recorded as runtime evidence ledger, exact accounting profit/margin promoted into a reviewed 90/91/99 route by phase93, and debt due-date aging promoted into a reviewed payment-term/open-balance route by phase94; confirmed vendor-risk/procurement-quality analysis and confirmed reserve/write-off/liquidation inventory evidence are still pending
- active Open-World Bounded Autonomy Breadth implementation breadth: `~99%`, with business-overview evidence fusion, the reviewed `business_overview` catalog/data-need/planner route-fabric slice, the fresh multi-probe runtime bridge, the explicit-period VAT/tax fact-family bridge, the explicit-period debt-position bridge, the explicit-date inventory-position bridge, the open-settlement quality bridge accepted by live semantic replay, selected-item profitability bridged by local semantic/runtime regression tests, contract-date debt age bridged locally, debt staleness-risk proxy bridged locally, debt due-date boundary arbitration bridged locally, inventory reserve/liquidation boundary arbitration bridged locally, supplier/procurement-quality boundary arbitration bridged locally, supplier concentration proxy bridged locally, document/account-section activity profile bridged locally, counterparty population/roles and contract usage profiles bridged locally, yearly operating-flow proxy bridged locally, earnings/best-year wording arbitration bridged locally, profit/margin wording boundary arbitration bridged locally, analyst synthesis added to business-overview answer drafting, company-period trading margin proxy bridged locally, inventory sales-to-stock proxy bridged locally, inventory staleness-risk proxy bridged locally, gap-specific answer shaping bridged locally, missing proof families recorded as runtime evidence ledger, exact accounting profit/margin promoted into a reviewed 90/91/99 route by phase93, debt due-date aging promoted into a reviewed payment-term/open-balance route by phase94, and vendor/procurement quality promoted into reviewed procurement-concentration evidence by phase95; confirmed reserve/write-off/liquidation inventory evidence is still pending
- active Open-World Bounded Autonomy Breadth accepted-module progress: `~99%`, because the EHMO-derived `Open-World Semantic Control Gate` critical subset accepts live at `21/21` after W5/W7 hardening; full closure is still held back for the fat manual GUI pack and remaining answer-shape residual review
- 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
- Open-World Route Candidate Promotion progress: `100%` for the declared phase90 slice, with structured `route_candidate` runtime contract, artifact propagation, live semantic replay accepted at `5/5`, and accepted AGENT autorun persistence; broader autonomous route enablement remains the next active slice
- Route-Candidate-Driven Enablement Loop progress: `84%`, with deterministic repair-target grouping, Lead Codex handoff surfacing, local tooling tests, live phase91 canary acceptance, phase92 proof-family candidates accepted/saved as a user-runnable AGENT autorun, `accounting_profit_margin` promoted into reviewed 90/91/99 execution by phase93 live replay, and `debt_due_date_aging_quality` promoted into reviewed payment-term/open-balance execution by phase94 live replay; the remaining unresolved proof families are vendor/procurement quality and inventory reserve/liquidation
- Route-Candidate-Driven Enablement Loop progress: `92%`, with deterministic repair-target grouping, Lead Codex handoff surfacing, local tooling tests, live phase91 canary acceptance, phase92 proof-family candidates accepted/saved as a user-runnable AGENT autorun, `accounting_profit_margin` promoted into reviewed 90/91/99 execution by phase93 live replay, `debt_due_date_aging_quality` promoted into reviewed payment-term/open-balance execution by phase94 live replay, and `vendor_risk_procurement_quality` promoted into reviewed procurement-concentration evidence by phase95 live replay; the remaining inventory reserve/liquidation proof family is still pending
- graph snapshot after latest rebuild: see `graphify-out/GRAPH_REPORT.md`
- current regression-gate breakpoint:
- the validated hot paths are no longer structurally broken;
@ -267,6 +271,7 @@ Latest live proof now includes:
- proof-family enablement candidates accepted locally/live: targeted runtime/answer/turn-input/candidate tests passed `178/178` with `8` skipped; `address_truth_harness_phase92_proof_family_enablement_candidates_live5_20260510` accepted `6/6`, proving exact profit/margin, debt due-date aging, vendor-risk/procurement-quality, and reserve/liquidation asks remain bounded while their missing reviewed proof families become concrete route-candidate enablement targets; the accepted autorun is `AGENT | Phase 92 proof-family enablement candidates` (`gen-ag05101045-374169`).
- accounting profit/margin reviewed route accepted locally/live: targeted runtime/answer/turn-input/candidate/intent tests passed `194/194` with `8` skipped; targeted VAT tax-period regression passed; `address_truth_harness_phase93_accounting_profit_margin_reviewed_route_live3_20260510` accepted `6/6`, proving 90/91/99 accounting result, short profit/loss follow-up continuity, VAT continuity, value-flow canary, and inventory reserve boundary canary together; the accepted autorun is `AGENT | Phase 93 accounting profit-margin reviewed route` (`gen-ag05101213-596d99`).
- debt due-date aging reviewed route accepted locally/live: transition policy passed `38/38`, turn-input adapter passed `103/103` with `7` skipped, executor/answer/candidate/runtime bridge passed `113/113` with `1` skipped, build passed; `phase94_debt_due_date_aging_reviewed_route_live4` accepted `7/7`, proving payment-term/open-balance checked-negative overdue answers, short due-date boundary follow-up continuity, profit/margin/VAT/value-flow canaries, and reserve/vendor boundary safety together; the accepted autorun is `AGENT | Phase 94 debt due-date aging reviewed route` (`gen-ag05101319-c04f79`).
- vendor/procurement quality reviewed route accepted locally/live: executor/runtime bridge/answer/candidate tests passed `118/118` with `1` skipped, build passed; `phase95_vendor_procurement_quality_reviewed_route_live2` accepted `7/7`; `vendor_risk_procurement_quality` now derives reviewed procurement-concentration evidence from confirmed outgoing payment rows, separates bank-like outgoing leaders from ordinary supplier dependency, removes the proof family from `missing_proof_families` when this reviewed evidence exists, and can promote `vendor_risk_procurement_boundary` route candidates to `ready_for_reviewed_execution`; the accepted autorun is `AGENT | Phase 95 vendor/procurement quality reviewed route` (`gen-ag05121357-9ea5d6`).
Current architectural reading:
@ -344,6 +349,8 @@ Read in this order:
27. `26 - route_candidate_driven_enablement_loop_2026-05-10.md`
28. `27 - proof_family_enablement_candidates_2026-05-10.md`
29. `28 - accounting_profit_margin_reviewed_route_2026-05-10.md`
30. `29 - debt_due_date_aging_reviewed_route_2026-05-10.md`
31. `30 - vendor_procurement_quality_reviewed_route_2026-05-12.md`
## Planning Rules

View File

@ -0,0 +1,249 @@
{
"schema_version": "domain_truth_harness_spec_v1",
"scenario_id": "address_truth_harness_phase95_vendor_procurement_quality_reviewed_route",
"domain": "address_phase95_vendor_procurement_quality_reviewed_route",
"title": "Phase 95 vendor/procurement quality reviewed route",
"description": "Focused semantic replay for promoting vendor_risk_procurement_quality from proxy-only enablement to reviewed procurement-concentration evidence while preserving debt, profit, VAT, value-flow, and inventory reserve canaries.",
"bindings": {},
"steps": [
{
"step_id": "step_01_vendor_procurement_concentration_ready",
"title": "Vendor/procurement boundary uses reviewed outgoing concentration evidence",
"question": "по ООО Альтернатива Плюс за 2020 есть ли риск, что мы зависим от одного поставщика?",
"allowed_reply_types": [
"factual",
"factual_with_explanation",
"partial_coverage"
],
"expected_catalog_alignment_status": "selected_matches_top",
"expected_catalog_chain_top_match": "business_overview",
"expected_catalog_selected_matches_top": true,
"expected_route_candidate_status": "ready_for_reviewed_execution",
"expected_route_candidate_executable_now": true,
"required_answer_patterns_all": [
"(?i)поставщик|vendor|supplier|закуп|procurement",
"(?i)концентрац|зависим|исходящ|получател",
"(?i)надежност|качество|назначени|не доказ|не подтвержд"
],
"forbidden_answer_patterns": [
"(?i)точно подтверждаю.*надежност",
"(?i)точно подтверждаю.*качество",
"(?i)route_candidate",
"(?i)query_movements",
"(?i)primitive",
"(?i)planner_",
"(?i)catalog_"
],
"criticality": "critical",
"semantic_tags": [
"business_overview",
"vendor_risk_procurement_boundary",
"vendor_risk_procurement_quality",
"ready_for_reviewed_execution"
]
},
{
"step_id": "step_02_short_vendor_followup_keeps_bank_boundary",
"title": "Short follow-up keeps bank-like recipient boundary",
"question": "а банк из этого ответа считать обычным поставщиком?",
"allowed_reply_types": [
"factual",
"factual_with_explanation",
"partial_coverage"
],
"expected_catalog_alignment_status": "selected_matches_top",
"expected_catalog_chain_top_match": "business_overview",
"expected_catalog_selected_matches_top": true,
"expected_route_candidate_status": "ready_for_reviewed_execution",
"expected_route_candidate_executable_now": true,
"required_answer_patterns_all": [
"(?i)банк|финансов",
"(?i)не.*обычн.*поставщик|не.*поставщик|отдельн",
"(?i)назначени.*платеж|договор|не доказ|не подтвержд"
],
"forbidden_answer_patterns": [
"(?i)уточните организац",
"(?i)какую компанию",
"(?i)route_candidate",
"(?i)primitive",
"(?i)planner_",
"(?i)catalog_"
],
"criticality": "critical",
"semantic_tags": [
"business_overview",
"context_carryover",
"vendor_risk_procurement_boundary",
"financial_institution_boundary"
]
},
{
"step_id": "step_03_debt_due_date_canary_still_reviewed",
"title": "Debt due-date canary still uses reviewed payment-term evidence",
"question": "по этой же компании на конец 2020 можно точно понять, какая дебиторка просрочена?",
"allowed_reply_types": [
"factual",
"factual_with_explanation",
"partial_coverage"
],
"expected_catalog_alignment_status": "selected_matches_top",
"expected_catalog_chain_top_match": "business_overview",
"expected_catalog_selected_matches_top": true,
"expected_route_candidate_status": "ready_for_reviewed_execution",
"expected_route_candidate_executable_now": true,
"required_answer_patterns_all": [
"(?i)дебитор|долг|открыт.*расчет|остат",
"(?i)2020|2020-12-31|конец 2020",
"(?i)срок.*оплат|due[- ]?date|просроч",
"(?i)не установлен|не подтвержд|не доказ|нет подтвержденной просроч"
],
"forbidden_answer_patterns": [
"(?i)точно подтверждаю.*просроч",
"(?i)route_candidate",
"(?i)primitive",
"(?i)planner_",
"(?i)catalog_"
],
"criticality": "critical",
"semantic_tags": [
"business_overview",
"debt_due_date_boundary",
"canary"
]
},
{
"step_id": "step_04_profit_margin_canary_still_reviewed",
"title": "Profit/margin canary still uses accounting result route",
"question": "а чистая прибыль и маржа за 2020 какие?",
"allowed_reply_types": [
"factual",
"factual_with_explanation",
"partial_coverage"
],
"expected_catalog_alignment_status": "selected_matches_top",
"expected_catalog_chain_top_match": "business_overview",
"expected_catalog_selected_matches_top": true,
"expected_route_candidate_status": "ready_for_reviewed_execution",
"expected_route_candidate_executable_now": true,
"required_answer_patterns_all": [
"(?i)90/91/99|90\\.01|99",
"(?i)учетн|финрезульт|прибыл|убыт",
"(?i)марж|рентаб",
"(?i)2020"
],
"forbidden_answer_patterns": [
"(?i)только bounded operating-flow/trading-margin proxy",
"(?i)route_candidate",
"(?i)primitive",
"(?i)planner_",
"(?i)catalog_"
],
"criticality": "critical",
"semantic_tags": [
"business_overview",
"profit_margin_boundary",
"accounting_profit_margin",
"canary"
]
},
{
"step_id": "step_05_vat_canary_still_answers",
"title": "VAT continuity canary still answers from reviewed tax route",
"question": "НДС за 2020 по ООО Альтернатива Плюс какой?",
"allowed_reply_types": [
"factual",
"factual_with_explanation",
"partial_coverage"
],
"required_answer_patterns_all": [
"(?i)НДС|VAT|налог",
"(?i)2020",
"(?i)продаж|покуп|к уплат|к возмещ|зачет",
"(?i)подтвержд|проверенн|1С"
],
"forbidden_answer_patterns": [
"(?i)route_candidate",
"(?i)primitive",
"(?i)planner_",
"(?i)catalog_"
],
"criticality": "critical",
"semantic_tags": [
"business_overview",
"vat_continuity",
"canary"
]
},
{
"step_id": "step_06_value_flow_ranking_context_still_works",
"title": "Value-flow ranking still uses organization context",
"question": "а кто принес больше всего денег за 2020?",
"allowed_reply_types": [
"factual",
"factual_with_explanation",
"partial_coverage"
],
"expected_catalog_alignment_status": "selected_matches_top",
"expected_catalog_chain_top_match": "value_flow_ranking",
"expected_catalog_selected_matches_top": true,
"expected_route_candidate_status": "ready_for_reviewed_execution",
"expected_route_candidate_executable_now": true,
"required_answer_patterns_all": [
"(?i)2020",
"(?i)контрагент|клиент|покупател",
"(?i)деньг|поступ|выруч|руб",
"(?i)подтвержд|проверенн|найден"
],
"forbidden_answer_patterns": [
"(?i)уточните организац",
"(?i)какую компанию",
"(?i)route_candidate",
"(?i)query_movements",
"(?i)primitive",
"(?i)planner_",
"(?i)catalog_"
],
"criticality": "critical",
"semantic_tags": [
"value_flow_ranking",
"context_carryover",
"canary"
]
},
{
"step_id": "step_07_inventory_reserve_boundary_still_needs_route",
"title": "Inventory reserve boundary remains honest and bounded",
"question": "по ООО Альтернатива Плюс на конец 2020 можно точно подтвердить резерв под неликвиды на складе?",
"allowed_reply_types": [
"factual",
"factual_with_explanation",
"partial_coverage",
"no_grounded_answer"
],
"expected_catalog_alignment_status": "selected_matches_top",
"expected_catalog_chain_top_match": "business_overview",
"expected_catalog_selected_matches_top": true,
"expected_route_candidate_status": "needs_route_enablement",
"expected_route_candidate_executable_now": false,
"required_answer_patterns_all": [
"(?i)резерв|неликвид|склад|товар",
"(?i)не подтвержд|не доказ|нельзя точно|нет точн",
"(?i)списан|ликвидац|учетн|провер"
],
"forbidden_answer_patterns": [
"(?i)точно подтверждаю",
"(?i)route_candidate",
"(?i)primitive",
"(?i)planner_",
"(?i)catalog_"
],
"criticality": "critical",
"semantic_tags": [
"business_overview",
"inventory_reserve_boundary",
"missing_proof_families",
"canary"
]
}
]
}

View File

@ -1758,6 +1758,10 @@ function resolveUnicodeAddressIntentBridge(text) {
if (!hasContractCue && hasCustomerConcentrationCue) {
return unicodeBridgeResolution("customer_revenue_and_payments", "high", "unicode_customer_concentration_bridge_signal_detected");
}
const hasTopYearRevenueRankingCue = /(?:(?:\u043a\u0430\u043a\u043e\u0439|\u043a\u0430\u043a\u0438\u0435|\u043a\u0430\u043a\u0430\u044f|what|which)[\s\S]{0,80}(?:\u0441\u0430\u043c\p{L}*|top|best|most)[\s\S]{0,80}(?:\u0434\u043e\u0445\u043e\u0434\u043d|\u0432\u044b\u0440\u0443\u0447\u043a|\u043e\u0431\u043e\u0440\u043e\u0442|revenue|turnover)[\s\S]{0,60}(?:\u0433\u043e\u0434|year)|(?:\u0434\u043e\u0445\u043e\u0434\u043d|\u0432\u044b\u0440\u0443\u0447\u043a|\u043e\u0431\u043e\u0440\u043e\u0442|revenue|turnover)[\s\S]{0,60}(?:\u0441\u0430\u043c\p{L}*|top|best|most)[\s\S]{0,60}(?:\u0433\u043e\u0434|year))/iu.test(normalized);
if (!hasContractCue && (hasTopYearRevenueRankingCue || hasCustomerRevenueRankingBridgeSignal(normalized))) {
return unicodeBridgeResolution("customer_revenue_and_payments", "high", "unicode_customer_revenue_ranking_bridge_signal_detected");
}
if (hasOrganizationLevelEarningsOverviewBridgeSignal(normalized)) {
return unicodeBridgeResolution("unknown", "high", "unicode_business_overview_earnings_deferred_to_discovery");
}
@ -2021,14 +2025,52 @@ function resolveUnicodeAddressIntentBridge(text) {
}
return null;
}
function resolveDirectDebtSnapshotIntent(text) {
const normalized = String(text ?? "").trim().toLowerCase();
if (!normalized) {
return null;
}
if (/(?:ндс|vat)/iu.test(normalized)) {
return null;
}
const hasSnapshotCue = /(?:кто|сколько|есть\s+ли|по\s+состоянию|на\s+сегодня|на\s+дату|срез|остаток|сальдо|баланс|на\s+(?:январ|феврал|март|апрел|ма[йя]|июн|июл|август|сентябр|октябр|ноябр|декабр)\S*(?:\s+(?:19|20)\d{2})?|на\s+(?:19|20)\d{2}|as\s+of|today|current|balance)/iu.test(normalized);
const hasReceivablesCue = /(?:кто\s+(?:является\s+)?дебитором|дебитор(?:[а-яё]{0,8})?|дебиторск(?:[а-яё]{0,8})?|кто\s+нам\s+долж(?:ен|ны|на|но)?|нам\s+(?:кто-то\s+|кто\s+)?долж(?:ен|ны|на|но)?|нам\s+торч(?:ат|ит|ишь|у|али)?|к\s+получению|к\s+взысканию|who\s+owes\s+us|receivables?|accounts\s+receivable)/iu.test(normalized);
const hasPayablesCue = /(?:кто\s+(?:является\s+)?кредитором|кредитор(?:[а-яё]{0,8})?|кому\s+мы\s+долж(?:ны|н[ао])?|мы\s+долж(?:ны|н[ао])?\s+кому|мы\s+долж(?:ны|н[ао])?|к\s+оплате|who\s+we\s+owe|payables?|accounts\s+payable)/iu.test(normalized);
if (hasReceivablesCue && !hasPayablesCue && hasSnapshotCue) {
return {
intent: "receivables_confirmed_as_of_date",
confidence: "high",
reasons: ["receivables_debt_lifecycle_signal_detected", "direct_debt_snapshot_signal_detected"]
};
}
if (hasPayablesCue && !hasReceivablesCue && hasSnapshotCue) {
return {
intent: "payables_confirmed_as_of_date",
confidence: "high",
reasons: ["payables_debt_lifecycle_signal_detected", "direct_debt_snapshot_signal_detected"]
};
}
return null;
}
function resolveAddressIntent(userMessage) {
const text = String(userMessage ?? "").trim().toLowerCase();
const repairedText = repairLikelyUtf8Mojibake(text).trim().toLowerCase();
const bridgeText = repairedText && repairedText !== text ? `${text} ${repairedText}` : text;
const turnNoiseNormalizedBridgeText = bridgeText
.replace(/(^|[^\p{L}0-9_])\u043d\u0430\u043c\u0441(?=$|[^\p{L}0-9_])/giu, "$1\u043d\u0430\u043c")
.replace(/(^|[^\p{L}0-9_])\u043a\u0430\u043a\u0438\u0435\u043a(?=$|[^\p{L}0-9_])/giu, "$1\u043a\u0430\u043a\u0438\u0435");
.replace(/(^|[^\p{L}0-9_])намс(?=$|[^\p{L}0-9_])/giu, "$1нам")
.replace(/(^|[^\p{L}0-9_])какиек(?=$|[^\p{L}0-9_])/giu, "$1какие");
const currentTurnBridgeText = turnNoiseNormalizedBridgeText !== bridgeText ? `${bridgeText} ${turnNoiseNormalizedBridgeText}` : bridgeText;
const directDebtSnapshotIntent = resolveDirectDebtSnapshotIntent(currentTurnBridgeText);
if (directDebtSnapshotIntent) {
const reasons = [...directDebtSnapshotIntent.reasons];
if (currentTurnBridgeText !== bridgeText && !reasons.includes("current_turn_noise_normalized")) {
reasons.push("current_turn_noise_normalized");
}
return {
...directDebtSnapshotIntent,
reasons
};
}
const unicodeAddressIntent = resolveUnicodeAddressIntentBridge(currentTurnBridgeText);
if (unicodeAddressIntent) {
const reasons = [...unicodeAddressIntent.reasons];

View File

@ -1822,10 +1822,10 @@ function buildAddressRecipePlan(recipe, filters) {
? toDateTimeExpr(filters.period_from, true)
: null) ??
"ТЕКУЩАЯДАТА()";
return PAYABLES_CONFIRMED_AS_OF_QUERY_TEMPLATE
return OPEN_CONTRACTS_CONFIRMED_AS_OF_QUERY_TEMPLATE
.replaceAll("__LIMIT__", String(resolvedLimit))
.replaceAll("__AS_OF_EXPR__", asOfExpr)
.replaceAll("__PAYABLE_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["60", "76"]))
.replaceAll("__OPEN_CONTRACT_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["60", "76"]))
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
})()
: recipe.query_template === "receivables_confirmed_as_of_balance_profile"
@ -1840,10 +1840,10 @@ function buildAddressRecipePlan(recipe, filters) {
? toDateTimeExpr(filters.period_from, true)
: null) ??
"ТЕКУЩАЯДАТА()";
return RECEIVABLES_CONFIRMED_AS_OF_QUERY_TEMPLATE
return OPEN_CONTRACTS_CONFIRMED_AS_OF_QUERY_TEMPLATE
.replaceAll("__LIMIT__", String(resolvedLimit))
.replaceAll("__AS_OF_EXPR__", asOfExpr)
.replaceAll("__RECEIVABLE_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["62", "76"]))
.replaceAll("__OPEN_CONTRACT_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["62", "76"]))
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
})()
: MOVEMENTS_QUERY_TEMPLATE

View File

@ -266,6 +266,17 @@ function normalizeQuestionText(value) {
.replace(/\s+/g, " ")
.trim();
}
function isReportStyleBusinessQuestion(userMessage) {
const text = normalizeQuestionText(userMessage);
return /(?:обзор|анализ|подроб|разверн|оцен|аудит|report|review|analysis)/iu.test(text);
}
function isDirectBalanceQuestion(userMessage) {
const text = normalizeQuestionText(userMessage);
if (!text || isReportStyleBusinessQuestion(text)) {
return false;
}
return /(?:кто|кому|сколько|какой|какая|какие|есть\s+ли|долж|дебитор|кредитор|payables?|receivables?|who|how\s+much)/iu.test(text);
}
function hasInventoryPurchaseDateActionFocus(userMessage) {
const text = normalizeQuestionText(userMessage);
if (!text) {
@ -579,25 +590,42 @@ function extractRequestedYearFromQuestion(userMessage) {
return 2000 + shortYear;
}
function extractCounterpartyName(row) {
const skipTokenPattern = /(?:^0$|^<пусто>$|^пустая ссылка$|договор|contract|документ|операц|счет[-\s]?фактур|накладн|акт|поступлен|списани|плат[её]ж|перевод|банк|касса|расчетн|проводк|movement|invoice|payment)/iu;
for (const token of row.analytics) {
const normalized = String(token ?? "").trim();
const isCounterpartyLikeToken = (value, skipPattern) => {
const normalized = String(value ?? "").trim();
if (!normalized) {
continue;
return null;
}
if (/^\d{4}-\d{2}-\d{2}/.test(normalized)) {
continue;
return null;
}
if (/^\d+(?:[./-]\d+)*$/.test(normalized)) {
continue;
return null;
}
if (!/[a-zа-я]/iu.test(normalized)) {
continue;
return null;
}
if (skipTokenPattern.test(normalized)) {
continue;
if (skipPattern.test(normalized)) {
return null;
}
return normalized;
};
const hardSkipTokenPattern = /(?:^0$|^<пусто>$|^пустая ссылка$|договор|contract|документ|операц|счет[-\s]?фактур|накладн|акт|поступлен|списани|плат[её]ж|перевод|касса|расчетн|проводк|movement|invoice|payment)/iu;
const skipTokenPattern = /(?:^0$|^<пусто>$|^пустая ссылка$|договор|contract|документ|операц|счет[-\s]?фактур|накладн|акт|поступлен|списани|плат[её]ж|перевод|банк|касса|расчетн|проводк|movement|invoice|payment)/iu;
const directCounterparty = isCounterpartyLikeToken(row.counterparty, hardSkipTokenPattern);
if (directCounterparty) {
return directCounterparty;
}
if (/остатки\s+на\s+дату/iu.test(row.registrator)) {
const balancePrimaryCounterparty = isCounterpartyLikeToken(row.analytics[0], hardSkipTokenPattern);
if (balancePrimaryCounterparty) {
return balancePrimaryCounterparty;
}
}
for (const token of row.analytics) {
const normalized = isCounterpartyLikeToken(token, skipTokenPattern);
if (normalized) {
return normalized;
}
}
for (const token of row.analytics) {
const normalized = String(token ?? "").trim();
@ -1151,6 +1179,16 @@ function hasReceivablesSectionPrefix(account) {
const section = extractAccountSectionCode(account);
return section === "62" || section === "76";
}
function normalizeSettlementAccount(value) {
const normalized = String(value ?? "")
.trim()
.replace(",", ".");
return normalized || null;
}
function extractSettlementOrganizationName(row) {
const direct = String(row.organization ?? "").trim();
return direct || null;
}
function resolvePayablesAsOfDate(options) {
const explicit = normalizeIsoDateOnly(options.asOfDate);
if (explicit) {
@ -1430,6 +1468,211 @@ function buildReceivablesConfirmedBalanceAggregate(rows, asOfDate) {
return left.name.localeCompare(right.name);
});
}
function buildConfirmedDebtBalanceSnapshot(rows, asOfDate, hasRelevantSectionPrefix, positiveSide) {
const bySettlementKey = new Map();
const asOfTimestamp = toUtcDayTimestamp(asOfDate);
for (const row of rows) {
const name = extractCounterpartyName(row);
if (!name) {
continue;
}
const rowTimestamp = toUtcDayTimestamp(row.period);
if (asOfTimestamp !== null && rowTimestamp !== null && rowTimestamp > asOfTimestamp) {
continue;
}
const amount = row.amount;
if (typeof amount !== "number" || !Number.isFinite(amount)) {
continue;
}
const absAmount = Math.abs(amount);
const debitAccount = normalizeSettlementAccount(row.account_dt);
const creditAccount = normalizeSettlementAccount(row.account_kt);
const contributions = [];
if (debitAccount && hasRelevantSectionPrefix(debitAccount)) {
contributions.push({ side: "debit", account: debitAccount });
}
if (creditAccount && hasRelevantSectionPrefix(creditAccount)) {
contributions.push({ side: "credit", account: creditAccount });
}
if (contributions.length === 0) {
continue;
}
const contract = extractSettlementBalanceAnalyticKey(row, name);
const organization = extractSettlementOrganizationName(row);
const classified = classifyPayablesLiabilityCategory(row, name);
const sourceRefs = extractPayablesSourceRefs(row, name, contract);
for (const contribution of contributions) {
const key = [
normalizeEntityToken(organization),
normalizeEntityToken(contribution.account),
normalizeEntityToken(name),
normalizeEntityToken(contract)
].join("|");
const current = bySettlementKey.get(key);
if (!current) {
bySettlementKey.set(key, {
name,
account: contribution.account,
contract,
organization,
debitAmount: contribution.side === "debit" ? absAmount : 0,
creditAmount: contribution.side === "credit" ? absAmount : 0,
operations: 1,
firstPeriod: row.period,
lastPeriod: row.period,
categoryScores: {
supplier_or_contractor: classified.scores.supplier_or_contractor,
bank_or_credit: classified.scores.bank_or_credit,
tax_or_state: classified.scores.tax_or_state,
other: classified.scores.other
},
reasons: new Set(classified.reasons),
contracts: new Set(contract ? [contract] : []),
documents: new Set(row.registrator ? [row.registrator] : []),
sourceRefs: new Set(sourceRefs)
});
continue;
}
if (contribution.side === "debit") {
current.debitAmount += absAmount;
}
else {
current.creditAmount += absAmount;
}
current.operations += 1;
if ((row.period ?? "") < (current.firstPeriod ?? "")) {
current.firstPeriod = row.period;
}
if ((row.period ?? "") > (current.lastPeriod ?? "")) {
current.lastPeriod = row.period;
}
current.categoryScores.supplier_or_contractor += classified.scores.supplier_or_contractor;
current.categoryScores.bank_or_credit += classified.scores.bank_or_credit;
current.categoryScores.tax_or_state += classified.scores.tax_or_state;
current.categoryScores.other += classified.scores.other;
for (const reason of classified.reasons) {
current.reasons.add(reason);
}
if (contract) {
current.contracts.add(contract);
}
if (row.registrator) {
current.documents.add(row.registrator);
}
for (const ref of sourceRefs) {
current.sourceRefs.add(ref);
}
}
}
const byCounterparty = new Map();
const mirrorGroups = [];
let mirroredOffsetAmount = 0;
for (const group of bySettlementKey.values()) {
const offsetAmount = Math.min(group.debitAmount, group.creditAmount);
const netDebitMinusCredit = group.debitAmount - group.creditAmount;
if (offsetAmount > 0.005) {
mirroredOffsetAmount += offsetAmount;
mirrorGroups.push({
name: group.name,
account: group.account,
contract: group.contract,
organization: group.organization,
debitAmount: group.debitAmount,
creditAmount: group.creditAmount,
offsetAmount,
netAmount: netDebitMinusCredit,
operations: group.operations,
sourceRefs: Array.from(group.sourceRefs).slice(0, 3)
});
}
const sideNetAmount = positiveSide === "credit" ? group.creditAmount - group.debitAmount : group.debitAmount - group.creditAmount;
if (sideNetAmount <= 0.005) {
continue;
}
const current = byCounterparty.get(group.name);
if (!current) {
byCounterparty.set(group.name, {
outstandingAmount: sideNetAmount,
operations: group.operations,
firstPeriod: group.firstPeriod,
lastPeriod: group.lastPeriod,
categoryScores: {
supplier_or_contractor: group.categoryScores.supplier_or_contractor,
bank_or_credit: group.categoryScores.bank_or_credit,
tax_or_state: group.categoryScores.tax_or_state,
other: group.categoryScores.other
},
reasons: new Set(group.reasons),
contracts: new Set(group.contracts),
documents: new Set(group.documents),
sourceRefs: new Set(group.sourceRefs)
});
continue;
}
current.outstandingAmount += sideNetAmount;
current.operations += group.operations;
if ((group.firstPeriod ?? "") < (current.firstPeriod ?? "")) {
current.firstPeriod = group.firstPeriod;
}
if ((group.lastPeriod ?? "") > (current.lastPeriod ?? "")) {
current.lastPeriod = group.lastPeriod;
}
current.categoryScores.supplier_or_contractor += group.categoryScores.supplier_or_contractor;
current.categoryScores.bank_or_credit += group.categoryScores.bank_or_credit;
current.categoryScores.tax_or_state += group.categoryScores.tax_or_state;
current.categoryScores.other += group.categoryScores.other;
for (const reason of group.reasons) {
current.reasons.add(reason);
}
for (const contract of group.contracts) {
current.contracts.add(contract);
}
for (const document of group.documents) {
current.documents.add(document);
}
for (const ref of group.sourceRefs) {
current.sourceRefs.add(ref);
}
}
return {
balances: Array.from(byCounterparty.entries())
.map(([name, item]) => ({
name,
outstandingAmount: item.outstandingAmount,
operations: item.operations,
firstPeriod: item.firstPeriod,
lastPeriod: item.lastPeriod,
category: resolvePayablesLiabilityCategory(item.categoryScores),
categoryReasons: Array.from(item.reasons).slice(0, 2),
contracts: Array.from(item.contracts).slice(0, 2),
documents: Array.from(item.documents).slice(0, 2),
sourceRefs: Array.from(item.sourceRefs).slice(0, 3)
}))
.filter((item) => item.outstandingAmount > 0.005)
.sort((left, right) => {
if (right.outstandingAmount !== left.outstandingAmount) {
return right.outstandingAmount - left.outstandingAmount;
}
if (right.operations !== left.operations) {
return right.operations - left.operations;
}
return left.name.localeCompare(right.name);
}),
mirrorGroups: mirrorGroups.sort((left, right) => {
if (right.offsetAmount !== left.offsetAmount) {
return right.offsetAmount - left.offsetAmount;
}
return left.name.localeCompare(right.name);
}),
mirroredOffsetAmount
};
}
function buildPayablesConfirmedBalanceSnapshot(rows, asOfDate) {
return buildConfirmedDebtBalanceSnapshot(rows, asOfDate, hasPayablesSectionPrefix, "credit");
}
function buildReceivablesConfirmedBalanceSnapshot(rows, asOfDate) {
return buildConfirmedDebtBalanceSnapshot(rows, asOfDate, hasReceivablesSectionPrefix, "debit");
}
function buildCounterpartyRiskAggregate(rows) {
const byCounterparty = new Map();
for (const row of rows) {
@ -1603,6 +1846,45 @@ function extractContractName(row) {
}
return null;
}
function extractSettlementBalanceAnalyticKey(row, counterparty) {
const counterpartyToken = normalizeSettlementComparableToken(counterparty);
const organizationToken = normalizeSettlementComparableToken(extractSettlementOrganizationName(row));
const contract = extractContractName(row);
if (contract) {
const contractToken = normalizeSettlementComparableToken(contract);
if (contractToken &&
contractToken !== counterpartyToken &&
contractToken !== organizationToken &&
!(Boolean(organizationToken) && contractToken.includes(organizationToken)) &&
!/^организац/.test(contractToken)) {
return contract;
}
}
for (const token of row.analytics) {
const normalized = String(token ?? "").trim();
const normalizedToken = normalizeSettlementComparableToken(normalized);
if (!normalized || !normalizedToken) {
continue;
}
if (/^(?:0|<пусто>|пустая ссылка)$/iu.test(normalized)) {
continue;
}
if (/^\d{4}-\d{2}-\d{2}/.test(normalized) || /^\d+(?:[.,]\d+)?$/.test(normalized)) {
continue;
}
if (/^\d{2}(?:\.\d{1,2})?$/.test(normalized)) {
continue;
}
if (normalizedToken === counterpartyToken ||
normalizedToken === organizationToken ||
(Boolean(organizationToken) && normalizedToken.includes(organizationToken)) ||
/^организац/.test(normalizedToken)) {
continue;
}
return normalized;
}
return null;
}
function normalizeEntityToken(value) {
return String(value ?? "")
.toLowerCase()
@ -1610,6 +1892,12 @@ function normalizeEntityToken(value) {
.replace(/\s+/g, " ")
.trim();
}
function normalizeSettlementComparableToken(value) {
return normalizeEntityToken(value)
.replace(/[^\p{L}0-9]+/giu, " ")
.replace(/\s+/g, " ")
.trim();
}
function extractPayablesSourceRefs(row, counterparty, contract) {
const refs = new Set();
const counterpartyToken = normalizeEntityToken(counterparty);
@ -1656,6 +1944,41 @@ function formatPayablesEvidenceSuffix(item) {
}
return parts.length > 0 ? ` | ${parts.join(" | ")}` : "";
}
function formatDebtMirrorGroupLine(item) {
const details = [
item.account ? `счет ${item.account}` : null,
item.contract ? `договор/аналитика: ${item.contract}` : null,
item.organization ? `организация: ${item.organization}` : null
].filter((part) => Boolean(part));
const netText = Math.abs(item.netAmount) <= 0.005
? "чисто: 0 ₽"
: item.netAmount > 0
? `чисто к получению: ${formatMoneyRub(item.netAmount)}`
: `чисто к оплате: ${formatMoneyRub(Math.abs(item.netAmount))}`;
return `${item.name}${details.length > 0 ? ` (${details.join(", ")})` : ""}: дебет ${formatMoneyRub(item.debitAmount)} / кредит ${formatMoneyRub(item.creditAmount)}, ${netText}.`;
}
function debtMirrorCleanScopeLabel(kind) {
return kind === "payables" ? "чистый долг к оплате" : "чистую дебиторку к получению";
}
function appendDebtMirrorCompactDisclosure(lines, snapshot, kind) {
if (snapshot.mirroredOffsetAmount <= 0.005) {
return;
}
lines.push(`Отдельно сверено встречных остатков: ${formatMoneyRub(snapshot.mirroredOffsetAmount)}; они не включены в ${debtMirrorCleanScopeLabel(kind)}.`);
const leadingMirror = snapshot.mirrorGroups[0] ?? null;
if (leadingMirror) {
lines.push(`Крупнейший встречный хвост: ${formatDebtMirrorGroupLine(leadingMirror)}`);
}
}
function appendDebtMirrorDisclosure(lines, snapshot, kind) {
if (snapshot.mirroredOffsetAmount <= 0.005) {
return;
}
lines.push("");
lines.push("Встречные остатки к сверке");
lines.push(`- Встречная часть: ${formatMoneyRub(snapshot.mirroredOffsetAmount)}; она исключена из ${debtMirrorCleanScopeLabel(kind)}.`);
lines.push(...snapshot.mirrorGroups.slice(0, 3).map((item, index) => `${index + 1}. ${formatDebtMirrorGroupLine(item)}`));
}
function deriveOperationalYearWindow(yearDocs, yearOps) {
const docsSeries = [...yearDocs].sort((a, b) => a.year - b.year);
const fallbackSeries = [...yearOps].sort((a, b) => a.year - b.year);
@ -2955,7 +3278,8 @@ function composeFactualReplyBody(intent, rows, options = {}) {
}
if (intent === "payables_confirmed_as_of_date") {
const payablesAsOfDate = resolvePayablesAsOfDate(options);
const confirmedBalances = buildPayablesConfirmedBalanceAggregate(rows, payablesAsOfDate);
const balanceSnapshot = buildPayablesConfirmedBalanceSnapshot(rows, payablesAsOfDate);
const confirmedBalances = balanceSnapshot.balances;
const asOfDate = normalizeIsoDateOnly(options.asOfDate);
const periodFrom = normalizeIsoDateOnly(options.periodFrom);
const periodTo = normalizeIsoDateOnly(options.periodTo);
@ -2970,6 +3294,35 @@ function composeFactualReplyBody(intent, rows, options = {}) {
acc[item.category] += 1;
return acc;
}, { supplier_or_contractor: 0, bank_or_credit: 0, tax_or_state: 0, other: 0 });
if (isDirectBalanceQuestion(options.userMessage)) {
const leading = confirmedBalances[0] ?? null;
const compactLines = leading
? [
`Коротко: на ${formatDateRu(payablesAsOfDate)} мы должны ${formatMoneyRub(totalOutstandingAmount)}; крупнейшая позиция — ${leading.name} (${formatMoneyRub(leading.outstandingAmount)}).`,
"Крупнейшие позиции к оплате:"
]
: [`Коротко: на ${formatDateRu(payablesAsOfDate)} подтвержденных обязательств к оплате не найдено.`];
if (leading) {
compactLines.push(...confirmedBalances.slice(0, 5).map((item, index) => {
const lastPeriod = item.lastPeriod ? `, последнее движение: ${item.lastPeriod}` : "";
return `${index + 1}. ${item.name}${formatMoneyRub(item.outstandingAmount)} (${formatNumberWithDots(item.operations)} опер.${lastPeriod}).`;
}));
if (confirmedBalances.length > 5) {
compactLines.push(`Показаны первые 5 из ${formatNumberWithDots(confirmedBalances.length)} подтвержденных позиций.`);
}
}
appendDebtMirrorCompactDisclosure(compactLines, balanceSnapshot, "payables");
compactLines.push(`Основа: подтвержденный остаток по счетам 60/76, срез ${formatDateRu(payablesAsOfDate)}.`);
return {
responseType: confirmedBalances.length > 0 ? "FACTUAL_LIST" : "FACTUAL_SUMMARY",
text: joinLines(compactLines),
semantics: {
result_mode: "confirmed_balance",
evidence_strength: confirmedBalances.length > 0 ? "strong" : "medium",
balance_confirmed: true
}
};
}
const lines = [
`Коротко: подтвержденный долг к оплате на ${formatDateRu(payablesAsOfDate)}${formatMoneyRub(totalOutstandingAmount)}.`,
"Это подтвержденный срез обязательств к оплате по точному остатку."
@ -2988,6 +3341,7 @@ function composeFactualReplyBody(intent, rows, options = {}) {
lines.push("Сводка");
lines.push(`- Строк в выборке: ${formatNumberWithDots(rows.length)}.`);
lines.push(`- Контрагентов с подтвержденным остатком к оплате: ${formatNumberWithDots(confirmedBalances.length)}.`);
appendDebtMirrorDisclosure(lines, balanceSnapshot, "payables");
lines.push("");
lines.push("Категории обязательств");
lines.push(`- ${liabilityCategoryLabel("supplier_or_contractor")}: ${formatNumberWithDots(categoryCounts.supplier_or_contractor)}.`);
@ -3020,7 +3374,8 @@ function composeFactualReplyBody(intent, rows, options = {}) {
}
if (intent === "receivables_confirmed_as_of_date") {
const receivablesAsOfDate = resolveReceivablesAsOfDate(options);
const confirmedBalances = buildReceivablesConfirmedBalanceAggregate(rows, receivablesAsOfDate);
const balanceSnapshot = buildReceivablesConfirmedBalanceSnapshot(rows, receivablesAsOfDate);
const confirmedBalances = balanceSnapshot.balances;
const asOfDate = normalizeIsoDateOnly(options.asOfDate);
const periodFrom = normalizeIsoDateOnly(options.periodFrom);
const periodTo = normalizeIsoDateOnly(options.periodTo);
@ -3035,6 +3390,35 @@ function composeFactualReplyBody(intent, rows, options = {}) {
acc[item.category] += 1;
return acc;
}, { supplier_or_contractor: 0, bank_or_credit: 0, tax_or_state: 0, other: 0 });
if (isDirectBalanceQuestion(options.userMessage)) {
const leading = confirmedBalances[0] ?? null;
const compactLines = leading
? [
`Коротко: на ${formatDateRu(receivablesAsOfDate)} нам должны ${formatMoneyRub(totalOutstandingAmount)}; крупнейшая позиция — ${leading.name} (${formatMoneyRub(leading.outstandingAmount)}).`,
"Крупнейшие позиции к получению:"
]
: [`Коротко: на ${formatDateRu(receivablesAsOfDate)} подтвержденной дебиторской задолженности не найдено.`];
if (leading) {
compactLines.push(...confirmedBalances.slice(0, 5).map((item, index) => {
const lastPeriod = item.lastPeriod ? `, последнее движение: ${item.lastPeriod}` : "";
return `${index + 1}. ${item.name}${formatMoneyRub(item.outstandingAmount)} (${formatNumberWithDots(item.operations)} опер.${lastPeriod}).`;
}));
if (confirmedBalances.length > 5) {
compactLines.push(`Показаны первые 5 из ${formatNumberWithDots(confirmedBalances.length)} подтвержденных позиций.`);
}
}
appendDebtMirrorCompactDisclosure(compactLines, balanceSnapshot, "receivables");
compactLines.push(`Основа: подтвержденный остаток по счетам 62/76, срез ${formatDateRu(receivablesAsOfDate)}.`);
return {
responseType: confirmedBalances.length > 0 ? "FACTUAL_LIST" : "FACTUAL_SUMMARY",
text: joinLines(compactLines),
semantics: {
result_mode: "confirmed_balance",
evidence_strength: confirmedBalances.length > 0 ? "strong" : "medium",
balance_confirmed: true
}
};
}
const lines = [
`Коротко: подтвержденная дебиторская задолженность на ${formatDateRu(receivablesAsOfDate)}${formatMoneyRub(totalOutstandingAmount)}.`,
"Это подтвержденный срез дебиторской задолженности, а не эвристический shortlist."
@ -3053,6 +3437,7 @@ function composeFactualReplyBody(intent, rows, options = {}) {
lines.push("Сводка");
lines.push(`- Строк в выборке: ${formatNumberWithDots(rows.length)}.`);
lines.push(`- Контрагентов с подтвержденным остатком к получению: ${formatNumberWithDots(confirmedBalances.length)}.`);
appendDebtMirrorDisclosure(lines, balanceSnapshot, "receivables");
lines.push("");
lines.push("Категории дебиторской задолженности");
lines.push(`- ${receivablesCategoryLabel("supplier_or_contractor")}: ${formatNumberWithDots(categoryCounts.supplier_or_contractor)}.`);
@ -3160,7 +3545,8 @@ function composeFactualReplyBody(intent, rows, options = {}) {
return lines;
};
if (options.requestedResultMode === "confirmed_balance") {
const confirmedBalances = buildPayablesConfirmedBalanceAggregate(rows, payablesAsOfDate);
const balanceSnapshot = buildPayablesConfirmedBalanceSnapshot(rows, payablesAsOfDate);
const confirmedBalances = balanceSnapshot.balances;
if (confirmedBalances.length > 0) {
const categoryCounts = confirmedBalances.reduce((acc, item) => {
acc[item.category] += 1;
@ -3195,6 +3581,7 @@ function composeFactualReplyBody(intent, rows, options = {}) {
"Блок 5. Крупнейшие подтвержденные позиции к оплате (по сумме остатка):",
...confirmedBalances.slice(0, 10).map((item, index) => `${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток к оплате: ${formatMoneyRub(item.outstandingAmount)} | операций в срезе: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`)
];
appendDebtMirrorDisclosure(lines, balanceSnapshot, "payables");
return {
responseType: "FACTUAL_LIST",
text: joinLines(lines),

View File

@ -391,7 +391,11 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
/(?:какой|кто|which|who|какой|кто)/iu.test(normalizedQuestion) &&
/(?:больше\s+всего|сам(?:ый|ая|ое|ые)|наибольш|прин[её]с|highest|most|больше\s+всего|сам(?:ый|ая|РѕРµ|ые)|наибол|РїСЂРёРЅ[её]СЃ)/iu.test(normalizedQuestion) &&
!/(?:\btop\b|топ|рейтинг|список|первые|покажи\s+топ|дай\s+топ|покаж\w*\s+топ|дай\s+топ)/iu.test(normalizedQuestion);
const effectiveLimit = asksSingleBestCounterparty ? 1 : limit;
const asksExplicitRankingList = /(?:\btop\b|топ|рейтинг|список|первые|покажи\s+(?:топ|список)|дай\s+(?:топ|список)|show\s+(?:top|list))/iu.test(normalizedQuestion);
const hasSingleBestCounterpartyCue = /(?:сам\p{L}*|больше\s+всего|наибольш|прин[её]с|определ\p{L}*|найд\p{L}*|highest|largest|most)/iu.test(normalizedQuestion) &&
/(?:клиент|заказчик|покупател|контрагент|customer|client|counterparty|buyer)/iu.test(normalizedQuestion);
const semanticSingleBestCounterparty = focus === "top_by_total" && hasSingleBestCounterpartyCue && !asksExplicitRankingList;
const effectiveLimit = asksSingleBestCounterparty || semanticSingleBestCounterparty ? 1 : limit;
const byCounterparty = new Map();
const byYear = new Map();
const deals = [];

View File

@ -126,6 +126,14 @@ function hasExplicitLooseByAnchorToken(text) {
return !pronounTokens.has(token) && !genericTokens.has(token);
}
const FOLLOWUP_LOW_QUALITY_COUNTERPARTY_TOKENS = new Set([
"мы",
"нам",
"нас",
"наш",
"наша",
"наше",
"наши",
"унас",
"есть",
"же",
"что",
@ -1148,6 +1156,12 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
previousOrganization ??
(followupContext.previous_anchor_type === "organization" ? previousAnchorValue : null);
const finalCounterparty = toNonEmptyString(merged.counterparty);
if (finalCounterparty && isLowQualityCounterpartyAnchor(finalCounterparty)) {
delete merged.counterparty;
if (!reasons.includes("counterparty_cleared_low_quality_followup_anchor")) {
reasons.push("counterparty_cleared_low_quality_followup_anchor");
}
}
if (shouldSuppressInventoryCounterpartyAlias(intent, finalCounterparty, finalOrganizationReference)) {
delete merged.counterparty;
if (!reasons.includes("counterparty_cleared_as_organization_scope_alias")) {

View File

@ -371,6 +371,14 @@ function isReferentialCounterpartyPlaceholder(value) {
return false;
}
return new Set([
"мы",
"нам",
"нас",
"наш",
"наша",
"наше",
"наши",
"унас",
"он",
"она",
"оно",

View File

@ -592,6 +592,35 @@ function businessOverviewDebtDueDateAgingText(overview) {
}
return `Due-date aging на ${aging.as_of_date} проверен: строк с установленным сроком оплаты ${aging.rows_with_payment_terms}, подтвержденной просрочки не найдено; не просрочено по расчету ${aging.not_yet_due_amount_human_ru}.`;
}
function businessOverviewVendorProcurementQualityText(overview) {
const quality = overview.vendor_procurement_quality;
if (!quality) {
return null;
}
const period = quality.period_scope ?? "проверенное окно";
const total = quality.total_outgoing_amount_human_ru;
const top = quality.top_outgoing_counterparty;
const topName = top?.axis_value ?? "получатель не распознан";
const topShare = quality.top_outgoing_share_pct === null ? "" : `, около ${quality.top_outgoing_share_pct}%`;
const topAmount = top?.total_amount_human_ru ? ` (${top.total_amount_human_ru})` : "";
const nonFinancial = quality.top_non_financial_supplier;
const nonFinancialShare = quality.top_non_financial_supplier_share_pct === null ? "" : `, около ${quality.top_non_financial_supplier_share_pct}%`;
const nonFinancialText = nonFinancial
? ` Крупнейший небанковский получатель: ${nonFinancial.axis_value}${nonFinancialShare}${nonFinancial.total_amount_human_ru ? ` (${nonFinancial.total_amount_human_ru})` : ""}.`
: "";
const contractText = quality.used_contracts === null
? ""
: quality.total_contracts === null
? ` Договорный профиль: используется ${quality.used_contracts} договоров.`
: ` Договорный профиль: используется ${quality.used_contracts}/${quality.total_contracts} договоров${quality.used_contract_share_pct === null ? "" : ` (${quality.used_contract_share_pct}%)`}.`;
if (quality.evidence_status === "financial_institution_leads_outgoing_cash") {
return `Проверенный procurement-concentration route за ${period}: крупнейший получатель исходящих денег ${topName}${topShare}${topAmount}, всего исходящих платежей ${total}. По названию это банк/финансовая организация, поэтому зависимость от обычного поставщика этим не подтверждается.${nonFinancialText}${contractText} Надежность поставщиков, качество поставок, назначение каждого платежа и полная структура расходов этим маршрутом не доказаны.`;
}
if (quality.evidence_status === "reviewed_procurement_concentration") {
return `Проверенный procurement-concentration route за ${period}: крупнейший поставщик/получатель исходящих платежей ${topName}${topShare}${topAmount}, всего исходящих платежей ${total}.${contractText} Это проверенный сигнал концентрации закупок/исходящих платежей, но не аудит надежности поставщика, качества поставок и полной структуры расходов.`;
}
return `Procurement-concentration route за ${period} отработал по исходящим платежам на ${total}, но надежной небанковской концентрации поставщика по найденным строкам не хватает.${contractText} Полный vendor-risk аудит не подтвержден.`;
}
function headlineFor(mode, pilot) {
const askedMonthlyBreakdown = pilot.derived_bidirectional_value_flow?.aggregation_axis === "month" ||
pilot.derived_value_flow?.aggregation_axis === "month";
@ -632,6 +661,10 @@ function headlineFor(mode, pilot) {
return `Коротко: точно подтвердить резерв под неликвиды по текущим данным нельзя; ${inventoryBasis}. Можно честно говорить только о необходимости отдельной проверки склада, списаний/резервов и ликвидационной стоимости, не превращая proxy в доказанный факт резерва.`;
}
if (isVendorRiskBoundaryTurn(pilot)) {
const vendorQualityText = businessOverviewVendorProcurementQualityText(overview);
if (vendorQualityText) {
return vendorQualityText;
}
const supplierLeader = overview.top_suppliers?.[0] ?? null;
const proxyLabel = isFinancialInstitutionBucket(supplierLeader)
? "outgoing cash concentration proxy"
@ -897,7 +930,12 @@ function buildMustNotClaim(pilot) {
claims.push("Do not present business overview cash-flow spread as profit or margin.");
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.");
if (pilot.derived_business_overview?.vendor_procurement_quality) {
claims.push("Do not present reviewed procurement concentration as supplier reliability, delivery quality, payment-purpose classification, or full expense structure.");
}
else {
claims.push("Do not present business overview supplier concentration as vendor-risk audit, procurement quality, or full expense structure.");
}
claims.push("Do not present business overview document/account-section activity profile as process quality, accounting correctness, or completeness of all 1C activity.");
claims.push("Do not present business overview counterparty or contract profile as CRM quality, counterparty due diligence, contract-risk audit, or legal completeness.");
claims.push("Do not claim debt quality, VAT position, inventory health, or company health unless those contours were separately checked.");
@ -1279,6 +1317,10 @@ function derivedBusinessOverviewConfirmedLines(pilot) {
if (outgoingLeaderLine) {
lines.push(outgoingLeaderLine);
}
const vendorQualityText = businessOverviewVendorProcurementQualityText(overview);
if (vendorQualityText) {
lines.push(vendorQualityText);
}
if (overview.yearly_breakdown?.length) {
lines.push(`Годовая раскладка операционного денежного потока построена по подтвержденным строкам 1С за ${yearCountHumanRu(overview.yearly_breakdown.length)}.`);
}
@ -1667,6 +1709,10 @@ function buildAssistantMcpDiscoveryAnswerDraft(pilot) {
if (pilot.derived_business_overview?.top_suppliers?.length) {
pushReason(reasonCodes, "answer_contains_business_overview_supplier_concentration");
}
if (pilot.derived_business_overview?.vendor_procurement_quality) {
pushReason(reasonCodes, "answer_contains_business_overview_vendor_procurement_quality");
pushReason(reasonCodes, `answer_contains_business_overview_vendor_procurement_quality_${pilot.derived_business_overview.vendor_procurement_quality.evidence_status}`);
}
if (pilot.derived_business_overview?.yearly_breakdown?.length) {
pushReason(reasonCodes, "answer_contains_business_overview_yearly_operating_breakdown");
}

View File

@ -3021,6 +3021,46 @@ function deriveBusinessOverviewInventoryStalenessRiskProxy(input) {
inference_basis: "purchase_date_age_and_sales_to_stock_proxy_confirmed_1c_rows"
};
}
function deriveBusinessOverviewVendorProcurementQuality(input) {
if (!input.rankedOutgoing ||
input.rankedOutgoing.ranked_values.length <= 0 ||
input.outgoing.rows_with_amount <= 0 ||
input.outgoing.total_amount <= 0) {
return null;
}
const topOutgoingCounterparty = input.rankedOutgoing.ranked_values[0] ?? null;
const topNonFinancialSupplier = input.rankedOutgoing.ranked_values.find((item) => !(0, counterpartyRoleHeuristics_1.isLikelyFinancialInstitutionCounterparty)(item.axis_value)) ?? null;
const financialInstitutionLeadsOutgoingCash = topOutgoingCounterparty
? (0, counterpartyRoleHeuristics_1.isLikelyFinancialInstitutionCounterparty)(topOutgoingCounterparty.axis_value)
: false;
const evidenceStatus = !topNonFinancialSupplier
? "insufficient_supplier_concentration_basis"
: financialInstitutionLeadsOutgoingCash
? "financial_institution_leads_outgoing_cash"
: "reviewed_procurement_concentration";
return {
period_scope: input.periodScope,
rows_with_amount: input.outgoing.rows_with_amount,
total_outgoing_amount: input.outgoing.total_amount,
total_outgoing_amount_human_ru: input.outgoing.total_amount_human_ru,
top_outgoing_counterparty: topOutgoingCounterparty,
top_outgoing_share_pct: topOutgoingCounterparty
? percentageOfTotal(topOutgoingCounterparty.total_amount, input.outgoing.total_amount)
: null,
top_non_financial_supplier: topNonFinancialSupplier,
top_non_financial_supplier_share_pct: topNonFinancialSupplier
? percentageOfTotal(topNonFinancialSupplier.total_amount, input.outgoing.total_amount)
: null,
financial_institution_leads_outgoing_cash: financialInstitutionLeadsOutgoingCash,
supplier_only_count: input.counterpartyProfile?.supplier_only_count ?? null,
mixed_role_count: input.counterpartyProfile?.mixed_role_count ?? null,
used_contracts: input.contractUsageProfile?.used_contracts ?? null,
total_contracts: input.contractUsageProfile?.total_contracts ?? null,
used_contract_share_pct: input.contractUsageProfile?.used_contract_share_pct ?? null,
evidence_status: evidenceStatus,
inference_basis: "supplier_payout_concentration_counterparty_contract_profile_confirmed_1c_rows"
};
}
function inventoryStalenessRiskBandRu(riskBand) {
if (riskBand === "high") {
return "высокая зона внимания";
@ -3091,7 +3131,7 @@ function buildBusinessOverviewMissingProofFamilies(input) {
must_not_claim: "confirmed_obsolete_stock_reserve_writeoff_or_liquidation_value"
});
}
if (input.hasSupplierConcentrationSignal) {
if (input.hasSupplierConcentrationSignal && !input.vendorProcurementQuality) {
pushUnique({
family: "vendor_risk_procurement_quality",
current_status: "proxy_only_currently",
@ -3155,6 +3195,13 @@ function deriveBusinessOverview(input) {
inventoryPosition,
inventoryTurnoverProxy
});
const vendorProcurementQuality = deriveBusinessOverviewVendorProcurementQuality({
rankedOutgoing,
outgoing,
counterpartyProfile,
contractUsageProfile,
periodScope: input.periodScope
});
const checkedSignalCount = [
incoming.rows_with_amount > 0,
outgoing.rows_with_amount > 0,
@ -3171,7 +3218,8 @@ function deriveBusinessOverview(input) {
Boolean(contractUsageProfile),
Boolean(inventoryPosition),
Boolean(inventoryTurnoverProxy),
Boolean(inventoryStalenessRiskProxy)
Boolean(inventoryStalenessRiskProxy),
Boolean(vendorProcurementQuality)
].filter(Boolean).length;
if (checkedSignalCount <= 0) {
return null;
@ -3202,6 +3250,7 @@ function deriveBusinessOverview(input) {
inventoryPosition,
inventoryTurnoverProxy,
inventoryStalenessRiskProxy,
vendorProcurementQuality,
hasSupplierConcentrationSignal: (rankedOutgoing?.ranked_values.length ?? 0) > 0
});
return {
@ -3229,6 +3278,7 @@ function deriveBusinessOverview(input) {
document_activity_profile: documentActivityProfile,
counterparty_profile: counterpartyProfile,
contract_usage_profile: contractUsageProfile,
vendor_procurement_quality: vendorProcurementQuality,
coverage_limited_by_probe_limit: incoming.coverage_limited_by_probe_limit || outgoing.coverage_limited_by_probe_limit,
checked_signal_count: checkedSignalCount,
missing_signal_families: missingSignalFamilies,
@ -4641,6 +4691,10 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
if (derivedBusinessOverview.top_suppliers.length > 0) {
pushReason(reasonCodes, "pilot_derived_business_overview_top_suppliers_from_confirmed_rows");
}
if (derivedBusinessOverview.vendor_procurement_quality) {
pushReason(reasonCodes, "pilot_derived_business_overview_vendor_procurement_quality_from_confirmed_rows");
pushReason(reasonCodes, `pilot_derived_business_overview_vendor_procurement_quality_${derivedBusinessOverview.vendor_procurement_quality.evidence_status}`);
}
if (derivedBusinessOverview.yearly_breakdown.length > 0) {
pushReason(reasonCodes, "pilot_derived_business_overview_yearly_operating_breakdown_from_confirmed_rows");
}

View File

@ -601,7 +601,7 @@ function recipeFor(input) {
extraReasons: primitiveSelection.reasonCodes
});
}
if (dataNeedGraph?.ranking_need && !hasSubjectCandidates(dataNeedGraph)) {
if (dataNeedGraph?.ranking_need) {
pushUnique(axes, "aggregate_axis");
pushUnique(axes, "amount");
pushUnique(axes, "coverage_target");
@ -621,7 +621,10 @@ function recipeFor(input) {
reason: dataNeedGraph.ranking_need === "bottom_asc"
? "planner_selected_bottom_ranked_value_flow_from_data_need_graph"
: "planner_selected_top_ranked_value_flow_from_data_need_graph",
extraReasons: primitiveSelection.reasonCodes
extraReasons: [
...primitiveSelection.reasonCodes,
...(hasSubjectCandidates(dataNeedGraph) ? ["planner_kept_ranking_over_subject_scope_candidates"] : [])
]
});
}
if (openScopeTotalWithoutSubject) {

View File

@ -773,6 +773,47 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
}
if (vendorRiskBoundary) {
const vendorProcurementQuality = toRecordObject(overview.vendor_procurement_quality);
if (vendorProcurementQuality) {
const status = toNonEmptyString(vendorProcurementQuality.evidence_status);
const totalOutgoing = moneyText(vendorProcurementQuality.total_outgoing_amount_human_ru);
const topOutgoingRecord = toRecordObject(vendorProcurementQuality.top_outgoing_counterparty);
const topOutgoingName = toNonEmptyString(topOutgoingRecord?.axis_value);
const topOutgoingAmount = moneyText(topOutgoingRecord?.total_amount_human_ru);
const topOutgoingShare = typeof vendorProcurementQuality.top_outgoing_share_pct === "number" &&
Number.isFinite(vendorProcurementQuality.top_outgoing_share_pct)
? `${vendorProcurementQuality.top_outgoing_share_pct}%`
: null;
const nonFinancialRecord = toRecordObject(vendorProcurementQuality.top_non_financial_supplier);
const nonFinancialName = toNonEmptyString(nonFinancialRecord?.axis_value);
const nonFinancialAmount = moneyText(nonFinancialRecord?.total_amount_human_ru);
const nonFinancialShare = typeof vendorProcurementQuality.top_non_financial_supplier_share_pct === "number" &&
Number.isFinite(vendorProcurementQuality.top_non_financial_supplier_share_pct)
? `${vendorProcurementQuality.top_non_financial_supplier_share_pct}%`
: null;
const periodScope = toNonEmptyString(vendorProcurementQuality.period_scope) ?? period;
const totalText = totalOutgoing ? `; всего исходящих платежей в проверенном срезе ${totalOutgoing}` : "";
if (status === "financial_institution_leads_outgoing_cash") {
lines.push(`Коротко: проверенный procurement-concentration route за ${periodScope} не подтверждает зависимость от обычного поставщика: крупнейший получатель исходящих денег ${topOutgoingName ?? "не распознан"}${topOutgoingShare ? ` держит около ${topOutgoingShare}` : ""}${topOutgoingAmount ? ` (${topOutgoingAmount})` : ""}, но по названию это банк/финансовая организация${totalText}.`);
if (nonFinancialName) {
lines.push(`Крупнейший небанковский получатель исходящих денег: ${nonFinancialName}${nonFinancialShare ? `, около ${nonFinancialShare}` : ""}${nonFinancialAmount ? ` (${nonFinancialAmount})` : ""}. Это уже сигнал закупочной/исходящей концентрации, но не аудит надежности поставщика.`);
}
}
else if (status === "reviewed_procurement_concentration") {
lines.push(`Коротко: проверенный procurement-concentration route за ${periodScope} нашел основную зависимость исходящего потока: ${topOutgoingName ?? nonFinancialName ?? "получатель не распознан"}${topOutgoingShare ? ` держит около ${topOutgoingShare}` : nonFinancialShare ? ` держит около ${nonFinancialShare}` : ""}${topOutgoingAmount ? ` (${topOutgoingAmount})` : nonFinancialAmount ? ` (${nonFinancialAmount})` : ""}${totalText}.`);
}
else {
lines.push(`Коротко: procurement-concentration route за ${periodScope} отработал, но надежной небанковской концентрации поставщика по найденным исходящим платежам не хватает${totalText}.`);
}
const contractText = typeof vendorProcurementQuality.used_contracts === "number" && Number.isFinite(vendorProcurementQuality.used_contracts)
? typeof vendorProcurementQuality.total_contracts === "number" && Number.isFinite(vendorProcurementQuality.total_contracts)
? ` Договорный профиль: используется ${vendorProcurementQuality.used_contracts}/${vendorProcurementQuality.total_contracts} договоров${typeof vendorProcurementQuality.used_contract_share_pct === "number" && Number.isFinite(vendorProcurementQuality.used_contract_share_pct) ? ` (${vendorProcurementQuality.used_contract_share_pct}%)` : ""}.`
: ` Договорный профиль: используется ${vendorProcurementQuality.used_contracts} договоров.`
: "";
lines.push(`Что не доказано этим маршрутом: надежность поставщика, качество поставок, договорные условия, назначение каждого платежа и полная структура всех расходов.${contractText}`);
const reply = lines.join("\n").trim();
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
}
const supplierBasis = topSupplier
? topSupplierLooksFinancial
? `крупнейший получатель исходящих денег: ${topSupplier}; по названию это банк/финансовая организация, поэтому это не доказанная зависимость от обычного поставщика${nonFinancialSupplier ? `; крупнейший небанковский получатель исходящих денег: ${nonFinancialSupplier}` : ""}`

View File

@ -167,6 +167,21 @@ function isMetadataDiscoveryTurn(entryPoint) {
toNonEmptyString(pilot?.pilot_scope) === "metadata_inspection_v1" ||
reasonCodes.some((reason) => toNonEmptyString(reason) === "mcp_discovery_metadata_signal_detected"));
}
function isExplicitMetadataDiscoveryTurn(entryPoint) {
const turnInput = toRecordObject(entryPoint?.turn_input);
const turnMeaning = readDiscoveryTurnMeaning(entryPoint);
const graph = readDiscoveryDataNeedGraph(entryPoint);
const reasonCodes = [
...(Array.isArray(entryPoint?.reason_codes) ? entryPoint.reason_codes : []),
...(Array.isArray(turnInput?.reason_codes) ? turnInput.reason_codes : [])
];
const decompositionCandidates = Array.isArray(graph?.decomposition_candidates) ? graph.decomposition_candidates : [];
return Boolean(toNonEmptyString(turnMeaning?.asked_domain_family) === "metadata" ||
toNonEmptyString(turnMeaning?.unsupported_but_understood_family) === "1c_metadata_surface" ||
toNonEmptyString(graph?.business_fact_family) === "schema_surface" ||
decompositionCandidates.some((candidate) => toNonEmptyString(candidate) === "inspect_metadata_surface") ||
reasonCodes.some((reason) => toNonEmptyString(reason) === "mcp_discovery_metadata_signal_detected"));
}
function isInventoryExactAddressIntent(intent) {
return /^(?:inventory_purchase_provenance_for_item|inventory_purchase_documents_for_item|inventory_sale_trace_for_item|inventory_profitability_for_item|inventory_purchase_to_sale_chain|inventory_aging_by_purchase_date|inventory_on_hand_as_of_date)$/u.test(String(intent ?? ""));
}
@ -180,6 +195,9 @@ function hasMetadataDiscoveryPriority(input, entryPoint) {
if (!isMetadataDiscoveryTurn(entryPoint)) {
return false;
}
if (!isExplicitMetadataDiscoveryTurn(entryPoint)) {
return false;
}
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
return !isInventoryExactAddressIntent(detectedIntent);
}
@ -250,6 +268,55 @@ function readStringArray(value) {
? value.map((item) => toNonEmptyString(item)).filter((item) => Boolean(item))
: [];
}
function hasConfirmedAddressExecution(input) {
const mcpCallStatus = toNonEmptyString(input.addressRuntimeMeta?.mcp_call_status);
const truthMode = toNonEmptyString(input.addressRuntimeMeta?.truth_mode);
const selectedRecipe = toNonEmptyString(input.addressRuntimeMeta?.selected_recipe);
const bindingStatus = toNonEmptyString(input.addressRuntimeMeta?.capability_binding_status);
const bindingViolations = readStringArray(input.addressRuntimeMeta?.capability_binding_violations);
return Boolean(mcpCallStatus === "matched_non_empty" &&
truthMode === "confirmed" &&
selectedRecipe?.startsWith("address_") &&
(bindingStatus === "bound" || bindingStatus === "bound_with_limits") &&
bindingViolations.length === 0);
}
function hasStaleMetadataDiscoveryFallbackAgainstExactAddressReply(input, entryPoint) {
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
return false;
}
if (!hasEffectivelyFactualAddressReply(input)) {
return false;
}
if (!isMetadataDiscoveryTurn(entryPoint) || isExplicitMetadataDiscoveryTurn(entryPoint)) {
return false;
}
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
return Boolean(detectedIntent &&
hasConfirmedAddressExecution(input) &&
isDetectedIntentAlignedWithTurnMeaning(detectedIntent, readDiscoveryTurnMeaning(entryPoint)));
}
function hasBusinessOverviewDirectMoneyClarification(entryPoint) {
const graph = readDiscoveryDataNeedGraph(entryPoint);
const businessFactFamily = toNonEmptyString(graph?.business_fact_family);
const reasonCodes = readStringArray(graph?.reason_codes);
const clarificationGaps = readStringArray(graph?.clarification_gaps);
return Boolean(businessFactFamily === "business_overview" &&
reasonCodes.includes("data_need_graph_business_overview_direct_money_answer") &&
(toNonEmptyString(graph?.ranking_need) || reasonCodes.includes("data_need_graph_ranking_top_desc")) &&
clarificationGaps.includes("organization"));
}
function hasExactValueFlowReplyForBusinessOverviewDirectMoneyNeed(input, entryPoint) {
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
return false;
}
if (!hasEffectivelyFactualAddressReply(input)) {
return false;
}
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
return Boolean(detectedIntent === "customer_revenue_and_payments" &&
hasConfirmedAddressExecution(input) &&
hasBusinessOverviewDirectMoneyClarification(entryPoint));
}
function hasValueFlowActionConflictWithDiscoveryTurnMeaning(input, entryPoint) {
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
return false;
@ -317,16 +384,7 @@ function hasExactMatchedFactualAddressReply(input, entryPoint) {
return false;
}
}
const mcpCallStatus = toNonEmptyString(input.addressRuntimeMeta?.mcp_call_status);
const truthMode = toNonEmptyString(input.addressRuntimeMeta?.truth_mode);
const selectedRecipe = toNonEmptyString(input.addressRuntimeMeta?.selected_recipe);
const bindingStatus = toNonEmptyString(input.addressRuntimeMeta?.capability_binding_status);
const bindingViolations = readStringArray(input.addressRuntimeMeta?.capability_binding_violations);
return Boolean(mcpCallStatus === "matched_non_empty" &&
truthMode === "confirmed" &&
selectedRecipe?.startsWith("address_") &&
(bindingStatus === "bound" || bindingStatus === "bound_with_limits") &&
bindingViolations.length === 0);
return hasConfirmedAddressExecution(input);
}
function hasOpenScopeValueFlowDiscoveryPriority(input, entryPoint) {
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
@ -506,6 +564,8 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
const exactMatchedFactualAddressReply = hasExactMatchedFactualAddressReply(input, entryPoint);
const runtimeAdjustedExactReply = hasRuntimeAdjustedExactReply(input, entryPoint);
const runtimeMatchedExactReply = hasRuntimeMatchedExactReply(input, entryPoint);
const staleMetadataDiscoveryFallbackAgainstExactAddressReply = hasStaleMetadataDiscoveryFallbackAgainstExactAddressReply(input, entryPoint);
const exactValueFlowReplyForBusinessOverviewDirectMoneyNeed = hasExactValueFlowReplyForBusinessOverviewDirectMoneyNeed(input, entryPoint);
const openScopeValueFlowDiscoveryPriority = hasOpenScopeValueFlowDiscoveryPriority(input, entryPoint);
const metadataDiscoveryPriority = hasMetadataDiscoveryPriority(input, entryPoint);
const valueFlowActionConflictWithDiscoveryTurnMeaning = hasValueFlowActionConflictWithDiscoveryTurnMeaning(input, entryPoint);
@ -561,6 +621,12 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
if (runtimeMatchedExactReply) {
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_runtime_matched_exact_reply_over_stale_discovery_turn_meaning");
}
if (staleMetadataDiscoveryFallbackAgainstExactAddressReply) {
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_exact_address_reply_over_stale_metadata_discovery");
}
if (exactValueFlowReplyForBusinessOverviewDirectMoneyNeed) {
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_exact_value_flow_reply_over_business_overview_direct_money_clarification");
}
if (deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") {
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_broad_business_summary_over_clarification_candidate");
}
@ -585,6 +651,8 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
!exactMatchedFactualAddressReply &&
!runtimeAdjustedExactReply &&
!runtimeMatchedExactReply &&
!staleMetadataDiscoveryFallbackAgainstExactAddressReply &&
!exactValueFlowReplyForBusinessOverviewDirectMoneyNeed &&
!(deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") &&
ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) &&
candidate.eligible_for_future_hot_runtime &&

View File

@ -2315,6 +2315,18 @@ function resolveUnicodeAddressIntentBridge(text: string): AddressIntentResolutio
);
}
const hasTopYearRevenueRankingCue =
/(?:(?:\u043a\u0430\u043a\u043e\u0439|\u043a\u0430\u043a\u0438\u0435|\u043a\u0430\u043a\u0430\u044f|what|which)[\s\S]{0,80}(?:\u0441\u0430\u043c\p{L}*|top|best|most)[\s\S]{0,80}(?:\u0434\u043e\u0445\u043e\u0434\u043d|\u0432\u044b\u0440\u0443\u0447\u043a|\u043e\u0431\u043e\u0440\u043e\u0442|revenue|turnover)[\s\S]{0,60}(?:\u0433\u043e\u0434|year)|(?:\u0434\u043e\u0445\u043e\u0434\u043d|\u0432\u044b\u0440\u0443\u0447\u043a|\u043e\u0431\u043e\u0440\u043e\u0442|revenue|turnover)[\s\S]{0,60}(?:\u0441\u0430\u043c\p{L}*|top|best|most)[\s\S]{0,60}(?:\u0433\u043e\u0434|year))/iu.test(
normalized
);
if (!hasContractCue && (hasTopYearRevenueRankingCue || hasCustomerRevenueRankingBridgeSignal(normalized))) {
return unicodeBridgeResolution(
"customer_revenue_and_payments",
"high",
"unicode_customer_revenue_ranking_bridge_signal_detected"
);
}
if (hasOrganizationLevelEarningsOverviewBridgeSignal(normalized)) {
return unicodeBridgeResolution("unknown", "high", "unicode_business_overview_earnings_deferred_to_discovery");
}
@ -3007,16 +3019,67 @@ function resolveUnicodeAddressIntentBridge(text: string): AddressIntentResolutio
return null;
}
function resolveDirectDebtSnapshotIntent(text: string): AddressIntentResolution | null {
const normalized = String(text ?? "").trim().toLowerCase();
if (!normalized) {
return null;
}
if (/(?:ндс|vat)/iu.test(normalized)) {
return null;
}
const hasSnapshotCue =
/(?:кто|сколько|есть\s+ли|по\s+состоянию|на\s+сегодня|на\s+дату|срез|остаток|сальдо|баланс|на\s+(?:январ|феврал|март|апрел|ма[йя]|июн|июл|август|сентябр|октябр|ноябр|декабр)\S*(?:\s+(?:19|20)\d{2})?|на\s+(?:19|20)\d{2}|as\s+of|today|current|balance)/iu.test(
normalized
);
const hasReceivablesCue =
/(?:кто\s+(?:является\s+)?дебитором|дебитор(?:[а-яё]{0,8})?|дебиторск(?:[а-яё]{0,8})?|кто\s+нам\s+долж(?:ен|ны|на|но)?|нам\s+(?:кто-то\s+|кто\s+)?долж(?:ен|ны|на|но)?|нам\s+торч(?:ат|ит|ишь|у|али)?|к\s+получению|к\s+взысканию|who\s+owes\s+us|receivables?|accounts\s+receivable)/iu.test(
normalized
);
const hasPayablesCue =
/(?:кто\s+(?:является\s+)?кредитором|кредитор(?:[а-яё]{0,8})?|кому\s+мы\s+долж(?:ны|н[ао])?|мы\s+долж(?:ны|н[ао])?\s+кому|мы\s+долж(?:ны|н[ао])?|к\s+оплате|who\s+we\s+owe|payables?|accounts\s+payable)/iu.test(
normalized
);
if (hasReceivablesCue && !hasPayablesCue && hasSnapshotCue) {
return {
intent: "receivables_confirmed_as_of_date",
confidence: "high",
reasons: ["receivables_debt_lifecycle_signal_detected", "direct_debt_snapshot_signal_detected"]
};
}
if (hasPayablesCue && !hasReceivablesCue && hasSnapshotCue) {
return {
intent: "payables_confirmed_as_of_date",
confidence: "high",
reasons: ["payables_debt_lifecycle_signal_detected", "direct_debt_snapshot_signal_detected"]
};
}
return null;
}
export function resolveAddressIntent(userMessage: string): AddressIntentResolution {
const text = String(userMessage ?? "").trim().toLowerCase();
const repairedText = repairLikelyUtf8Mojibake(text).trim().toLowerCase();
const bridgeText = repairedText && repairedText !== text ? `${text} ${repairedText}` : text;
const turnNoiseNormalizedBridgeText = bridgeText
.replace(/(^|[^\p{L}0-9_])\u043d\u0430\u043c\u0441(?=$|[^\p{L}0-9_])/giu, "$1\u043d\u0430\u043c")
.replace(/(^|[^\p{L}0-9_])\u043a\u0430\u043a\u0438\u0435\u043a(?=$|[^\p{L}0-9_])/giu, "$1\u043a\u0430\u043a\u0438\u0435");
.replace(/(^|[^\p{L}0-9_])намс(?=$|[^\p{L}0-9_])/giu, "$1нам")
.replace(/(^|[^\p{L}0-9_])какиек(?=$|[^\p{L}0-9_])/giu, "$1какие");
const currentTurnBridgeText =
turnNoiseNormalizedBridgeText !== bridgeText ? `${bridgeText} ${turnNoiseNormalizedBridgeText}` : bridgeText;
const directDebtSnapshotIntent = resolveDirectDebtSnapshotIntent(currentTurnBridgeText);
if (directDebtSnapshotIntent) {
const reasons = [...directDebtSnapshotIntent.reasons];
if (currentTurnBridgeText !== bridgeText && !reasons.includes("current_turn_noise_normalized")) {
reasons.push("current_turn_noise_normalized");
}
return {
...directDebtSnapshotIntent,
reasons
};
}
const unicodeAddressIntent = resolveUnicodeAddressIntentBridge(currentTurnBridgeText);
if (unicodeAddressIntent) {
const reasons = [...unicodeAddressIntent.reasons];

View File

@ -2071,10 +2071,10 @@ export function buildAddressRecipePlan(
? toDateTimeExpr(filters.period_from, true)
: null) ??
"ТЕКУЩАЯДАТА()";
return PAYABLES_CONFIRMED_AS_OF_QUERY_TEMPLATE
return OPEN_CONTRACTS_CONFIRMED_AS_OF_QUERY_TEMPLATE
.replaceAll("__LIMIT__", String(resolvedLimit))
.replaceAll("__AS_OF_EXPR__", asOfExpr)
.replaceAll("__PAYABLE_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["60", "76"]))
.replaceAll("__OPEN_CONTRACT_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["60", "76"]))
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
})()
: recipe.query_template === "receivables_confirmed_as_of_balance_profile"
@ -2090,10 +2090,10 @@ export function buildAddressRecipePlan(
? toDateTimeExpr(filters.period_from, true)
: null) ??
"ТЕКУЩАЯДАТА()";
return RECEIVABLES_CONFIRMED_AS_OF_QUERY_TEMPLATE
return OPEN_CONTRACTS_CONFIRMED_AS_OF_QUERY_TEMPLATE
.replaceAll("__LIMIT__", String(resolvedLimit))
.replaceAll("__AS_OF_EXPR__", asOfExpr)
.replaceAll("__RECEIVABLE_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["62", "76"]))
.replaceAll("__OPEN_CONTRACT_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["62", "76"]))
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
})()
: MOVEMENTS_QUERY_TEMPLATE

View File

@ -401,6 +401,21 @@ function normalizeQuestionText(value: string | null | undefined): string {
.trim();
}
function isReportStyleBusinessQuestion(userMessage: string | null | undefined): boolean {
const text = normalizeQuestionText(userMessage);
return /(?:обзор|анализ|подроб|разверн|оцен|аудит|report|review|analysis)/iu.test(text);
}
function isDirectBalanceQuestion(userMessage: string | null | undefined): boolean {
const text = normalizeQuestionText(userMessage);
if (!text || isReportStyleBusinessQuestion(text)) {
return false;
}
return /(?:кто|кому|сколько|какой|какая|какие|есть\s+ли|долж|дебитор|кредитор|payables?|receivables?|who|how\s+much)/iu.test(
text
);
}
function hasInventoryPurchaseDateActionFocus(userMessage: string | null | undefined): boolean {
const text = normalizeQuestionText(userMessage);
if (!text) {
@ -780,27 +795,46 @@ function extractRequestedYearFromQuestion(userMessage: string | null | undefined
}
function extractCounterpartyName(row: ComposeStageRow): string | null {
const isCounterpartyLikeToken = (value: unknown, skipPattern: RegExp): string | null => {
const normalized = String(value ?? "").trim();
if (!normalized) {
return null;
}
if (/^\d{4}-\d{2}-\d{2}/.test(normalized)) {
return null;
}
if (/^\d+(?:[./-]\d+)*$/.test(normalized)) {
return null;
}
if (!/[a-zа-я]/iu.test(normalized)) {
return null;
}
if (skipPattern.test(normalized)) {
return null;
}
return normalized;
};
const hardSkipTokenPattern =
/(?:^0$|^<пусто>$|^пустая ссылка$|договор|contract|документ|операц|счет[-\s]?фактур|накладн|акт|поступлен|списани|плат[её]ж|перевод|касса|расчетн|проводк|movement|invoice|payment)/iu;
const skipTokenPattern =
/(?:^0$|^<пусто>$|^пустая ссылка$|договор|contract|документ|операц|счет[-\s]?фактур|накладн|акт|поступлен|списани|плат[её]ж|перевод|банк|касса|расчетн|проводк|movement|invoice|payment)/iu;
const directCounterparty = isCounterpartyLikeToken(row.counterparty, hardSkipTokenPattern);
if (directCounterparty) {
return directCounterparty;
}
if (/остатки\s+на\s+дату/iu.test(row.registrator)) {
const balancePrimaryCounterparty = isCounterpartyLikeToken(row.analytics[0], hardSkipTokenPattern);
if (balancePrimaryCounterparty) {
return balancePrimaryCounterparty;
}
}
for (const token of row.analytics) {
const normalized = String(token ?? "").trim();
if (!normalized) {
continue;
const normalized = isCounterpartyLikeToken(token, skipTokenPattern);
if (normalized) {
return normalized;
}
if (/^\d{4}-\d{2}-\d{2}/.test(normalized)) {
continue;
}
if (/^\d+(?:[./-]\d+)*$/.test(normalized)) {
continue;
}
if (!/[a-zа-я]/iu.test(normalized)) {
continue;
}
if (skipTokenPattern.test(normalized)) {
continue;
}
return normalized;
}
for (const token of row.analytics) {
const normalized = String(token ?? "").trim();
@ -1457,6 +1491,25 @@ interface PayablesConfirmedBalanceAggregate {
sourceRefs: string[];
}
interface DebtMirrorBalanceGroup {
name: string;
account: string | null;
contract: string | null;
organization: string | null;
debitAmount: number;
creditAmount: number;
offsetAmount: number;
netAmount: number;
operations: number;
sourceRefs: string[];
}
interface ConfirmedDebtBalanceSnapshot {
balances: PayablesConfirmedBalanceAggregate[];
mirrorGroups: DebtMirrorBalanceGroup[];
mirroredOffsetAmount: number;
}
function liabilityCategoryLabel(category: PayablesLiabilityCategory): string {
if (category === "supplier_or_contractor") {
return "поставщики/подрядчики";
@ -1572,6 +1625,18 @@ function hasReceivablesSectionPrefix(account: string | null): boolean {
return section === "62" || section === "76";
}
function normalizeSettlementAccount(value: string | null | undefined): string | null {
const normalized = String(value ?? "")
.trim()
.replace(",", ".");
return normalized || null;
}
function extractSettlementOrganizationName(row: ComposeStageRow): string | null {
const direct = String(row.organization ?? "").trim();
return direct || null;
}
function resolvePayablesAsOfDate(options: ComposeFactualReplyOptions): string {
const explicit = normalizeIsoDateOnly(options.asOfDate);
if (explicit) {
@ -1906,6 +1971,259 @@ function buildReceivablesConfirmedBalanceAggregate(
});
}
function buildConfirmedDebtBalanceSnapshot(
rows: ComposeStageRow[],
asOfDate: string,
hasRelevantSectionPrefix: (account: string | null) => boolean,
positiveSide: "debit" | "credit"
): ConfirmedDebtBalanceSnapshot {
const bySettlementKey = new Map<
string,
{
name: string;
account: string | null;
contract: string | null;
organization: string | null;
debitAmount: number;
creditAmount: number;
operations: number;
firstPeriod: string | null;
lastPeriod: string | null;
categoryScores: Record<PayablesLiabilityCategory, number>;
reasons: Set<string>;
contracts: Set<string>;
documents: Set<string>;
sourceRefs: Set<string>;
}
>();
const asOfTimestamp = toUtcDayTimestamp(asOfDate);
for (const row of rows) {
const name = extractCounterpartyName(row);
if (!name) {
continue;
}
const rowTimestamp = toUtcDayTimestamp(row.period);
if (asOfTimestamp !== null && rowTimestamp !== null && rowTimestamp > asOfTimestamp) {
continue;
}
const amount = row.amount;
if (typeof amount !== "number" || !Number.isFinite(amount)) {
continue;
}
const absAmount = Math.abs(amount);
const debitAccount = normalizeSettlementAccount(row.account_dt);
const creditAccount = normalizeSettlementAccount(row.account_kt);
const contributions: Array<{ side: "debit" | "credit"; account: string }> = [];
if (debitAccount && hasRelevantSectionPrefix(debitAccount)) {
contributions.push({ side: "debit", account: debitAccount });
}
if (creditAccount && hasRelevantSectionPrefix(creditAccount)) {
contributions.push({ side: "credit", account: creditAccount });
}
if (contributions.length === 0) {
continue;
}
const contract = extractSettlementBalanceAnalyticKey(row, name);
const organization = extractSettlementOrganizationName(row);
const classified = classifyPayablesLiabilityCategory(row, name);
const sourceRefs = extractPayablesSourceRefs(row, name, contract);
for (const contribution of contributions) {
const key = [
normalizeEntityToken(organization),
normalizeEntityToken(contribution.account),
normalizeEntityToken(name),
normalizeEntityToken(contract)
].join("|");
const current = bySettlementKey.get(key);
if (!current) {
bySettlementKey.set(key, {
name,
account: contribution.account,
contract,
organization,
debitAmount: contribution.side === "debit" ? absAmount : 0,
creditAmount: contribution.side === "credit" ? absAmount : 0,
operations: 1,
firstPeriod: row.period,
lastPeriod: row.period,
categoryScores: {
supplier_or_contractor: classified.scores.supplier_or_contractor,
bank_or_credit: classified.scores.bank_or_credit,
tax_or_state: classified.scores.tax_or_state,
other: classified.scores.other
},
reasons: new Set(classified.reasons),
contracts: new Set(contract ? [contract] : []),
documents: new Set(row.registrator ? [row.registrator] : []),
sourceRefs: new Set(sourceRefs)
});
continue;
}
if (contribution.side === "debit") {
current.debitAmount += absAmount;
} else {
current.creditAmount += absAmount;
}
current.operations += 1;
if ((row.period ?? "") < (current.firstPeriod ?? "")) {
current.firstPeriod = row.period;
}
if ((row.period ?? "") > (current.lastPeriod ?? "")) {
current.lastPeriod = row.period;
}
current.categoryScores.supplier_or_contractor += classified.scores.supplier_or_contractor;
current.categoryScores.bank_or_credit += classified.scores.bank_or_credit;
current.categoryScores.tax_or_state += classified.scores.tax_or_state;
current.categoryScores.other += classified.scores.other;
for (const reason of classified.reasons) {
current.reasons.add(reason);
}
if (contract) {
current.contracts.add(contract);
}
if (row.registrator) {
current.documents.add(row.registrator);
}
for (const ref of sourceRefs) {
current.sourceRefs.add(ref);
}
}
}
const byCounterparty = new Map<
string,
{
outstandingAmount: number;
operations: number;
firstPeriod: string | null;
lastPeriod: string | null;
categoryScores: Record<PayablesLiabilityCategory, number>;
reasons: Set<string>;
contracts: Set<string>;
documents: Set<string>;
sourceRefs: Set<string>;
}
>();
const mirrorGroups: DebtMirrorBalanceGroup[] = [];
let mirroredOffsetAmount = 0;
for (const group of bySettlementKey.values()) {
const offsetAmount = Math.min(group.debitAmount, group.creditAmount);
const netDebitMinusCredit = group.debitAmount - group.creditAmount;
if (offsetAmount > 0.005) {
mirroredOffsetAmount += offsetAmount;
mirrorGroups.push({
name: group.name,
account: group.account,
contract: group.contract,
organization: group.organization,
debitAmount: group.debitAmount,
creditAmount: group.creditAmount,
offsetAmount,
netAmount: netDebitMinusCredit,
operations: group.operations,
sourceRefs: Array.from(group.sourceRefs).slice(0, 3)
});
}
const sideNetAmount = positiveSide === "credit" ? group.creditAmount - group.debitAmount : group.debitAmount - group.creditAmount;
if (sideNetAmount <= 0.005) {
continue;
}
const current = byCounterparty.get(group.name);
if (!current) {
byCounterparty.set(group.name, {
outstandingAmount: sideNetAmount,
operations: group.operations,
firstPeriod: group.firstPeriod,
lastPeriod: group.lastPeriod,
categoryScores: {
supplier_or_contractor: group.categoryScores.supplier_or_contractor,
bank_or_credit: group.categoryScores.bank_or_credit,
tax_or_state: group.categoryScores.tax_or_state,
other: group.categoryScores.other
},
reasons: new Set(group.reasons),
contracts: new Set(group.contracts),
documents: new Set(group.documents),
sourceRefs: new Set(group.sourceRefs)
});
continue;
}
current.outstandingAmount += sideNetAmount;
current.operations += group.operations;
if ((group.firstPeriod ?? "") < (current.firstPeriod ?? "")) {
current.firstPeriod = group.firstPeriod;
}
if ((group.lastPeriod ?? "") > (current.lastPeriod ?? "")) {
current.lastPeriod = group.lastPeriod;
}
current.categoryScores.supplier_or_contractor += group.categoryScores.supplier_or_contractor;
current.categoryScores.bank_or_credit += group.categoryScores.bank_or_credit;
current.categoryScores.tax_or_state += group.categoryScores.tax_or_state;
current.categoryScores.other += group.categoryScores.other;
for (const reason of group.reasons) {
current.reasons.add(reason);
}
for (const contract of group.contracts) {
current.contracts.add(contract);
}
for (const document of group.documents) {
current.documents.add(document);
}
for (const ref of group.sourceRefs) {
current.sourceRefs.add(ref);
}
}
return {
balances: Array.from(byCounterparty.entries())
.map(([name, item]) => ({
name,
outstandingAmount: item.outstandingAmount,
operations: item.operations,
firstPeriod: item.firstPeriod,
lastPeriod: item.lastPeriod,
category: resolvePayablesLiabilityCategory(item.categoryScores),
categoryReasons: Array.from(item.reasons).slice(0, 2),
contracts: Array.from(item.contracts).slice(0, 2),
documents: Array.from(item.documents).slice(0, 2),
sourceRefs: Array.from(item.sourceRefs).slice(0, 3)
}))
.filter((item) => item.outstandingAmount > 0.005)
.sort((left, right) => {
if (right.outstandingAmount !== left.outstandingAmount) {
return right.outstandingAmount - left.outstandingAmount;
}
if (right.operations !== left.operations) {
return right.operations - left.operations;
}
return left.name.localeCompare(right.name);
}),
mirrorGroups: mirrorGroups.sort((left, right) => {
if (right.offsetAmount !== left.offsetAmount) {
return right.offsetAmount - left.offsetAmount;
}
return left.name.localeCompare(right.name);
}),
mirroredOffsetAmount
};
}
function buildPayablesConfirmedBalanceSnapshot(rows: ComposeStageRow[], asOfDate: string): ConfirmedDebtBalanceSnapshot {
return buildConfirmedDebtBalanceSnapshot(rows, asOfDate, hasPayablesSectionPrefix, "credit");
}
function buildReceivablesConfirmedBalanceSnapshot(rows: ComposeStageRow[], asOfDate: string): ConfirmedDebtBalanceSnapshot {
return buildConfirmedDebtBalanceSnapshot(rows, asOfDate, hasReceivablesSectionPrefix, "debit");
}
function buildCounterpartyRiskAggregate(rows: ComposeStageRow[]): CounterpartyRiskAggregate[] {
const byCounterparty = new Map<string, CounterpartyRiskAggregate>();
@ -2109,6 +2427,51 @@ function extractContractName(row: ComposeStageRow): string | null {
return null;
}
function extractSettlementBalanceAnalyticKey(row: ComposeStageRow, counterparty: string): string | null {
const counterpartyToken = normalizeSettlementComparableToken(counterparty);
const organizationToken = normalizeSettlementComparableToken(extractSettlementOrganizationName(row));
const contract = extractContractName(row);
if (contract) {
const contractToken = normalizeSettlementComparableToken(contract);
if (
contractToken &&
contractToken !== counterpartyToken &&
contractToken !== organizationToken &&
!(Boolean(organizationToken) && contractToken.includes(organizationToken)) &&
!/^организац/.test(contractToken)
) {
return contract;
}
}
for (const token of row.analytics) {
const normalized = String(token ?? "").trim();
const normalizedToken = normalizeSettlementComparableToken(normalized);
if (!normalized || !normalizedToken) {
continue;
}
if (/^(?:0|<пусто>|пустая ссылка)$/iu.test(normalized)) {
continue;
}
if (/^\d{4}-\d{2}-\d{2}/.test(normalized) || /^\d+(?:[.,]\d+)?$/.test(normalized)) {
continue;
}
if (/^\d{2}(?:\.\d{1,2})?$/.test(normalized)) {
continue;
}
if (
normalizedToken === counterpartyToken ||
normalizedToken === organizationToken ||
(Boolean(organizationToken) && normalizedToken.includes(organizationToken)) ||
/^организац/.test(normalizedToken)
) {
continue;
}
return normalized;
}
return null;
}
function normalizeEntityToken(value: string | null | undefined): string {
return String(value ?? "")
.toLowerCase()
@ -2117,6 +2480,13 @@ function normalizeEntityToken(value: string | null | undefined): string {
.trim();
}
function normalizeSettlementComparableToken(value: string | null | undefined): string {
return normalizeEntityToken(value)
.replace(/[^\p{L}0-9]+/giu, " ")
.replace(/\s+/g, " ")
.trim();
}
function extractPayablesSourceRefs(
row: ComposeStageRow,
counterparty: string,
@ -2172,6 +2542,58 @@ function formatPayablesEvidenceSuffix(item: PayablesConfirmedBalanceAggregate):
return parts.length > 0 ? ` | ${parts.join(" | ")}` : "";
}
function formatDebtMirrorGroupLine(item: DebtMirrorBalanceGroup): string {
const details = [
item.account ? `счет ${item.account}` : null,
item.contract ? `договор/аналитика: ${item.contract}` : null,
item.organization ? `организация: ${item.organization}` : null
].filter((part): part is string => Boolean(part));
const netText =
Math.abs(item.netAmount) <= 0.005
? "чисто: 0 ₽"
: item.netAmount > 0
? `чисто к получению: ${formatMoneyRub(item.netAmount)}`
: `чисто к оплате: ${formatMoneyRub(Math.abs(item.netAmount))}`;
return `${item.name}${details.length > 0 ? ` (${details.join(", ")})` : ""}: дебет ${formatMoneyRub(item.debitAmount)} / кредит ${formatMoneyRub(item.creditAmount)}, ${netText}.`;
}
function debtMirrorCleanScopeLabel(kind: "payables" | "receivables"): string {
return kind === "payables" ? "чистый долг к оплате" : "чистую дебиторку к получению";
}
function appendDebtMirrorCompactDisclosure(
lines: string[],
snapshot: ConfirmedDebtBalanceSnapshot,
kind: "payables" | "receivables"
): void {
if (snapshot.mirroredOffsetAmount <= 0.005) {
return;
}
lines.push(
`Отдельно сверено встречных остатков: ${formatMoneyRub(snapshot.mirroredOffsetAmount)}; они не включены в ${debtMirrorCleanScopeLabel(kind)}.`
);
const leadingMirror = snapshot.mirrorGroups[0] ?? null;
if (leadingMirror) {
lines.push(`Крупнейший встречный хвост: ${formatDebtMirrorGroupLine(leadingMirror)}`);
}
}
function appendDebtMirrorDisclosure(
lines: string[],
snapshot: ConfirmedDebtBalanceSnapshot,
kind: "payables" | "receivables"
): void {
if (snapshot.mirroredOffsetAmount <= 0.005) {
return;
}
lines.push("");
lines.push("Встречные остатки к сверке");
lines.push(
`- Встречная часть: ${formatMoneyRub(snapshot.mirroredOffsetAmount)}; она исключена из ${debtMirrorCleanScopeLabel(kind)}.`
);
lines.push(...snapshot.mirrorGroups.slice(0, 3).map((item, index) => `${index + 1}. ${formatDebtMirrorGroupLine(item)}`));
}
function deriveOperationalYearWindow(
yearDocs: YearAggPoint[],
yearOps: YearAggPoint[]
@ -3813,7 +4235,8 @@ function composeFactualReplyBody(
if (intent === "payables_confirmed_as_of_date") {
const payablesAsOfDate = resolvePayablesAsOfDate(options);
const confirmedBalances = buildPayablesConfirmedBalanceAggregate(rows, payablesAsOfDate);
const balanceSnapshot = buildPayablesConfirmedBalanceSnapshot(rows, payablesAsOfDate);
const confirmedBalances = balanceSnapshot.balances;
const asOfDate = normalizeIsoDateOnly(options.asOfDate);
const periodFrom = normalizeIsoDateOnly(options.periodFrom);
const periodTo = normalizeIsoDateOnly(options.periodTo);
@ -3834,6 +4257,39 @@ function composeFactualReplyBody(
{ supplier_or_contractor: 0, bank_or_credit: 0, tax_or_state: 0, other: 0 }
);
if (isDirectBalanceQuestion(options.userMessage)) {
const leading = confirmedBalances[0] ?? null;
const compactLines: string[] = leading
? [
`Коротко: на ${formatDateRu(payablesAsOfDate)} мы должны ${formatMoneyRub(totalOutstandingAmount)}; крупнейшая позиция — ${leading.name} (${formatMoneyRub(leading.outstandingAmount)}).`,
"Крупнейшие позиции к оплате:"
]
: [`Коротко: на ${formatDateRu(payablesAsOfDate)} подтвержденных обязательств к оплате не найдено.`];
if (leading) {
compactLines.push(
...confirmedBalances.slice(0, 5).map((item, index) => {
const lastPeriod = item.lastPeriod ? `, последнее движение: ${item.lastPeriod}` : "";
return `${index + 1}. ${item.name}${formatMoneyRub(item.outstandingAmount)} (${formatNumberWithDots(item.operations)} опер.${lastPeriod}).`;
})
);
if (confirmedBalances.length > 5) {
compactLines.push(`Показаны первые 5 из ${formatNumberWithDots(confirmedBalances.length)} подтвержденных позиций.`);
}
}
appendDebtMirrorCompactDisclosure(compactLines, balanceSnapshot, "payables");
compactLines.push(`Основа: подтвержденный остаток по счетам 60/76, срез ${formatDateRu(payablesAsOfDate)}.`);
return {
responseType: confirmedBalances.length > 0 ? "FACTUAL_LIST" : "FACTUAL_SUMMARY",
text: joinLines(compactLines),
semantics: {
result_mode: "confirmed_balance",
evidence_strength: confirmedBalances.length > 0 ? "strong" : "medium",
balance_confirmed: true
}
};
}
const lines: string[] = [
`Коротко: подтвержденный долг к оплате на ${formatDateRu(payablesAsOfDate)}${formatMoneyRub(totalOutstandingAmount)}.`,
"Это подтвержденный срез обязательств к оплате по точному остатку."
@ -3854,6 +4310,7 @@ function composeFactualReplyBody(
lines.push("Сводка");
lines.push(`- Строк в выборке: ${formatNumberWithDots(rows.length)}.`);
lines.push(`- Контрагентов с подтвержденным остатком к оплате: ${formatNumberWithDots(confirmedBalances.length)}.`);
appendDebtMirrorDisclosure(lines, balanceSnapshot, "payables");
lines.push("");
lines.push("Категории обязательств");
@ -3891,7 +4348,8 @@ function composeFactualReplyBody(
if (intent === "receivables_confirmed_as_of_date") {
const receivablesAsOfDate = resolveReceivablesAsOfDate(options);
const confirmedBalances = buildReceivablesConfirmedBalanceAggregate(rows, receivablesAsOfDate);
const balanceSnapshot = buildReceivablesConfirmedBalanceSnapshot(rows, receivablesAsOfDate);
const confirmedBalances = balanceSnapshot.balances;
const asOfDate = normalizeIsoDateOnly(options.asOfDate);
const periodFrom = normalizeIsoDateOnly(options.periodFrom);
const periodTo = normalizeIsoDateOnly(options.periodTo);
@ -3912,6 +4370,39 @@ function composeFactualReplyBody(
{ supplier_or_contractor: 0, bank_or_credit: 0, tax_or_state: 0, other: 0 }
);
if (isDirectBalanceQuestion(options.userMessage)) {
const leading = confirmedBalances[0] ?? null;
const compactLines: string[] = leading
? [
`Коротко: на ${formatDateRu(receivablesAsOfDate)} нам должны ${formatMoneyRub(totalOutstandingAmount)}; крупнейшая позиция — ${leading.name} (${formatMoneyRub(leading.outstandingAmount)}).`,
"Крупнейшие позиции к получению:"
]
: [`Коротко: на ${formatDateRu(receivablesAsOfDate)} подтвержденной дебиторской задолженности не найдено.`];
if (leading) {
compactLines.push(
...confirmedBalances.slice(0, 5).map((item, index) => {
const lastPeriod = item.lastPeriod ? `, последнее движение: ${item.lastPeriod}` : "";
return `${index + 1}. ${item.name}${formatMoneyRub(item.outstandingAmount)} (${formatNumberWithDots(item.operations)} опер.${lastPeriod}).`;
})
);
if (confirmedBalances.length > 5) {
compactLines.push(`Показаны первые 5 из ${formatNumberWithDots(confirmedBalances.length)} подтвержденных позиций.`);
}
}
appendDebtMirrorCompactDisclosure(compactLines, balanceSnapshot, "receivables");
compactLines.push(`Основа: подтвержденный остаток по счетам 62/76, срез ${formatDateRu(receivablesAsOfDate)}.`);
return {
responseType: confirmedBalances.length > 0 ? "FACTUAL_LIST" : "FACTUAL_SUMMARY",
text: joinLines(compactLines),
semantics: {
result_mode: "confirmed_balance",
evidence_strength: confirmedBalances.length > 0 ? "strong" : "medium",
balance_confirmed: true
}
};
}
const lines: string[] = [
`Коротко: подтвержденная дебиторская задолженность на ${formatDateRu(receivablesAsOfDate)}${formatMoneyRub(totalOutstandingAmount)}.`,
"Это подтвержденный срез дебиторской задолженности, а не эвристический shortlist."
@ -3932,6 +4423,7 @@ function composeFactualReplyBody(
lines.push("Сводка");
lines.push(`- Строк в выборке: ${formatNumberWithDots(rows.length)}.`);
lines.push(`- Контрагентов с подтвержденным остатком к получению: ${formatNumberWithDots(confirmedBalances.length)}.`);
appendDebtMirrorDisclosure(lines, balanceSnapshot, "receivables");
lines.push("");
lines.push("Категории дебиторской задолженности");
@ -4062,7 +4554,8 @@ function composeFactualReplyBody(
};
if (options.requestedResultMode === "confirmed_balance") {
const confirmedBalances = buildPayablesConfirmedBalanceAggregate(rows, payablesAsOfDate);
const balanceSnapshot = buildPayablesConfirmedBalanceSnapshot(rows, payablesAsOfDate);
const confirmedBalances = balanceSnapshot.balances;
if (confirmedBalances.length > 0) {
const categoryCounts = confirmedBalances.reduce<Record<PayablesLiabilityCategory, number>>(
(acc, item) => {
@ -4103,6 +4596,7 @@ function composeFactualReplyBody(
`${index + 1}. ${item.name} | категория: ${liabilityCategoryLabel(item.category)} | остаток к оплате: ${formatMoneyRub(item.outstandingAmount)} | операций в срезе: ${formatNumberWithDots(item.operations)}${item.lastPeriod ? ` | последнее движение: ${item.lastPeriod}` : ""}${item.categoryReasons.length > 0 ? ` | основание: ${item.categoryReasons.join(", ")}` : ""}${formatPayablesEvidenceSuffix(item)}`
)
];
appendDebtMirrorDisclosure(lines, balanceSnapshot, "payables");
return {
responseType: "FACTUAL_LIST",
text: joinLines(lines),

View File

@ -534,7 +534,18 @@ export function composeCounterpartyAnalyticsReply(
!/(?:\btop\b|топ|рейтинг|список|первые|покажи\s+топ|дай\s+топ|РїРѕРєР°Р\w*\s+СРѕРї|РґР°Р\s+СРѕРї)/iu.test(
normalizedQuestion
);
const effectiveLimit = asksSingleBestCounterparty ? 1 : limit;
const asksExplicitRankingList =
/(?:\btop\b|топ|рейтинг|список|первые|покажи\s+(?:топ|список)|дай\s+(?:топ|список)|show\s+(?:top|list))/iu.test(
normalizedQuestion
);
const hasSingleBestCounterpartyCue =
/(?:сам\p{L}*|больше\s+всего|наибольш|прин[её]с|определ\p{L}*|найд\p{L}*|highest|largest|most)/iu.test(
normalizedQuestion
) &&
/(?:клиент|заказчик|покупател|контрагент|customer|client|counterparty|buyer)/iu.test(normalizedQuestion);
const semanticSingleBestCounterparty =
focus === "top_by_total" && hasSingleBestCounterpartyCue && !asksExplicitRankingList;
const effectiveLimit = asksSingleBestCounterparty || semanticSingleBestCounterparty ? 1 : limit;
const byCounterparty = new Map<string, CounterpartyValuePoint>();
const byYear = new Map<number, CounterpartyYearPoint>();

View File

@ -220,6 +220,14 @@ function hasExplicitLooseByAnchorToken(text: string): boolean {
}
const FOLLOWUP_LOW_QUALITY_COUNTERPARTY_TOKENS = new Set([
"мы",
"нам",
"нас",
"наш",
"наша",
"наше",
"наши",
"унас",
"есть",
"же",
"что",
@ -1445,6 +1453,12 @@ function mergeFollowupFilters(
previousOrganization ??
(followupContext.previous_anchor_type === "organization" ? previousAnchorValue : null);
const finalCounterparty = toNonEmptyString(merged.counterparty);
if (finalCounterparty && isLowQualityCounterpartyAnchor(finalCounterparty)) {
delete merged.counterparty;
if (!reasons.includes("counterparty_cleared_low_quality_followup_anchor")) {
reasons.push("counterparty_cleared_low_quality_followup_anchor");
}
}
if (shouldSuppressInventoryCounterpartyAlias(intent, finalCounterparty, finalOrganizationReference)) {
delete merged.counterparty;
if (!reasons.includes("counterparty_cleared_as_organization_scope_alias")) {

View File

@ -575,6 +575,14 @@ function isReferentialCounterpartyPlaceholder(
return false;
}
return new Set([
"мы",
"нам",
"нас",
"наш",
"наша",
"наше",
"наши",
"унас",
"он",
"она",
"оно",

View File

@ -733,6 +733,37 @@ function businessOverviewDebtDueDateAgingText(overview: BusinessOverview): strin
return `Due-date aging на ${aging.as_of_date} проверен: строк с установленным сроком оплаты ${aging.rows_with_payment_terms}, подтвержденной просрочки не найдено; не просрочено по расчету ${aging.not_yet_due_amount_human_ru}.`;
}
function businessOverviewVendorProcurementQualityText(overview: BusinessOverview): string | null {
const quality = overview.vendor_procurement_quality;
if (!quality) {
return null;
}
const period = quality.period_scope ?? "проверенное окно";
const total = quality.total_outgoing_amount_human_ru;
const top = quality.top_outgoing_counterparty;
const topName = top?.axis_value ?? "получатель не распознан";
const topShare = quality.top_outgoing_share_pct === null ? "" : `, около ${quality.top_outgoing_share_pct}%`;
const topAmount = top?.total_amount_human_ru ? ` (${top.total_amount_human_ru})` : "";
const nonFinancial = quality.top_non_financial_supplier;
const nonFinancialShare =
quality.top_non_financial_supplier_share_pct === null ? "" : `, около ${quality.top_non_financial_supplier_share_pct}%`;
const nonFinancialText = nonFinancial
? ` Крупнейший небанковский получатель: ${nonFinancial.axis_value}${nonFinancialShare}${nonFinancial.total_amount_human_ru ? ` (${nonFinancial.total_amount_human_ru})` : ""}.`
: "";
const contractText = quality.used_contracts === null
? ""
: quality.total_contracts === null
? ` Договорный профиль: используется ${quality.used_contracts} договоров.`
: ` Договорный профиль: используется ${quality.used_contracts}/${quality.total_contracts} договоров${quality.used_contract_share_pct === null ? "" : ` (${quality.used_contract_share_pct}%)`}.`;
if (quality.evidence_status === "financial_institution_leads_outgoing_cash") {
return `Проверенный procurement-concentration route за ${period}: крупнейший получатель исходящих денег ${topName}${topShare}${topAmount}, всего исходящих платежей ${total}. По названию это банк/финансовая организация, поэтому зависимость от обычного поставщика этим не подтверждается.${nonFinancialText}${contractText} Надежность поставщиков, качество поставок, назначение каждого платежа и полная структура расходов этим маршрутом не доказаны.`;
}
if (quality.evidence_status === "reviewed_procurement_concentration") {
return `Проверенный procurement-concentration route за ${period}: крупнейший поставщик/получатель исходящих платежей ${topName}${topShare}${topAmount}, всего исходящих платежей ${total}.${contractText} Это проверенный сигнал концентрации закупок/исходящих платежей, но не аудит надежности поставщика, качества поставок и полной структуры расходов.`;
}
return `Procurement-concentration route за ${period} отработал по исходящим платежам на ${total}, но надежной небанковской концентрации поставщика по найденным строкам не хватает.${contractText} Полный vendor-risk аудит не подтвержден.`;
}
function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpDiscoveryPilotExecutionContract): string {
const askedMonthlyBreakdown =
pilot.derived_bidirectional_value_flow?.aggregation_axis === "month" ||
@ -774,6 +805,10 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
return `Коротко: точно подтвердить резерв под неликвиды по текущим данным нельзя; ${inventoryBasis}. Можно честно говорить только о необходимости отдельной проверки склада, списаний/резервов и ликвидационной стоимости, не превращая proxy в доказанный факт резерва.`;
}
if (isVendorRiskBoundaryTurn(pilot)) {
const vendorQualityText = businessOverviewVendorProcurementQualityText(overview);
if (vendorQualityText) {
return vendorQualityText;
}
const supplierLeader = overview.top_suppliers?.[0] ?? null;
const proxyLabel = isFinancialInstitutionBucket(supplierLeader)
? "outgoing cash concentration proxy"
@ -1051,7 +1086,11 @@ function buildMustNotClaim(pilot: AssistantMcpDiscoveryPilotExecutionContract):
claims.push("Do not present business overview cash-flow spread as profit or margin.");
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.");
if (pilot.derived_business_overview?.vendor_procurement_quality) {
claims.push("Do not present reviewed procurement concentration as supplier reliability, delivery quality, payment-purpose classification, or full expense structure.");
} else {
claims.push("Do not present business overview supplier concentration as vendor-risk audit, procurement quality, or full expense structure.");
}
claims.push("Do not present business overview document/account-section activity profile as process quality, accounting correctness, or completeness of all 1C activity.");
claims.push("Do not present business overview counterparty or contract profile as CRM quality, counterparty due diligence, contract-risk audit, or legal completeness.");
claims.push("Do not claim debt quality, VAT position, inventory health, or company health unless those contours were separately checked.");
@ -1486,6 +1525,10 @@ function derivedBusinessOverviewConfirmedLines(pilot: AssistantMcpDiscoveryPilot
if (outgoingLeaderLine) {
lines.push(outgoingLeaderLine);
}
const vendorQualityText = businessOverviewVendorProcurementQualityText(overview);
if (vendorQualityText) {
lines.push(vendorQualityText);
}
if (overview.yearly_breakdown?.length) {
lines.push(
`Годовая раскладка операционного денежного потока построена по подтвержденным строкам 1С за ${yearCountHumanRu(overview.yearly_breakdown.length)}.`
@ -1936,6 +1979,10 @@ export function buildAssistantMcpDiscoveryAnswerDraft(
if (pilot.derived_business_overview?.top_suppliers?.length) {
pushReason(reasonCodes, "answer_contains_business_overview_supplier_concentration");
}
if (pilot.derived_business_overview?.vendor_procurement_quality) {
pushReason(reasonCodes, "answer_contains_business_overview_vendor_procurement_quality");
pushReason(reasonCodes, `answer_contains_business_overview_vendor_procurement_quality_${pilot.derived_business_overview.vendor_procurement_quality.evidence_status}`);
}
if (pilot.derived_business_overview?.yearly_breakdown?.length) {
pushReason(reasonCodes, "answer_contains_business_overview_yearly_operating_breakdown");
}

View File

@ -188,6 +188,28 @@ export interface AssistantMcpDiscoveryDerivedBusinessOverviewContractUsageProfil
inference_basis: "contract_usage_overview_confirmed_1c_rows";
}
export interface AssistantMcpDiscoveryDerivedBusinessOverviewVendorProcurementQuality {
period_scope: string | null;
rows_with_amount: number;
total_outgoing_amount: number;
total_outgoing_amount_human_ru: string;
top_outgoing_counterparty: AssistantMcpDiscoveryRankedValueFlowBucket | null;
top_outgoing_share_pct: number | null;
top_non_financial_supplier: AssistantMcpDiscoveryRankedValueFlowBucket | null;
top_non_financial_supplier_share_pct: number | null;
financial_institution_leads_outgoing_cash: boolean;
supplier_only_count: number | null;
mixed_role_count: number | null;
used_contracts: number | null;
total_contracts: number | null;
used_contract_share_pct: number | null;
evidence_status:
| "reviewed_procurement_concentration"
| "financial_institution_leads_outgoing_cash"
| "insufficient_supplier_concentration_basis";
inference_basis: "supplier_payout_concentration_counterparty_contract_profile_confirmed_1c_rows";
}
export type AssistantMcpDiscoveryBusinessOverviewMissingProofFamilyId =
| "accounting_profit_margin"
| "debt_due_date_aging_quality"
@ -246,6 +268,7 @@ export interface AssistantMcpDiscoveryDerivedBusinessOverview {
document_activity_profile: AssistantMcpDiscoveryDerivedBusinessOverviewDocumentActivityProfile | null;
counterparty_profile: AssistantMcpDiscoveryDerivedBusinessOverviewCounterpartyProfile | null;
contract_usage_profile: AssistantMcpDiscoveryDerivedBusinessOverviewContractUsageProfile | null;
vendor_procurement_quality: AssistantMcpDiscoveryDerivedBusinessOverviewVendorProcurementQuality | null;
coverage_limited_by_probe_limit: boolean;
checked_signal_count: number;
missing_signal_families: string[];
@ -4125,6 +4148,59 @@ function deriveBusinessOverviewInventoryStalenessRiskProxy(input: {
};
}
function deriveBusinessOverviewVendorProcurementQuality(input: {
rankedOutgoing: AssistantMcpDiscoveryDerivedRankedValueFlow | null;
outgoing: AssistantMcpDiscoveryValueFlowSideSummary;
counterpartyProfile: AssistantMcpDiscoveryDerivedBusinessOverviewCounterpartyProfile | null;
contractUsageProfile: AssistantMcpDiscoveryDerivedBusinessOverviewContractUsageProfile | null;
periodScope: string | null;
}): AssistantMcpDiscoveryDerivedBusinessOverviewVendorProcurementQuality | null {
if (
!input.rankedOutgoing ||
input.rankedOutgoing.ranked_values.length <= 0 ||
input.outgoing.rows_with_amount <= 0 ||
input.outgoing.total_amount <= 0
) {
return null;
}
const topOutgoingCounterparty = input.rankedOutgoing.ranked_values[0] ?? null;
const topNonFinancialSupplier =
input.rankedOutgoing.ranked_values.find((item) => !isLikelyFinancialInstitutionCounterparty(item.axis_value)) ?? null;
const financialInstitutionLeadsOutgoingCash = topOutgoingCounterparty
? isLikelyFinancialInstitutionCounterparty(topOutgoingCounterparty.axis_value)
: false;
const evidenceStatus: AssistantMcpDiscoveryDerivedBusinessOverviewVendorProcurementQuality["evidence_status"] =
!topNonFinancialSupplier
? "insufficient_supplier_concentration_basis"
: financialInstitutionLeadsOutgoingCash
? "financial_institution_leads_outgoing_cash"
: "reviewed_procurement_concentration";
return {
period_scope: input.periodScope,
rows_with_amount: input.outgoing.rows_with_amount,
total_outgoing_amount: input.outgoing.total_amount,
total_outgoing_amount_human_ru: input.outgoing.total_amount_human_ru,
top_outgoing_counterparty: topOutgoingCounterparty,
top_outgoing_share_pct: topOutgoingCounterparty
? percentageOfTotal(topOutgoingCounterparty.total_amount, input.outgoing.total_amount)
: null,
top_non_financial_supplier: topNonFinancialSupplier,
top_non_financial_supplier_share_pct: topNonFinancialSupplier
? percentageOfTotal(topNonFinancialSupplier.total_amount, input.outgoing.total_amount)
: null,
financial_institution_leads_outgoing_cash: financialInstitutionLeadsOutgoingCash,
supplier_only_count: input.counterpartyProfile?.supplier_only_count ?? null,
mixed_role_count: input.counterpartyProfile?.mixed_role_count ?? null,
used_contracts: input.contractUsageProfile?.used_contracts ?? null,
total_contracts: input.contractUsageProfile?.total_contracts ?? null,
used_contract_share_pct: input.contractUsageProfile?.used_contract_share_pct ?? null,
evidence_status: evidenceStatus,
inference_basis: "supplier_payout_concentration_counterparty_contract_profile_confirmed_1c_rows"
};
}
function inventoryStalenessRiskBandRu(
riskBand: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryStalenessRiskProxy["risk_band"]
): string {
@ -4150,6 +4226,7 @@ function buildBusinessOverviewMissingProofFamilies(input: {
inventoryPosition: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryPosition | null;
inventoryTurnoverProxy: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryTurnoverProxy | null;
inventoryStalenessRiskProxy: AssistantMcpDiscoveryDerivedBusinessOverviewInventoryStalenessRiskProxy | null;
vendorProcurementQuality: AssistantMcpDiscoveryDerivedBusinessOverviewVendorProcurementQuality | null;
hasSupplierConcentrationSignal: boolean;
}): AssistantMcpDiscoveryBusinessOverviewMissingProofFamily[] {
const missing = new Set(input.missingSignalFamilies);
@ -4215,7 +4292,7 @@ function buildBusinessOverviewMissingProofFamilies(input: {
});
}
if (input.hasSupplierConcentrationSignal) {
if (input.hasSupplierConcentrationSignal && !input.vendorProcurementQuality) {
pushUnique({
family: "vendor_risk_procurement_quality",
current_status: "proxy_only_currently",
@ -4313,6 +4390,13 @@ function deriveBusinessOverview(input: {
inventoryPosition,
inventoryTurnoverProxy
});
const vendorProcurementQuality = deriveBusinessOverviewVendorProcurementQuality({
rankedOutgoing,
outgoing,
counterpartyProfile,
contractUsageProfile,
periodScope: input.periodScope
});
const checkedSignalCount = [
incoming.rows_with_amount > 0,
outgoing.rows_with_amount > 0,
@ -4329,7 +4413,8 @@ function deriveBusinessOverview(input: {
Boolean(contractUsageProfile),
Boolean(inventoryPosition),
Boolean(inventoryTurnoverProxy),
Boolean(inventoryStalenessRiskProxy)
Boolean(inventoryStalenessRiskProxy),
Boolean(vendorProcurementQuality)
].filter(Boolean).length;
if (checkedSignalCount <= 0) {
return null;
@ -4363,6 +4448,7 @@ function deriveBusinessOverview(input: {
inventoryPosition,
inventoryTurnoverProxy,
inventoryStalenessRiskProxy,
vendorProcurementQuality,
hasSupplierConcentrationSignal: (rankedOutgoing?.ranked_values.length ?? 0) > 0
});
return {
@ -4390,6 +4476,7 @@ function deriveBusinessOverview(input: {
document_activity_profile: documentActivityProfile,
counterparty_profile: counterpartyProfile,
contract_usage_profile: contractUsageProfile,
vendor_procurement_quality: vendorProcurementQuality,
coverage_limited_by_probe_limit:
incoming.coverage_limited_by_probe_limit || outgoing.coverage_limited_by_probe_limit,
checked_signal_count: checkedSignalCount,
@ -5986,6 +6073,10 @@ export async function executeAssistantMcpDiscoveryPilot(
if (derivedBusinessOverview.top_suppliers.length > 0) {
pushReason(reasonCodes, "pilot_derived_business_overview_top_suppliers_from_confirmed_rows");
}
if (derivedBusinessOverview.vendor_procurement_quality) {
pushReason(reasonCodes, "pilot_derived_business_overview_vendor_procurement_quality_from_confirmed_rows");
pushReason(reasonCodes, `pilot_derived_business_overview_vendor_procurement_quality_${derivedBusinessOverview.vendor_procurement_quality.evidence_status}`);
}
if (derivedBusinessOverview.yearly_breakdown.length > 0) {
pushReason(reasonCodes, "pilot_derived_business_overview_yearly_operating_breakdown_from_confirmed_rows");
}

View File

@ -843,7 +843,7 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
extraReasons: primitiveSelection.reasonCodes
});
}
if (dataNeedGraph?.ranking_need && !hasSubjectCandidates(dataNeedGraph)) {
if (dataNeedGraph?.ranking_need) {
pushUnique(axes, "aggregate_axis");
pushUnique(axes, "amount");
pushUnique(axes, "coverage_target");
@ -864,7 +864,10 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
dataNeedGraph.ranking_need === "bottom_asc"
? "planner_selected_bottom_ranked_value_flow_from_data_need_graph"
: "planner_selected_top_ranked_value_flow_from_data_need_graph",
extraReasons: primitiveSelection.reasonCodes
extraReasons: [
...primitiveSelection.reasonCodes,
...(hasSubjectCandidates(dataNeedGraph) ? ["planner_kept_ranking_over_subject_scope_candidates"] : [])
]
});
}
if (openScopeTotalWithoutSubject) {

View File

@ -922,6 +922,58 @@ function buildCompactBusinessOverviewReply(
}
if (vendorRiskBoundary) {
const vendorProcurementQuality = toRecordObject(overview.vendor_procurement_quality);
if (vendorProcurementQuality) {
const status = toNonEmptyString(vendorProcurementQuality.evidence_status);
const totalOutgoing = moneyText(vendorProcurementQuality.total_outgoing_amount_human_ru);
const topOutgoingRecord = toRecordObject(vendorProcurementQuality.top_outgoing_counterparty);
const topOutgoingName = toNonEmptyString(topOutgoingRecord?.axis_value);
const topOutgoingAmount = moneyText(topOutgoingRecord?.total_amount_human_ru);
const topOutgoingShare =
typeof vendorProcurementQuality.top_outgoing_share_pct === "number" &&
Number.isFinite(vendorProcurementQuality.top_outgoing_share_pct)
? `${vendorProcurementQuality.top_outgoing_share_pct}%`
: null;
const nonFinancialRecord = toRecordObject(vendorProcurementQuality.top_non_financial_supplier);
const nonFinancialName = toNonEmptyString(nonFinancialRecord?.axis_value);
const nonFinancialAmount = moneyText(nonFinancialRecord?.total_amount_human_ru);
const nonFinancialShare =
typeof vendorProcurementQuality.top_non_financial_supplier_share_pct === "number" &&
Number.isFinite(vendorProcurementQuality.top_non_financial_supplier_share_pct)
? `${vendorProcurementQuality.top_non_financial_supplier_share_pct}%`
: null;
const periodScope = toNonEmptyString(vendorProcurementQuality.period_scope) ?? period;
const totalText = totalOutgoing ? `; всего исходящих платежей в проверенном срезе ${totalOutgoing}` : "";
if (status === "financial_institution_leads_outgoing_cash") {
lines.push(
`Коротко: проверенный procurement-concentration route за ${periodScope} не подтверждает зависимость от обычного поставщика: крупнейший получатель исходящих денег ${topOutgoingName ?? "не распознан"}${topOutgoingShare ? ` держит около ${topOutgoingShare}` : ""}${topOutgoingAmount ? ` (${topOutgoingAmount})` : ""}, но по названию это банк/финансовая организация${totalText}.`
);
if (nonFinancialName) {
lines.push(
`Крупнейший небанковский получатель исходящих денег: ${nonFinancialName}${nonFinancialShare ? `, около ${nonFinancialShare}` : ""}${nonFinancialAmount ? ` (${nonFinancialAmount})` : ""}. Это уже сигнал закупочной/исходящей концентрации, но не аудит надежности поставщика.`
);
}
} else if (status === "reviewed_procurement_concentration") {
lines.push(
`Коротко: проверенный procurement-concentration route за ${periodScope} нашел основную зависимость исходящего потока: ${topOutgoingName ?? nonFinancialName ?? "получатель не распознан"}${topOutgoingShare ? ` держит около ${topOutgoingShare}` : nonFinancialShare ? ` держит около ${nonFinancialShare}` : ""}${topOutgoingAmount ? ` (${topOutgoingAmount})` : nonFinancialAmount ? ` (${nonFinancialAmount})` : ""}${totalText}.`
);
} else {
lines.push(
`Коротко: procurement-concentration route за ${periodScope} отработал, но надежной небанковской концентрации поставщика по найденным исходящим платежам не хватает${totalText}.`
);
}
const contractText =
typeof vendorProcurementQuality.used_contracts === "number" && Number.isFinite(vendorProcurementQuality.used_contracts)
? typeof vendorProcurementQuality.total_contracts === "number" && Number.isFinite(vendorProcurementQuality.total_contracts)
? ` Договорный профиль: используется ${vendorProcurementQuality.used_contracts}/${vendorProcurementQuality.total_contracts} договоров${typeof vendorProcurementQuality.used_contract_share_pct === "number" && Number.isFinite(vendorProcurementQuality.used_contract_share_pct) ? ` (${vendorProcurementQuality.used_contract_share_pct}%)` : ""}.`
: ` Договорный профиль: используется ${vendorProcurementQuality.used_contracts} договоров.`
: "";
lines.push(
`Что не доказано этим маршрутом: надежность поставщика, качество поставок, договорные условия, назначение каждого платежа и полная структура всех расходов.${contractText}`
);
const reply = lines.join("\n").trim();
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
}
const supplierBasis = topSupplier
? topSupplierLooksFinancial
? `крупнейший получатель исходящих денег: ${topSupplier}; по названию это банк/финансовая организация, поэтому это не доказанная зависимость от обычного поставщика${nonFinancialSupplier ? `; крупнейший небанковский получатель исходящих денег: ${nonFinancialSupplier}` : ""}`

View File

@ -255,6 +255,26 @@ function isMetadataDiscoveryTurn(
);
}
function isExplicitMetadataDiscoveryTurn(
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
): boolean {
const turnInput = toRecordObject(entryPoint?.turn_input);
const turnMeaning = readDiscoveryTurnMeaning(entryPoint);
const graph = readDiscoveryDataNeedGraph(entryPoint);
const reasonCodes = [
...(Array.isArray(entryPoint?.reason_codes) ? entryPoint.reason_codes : []),
...(Array.isArray(turnInput?.reason_codes) ? turnInput.reason_codes : [])
];
const decompositionCandidates = Array.isArray(graph?.decomposition_candidates) ? graph.decomposition_candidates : [];
return Boolean(
toNonEmptyString(turnMeaning?.asked_domain_family) === "metadata" ||
toNonEmptyString(turnMeaning?.unsupported_but_understood_family) === "1c_metadata_surface" ||
toNonEmptyString(graph?.business_fact_family) === "schema_surface" ||
decompositionCandidates.some((candidate) => toNonEmptyString(candidate) === "inspect_metadata_surface") ||
reasonCodes.some((reason) => toNonEmptyString(reason) === "mcp_discovery_metadata_signal_detected")
);
}
function isInventoryExactAddressIntent(intent: string | null): boolean {
return /^(?:inventory_purchase_provenance_for_item|inventory_purchase_documents_for_item|inventory_sale_trace_for_item|inventory_profitability_for_item|inventory_purchase_to_sale_chain|inventory_aging_by_purchase_date|inventory_on_hand_as_of_date)$/u.test(
String(intent ?? "")
@ -274,6 +294,9 @@ function hasMetadataDiscoveryPriority(
if (!isMetadataDiscoveryTurn(entryPoint)) {
return false;
}
if (!isExplicitMetadataDiscoveryTurn(entryPoint)) {
return false;
}
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
return !isInventoryExactAddressIntent(detectedIntent);
}
@ -363,6 +386,75 @@ function readStringArray(value: unknown): string[] {
: [];
}
function hasConfirmedAddressExecution(input: ApplyAssistantMcpDiscoveryResponsePolicyInput): boolean {
const mcpCallStatus = toNonEmptyString(input.addressRuntimeMeta?.mcp_call_status);
const truthMode = toNonEmptyString(input.addressRuntimeMeta?.truth_mode);
const selectedRecipe = toNonEmptyString(input.addressRuntimeMeta?.selected_recipe);
const bindingStatus = toNonEmptyString(input.addressRuntimeMeta?.capability_binding_status);
const bindingViolations = readStringArray(input.addressRuntimeMeta?.capability_binding_violations);
return Boolean(
mcpCallStatus === "matched_non_empty" &&
truthMode === "confirmed" &&
selectedRecipe?.startsWith("address_") &&
(bindingStatus === "bound" || bindingStatus === "bound_with_limits") &&
bindingViolations.length === 0
);
}
function hasStaleMetadataDiscoveryFallbackAgainstExactAddressReply(
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
): boolean {
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
return false;
}
if (!hasEffectivelyFactualAddressReply(input)) {
return false;
}
if (!isMetadataDiscoveryTurn(entryPoint) || isExplicitMetadataDiscoveryTurn(entryPoint)) {
return false;
}
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
return Boolean(
detectedIntent &&
hasConfirmedAddressExecution(input) &&
isDetectedIntentAlignedWithTurnMeaning(detectedIntent, readDiscoveryTurnMeaning(entryPoint))
);
}
function hasBusinessOverviewDirectMoneyClarification(
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
): boolean {
const graph = readDiscoveryDataNeedGraph(entryPoint);
const businessFactFamily = toNonEmptyString(graph?.business_fact_family);
const reasonCodes = readStringArray(graph?.reason_codes);
const clarificationGaps = readStringArray(graph?.clarification_gaps);
return Boolean(
businessFactFamily === "business_overview" &&
reasonCodes.includes("data_need_graph_business_overview_direct_money_answer") &&
(toNonEmptyString(graph?.ranking_need) || reasonCodes.includes("data_need_graph_ranking_top_desc")) &&
clarificationGaps.includes("organization")
);
}
function hasExactValueFlowReplyForBusinessOverviewDirectMoneyNeed(
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
): boolean {
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
return false;
}
if (!hasEffectivelyFactualAddressReply(input)) {
return false;
}
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
return Boolean(
detectedIntent === "customer_revenue_and_payments" &&
hasConfirmedAddressExecution(input) &&
hasBusinessOverviewDirectMoneyClarification(entryPoint)
);
}
function hasValueFlowActionConflictWithDiscoveryTurnMeaning(
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
@ -443,18 +535,7 @@ function hasExactMatchedFactualAddressReply(
return false;
}
}
const mcpCallStatus = toNonEmptyString(input.addressRuntimeMeta?.mcp_call_status);
const truthMode = toNonEmptyString(input.addressRuntimeMeta?.truth_mode);
const selectedRecipe = toNonEmptyString(input.addressRuntimeMeta?.selected_recipe);
const bindingStatus = toNonEmptyString(input.addressRuntimeMeta?.capability_binding_status);
const bindingViolations = readStringArray(input.addressRuntimeMeta?.capability_binding_violations);
return Boolean(
mcpCallStatus === "matched_non_empty" &&
truthMode === "confirmed" &&
selectedRecipe?.startsWith("address_") &&
(bindingStatus === "bound" || bindingStatus === "bound_with_limits") &&
bindingViolations.length === 0
);
return hasConfirmedAddressExecution(input);
}
function hasOpenScopeValueFlowDiscoveryPriority(
@ -682,6 +763,14 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
const exactMatchedFactualAddressReply = hasExactMatchedFactualAddressReply(input, entryPoint);
const runtimeAdjustedExactReply = hasRuntimeAdjustedExactReply(input, entryPoint);
const runtimeMatchedExactReply = hasRuntimeMatchedExactReply(input, entryPoint);
const staleMetadataDiscoveryFallbackAgainstExactAddressReply = hasStaleMetadataDiscoveryFallbackAgainstExactAddressReply(
input,
entryPoint
);
const exactValueFlowReplyForBusinessOverviewDirectMoneyNeed = hasExactValueFlowReplyForBusinessOverviewDirectMoneyNeed(
input,
entryPoint
);
const openScopeValueFlowDiscoveryPriority = hasOpenScopeValueFlowDiscoveryPriority(input, entryPoint);
const metadataDiscoveryPriority = hasMetadataDiscoveryPriority(input, entryPoint);
const valueFlowActionConflictWithDiscoveryTurnMeaning = hasValueFlowActionConflictWithDiscoveryTurnMeaning(
@ -750,6 +839,18 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
"mcp_discovery_response_policy_keep_runtime_matched_exact_reply_over_stale_discovery_turn_meaning"
);
}
if (staleMetadataDiscoveryFallbackAgainstExactAddressReply) {
pushReason(
reasonCodes,
"mcp_discovery_response_policy_keep_exact_address_reply_over_stale_metadata_discovery"
);
}
if (exactValueFlowReplyForBusinessOverviewDirectMoneyNeed) {
pushReason(
reasonCodes,
"mcp_discovery_response_policy_keep_exact_value_flow_reply_over_business_overview_direct_money_clarification"
);
}
if (deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") {
pushReason(
reasonCodes,
@ -779,6 +880,8 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
!exactMatchedFactualAddressReply &&
!runtimeAdjustedExactReply &&
!runtimeMatchedExactReply &&
!staleMetadataDiscoveryFallbackAgainstExactAddressReply &&
!exactValueFlowReplyForBusinessOverviewDirectMoneyNeed &&
!(deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") &&
ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) &&
candidate.eligible_for_future_hot_runtime &&

View File

@ -96,6 +96,27 @@ describe("address follow-up temporal regressions", () => {
expect(result).not.toBeNull();
expect(result?.intent.intent).toBe("receivables_confirmed_as_of_date");
expect(result?.filters.extracted_filters.as_of_date).toBe("2026-04-16");
expect(result?.filters.extracted_filters.counterparty).toBeUndefined();
});
it("does not carry self-scope pronouns as counterparty anchors for short debt mirror follow-up", () => {
const result = runAddressDecomposeStage("а нам?", {
previous_intent: "payables_confirmed_as_of_date",
target_intent: "receivables_confirmed_as_of_date",
previous_filters: {
as_of_date: "2026-05-12",
organization: "ООО Альтернатива Плюс",
counterparty: "нас"
},
previous_anchor_type: "counterparty",
previous_anchor_value: "нас"
});
expect(result).not.toBeNull();
expect(result?.intent.intent).toBe("receivables_confirmed_as_of_date");
expect(result?.filters.extracted_filters.as_of_date).toBe("2026-05-12");
expect(result?.filters.extracted_filters.organization).toBe("ООО Альтернатива Плюс");
expect(result?.filters.extracted_filters.counterparty).toBeUndefined();
});
it("keeps same-date inventory pivot anchored to the previous VAT date", () => {

View File

@ -56,11 +56,11 @@ describe("addressIntentResolver regression bridges", () => {
expect(result.reasons).toContain("unicode_customer_concentration_bridge_signal_detected");
});
it("defers top-year company revenue wording to business overview discovery", () => {
it("routes top-year company revenue wording to exact value-flow ranking", () => {
const result = resolveAddressIntent("какой у нас самый доходный год");
expect(result.intent).toBe("unknown");
expect(result.reasons).toContain("unicode_business_overview_earnings_deferred_to_discovery");
expect(result.intent).toBe("customer_revenue_and_payments");
expect(result.reasons).toContain("unicode_customer_revenue_ranking_bridge_signal_detected");
});
it("defers paired receivables and payables wording to business overview instead of one debt side", () => {

View File

@ -2608,12 +2608,12 @@ describe("address intent resolver expansion (M2.3a)", () => {
expect(result.intent).toBe("customer_revenue_and_payments");
});
it("defers organization-level yearly profitability wording to business overview discovery", () => {
it("routes organization-level top-year revenue wording to exact value-flow ranking", () => {
const result = resolveAddressIntent(
"\u043a\u0430\u043a\u0438\u0435 \u0441\u0430\u043c\u044b\u0435 \u0434\u043e\u0445\u043e\u0434\u043d\u044b\u0435 \u0433\u043e\u0434\u0430 \u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u044b"
);
expect(result.intent).toBe("unknown");
expect(result.reasons).toContain("unicode_business_overview_earnings_deferred_to_discovery");
expect(result.intent).toBe("customer_revenue_and_payments");
expect(result.reasons).toContain("unicode_customer_revenue_ranking_bridge_signal_detected");
});
it("defers organization-level profit and margin wording to business overview discovery", () => {
@ -3496,12 +3496,10 @@ describe("address query limited taxonomy and stage diagnostics", { timeout: 1500
expect(reply.toLowerCase()).toContain("shortlist");
} else {
expect(result?.debug.balance_confirmed).toBe(true);
expect(reply).toContain("Коротко: подтвержденный долг к оплате");
expect(reply).toContain("Это подтвержденный срез обязательств к оплате");
expect(reply).toMatch(/\n\nЧто учтено/u);
expect(reply).toMatch(/\n\nСводка/u);
expect(reply).toMatch(/\n\nКатегории обязательств/u);
expect(reply).toMatch(/\n\nПодтвержденные позиции к оплате/u);
expect(reply).toContain("Коротко: на 31.05.2020 мы должны");
expect(reply).toContain("Крупнейшие позиции к оплате");
expect(reply).toContain("Основа: подтвержденный остаток по счетам 60/76");
expect(reply.length).toBeLessThan(1800);
expect(reply).not.toContain("Блок 1");
expect(reply).not.toContain("эвристический");
}
@ -3793,17 +3791,16 @@ describe("address query limited taxonomy and stage diagnostics", { timeout: 1500
expect(["FACTUAL_LIST", "LIMITED_WITH_REASON", "FACTUAL_SUMMARY"]).toContain(result?.response_type);
});
it("defers yearly profitability wording out of the exact customer value aggregate recipe", async () => {
it("routes organization-level yearly revenue ranking wording into the exact customer value aggregate recipe", async () => {
const service = new AddressQueryService();
const result = await service.tryHandle(
"\u043a\u0430\u043a\u0438\u0435 \u0441\u0430\u043c\u044b\u0435 \u0434\u043e\u0445\u043e\u0434\u043d\u044b\u0435 \u0433\u043e\u0434\u0430 \u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u044b"
);
expect(result?.handled).toBe(true);
expect(result?.debug.detected_intent).toBe("unknown");
expect(result?.debug.selected_recipe).toBeNull();
expect(result?.debug.limited_reason_category).toBe("unsupported");
expect(result?.debug.mcp_call_status).toBe("skipped");
expect(result?.response_type).toBe("LIMITED_WITH_REASON");
expect(result?.debug.detected_intent).toBe("customer_revenue_and_payments");
expect(result?.debug.selected_recipe).toBe("address_customer_revenue_and_payments_v1");
expect(result?.debug.mcp_call_status).not.toBe("skipped");
expect(["FACTUAL_LIST", "LIMITED_WITH_REASON", "FACTUAL_SUMMARY"]).toContain(result?.response_type);
});
it("routes typo highest-check wording into customer value aggregate recipe", async () => {

View File

@ -53,6 +53,7 @@ describe("receivables confirmed as-of route", () => {
const plan = buildAddressRecipePlan(selected.selected_recipe!, filters);
expect(plan.query).toContain("РегистрБухгалтерии.Хозрасчетный.Остатки");
expect(plan.query).toContain("СуммаРазвернутыйОстатокДт");
expect(plan.query).toContain("СуммаРазвернутыйОстатокКт");
expect(plan.query).toContain("Остатки.Счет");
expect(plan.query).toContain("62");
expect(plan.query).toContain("76");

View File

@ -1,6 +1,7 @@
import { describe, expect, it } from "vitest";
import { composeCounterpartyAnalyticsReply } from "../src/services/address_runtime/counterpartyAnalyticsReplyBuilders";
import { composeFactualReply } from "../src/services/address_runtime/composeStage";
import { composeInventoryReply } from "../src/services/address_runtime/inventoryReplyBuilders";
describe("address reply builders regressions", () => {
@ -51,6 +52,88 @@ describe("address reply builders regressions", () => {
expect(result?.text.split("\n")[0]).toContain("Чапурнов");
});
it("keeps canonical single-best customer wording compact instead of returning the default top list", () => {
const result = composeCounterpartyAnalyticsReply(
"customer_revenue_and_payments",
[
{
counterparty: "Крупный клиент",
amount: 250000,
period: "2020-03-31",
registrator: "Поступление 1"
} as any,
{
counterparty: "Малый клиент",
amount: 100000,
period: "2020-04-30",
registrator: "Поступление 2"
} as any
],
{
userMessage: "определить самого доходного клиента за весь период"
},
{
formatPercent: () => null,
formatDateRu: (value: string) => value,
formatMoneyRub: (value: number) => `${value} руб.`,
extractYearFromIso: (value: string | null) => (value ? Number(value.slice(0, 4)) : null),
detectCounterpartyProfileFocus: () => "full_profile",
detectCounterpartyLifecycleFocus: () => "active_customers_all_time",
hasCounterpartyLifecycleLongevityQuestion: () => false,
hasCounterpartyActivityAgeQuestion: () => false,
detectRankingLimit: () => 20,
detectValueRankingFocus: () => "top_by_total",
detectContractValueFocus: () => "top_by_turnover",
detectMinOpsForAvgCheck: () => 1,
extractRequestedYearFromQuestion: () => null,
extractCounterpartyName: (row: any) => row.counterparty,
extractContractName: () => null,
counterpartyLookupMatches: () => false,
toUtcDayTimestamp: () => null,
formatAgeYearsMonthsDays: () => "0 дней",
normalizeQuestionText: (value: string | null | undefined) => String(value ?? "").toLowerCase()
}
);
expect(result?.text.split("\n")[0]).toContain("Крупный клиент");
expect(result?.text).not.toContain("2. Малый клиент");
});
it("keeps direct receivables snapshot answers compact", () => {
const result = composeFactualReply(
"receivables_confirmed_as_of_date",
[
{
period: "2020-05-10",
registrator: "Поступление 1",
account_dt: "62.01",
account_kt: "90.01",
amount: 100000,
analytics: ["Клиент А"],
counterparty: "Клиент А"
},
{
period: "2020-05-11",
registrator: "Поступление 2",
account_dt: "62.01",
account_kt: "90.01",
amount: 50000,
analytics: ["Клиент Б"],
counterparty: "Клиент Б"
}
],
{
userMessage: "кто нам должен денег на май 2020",
asOfDate: "2020-05-31"
}
);
expect(result.text.split("\n")[0]).toContain("Коротко:");
expect(result.text.split("\n")[0]).toContain("Клиент А");
expect(result.text.length).toBeLessThan(1200);
expect(result.text).toContain("Крупнейшие позиции к получению");
});
it("does not overclaim a comparative top customer ranking when only one candidate is present", () => {
const result = composeCounterpartyAnalyticsReply(
"customer_revenue_and_payments",
@ -190,4 +273,174 @@ describe("address reply builders regressions", () => {
expect(firstLine).toContain("К самым старым закупкам");
expect(firstLine).not.toContain("2026-04-18");
});
it("excludes mirrored 76 settlements from clean payables", () => {
const result = composeFactualReply(
"payables_confirmed_as_of_date",
[
{
period: "2026-05-12",
registrator: "",
account_dt: "51",
account_kt: "76.09",
amount: 3677454.14,
analytics: ["Комитет государственных услуг г. Москвы", "ООО \\Альтернатива Плюс\\", "Финансовое обеспечение заявки"],
counterparty: "Комитет государственных услуг г. Москвы",
organization: "ООО \\Альтернатива Плюс\\"
},
{
period: "2026-05-12",
registrator: "Списание с расчетного счета 1",
account_dt: "76.09",
account_kt: "51",
amount: 1000000,
analytics: ["Комитет государственных услуг г. Москвы", "ООО \\Альтернатива Плюс\\", "Финансовое обеспечение заявки"],
counterparty: "Комитет государственных услуг г. Москвы",
organization: "ООО \\Альтернатива Плюс\\"
},
{
period: "2026-05-12",
registrator: "Списание с расчетного счета 2",
account_dt: "76.09",
account_kt: "51",
amount: 2677454.14,
analytics: ["Комитет государственных услуг г. Москвы", "ООО \\Альтернатива Плюс\\", "Финансовое обеспечение заявки"],
counterparty: "Комитет государственных услуг г. Москвы",
organization: "ООО \\Альтернатива Плюс\\"
},
{
period: "2026-05-12",
registrator: "Поступление товаров 1",
account_dt: "41.01",
account_kt: "60.01",
amount: 7271.2,
analytics: ["Авант мебель"],
counterparty: "Авант мебель",
organization: "ООО Альтернатива Плюс"
}
],
{
userMessage: "мы должны кому-то денег на сегодня?",
asOfDate: "2026-05-12"
}
);
const firstLine = result.text.split("\n")[0] ?? "";
expect(firstLine).toContain("7.271,20");
expect(firstLine).toContain("Авант мебель");
expect(firstLine).not.toContain("3.677.454,14");
expect(result.text).toContain("встречных остатков");
expect(result.text).toContain("Комитет государственных услуг г. Москвы");
expect(result.text).toContain("Финансовое обеспечение заявки");
expect(result.text).not.toContain("договор/аналитика: ООО \\Альтернатива Плюс\\");
});
it("excludes mirrored 76 settlements from clean receivables", () => {
const result = composeFactualReply(
"receivables_confirmed_as_of_date",
[
{
period: "2026-05-12",
registrator: "Реализация 1",
account_dt: "62.01",
account_kt: "90.01",
amount: 9612904.9,
analytics: ["Департамент капитального ремонта города Москвы."],
counterparty: "Департамент капитального ремонта города Москвы.",
organization: "ООО Альтернатива Плюс"
},
{
period: "2026-05-12",
registrator: "",
account_dt: "51",
account_kt: "76.09",
amount: 3677454.14,
analytics: ["Комитет государственных услуг г. Москвы", "Финансовое обеспечение заявки"],
counterparty: "Комитет государственных услуг г. Москвы",
organization: "ООО Альтернатива Плюс"
},
{
period: "2026-05-12",
registrator: "Списание с расчетного счета 1",
account_dt: "76.09",
account_kt: "51",
amount: 3677454.14,
analytics: ["Комитет государственных услуг г. Москвы", "Финансовое обеспечение заявки"],
counterparty: "Комитет государственных услуг г. Москвы",
organization: "ООО Альтернатива Плюс"
}
],
{
userMessage: "а нам кто должен на сегодня?",
asOfDate: "2026-05-12"
}
);
const firstLine = result.text.split("\n")[0] ?? "";
expect(firstLine).toContain("9.612.904,90");
expect(firstLine).toContain("Департамент капитального ремонта города Москвы.");
expect(firstLine).not.toContain("13.290.359,04");
expect(result.text).toContain("встречных остатков");
expect(result.text).toContain("Комитет государственных услуг г. Москвы");
});
it("keeps bank-named balance counterparties as counterparties, not settlement analytics", () => {
const result = composeFactualReply(
"receivables_confirmed_as_of_date",
[
{
period: "2017-05-31",
registrator: "Остатки на дату",
account_dt: "76.09",
account_kt: null,
amount: 39079.12,
analytics: ["Сбербанк-АСТ, ЗАО", "Финансовое обеспечение заявки"],
organization: "ООО \\Альтернатива Плюс\\"
}
],
{
userMessage: "кто нам должен денег на май 2017?",
asOfDate: "2017-05-31"
}
);
const firstLine = result.text.split("\n")[0] ?? "";
expect(firstLine).toContain("Сбербанк-АСТ, ЗАО");
expect(firstLine).not.toContain("Финансовое обеспечение заявки");
});
it("keeps different 76 contracts separated instead of netting by counterparty only", () => {
const result = composeFactualReply(
"payables_confirmed_as_of_date",
[
{
period: "2026-05-12",
registrator: "",
account_dt: "51",
account_kt: "76.09",
amount: 1000,
analytics: ["Комитет государственных услуг г. Москвы", "Договор А"],
counterparty: "Комитет государственных услуг г. Москвы",
organization: "ООО Альтернатива Плюс"
},
{
period: "2026-05-12",
registrator: "Списание с расчетного счета 1",
account_dt: "76.09",
account_kt: "51",
amount: 1000,
analytics: ["Комитет государственных услуг г. Москвы", "Договор Б"],
counterparty: "Комитет государственных услуг г. Москвы",
organization: "ООО Альтернатива Плюс"
}
],
{
userMessage: "мы должны кому-то денег на сегодня?",
asOfDate: "2026-05-12"
}
);
expect(result.text.split("\n")[0]).toContain("1.000,00");
expect(result.text).not.toContain("встречных остатков");
});
});

View File

@ -362,12 +362,14 @@ describe("assistant MCP discovery answer adapter", () => {
expect(draft.next_step_line).toContain("прибыль/маржу");
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 reviewed procurement concentration as supplier reliability, delivery quality, payment-purpose classification, or full expense structure.");
expect(draft.must_not_claim).toContain("Do not present business overview document/account-section activity profile as process quality, accounting correctness, or completeness of all 1C activity.");
expect(draft.must_not_claim).toContain("Do not present business overview counterparty or contract profile as CRM quality, counterparty due diligence, contract-risk audit, or legal completeness.");
expect(draft.must_not_claim).toContain("Do not present business overview missing proof families as checked, executed, or confirmed routes.");
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_vendor_procurement_quality");
expect(draft.reason_codes).toContain("answer_contains_business_overview_vendor_procurement_quality_reviewed_procurement_concentration");
expect(draft.reason_codes).toContain("answer_contains_business_overview_yearly_operating_breakdown");
expect(draft.reason_codes).toContain("answer_contains_business_overview_document_activity_profile");
expect(draft.reason_codes).toContain("answer_contains_business_overview_counterparty_profile");

View File

@ -258,20 +258,36 @@ describe("assistant MCP discovery pilot executor", () => {
unused_contracts: 372,
used_contract_share_pct: 28.46
});
expect(result.derived_business_overview?.vendor_procurement_quality).toMatchObject({
rows_with_amount: 2,
total_outgoing_amount: 200000,
top_outgoing_share_pct: 75,
top_non_financial_supplier_share_pct: 75,
financial_institution_leads_outgoing_cash: false,
supplier_only_count: 71,
mixed_role_count: 23,
used_contracts: 148,
total_contracts: 520,
used_contract_share_pct: 28.46,
evidence_status: "reviewed_procurement_concentration",
inference_basis: "supplier_payout_concentration_counterparty_contract_profile_confirmed_1c_rows"
});
expect(result.derived_business_overview?.vendor_procurement_quality?.top_outgoing_counterparty).toMatchObject({
axis_value: "Поставщик А",
total_amount: 150000
});
expect(result.derived_business_overview?.missing_proof_families).toEqual(
expect.arrayContaining([
expect.objectContaining({
family: "accounting_profit_margin",
current_status: "reviewed_route_not_wired",
current_supported_evidence: null
}),
expect.objectContaining({
family: "vendor_risk_procurement_quality",
current_status: "proxy_only_currently",
current_supported_evidence: "supplier_concentration_proxy_from_confirmed_outgoing_payment_rows"
})
])
);
expect(result.derived_business_overview?.missing_proof_families.map((item) => item.family)).not.toContain(
"vendor_risk_procurement_quality"
);
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("Годовая раскладка операционного денежного потока");
@ -289,6 +305,8 @@ describe("assistant MCP discovery pilot executor", () => {
expect(result.reason_codes).toContain("pilot_derived_business_overview_document_activity_profile_from_confirmed_rows");
expect(result.reason_codes).toContain("pilot_derived_business_overview_counterparty_profile_from_confirmed_rows");
expect(result.reason_codes).toContain("pilot_derived_business_overview_contract_usage_profile_from_confirmed_rows");
expect(result.reason_codes).toContain("pilot_derived_business_overview_vendor_procurement_quality_from_confirmed_rows");
expect(result.reason_codes).toContain("pilot_derived_business_overview_vendor_procurement_quality_reviewed_procurement_concentration");
expect(result.reason_codes).toContain("pilot_business_overview_missing_proof_families_recorded");
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(6);
});

View File

@ -904,6 +904,40 @@ describe("assistant MCP discovery planner", () => {
expect(result.selected_chain_id).not.toBe("entity_resolution");
});
it("keeps ranked value-flow when subject candidates only scope the ranking question", () => {
const result = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: ["Альтернатива Плюс"],
business_fact_family: "value_flow",
action_family: "turnover",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: null,
ranking_need: "top_desc",
proof_expectation: "coverage_checked_fact",
clarification_gaps: [],
decomposition_candidates: ["collect_scoped_movements", "aggregate_ranked_axis_values", "probe_coverage"],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
reason_codes: ["data_need_graph_built", "data_need_graph_ranking_top_desc"]
},
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_date_scope: "all_time",
explicit_organization_scope: "Альтернатива Плюс"
}
});
expect(result.planner_status).toBe("ready_for_execution");
expect(result.selected_chain_id).toBe("value_flow_ranking");
expect(result.catalog_chain_template_matches[0]).toBe("value_flow_ranking");
expect(result.catalog_chain_template_alignment.selected_chain_matches_top).toBe(true);
expect(result.reason_codes).toContain("planner_selected_top_ranked_value_flow_from_data_need_graph");
expect(result.reason_codes).toContain("planner_kept_ranking_over_subject_scope_candidates");
});
it("keeps ranked value-flow ready for execution once checked period and organization are known", () => {
const result = planAssistantMcpDiscovery({
dataNeedGraph: {

View File

@ -215,6 +215,95 @@ describe("assistant MCP discovery response candidate", () => {
expect(candidate.reply_text).not.toContain("операционное нетто");
});
it("uses reviewed procurement-concentration evidence for vendor-risk boundary answers", () => {
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
entryPoint({
turn_input: {
adapter_status: "ready",
turn_meaning_ref: {
asked_domain_family: "business_overview",
asked_action_family: "vendor_risk_procurement_boundary",
unsupported_but_understood_family: "vendor_risk_procurement_boundary"
},
data_need_graph: {
business_fact_family: "business_overview",
ranking_need: null,
reason_codes: ["data_need_graph_family_business_overview"]
}
},
bridge: {
bridge_status: "answer_draft_ready",
user_facing_response_allowed: true,
business_fact_answer_allowed: true,
requires_user_clarification: false,
pilot: {
pilot_scope: "business_overview_route_template_v1",
derived_business_overview: {
period_scope: "2020",
outgoing_supplier_payout: {
total_amount_human_ru: "1 000 000 руб."
},
top_suppliers: [
{
axis_value: "СБЕРБАНК, ПАО",
total_amount_human_ru: "700 000 руб."
}
],
vendor_procurement_quality: {
period_scope: "2020",
rows_with_amount: 2,
total_outgoing_amount: 1000000,
total_outgoing_amount_human_ru: "1 000 000 руб.",
top_outgoing_counterparty: {
axis_value: "СБЕРБАНК, ПАО",
total_amount: 700000,
total_amount_human_ru: "700 000 руб.",
rows_with_amount: 1,
share_pct: 70,
is_likely_financial_institution: true
},
top_outgoing_share_pct: 70,
top_non_financial_supplier: {
axis_value: "Поставщик А",
total_amount: 300000,
total_amount_human_ru: "300 000 руб.",
rows_with_amount: 1,
share_pct: 30,
is_likely_financial_institution: false
},
top_non_financial_supplier_share_pct: 30,
financial_institution_leads_outgoing_cash: true,
supplier_only_count: null,
mixed_role_count: null,
used_contracts: null,
total_contracts: null,
used_contract_share_pct: null,
evidence_status: "financial_institution_leads_outgoing_cash",
inference_basis: "supplier_payout_concentration_counterparty_contract_profile_confirmed_1c_rows"
}
}
},
answer_draft: {
answer_mode: "confirmed_with_bounded_inference",
headline: "Vendor route.",
confirmed_lines: [],
inference_lines: [],
unknown_lines: [],
limitation_lines: [],
next_step_line: null
}
}
})
);
expect(candidate.reply_text).toContain("procurement-concentration route");
expect(candidate.reply_text).toContain("банк/финансовая организация");
expect(candidate.reply_text).toContain("Поставщик А");
expect(candidate.reply_text).toContain("надежность поставщика");
expect(candidate.reply_text).not.toContain("outgoing cash concentration proxy");
expect(candidate.reply_text).not.toContain("business_overview_route_template_v1");
});
it("uses a compact direct answer for business-overview top year questions", () => {
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
entryPoint({

View File

@ -367,6 +367,83 @@ describe("assistant MCP discovery response policy", () => {
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_exact_matched_factual_address_reply");
});
it("keeps exact receivables replies when metadata inspection is only a stale fallback candidate", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply:
"Коротко: подтвержденная дебиторская задолженность на 31.05.2017 — 1 234 567,89 руб.",
currentReplySource: "address_query_runtime_v1",
currentReplyType: "factual",
addressRuntimeMeta: {
detected_intent: "receivables_confirmed_as_of_date",
selected_recipe: "address_receivables_confirmed_as_of_date_v1",
mcp_call_status: "matched_non_empty",
truth_mode: "confirmed",
capability_binding_status: "bound",
capability_binding_violations: [],
answer_shape_contract: {
reply_type: "factual",
capability_contract_id: "confirmed_receivables_as_of_date"
},
assistant_mcp_discovery_entry_point_v1: entryPoint({
turn_input: {
adapter_status: "ready",
should_run_discovery: true,
data_need_graph: {
business_fact_family: null,
action_family: "confirmed_snapshot",
clarification_gaps: ["subject"],
reason_codes: [
"data_need_graph_built",
"data_need_graph_family_unknown",
"data_need_graph_has_clarification_gaps"
]
},
turn_meaning_ref: {
asked_domain_family: "receivables",
asked_action_family: "confirmed_snapshot",
explicit_date_scope: "2017-05-31",
unsupported_but_understood_family: "profit_margin_boundary",
stale_replay_forbidden: true
},
reason_codes: [
"mcp_discovery_seeded_from_followup_context",
"mcp_discovery_resumed_from_saved_loop_state",
"mcp_discovery_data_need_graph_built"
]
},
bridge: {
bridge_status: "answer_draft_ready",
user_facing_response_allowed: true,
business_fact_answer_allowed: true,
requires_user_clarification: false,
pilot: {
pilot_scope: "metadata_inspection_v1"
},
answer_draft: {
answer_mode: "confirmed_with_bounded_inference",
headline: "Metadata surface confirmed.",
confirmed_lines: ["Available metadata object sets: Document, AccumulationRegister"],
inference_lines: [],
unknown_lines: ["Detailed metadata fields were not returned by this probe."],
limitation_lines: [],
next_step_line: null
}
}
})
}
});
expect(result.applied).toBe(false);
expect(result.decision).toBe("keep_current_reply");
expect(result.reply_text).toContain("дебиторская задолженность");
expect(result.reason_codes).toContain("mcp_discovery_response_policy_keep_exact_matched_factual_address_reply");
expect(result.reason_codes).toContain(
"mcp_discovery_response_policy_keep_exact_address_reply_over_stale_metadata_discovery"
);
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_metadata_candidate_priority");
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_candidate_applied");
});
it("keeps aligned factual address lane answers when the exact lane already matched the same semantic intent", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: "ИП Калинин Н.М. | сумма: 216600 | операций: 2",
@ -582,6 +659,71 @@ describe("assistant MCP discovery response policy", () => {
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_full_confirmed_factual_address_reply");
});
it("keeps exact top-year value-flow replies over business-overview direct-money clarification", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply:
"Самый доходный год по подтвержденным поступлениям: 2020 (15 744 052,48 руб. по 20 операциям). Это денежный поток, а не чистая прибыль.",
currentReplySource: "address_query_runtime_v1",
currentReplyType: "factual",
addressRuntimeMeta: {
detected_intent: "customer_revenue_and_payments",
selected_recipe: "address_customer_revenue_and_payments_v1",
mcp_call_status: "matched_non_empty",
truth_mode: "confirmed",
capability_binding_status: "bound",
capability_binding_violations: [],
assistant_mcp_discovery_entry_point_v1: entryPoint({
turn_input: {
adapter_status: "ready",
should_run_discovery: true,
data_need_graph: {
business_fact_family: "business_overview",
action_family: "profit_margin_boundary",
ranking_need: "top_desc",
clarification_gaps: ["organization"],
reason_codes: [
"data_need_graph_built",
"data_need_graph_family_business_overview",
"data_need_graph_ranking_top_desc",
"data_need_graph_business_overview_direct_money_answer",
"data_need_graph_open_scope_total_needs_organization"
]
},
turn_meaning_ref: {
asked_domain_family: "business_overview",
asked_action_family: "profit_margin_boundary",
unsupported_but_understood_family: "profit_margin_boundary",
stale_replay_forbidden: true
}
},
bridge: {
bridge_status: "answer_draft_ready",
user_facing_response_allowed: true,
business_fact_answer_allowed: false,
requires_user_clarification: true,
answer_draft: {
answer_mode: "needs_clarification",
headline: "Нужно уточнить организацию.",
confirmed_lines: [],
inference_lines: [],
unknown_lines: ["Без организации бизнес-обзор не запускаю."],
limitation_lines: [],
next_step_line: "Уточните организацию."
}
}
})
}
});
expect(result.applied).toBe(false);
expect(result.decision).toBe("keep_current_reply");
expect(result.reply_text).toContain("Самый доходный год");
expect(result.reason_codes).toContain(
"mcp_discovery_response_policy_keep_exact_value_flow_reply_over_business_overview_direct_money_clarification"
);
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_candidate_applied");
});
it("overrides an exact ranking-shaped address reply when bounded open-scope ranking already has organization and period", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply:

View File

@ -757,6 +757,107 @@ describe("assistant MCP discovery runtime bridge", () => {
expect(result.reason_codes).toContain("answer_contains_business_overview_debt_due_date_aging_no_payment_terms_configured");
});
it("promotes vendor-risk boundary through reviewed procurement concentration evidence", async () => {
const deps = buildSequentialDeps([
{ rows: [] },
{
rows: [
{ Period: "2020-01-20T00:00:00", Amount: 700000, Counterparty: "СБЕРБАНК, ПАО" },
{ Period: "2020-02-20T00:00:00", Amount: 300000, Counterparty: "Поставщик А" }
]
},
{ rows: [] },
{ rows: [] },
{
rows: [
{ Period: "2020-01-01T00:00:00", Registrator: "CP_TOTAL", Amount: 12 },
{ Period: "2020-01-01T00:00:00", Registrator: "CP_SUPPLIER_ACTIVE", Amount: 5 },
{ Period: "2020-01-01T00:00:00", Registrator: "CP_MIXED_ACTIVE", Amount: 2 },
{ Period: "2020-01-01T00:00:00", Registrator: "CP_ACTIVE_UNION", Amount: 7 }
]
},
{
rows: [
{ Period: "2020-01-01T00:00:00", Registrator: "CT_TOTAL", Amount: 11 },
{ Period: "2020-01-01T00:00:00", Registrator: "CT_USED", Amount: 4 }
]
}
]);
const result = await runAssistantMcpDiscoveryRuntimeBridge({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
business_fact_family: "business_overview",
action_family: "vendor_risk_procurement_boundary",
aggregation_need: null,
time_scope_need: "all_time_scope",
comparison_need: null,
ranking_need: null,
proof_expectation: "vendor_risk_procurement_quality",
clarification_gaps: [],
decomposition_candidates: [
"collect_scoped_movements",
"aggregate_checked_amounts",
"aggregate_ranked_axis_values",
"fetch_supporting_documents",
"probe_coverage",
"explain_evidence_basis"
],
forbidden_overclaim_flags: [
"no_raw_model_claims",
"no_unchecked_vendor_reliability_claim"
],
reason_codes: ["data_need_graph_built", "data_need_graph_family_business_overview"]
},
turnMeaning: {
asked_domain_family: "business_overview",
asked_action_family: "vendor_risk_procurement_boundary",
explicit_organization_scope: "OOO Alternative Plus"
},
deps
});
const overview = result.pilot.derived_business_overview;
const missingFamilies = overview?.missing_proof_families.map((item) => item.family) ?? [];
const userFacing = [
result.answer_draft.headline,
...result.answer_draft.confirmed_lines,
...result.answer_draft.inference_lines,
...result.answer_draft.unknown_lines,
...result.answer_draft.limitation_lines,
result.answer_draft.next_step_line ?? ""
].join("\n");
expect(result.bridge_status).toBe("answer_draft_ready");
expect(result.business_fact_answer_allowed).toBe(true);
expect(result.route_candidate).toMatchObject({
candidate_status: "ready_for_reviewed_execution",
selected_chain_id: "business_overview",
business_fact_family: "business_overview",
action_family: "vendor_risk_procurement_boundary",
executable_now: true
});
expect(overview?.vendor_procurement_quality).toMatchObject({
total_outgoing_amount: 1000000,
top_outgoing_share_pct: 70,
top_non_financial_supplier_share_pct: 30,
financial_institution_leads_outgoing_cash: true,
supplier_only_count: 3,
mixed_role_count: 2,
used_contracts: 4,
total_contracts: 11,
evidence_status: "financial_institution_leads_outgoing_cash"
});
expect(missingFamilies).not.toContain("vendor_risk_procurement_quality");
expect(userFacing).toContain("procurement-concentration route");
expect(userFacing).toContain("банк/финансовая организация");
expect(userFacing).toContain("Поставщик А");
expect(userFacing).toContain("Надежность поставщиков");
expect(userFacing).not.toContain("outgoing cash concentration proxy");
expect(result.reason_codes).toContain("runtime_bridge_route_candidate_ready_for_reviewed_execution");
expect(result.reason_codes).toContain("answer_contains_business_overview_vendor_procurement_quality_financial_institution_leads_outgoing_cash");
});
it("bridges selected-item inventory provenance templates through exact document evidence", async () => {
const deps = buildDeps([
{

View File

@ -1,4 +1,109 @@
[
{
"generation_id": "gen-ag05121628-50ea6c",
"created_at": "2026-05-12T16:28:41+00:00",
"mode": "saved_user_sessions",
"title": "AGENT | Phase20 continuity/ranking/self-scope accepted replay 2026-05-12",
"count": 6,
"domain": "address_phase20_continuity_stabilization",
"questions": [
"кто у нас самый доходный клиент за все время?",
"какой у нас самый доходный год?",
"кто нам должен денег на май 2017?",
"а какой ндс мы должны примерно заплатить за этот период?",
"мы должны комуто денег на сегодня?",
"а нам?"
],
"generated_by": "Codex",
"saved_case_set_file": "assistant_autogen_saved_user_sessions_20260512162841_gen-ag05121628-50ea6c.json",
"context": {
"llm_provider": null,
"model": null,
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"autogen_personality_id": null,
"autogen_personality_prompt": null,
"source_session_id": null,
"saved_session_file": "assistant_saved_session_20260512162841_gen-ag05121628-50ea6c.json",
"saved_case_set_kind": "agent_semantic_scenario",
"agent_run": true,
"agent_focus": "ranking catalog alignment, self-scope pronoun, compact debt answers",
"architecture_phase": "MCP автономка / semantic integrity",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase20_continuity_stabilization.json",
"scenario_id": "address_truth_harness_phase20_continuity_stabilization",
"semantic_tags": [
"exact_not_overwritten",
"garbage_anchor_forbidden",
"payables_snapshot",
"period_carryover",
"pronoun_followup",
"receivables_snapshot",
"temporal_tail_not_entity",
"value_flow_ranking",
"vat_followup",
"year_tail_not_entity"
],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase20_continuity_stabilization_rerun_planner_ranking_fix",
"saved_after_validated_replay": true
}
},
{
"generation_id": "gen-ag05121357-9ea5d6",
"created_at": "2026-05-12T13:57:26+00:00",
"mode": "saved_user_sessions",
"title": "AGENT | Phase 95 vendor/procurement quality reviewed route",
"count": 7,
"domain": "address_phase95_vendor_procurement_quality_reviewed_route",
"questions": [
"по ООО Альтернатива Плюс за 2020 есть ли риск, что мы зависим от одного поставщика?",
"а банк из этого ответа считать обычным поставщиком?",
"по этой же компании на конец 2020 можно точно понять, какая дебиторка просрочена?",
"а чистая прибыль и маржа за 2020 какие?",
"НДС за 2020 по ООО Альтернатива Плюс какой?",
"а кто принес больше всего денег за 2020?",
"по ООО Альтернатива Плюс на конец 2020 можно точно подтвердить резерв под неликвиды на складе?"
],
"generated_by": "codex_agent",
"saved_case_set_file": "assistant_autogen_saved_user_sessions_20260512135726_gen-ag05121357-9ea5d6.json",
"context": {
"llm_provider": null,
"model": null,
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"autogen_personality_id": null,
"autogen_personality_prompt": null,
"source_session_id": null,
"saved_session_file": "assistant_saved_session_20260512135726_gen-ag05121357-9ea5d6.json",
"saved_case_set_kind": "agent_semantic_scenario",
"agent_run": true,
"agent_focus": "Focused semantic replay for promoting vendor_risk_procurement_quality from proxy-only enablement to reviewed procurement-concentration evidence while preserving debt, profit, VAT, value-flow, and inventory reserve canaries.",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase95_vendor_procurement_quality_reviewed_route.json",
"scenario_id": "address_truth_harness_phase95_vendor_procurement_quality_reviewed_route",
"semantic_tags": [
"accounting_profit_margin",
"business_overview",
"canary",
"context_carryover",
"debt_due_date_boundary",
"financial_institution_boundary",
"inventory_reserve_boundary",
"missing_proof_families",
"profit_margin_boundary",
"ready_for_reviewed_execution",
"value_flow_ranking",
"vat_continuity",
"vendor_risk_procurement_boundary",
"vendor_risk_procurement_quality"
],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase95_vendor_procurement_quality_reviewed_route_live2",
"saved_after_validated_replay": true
}
},
{
"generation_id": "gen-ag05101319-c04f79",
"created_at": "2026-05-10T13:19:22+00:00",
@ -1392,4 +1497,4 @@
"latest_acceptance": null
}
}
]
]

View File

@ -0,0 +1,173 @@
{
"saved_at": "2026-05-12T13:57:26+00:00",
"generation_id": "gen-ag05121357-9ea5d6",
"mode": "saved_user_sessions",
"title": "AGENT | Phase 95 vendor/procurement quality reviewed route",
"agent_run": true,
"questions": [
"по ООО Альтернатива Плюс за 2020 есть ли риск, что мы зависим от одного поставщика?",
"а банк из этого ответа считать обычным поставщиком?",
"по этой же компании на конец 2020 можно точно понять, какая дебиторка просрочена?",
"а чистая прибыль и маржа за 2020 какие?",
"НДС за 2020 по ООО Альтернатива Плюс какой?",
"а кто принес больше всего денег за 2020?",
"по ООО Альтернатива Плюс на конец 2020 можно точно подтвердить резерв под неликвиды на складе?"
],
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "Focused semantic replay for promoting vendor_risk_procurement_quality from proxy-only enablement to reviewed procurement-concentration evidence while preserving debt, profit, VAT, value-flow, and inventory reserve canaries.",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase95_vendor_procurement_quality_reviewed_route.json",
"scenario_id": "address_truth_harness_phase95_vendor_procurement_quality_reviewed_route",
"semantic_tags": [
"accounting_profit_margin",
"business_overview",
"canary",
"context_carryover",
"debt_due_date_boundary",
"financial_institution_boundary",
"inventory_reserve_boundary",
"missing_proof_families",
"profit_margin_boundary",
"ready_for_reviewed_execution",
"value_flow_ranking",
"vat_continuity",
"vendor_risk_procurement_boundary",
"vendor_risk_procurement_quality"
],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase95_vendor_procurement_quality_reviewed_route_live2",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase95_vendor_procurement_quality_reviewed_route_live2",
"final_status": "accepted",
"review_overall_status": "pass",
"business_overall_status": "pass",
"steps_total": 7,
"steps_passed": 7,
"steps_failed": 0,
"steps_with_business_failures": 0,
"steps_with_business_warnings": 0,
"acceptance_gate_passed": true,
"saved_after_validated_replay": true
}
},
"source_session_id": null,
"session": {
"session_id": null,
"mode": "agent_semantic_run",
"items": [
{
"message_id": "agent-user-001",
"role": "user",
"text": "по ООО Альтернатива Плюс за 2020 есть ли риск, что мы зависим от одного поставщика?",
"created_at": "2026-05-12T13:57:26+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-002",
"role": "user",
"text": "а банк из этого ответа считать обычным поставщиком?",
"created_at": "2026-05-12T13:57:26+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-003",
"role": "user",
"text": "по этой же компании на конец 2020 можно точно понять, какая дебиторка просрочена?",
"created_at": "2026-05-12T13:57:26+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-004",
"role": "user",
"text": "а чистая прибыль и маржа за 2020 какие?",
"created_at": "2026-05-12T13:57:26+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-005",
"role": "user",
"text": "НДС за 2020 по ООО Альтернатива Плюс какой?",
"created_at": "2026-05-12T13:57:26+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-006",
"role": "user",
"text": "а кто принес больше всего денег за 2020?",
"created_at": "2026-05-12T13:57:26+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-007",
"role": "user",
"text": "по ООО Альтернатива Плюс на конец 2020 можно точно подтвердить резерв под неликвиды на складе?",
"created_at": "2026-05-12T13:57:26+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
}
],
"agent_run": true,
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "Focused semantic replay for promoting vendor_risk_procurement_quality from proxy-only enablement to reviewed procurement-concentration evidence while preserving debt, profit, VAT, value-flow, and inventory reserve canaries.",
"architecture_phase": "turnaround_11",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase95_vendor_procurement_quality_reviewed_route.json",
"scenario_id": "address_truth_harness_phase95_vendor_procurement_quality_reviewed_route",
"semantic_tags": [
"accounting_profit_margin",
"business_overview",
"canary",
"context_carryover",
"debt_due_date_boundary",
"financial_institution_boundary",
"inventory_reserve_boundary",
"missing_proof_families",
"profit_margin_boundary",
"ready_for_reviewed_execution",
"value_flow_ranking",
"vat_continuity",
"vendor_risk_procurement_boundary",
"vendor_risk_procurement_quality"
],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase95_vendor_procurement_quality_reviewed_route_live2",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase95_vendor_procurement_quality_reviewed_route_live2",
"final_status": "accepted",
"review_overall_status": "pass",
"business_overall_status": "pass",
"steps_total": 7,
"steps_passed": 7,
"steps_failed": 0,
"steps_with_business_failures": 0,
"steps_with_business_warnings": 0,
"acceptance_gate_passed": true,
"saved_after_validated_replay": true
}
}
}
}

View File

@ -0,0 +1,155 @@
{
"saved_at": "2026-05-12T16:28:41+00:00",
"generation_id": "gen-ag05121628-50ea6c",
"mode": "saved_user_sessions",
"title": "AGENT | Phase20 continuity/ranking/self-scope accepted replay 2026-05-12",
"agent_run": true,
"questions": [
"кто у нас самый доходный клиент за все время?",
"какой у нас самый доходный год?",
"кто нам должен денег на май 2017?",
"а какой ндс мы должны примерно заплатить за этот период?",
"мы должны комуто денег на сегодня?",
"а нам?"
],
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "ranking catalog alignment, self-scope pronoun, compact debt answers",
"architecture_phase": "MCP автономка / semantic integrity",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase20_continuity_stabilization.json",
"scenario_id": "address_truth_harness_phase20_continuity_stabilization",
"semantic_tags": [
"exact_not_overwritten",
"garbage_anchor_forbidden",
"payables_snapshot",
"period_carryover",
"pronoun_followup",
"receivables_snapshot",
"temporal_tail_not_entity",
"value_flow_ranking",
"vat_followup",
"year_tail_not_entity"
],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase20_continuity_stabilization_rerun_planner_ranking_fix",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase20_continuity_stabilization_rerun_planner_ranking_fix",
"final_status": "accepted",
"review_overall_status": "pass",
"business_overall_status": "pass",
"steps_total": 6,
"steps_passed": 6,
"steps_failed": 0,
"steps_with_business_failures": 0,
"steps_with_business_warnings": 0,
"acceptance_gate_passed": true,
"saved_after_validated_replay": true
}
},
"source_session_id": null,
"session": {
"session_id": null,
"mode": "agent_semantic_run",
"items": [
{
"message_id": "agent-user-001",
"role": "user",
"text": "кто у нас самый доходный клиент за все время?",
"created_at": "2026-05-12T16:28:41+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-002",
"role": "user",
"text": "какой у нас самый доходный год?",
"created_at": "2026-05-12T16:28:41+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-003",
"role": "user",
"text": "кто нам должен денег на май 2017?",
"created_at": "2026-05-12T16:28:41+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-004",
"role": "user",
"text": "а какой ндс мы должны примерно заплатить за этот период?",
"created_at": "2026-05-12T16:28:41+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-005",
"role": "user",
"text": "мы должны комуто денег на сегодня?",
"created_at": "2026-05-12T16:28:41+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-006",
"role": "user",
"text": "а нам?",
"created_at": "2026-05-12T16:28:41+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
}
],
"agent_run": true,
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "ranking catalog alignment, self-scope pronoun, compact debt answers",
"architecture_phase": "MCP автономка / semantic integrity",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase20_continuity_stabilization.json",
"scenario_id": "address_truth_harness_phase20_continuity_stabilization",
"semantic_tags": [
"exact_not_overwritten",
"garbage_anchor_forbidden",
"payables_snapshot",
"period_carryover",
"pronoun_followup",
"receivables_snapshot",
"temporal_tail_not_entity",
"value_flow_ranking",
"vat_followup",
"year_tail_not_entity"
],
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase20_continuity_stabilization_rerun_planner_ranking_fix",
"saved_after_validated_replay": true,
"save_gate": {
"schema_version": "agent_semantic_save_gate_v1",
"validation_status": "accepted_live_replay",
"validated_run_dir": "artifacts\\domain_runs\\phase20_continuity_stabilization_rerun_planner_ranking_fix",
"final_status": "accepted",
"review_overall_status": "pass",
"business_overall_status": "pass",
"steps_total": 6,
"steps_passed": 6,
"steps_failed": 0,
"steps_with_business_failures": 0,
"steps_with_business_warnings": 0,
"acceptance_gate_passed": true,
"saved_after_validated_replay": true
}
}
}
}

View File

@ -0,0 +1,46 @@
{
"suite_id": "assistant_saved_session_gen-ag05121357-9ea5d6",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_suite_v0_1",
"generated_at": "2026-05-12T13:57:26+00:00",
"generation_id": "gen-ag05121357-9ea5d6",
"mode": "saved_user_sessions",
"title": "AGENT | Phase 95 vendor/procurement quality reviewed route",
"domain": "address_phase95_vendor_procurement_quality_reviewed_route",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "agent_saved_user_sessions",
"title": "AGENT | Phase 95 vendor/procurement quality reviewed route",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "по ООО Альтернатива Плюс за 2020 есть ли риск, что мы зависим от одного поставщика?"
},
{
"user_message": "а банк из этого ответа считать обычным поставщиком?"
},
{
"user_message": "по этой же компании на конец 2020 можно точно понять, какая дебиторка просрочена?"
},
{
"user_message": "а чистая прибыль и маржа за 2020 какие?"
},
{
"user_message": "НДС за 2020 по ООО Альтернатива Плюс какой?"
},
{
"user_message": "а кто принес больше всего денег за 2020?"
},
{
"user_message": "по ООО Альтернатива Плюс на конец 2020 можно точно подтвердить резерв под неликвиды на складе?"
}
]
}
]
}

View File

@ -0,0 +1,43 @@
{
"suite_id": "assistant_saved_session_gen-ag05121628-50ea6c",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_suite_v0_1",
"generated_at": "2026-05-12T16:28:41+00:00",
"generation_id": "gen-ag05121628-50ea6c",
"mode": "saved_user_sessions",
"title": "AGENT | Phase20 continuity/ranking/self-scope accepted replay 2026-05-12",
"domain": "address_phase20_continuity_stabilization",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "agent_saved_user_sessions",
"title": "AGENT | Phase20 continuity/ranking/self-scope accepted replay 2026-05-12",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "кто у нас самый доходный клиент за все время?"
},
{
"user_message": "какой у нас самый доходный год?"
},
{
"user_message": "кто нам должен денег на май 2017?"
},
{
"user_message": "а какой ндс мы должны примерно заплатить за этот период?"
},
{
"user_message": "мы должны комуто денег на сегодня?"
},
{
"user_message": "а нам?"
}
]
}
]
}