Укрепить автономные reviewed-маршруты и срезы задолженности
This commit is contained in:
parent
5dedfeee86
commit
b99a3be083
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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 = [];
|
||||
|
|
|
|||
|
|
@ -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")) {
|
||||
|
|
|
|||
|
|
@ -371,6 +371,14 @@ function isReferentialCounterpartyPlaceholder(value) {
|
|||
return false;
|
||||
}
|
||||
return new Set([
|
||||
"мы",
|
||||
"нам",
|
||||
"нас",
|
||||
"наш",
|
||||
"наша",
|
||||
"наше",
|
||||
"наши",
|
||||
"унас",
|
||||
"он",
|
||||
"она",
|
||||
"оно",
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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}` : ""}`
|
||||
|
|
|
|||
|
|
@ -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 &&
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,28 +795,47 @@ 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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
const normalized = isCounterpartyLikeToken(token, skipTokenPattern);
|
||||
if (normalized) {
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
for (const token of row.analytics) {
|
||||
const normalized = String(token ?? "").trim();
|
||||
if (!normalized) {
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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>();
|
||||
|
|
|
|||
|
|
@ -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")) {
|
||||
|
|
|
|||
|
|
@ -575,6 +575,14 @@ function isReferentialCounterpartyPlaceholder(
|
|||
return false;
|
||||
}
|
||||
return new Set([
|
||||
"мы",
|
||||
"нам",
|
||||
"нас",
|
||||
"наш",
|
||||
"наша",
|
||||
"наше",
|
||||
"наши",
|
||||
"унас",
|
||||
"он",
|
||||
"она",
|
||||
"оно",
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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}` : ""}`
|
||||
|
|
|
|||
|
|
@ -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 &&
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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", () => {
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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("встречных остатков");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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([
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 можно точно подтвердить резерв под неликвиды на складе?"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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": "а нам?"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
Reference in New Issue