Compare commits
No commits in common. "a5fa940953191ce895220684747bddaa12828f26" and "9b02083493524d443631d35a3e4e0aac03a82ecf" have entirely different histories.
a5fa940953
...
9b02083493
|
|
@ -27,9 +27,7 @@ Current reporting baseline:
|
|||
- Post-F Semantic Integrity Hardening: `99%`, operationally closed/regression gate.
|
||||
- Planner Autonomy Consolidation: `100%` for the declared phase83 slice.
|
||||
- Open-World Business Overview implementation breadth: `~99%` through Slice 25.
|
||||
- Open-World Semantic Control Gate: accepted critical subset after EHMO/W5/W7 hardening; fat GUI pack review remains a broad human-pressure gate.
|
||||
- Route-Candidate-Driven Enablement Loop: `100%`, now regression-gated by phase91-phase98 canaries.
|
||||
- Open-World Schema/Primitive Discovery: `95%`, phases97-105 accepted live and saved as user-runnable AGENT autoruns; latest closure replay `phase105_mixed_schema_primitive_closure_live3` accepted `13/13`. Remaining gate is manual GUI/fat-pack review before final closure or a narrow phase106 repair if that review reveals a new semantic defect.
|
||||
- Active next pressure: `Open-World Semantic Control Gate`, accepted module progress `~98%` after the EHMO-derived critical subset accepted live; fat GUI pack review remains before full closure.
|
||||
|
||||
## Archived Execution Snapshot (2026-04-17)
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ This note is the short source of truth for current module wording after the Post
|
|||
|
||||
It exists to prevent stale percentage drift in planning discussions.
|
||||
|
||||
If another document says `78%`, `84%`, `87%`, `92%`, or `85%` for a module that is now closed or has since advanced, read that value as a historical snapshot unless this note explicitly repeats it as current.
|
||||
If another document says `78%`, `87%`, `92%`, or `85%` for a module that is now closed, read that value as a historical snapshot unless this note explicitly repeats it as current.
|
||||
|
||||
## 2026-05-05 Overlay - EHMO Manual QA Gate
|
||||
|
||||
|
|
@ -23,27 +23,6 @@ From this point forward:
|
|||
|
||||
For the current execution spine, read `23 - current_execution_spine_and_semantic_control_gate_2026-05-05.md`.
|
||||
|
||||
## 2026-05-10 Overlay - Agentic Loop And Autorun Hygiene
|
||||
|
||||
The next active operating layer is now the repo-native `Agentic Semantic Development Loop`, not another isolated route patch.
|
||||
|
||||
Current interpretation:
|
||||
|
||||
- the Open-World Semantic Control Gate remains the semantic pressure surface;
|
||||
- the stage-loop is the development operating system around that surface: generate/review/replay/audit/repair/rerun, then save accepted AGENT autoruns only after reviewed acceptance;
|
||||
- Lead Codex remains the repair brain, while the loop produces strong business-audit artifacts and lead-coder handoff instead of relying on a weak autonomous coder;
|
||||
- the first dogfood loop artifact is accepted at `artifacts/domain_runs/stage_agent_loops/agentic_semantic_development_loop/domain_loops/asl/final_status.md`;
|
||||
- manual GUI confirmation remains required after accepted replay artifacts;
|
||||
- autorun/runtime Cyrillic hygiene is now part of the acceptance surface, because broken saved-session text can invalidate the GUI review even when the backend route is correct.
|
||||
|
||||
Fresh validation cut:
|
||||
|
||||
- commit `3be06b5 Починить восстановление кириллицы в автопрогонах`;
|
||||
- targeted mojibake/autorun/runtime tests passed `20/20`;
|
||||
- targeted organization-clarification carryover tests passed `2/2`;
|
||||
- `npm.cmd run build` passed;
|
||||
- graphify rebuilt to `6371` nodes, `14048` edges, `141` communities.
|
||||
|
||||
## Current Module Map
|
||||
|
||||
- `Post-F Semantic Integrity Hardening`: `99%`, operationally closed as a hardening slice and now used as a regression gate.
|
||||
|
|
@ -76,30 +55,9 @@ Fresh validation cut:
|
|||
- Completed active slice: `Business Overview Counterparty/Contract Profile Bridge`: business overview now executes reviewed `counterparty_population_and_roles` and `contract_usage_overview` recipes, surfacing active counterparty role split and contract usage without claiming CRM quality, counterparty due diligence, legal completeness, or contract-risk.
|
||||
- Completed active slice: `Business Overview Missing Proof Ledger`: business overview now records machine-readable hard proof gaps for accounting profit/margin, due-date debt aging, inventory reserve/liquidation quality, and vendor/procurement quality, distinguishing proxy-only evidence from reviewed routes that are not wired yet.
|
||||
- Completed semantic-control slice: `W5/W7 Counterparty Value-Flow And Money-Breakdown Integrity`: bank-document/value-flow recipes now materialize explicit counterparty predicates, zero-row supplier-payment checks answer as checked negative evidence, compound money-breakdown wording stays in `business_overview`, and MCP discovery receives active organization scope only when the current turn has no explicit organization.
|
||||
- Completed operating-system slice: `Agentic Semantic Development Loop Dogfood Gate`: stage manifest, stage pack, stage-loop wrapper, review/status/continue safety, lead-coder handoff, and save-after-acceptance gating are wired and accepted by the `asl` dogfood loop artifact.
|
||||
- Completed hygiene slice: `Autorun Cyrillic C1 Repair`: old autorun cards/questions/runtime materialization now repair C1-control mojibake before UI or assistant-lane use, including the historical `БОЛЬШОЙ ОБЩИЙ` / `АЛЬТЕРНАТИВА` failure class.
|
||||
- Completed autonomy slice: `Open-World Route Candidate Promotion`: `assistantMcpDiscoveryRuntimeBridge` now emits a structured `route_candidate` handoff with candidate status, selected chain, catalog alignment, fact/action family, required/provided/missing axes, execution readiness, enablement reason, safe next action, and forbidden-overclaim flags; debug attachment, truth-harness artifacts, acceptance matrix rows, source-catalog tags, and lead-coder handoff preserve those fields, and the phase90 live replay is accepted at `5/5`.
|
||||
- Active autonomy slice: `Route-Candidate-Driven Enablement Loop`: `needs_route_enablement` route candidates now become first-class deterministic repair targets and Lead Codex handoff groups, while `needs_user_scope` remains a valid clarification signal rather than a false bug.
|
||||
- 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.
|
||||
- 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.
|
||||
- Completed autonomy slice inside that loop: `Inventory Reserve/Liquidation Quality Reviewed Route`: `inventory_reserve_liquidation_quality` now promotes to reviewed inventory quality-event evidence from posted write-off, receipt-adjustment, stocktaking, and revaluation documents; phase96 live replay is accepted.
|
||||
- Completed broader schema/primitive discovery slice: `Financial Counterparty Flow Hints`: bank-document money-flow recipes expose operation/purpose/comment fields, ranked value-flow buckets carry `financial_flow_hint`, explicit `СБЕРБАНК` wording is not swallowed by supplier/customer tails, and bank-like leaders are bounded away from ordinary supplier/customer overclaim; phase97 live replay is accepted.
|
||||
- Completed broader schema/primitive discovery support slice: `Limit Honesty And Business Language Gate`: compact business-overview replies sanitize route/proxy/MCP-style wording, keep row-limit disclosure relevant to the asked contour, and preserve debt/VAT/bank/inventory/supplier canaries; phase98 live replay is accepted.
|
||||
- Completed broader schema/primitive discovery support slice: `Large-Query Budget And Continuation Policy`: explicit-year `business_overview` now receives the chunked monthly recovery budget already used by value-flow routes, yearly money-flow coverage can recover from broad-row caps without fake limit refusal, and profit follow-ups answer direct-first that cash-flow net is not clean profit while still surfacing checked accounting close evidence when present; phase99 live replay is accepted.
|
||||
- Completed broader schema/primitive discovery support slice: `Large-Query Continuation UX`: all-time broad `business_overview` row-cap disclosure now becomes a safe year/quarter continuation path, while narrowed explicit-year follow-ups keep company scope instead of falling into placeholder counterparty wording; phase100 live replay is accepted.
|
||||
- Completed broader schema/primitive discovery support slice: `Inventory Root Scope Without Warehouse Clarification`: broad stock-on-hand root wording now has replay proof that the assistant asks only for company when organization scope is ambiguous, resumes the all-warehouse company snapshot after the company choice, and does not invent warehouse/item/category/material requirements for root inventory questions; phase101 live replay is accepted.
|
||||
- Completed broader schema/primitive discovery support slice: `Debt Mirror Clean-Scope Polarity`: fresh bare organization-name turns can bind scope from a live data-scope probe, confirmed payables/receivables keep the selected organization, short mirror follow-ups override stale/open-items LLM expansion, and mirrored 76.09 financial-security rows are disclosed as offset evidence rather than counted as clean debt in both directions; phase102 live replay is accepted.
|
||||
- Completed broader schema/primitive discovery support slice: `Financial Role/Purpose Arbitration`: grounded exact `bank_operations_*` answers now win over generic value-flow discovery when bank-like counterparties need role/purpose classification; compact bank answers summarize incoming/outgoing rows and do not overclaim ordinary customer revenue or supplier dependency without operation/purpose/contract evidence; phase103 live replay is accepted.
|
||||
- Completed broader schema/primitive discovery support slice: `Generic Role-Tail Anchor Hygiene`: broad role wording such as `не обычный клиент или поставщик` no longer leaks `или поставщик` into counterparty filters, selected objects, or discovery predecompose input, while explicit supplier-payment wording still keeps real counterparties and routes to `supplier_payouts_profile`; phase104 live replay is accepted.
|
||||
- Completed broader schema/primitive discovery closure slice: `Mixed Schema/Primitive Closure Replay`: phase105 validates the combined current module surface across inventory root scope, historical inventory carryover, role-tail hygiene, bank role/purpose, supplier payout, bidirectional SVK value-flow, clean debt polarity, VAT tax-period continuity, and cash-flow/profit boundary; phase105 live replay is accepted.
|
||||
- Current live canary: `phase105_mixed_schema_primitive_closure_live3` accepted `13/13`.
|
||||
- Current accepted autorun: `AGENT | Phase 105 mixed schema/primitive closure replay` (`gen-ag05131312-2d0445`).
|
||||
- Implementation breadth: `~99% (Open-World Bounded Autonomy Breadth through Slice 25)`.
|
||||
- Active broader autonomy module: `Open-World Schema/Primitive Discovery`, with phases97-105 accepted and saved; the module is now at manual-review readiness rather than another blind coding slice.
|
||||
- Next active slice: run/review the phase105 GUI autorun or the user's fat manual pack; if it stays clean, close this module, otherwise convert the next observed failure into a narrow phase106 repair/replay.
|
||||
- Operating-layer progress: `~99% (Agentic Semantic Development Loop, accepted dogfood loop + autorun hygiene; manual GUI confirmation still required)`.
|
||||
- Next active slice: `Open-World Semantic Control Gate`, covering garbage-anchor protection, business-overview continuation, intent dominance, frame hygiene, counterparty/organization arbitration, and final-summary answer shape.
|
||||
- Active module progress: `~99% (Open-World Bounded Autonomy Breadth, active slice: Semantic Control Gate)`.
|
||||
|
||||
## Reporting Rule
|
||||
|
||||
|
|
@ -108,10 +66,6 @@ Use these labels when reporting progress:
|
|||
- `Прогресс модуля: 99% (Post-F Semantic Integrity Hardening, operationally closed/regression gate)` when discussing the Post-F slice itself.
|
||||
- `Прогресс модуля: 100% (Planner Autonomy Consolidation, declared phase83 slice closed)` when discussing the planner-autonomy slice that was just completed.
|
||||
- `Прогресс модуля: 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.
|
||||
- `Прогресс модуля: 100% (Route-Candidate-Driven Enablement Loop, final reviewed proof-family route accepted; use as regression gate)` when discussing the current candidate-driven enablement loop.
|
||||
- `Прогресс модуля: 95% (Open-World Schema/Primitive Discovery, phases97-105 accepted; phase105 GUI/manual checkpoint pending before final closure)` when discussing the current broader schema/primitive discovery module.
|
||||
- `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.
|
||||
|
||||
|
|
@ -144,10 +98,8 @@ The project is not yet a universal arbitrary-1C agent.
|
|||
|
||||
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 open-world coverage beyond the reviewed `business_overview` families already wired for money-flow/activity, customer and supplier concentration, document/account-section activity mix, counterparty role split, contract usage, yearly operating-flow dynamics, explicit profit/margin, debt due-date aging, inventory reserve/liquidation quality events, supplier/procurement quality, bank-like financial counterparty role/purpose hints, business-language/limit-honesty gating, explicit-period VAT/tax, as-of-date debt and inventory position, open-settlement concentration, contract-date debt age, staleness proxies, trading-margin proxy, sales-to-stock inventory proxy, the missing-proof ledger, and the phase93-phase98 reviewed/canary routes;
|
||||
- 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, and the missing-proof ledger into separately proven exact accounting profit/margin, due-date debt aging/overdue, confirmed vendor-risk/procurement-quality analysis, and confirmed reserve/write-off/liquidation inventory evidence families;
|
||||
- broader dynamic schema traversal for unfamiliar 1C asks;
|
||||
- more primitive descriptors where live evidence proves a real gap;
|
||||
- more replay-backed domain packs that start from user business meaning, not from route convenience;
|
||||
|
|
@ -168,27 +120,11 @@ For current planning, read:
|
|||
|
||||
1. `README.md`
|
||||
2. this document
|
||||
3. `31 - inventory_reserve_liquidation_quality_reviewed_route_2026-05-12.md`
|
||||
4. `33 - limit_honesty_business_language_2026-05-13.md`
|
||||
5. `32 - financial_counterparty_flow_hints_2026-05-13.md`
|
||||
6. `30 - vendor_procurement_quality_reviewed_route_2026-05-12.md`
|
||||
7. `29 - debt_due_date_aging_reviewed_route_2026-05-10.md`
|
||||
8. `28 - accounting_profit_margin_reviewed_route_2026-05-10.md`
|
||||
9. `27 - proof_family_enablement_candidates_2026-05-10.md`
|
||||
10. `26 - route_candidate_driven_enablement_loop_2026-05-10.md`
|
||||
11. `25 - open_world_route_candidate_promotion_2026-05-10.md`
|
||||
12. `34 - large_query_budget_continuation_2026-05-13.md`
|
||||
13. `35 - large_query_continuation_ux_2026-05-13.md`
|
||||
14. `36 - inventory_root_scope_no_warehouse_clarification_2026-05-13.md`
|
||||
15. `37 - debt_mirror_clean_scope_polarity_2026-05-13.md`
|
||||
16. `24 - agentic_semantic_development_loop_and_autorun_hygiene_2026-05-10.md`
|
||||
17. `23 - current_execution_spine_and_semantic_control_gate_2026-05-05.md`
|
||||
18. `22 - open_world_bounded_autonomy_breadth_2026-05-01.md`
|
||||
19. `20 - planner_autonomy_consolidation_2026-05-01.md`
|
||||
20. `19 - inventory_stock_open_world_breadth_proof_2026-05-01.md`
|
||||
21. `40 - mixed_schema_primitive_closure_replay_2026-05-13.md`
|
||||
22. `39 - generic_role_tail_anchor_hygiene_2026-05-13.md`
|
||||
23. `17 - post_f_semantic_integrity_hardening_2026-04-23.md`
|
||||
24. `16 - data_need_graph_and_open_world_mcp_plan_2026-04-22.md`
|
||||
3. `23 - current_execution_spine_and_semantic_control_gate_2026-05-05.md`
|
||||
4. `22 - open_world_bounded_autonomy_breadth_2026-05-01.md`
|
||||
5. `20 - planner_autonomy_consolidation_2026-05-01.md`
|
||||
6. `19 - inventory_stock_open_world_breadth_proof_2026-05-01.md`
|
||||
7. `17 - post_f_semantic_integrity_hardening_2026-04-23.md`
|
||||
8. `16 - data_need_graph_and_open_world_mcp_plan_2026-04-22.md`
|
||||
|
||||
Documents `01` through `15` remain valuable, but mostly as the historical architecture trail.
|
||||
|
|
|
|||
|
|
@ -660,7 +660,7 @@ Implemented now:
|
|||
- evidence and answer drafting can surface the leading document type and leading account section as confirmed operational activity mix;
|
||||
- risk synthesis and headline wording can mention this activity mix when present;
|
||||
- `must_not_claim` explicitly forbids treating the profile as process quality, accounting correctness, or complete 1C activity coverage;
|
||||
- missing exact vendor-risk/procurement quality and reserve/liquidation inventory evidence remain pending reviewed-route work; accounting profit/margin and due-date debt aging are now promoted by later route-candidate-driven slices.
|
||||
- missing exact accounting profit, due-date debt aging, vendor-risk/procurement quality, and reserve/liquidation inventory evidence remain pending reviewed-route work.
|
||||
|
||||
This is not a process-audit engine. It is a bounded management context bridge: "what kind of confirmed 1C activity is visible" without pretending to prove whether the business processes or accounting are healthy.
|
||||
|
||||
|
|
@ -711,7 +711,7 @@ The important distinction is not "can we say something useful?" because the curr
|
|||
|
||||
Implemented now:
|
||||
|
||||
- `derived_business_overview.missing_proof_families` originally recorded hard proof gaps for accounting profit/margin, due-date debt aging, inventory reserve/liquidation quality, and vendor-risk/procurement quality; later phase93 and phase94 slices promote accounting profit/margin and due-date debt aging into reviewed routes;
|
||||
- `derived_business_overview.missing_proof_families` records the remaining hard proof families for accounting profit/margin, due-date debt aging, inventory reserve/liquidation quality, and vendor-risk/procurement quality;
|
||||
- each missing proof family exposes `current_status`, `current_supported_evidence`, `next_required_evidence`, and `must_not_claim`;
|
||||
- trading-margin evidence is explicitly recorded as `proxy_only_currently` for accounting profit/margin, not as clean profit or P&L;
|
||||
- open-settlement/debt-staleness evidence is explicitly recorded as proxy-only for due-date aging, not as contractual overdue debt;
|
||||
|
|
@ -779,8 +779,8 @@ Suggested first subset:
|
|||
Current status:
|
||||
|
||||
- implementation breadth through Slice 25: `~99%`;
|
||||
- accepted Open-World module progress after the EHMO-derived Semantic Control Gate subset accepted live and later W5/W7 hardening: `~99%`;
|
||||
- exact P&L and real due-date debt aging have since moved through reviewed route promotion; reserve/write-off/liquidation evidence and vendor-risk engines stay queued behind this semantic gate.
|
||||
- accepted Open-World module progress after the EHMO-derived Semantic Control Gate subset accepted live: `~98%`;
|
||||
- exact P&L, real due-date debt aging, reserve/write-off/liquidation evidence, and vendor-risk engines stay queued behind this semantic gate.
|
||||
|
||||
### Slice 26 local cut 1 - anchor hygiene and overview continuation
|
||||
|
||||
|
|
@ -835,97 +835,3 @@ Remaining before acceptance:
|
|||
- keep the EHMO-derived semantic subset as a regression gate for nearby edits;
|
||||
- review remaining W5/SVK answer-shape roughness for counterparty/organization arbitration after pivots;
|
||||
- rerun the fat manual GUI pack for final acceptance or explicitly classify residuals outside the declared contour.
|
||||
|
||||
### Slice 27 - route candidate promotion
|
||||
|
||||
Implemented first cut:
|
||||
|
||||
- `assistantMcpDiscoveryRuntimeBridge` now emits a versioned internal `route_candidate` contract;
|
||||
- the contract records selected chain, nearest catalog chain template, catalog alignment status, business fact/action family, proof expectation, required/provided/missing axes, execution readiness, enablement reason, recommended next action, and forbidden-overclaim flags;
|
||||
- clarification paths become `needs_user_scope` candidates instead of prose-only stops;
|
||||
- executable reviewed paths become `ready_for_reviewed_execution` candidates;
|
||||
- unsupported-but-understood paths have a dedicated `needs_route_enablement` status for future route-growth work;
|
||||
- the contract is internal and must not leak into user-facing answers;
|
||||
- route-candidate fields are now flattened into debug attachment, truth-harness step state, truth-review markdown, acceptance matrix rows, source-catalog tags, and lead-coder handoff context.
|
||||
|
||||
Validation:
|
||||
|
||||
- `npm.cmd test -- --run tests/assistantMcpDiscoveryRuntimeBridge.test.ts`: passed `17/17`.
|
||||
- `npm.cmd test -- --run tests/assistantMcpDiscoveryDebugAttachment.test.ts tests/assistantDebugPayloadAssembler.test.ts tests/assistantMcpDiscoveryRuntimeBridge.test.ts`: passed `23/23`.
|
||||
- `python -m unittest scripts.test_domain_case_loop_step_state scripts.test_agent_semantic_pack_builder scripts.test_scenario_acceptance_policy`: passed `30/30`.
|
||||
- `npm.cmd run build`: passed.
|
||||
|
||||
Next:
|
||||
|
||||
- create an unfamiliar-1C semantic replay that checks safe clarification, route enablement, and no overclaim;
|
||||
- only promote a candidate to executable route after reviewed code, local tests, and semantic replay acceptance.
|
||||
|
||||
### Slice 28 - proof-family enablement candidates
|
||||
|
||||
Implemented now:
|
||||
|
||||
- exact organization-level proof asks over business overview are separated from broad proxy overview answers;
|
||||
- `profit_margin_boundary`, `debt_due_date_boundary`, `inventory_reserve_boundary`, and `vendor_risk_procurement_boundary` are classified as explicit boundary action families;
|
||||
- `route_candidate` now marks the matching missing proof family as `needs_route_enablement` instead of `ready_for_reviewed_execution`;
|
||||
- user-facing compact answers remain direct and bounded: they may mention proxy evidence, but must not claim exact P&L, contractual overdue debt, confirmed reserve/liquidation value, or vendor-risk/procurement quality.
|
||||
|
||||
Validation:
|
||||
|
||||
- `npm.cmd test -- --run tests/assistantMcpDiscoveryResponseCandidate.test.ts tests/assistantMcpDiscoveryAnswerAdapter.test.ts tests/assistantMcpDiscoveryTurnInputAdapter.test.ts tests/assistantMcpDiscoveryRuntimeBridge.test.ts`: passed `178/178` with `8` skipped.
|
||||
- `npm.cmd run build`: passed.
|
||||
- `address_truth_harness_phase92_proof_family_enablement_candidates_live5_20260510`: accepted `6/6`, `0` warnings, `0` failures.
|
||||
- accepted autorun: `AGENT | Phase 92 proof-family enablement candidates` (`gen-ag05101045-374169`).
|
||||
|
||||
Next at that point:
|
||||
|
||||
- select one phase92 proof family and wire the smallest reviewed exact route;
|
||||
- rerun phase92 plus a focused route-specific pack until the chosen proof family moves from `needs_route_enablement` to `ready_for_reviewed_execution` or a consciously bounded non-implementation decision.
|
||||
|
||||
### Slice 29 - accounting profit-margin reviewed route
|
||||
|
||||
Implemented now:
|
||||
|
||||
- `accounting_profit_margin` was selected as the first proof family to promote from route candidate to reviewed execution;
|
||||
- `address_accounting_financial_result_for_organization_v1` queries confirmed 90/91/99/84 accounting rows for an organization and explicit period;
|
||||
- business-overview derives an `accounting_financial_result` summary from confirmed rows and can answer exact accounting profit/loss and margin basis directly;
|
||||
- short profit/loss follow-ups stay attached to the accounting-result frame instead of drifting back into generic overview;
|
||||
- canonical VAT charged-or-paid wording remains protected by the exact VAT route inside the same replay.
|
||||
|
||||
Validation:
|
||||
|
||||
- `npm.cmd test -- --run tests/addressIntentResolverRegression.test.ts tests/assistantMcpDiscoveryTurnInputAdapter.test.ts tests/assistantMcpDiscoveryRuntimeBridge.test.ts tests/assistantMcpDiscoveryAnswerAdapter.test.ts tests/assistantMcpDiscoveryResponseCandidate.test.ts`: passed `194/194` with `8` skipped.
|
||||
- `npm.cmd test -- --run tests/addressQueryRuntimeM23.test.ts -t "confirmed VAT tax-period"`: passed.
|
||||
- `npm.cmd run build`: passed.
|
||||
- `address_truth_harness_phase93_accounting_profit_margin_reviewed_route_live3_20260510`: accepted `6/6`, `0` warnings, `0` failures.
|
||||
- accepted autorun: `AGENT | Phase 93 accounting profit-margin reviewed route` (`gen-ag05101213-596d99`).
|
||||
|
||||
Next:
|
||||
|
||||
- keep phase94 as the live canary for the candidate-driven route enablement loop;
|
||||
- select the next phase92 proof family, likely vendor/procurement quality or inventory reserve/liquidation;
|
||||
- do not promote debt, vendor-risk, or reserve/liquidation wording without reviewed evidence and replay acceptance.
|
||||
|
||||
### Slice 30 - debt due-date aging reviewed route
|
||||
|
||||
Implemented now:
|
||||
|
||||
- `debt_due_date_aging_quality` was selected as the second proof family to promote from route candidate to reviewed execution;
|
||||
- `address_debt_due_date_aging_for_organization_v1` checks organization-scoped open balances on accounts `60`, `62`, and `76` together with reachable contract payment-term fields;
|
||||
- business-overview derives a `debt_due_date_aging` summary from confirmed rows and distinguishes confirmed overdue, no configured payment terms, insufficient due-date basis, and no overdue found;
|
||||
- noisy organization tails are trimmed before SQL filtering, so the current-turn company scope is not polluted by the rest of the question;
|
||||
- short due-date boundary follow-ups stay attached to the reviewed debt-aging frame instead of drifting into generic chat.
|
||||
|
||||
Validation:
|
||||
|
||||
- `npm.cmd test -- --run tests/assistantTransitionPolicy.test.ts`: passed `38/38`.
|
||||
- `npm.cmd test -- --run tests/assistantMcpDiscoveryTurnInputAdapter.test.ts`: passed `103/103` with `7` skipped.
|
||||
- `npm.cmd test -- --run tests/assistantMcpDiscoveryPilotExecutor.test.ts tests/assistantMcpDiscoveryAnswerAdapter.test.ts tests/assistantMcpDiscoveryResponseCandidate.test.ts tests/assistantMcpDiscoveryRuntimeBridge.test.ts`: passed `113/113` with `1` skipped.
|
||||
- `npm.cmd run build`: passed.
|
||||
- `phase94_debt_due_date_aging_reviewed_route_live4`: accepted `7/7`, `0` warnings, `0` failures.
|
||||
- accepted autorun: `AGENT | Phase 94 debt due-date aging reviewed route` (`gen-ag05101319-c04f79`).
|
||||
|
||||
Next:
|
||||
|
||||
- keep phase94 as the live canary for the candidate-driven route enablement loop;
|
||||
- choose between vendor/procurement quality and inventory reserve/liquidation as the next reviewed proof family;
|
||||
- do not promote vendor-risk or reserve/liquidation wording without reviewed evidence and replay acceptance.
|
||||
|
|
|
|||
|
|
@ -73,22 +73,6 @@ This is not a regression from `99%` to `96%`. It is a metric split:
|
|||
- `99%` describes wired breadth;
|
||||
- `99%` describes closure confidence after the EHMO-derived critical subset passed live replay again with W5/W7 hardening; the gate is still not full module closure until the fat manual GUI pack and remaining answer-shape residuals are reviewed.
|
||||
|
||||
## 2026-05-10 Status Overlay
|
||||
|
||||
This document remains the Semantic Control Gate spine, but it is no longer the latest operating overlay.
|
||||
|
||||
Read `24 - agentic_semantic_development_loop_and_autorun_hygiene_2026-05-10.md` after this file.
|
||||
|
||||
Newer status:
|
||||
|
||||
- the current development operating layer is `Agentic Semantic Development Loop`;
|
||||
- the first dogfood loop artifact for `agentic_semantic_development_loop` is accepted under `artifacts/domain_runs/stage_agent_loops/.../asl/final_status.md`;
|
||||
- the loop uses Lead Codex handoff for repairs, not a weak autonomous coder as the primary repair actor;
|
||||
- accepted AGENT autoruns should be saved only after reviewed replay/loop acceptance;
|
||||
- latest autorun/runtime hygiene cut repairs old C1-control Cyrillic mojibake before GUI cards, runtime questions, or assistant-lane continuation.
|
||||
|
||||
The Semantic Control Gate work units below remain valid regression classes, but current execution should go through the stage-loop machinery when a substantial pack is being validated.
|
||||
|
||||
## Current Local Cut
|
||||
|
||||
Local cut 1 is implemented:
|
||||
|
|
|
|||
|
|
@ -1,145 +0,0 @@
|
|||
# 24 - Agentic Semantic Development Loop And Autorun Hygiene (2026-05-10)
|
||||
|
||||
## Purpose
|
||||
|
||||
This note is the current status overlay after the Open-World Semantic Control Gate work moved from direct manual Codex operation into a repo-native agentic development loop.
|
||||
|
||||
It exists to keep the project spine clear:
|
||||
|
||||
- the target product is still a bounded MCP-first 1C analyst assistant;
|
||||
- Post-F, planner autonomy, inventory breadth, and business overview remain regression gates;
|
||||
- the active operational work is now the development loop that generates, reviews, replays, audits, repairs, reruns, and only then saves AGENT autoruns;
|
||||
- autorun/runtime hygiene is part of that loop, because broken saved-session text can invalidate the human GUI review even when the backend route is correct.
|
||||
|
||||
## What Changed Since Document 23
|
||||
|
||||
The previous execution-spine document stopped at the EHMO-derived Semantic Control Gate.
|
||||
|
||||
Since then, the project added a repo-native stage-loop layer:
|
||||
|
||||
- `scripts/stage_agent_loop.py` is the stage-level wrapper for pack-loop execution, review, status, safe continuation, and optional save-to-autoruns after acceptance.
|
||||
- `docs/orchestration/stage_agent_loop_agentic_semantic_development_loop.json` is the active stage manifest.
|
||||
- `docs/orchestration/agentic_semantic_development_loop_stage_pack.json` is the dogfood pack for business overview, VAT, stale scope, counterparty pivots, legacy route canaries, and answer-shape quality.
|
||||
- `docs/orchestration/schemas/stage_agent_loop_manifest.schema.json` defines the manifest contract.
|
||||
- `scripts/save_agent_semantic_run.py` refuses to save AGENT autoruns before reviewed live replay/loop acceptance unless explicitly forced as a draft.
|
||||
- `docs/orchestration/agent_semantic_source_catalog.*` remains the reusable source catalog for mixed AGENT pack construction.
|
||||
|
||||
The accepted dogfood status is recorded at:
|
||||
|
||||
- `artifacts/domain_runs/stage_agent_loops/agentic_semantic_development_loop/domain_loops/asl/final_status.md`
|
||||
|
||||
Current recorded result:
|
||||
|
||||
- status: `accepted`
|
||||
- loop id: `asl`
|
||||
- repair mode: `lead-handoff`
|
||||
- target score: `88`
|
||||
- iterations ran: `1`
|
||||
- stop reason: analyst accepted plus deterministic gate passed at `iteration_00`
|
||||
- manual GUI confirmation is still required after acceptance
|
||||
|
||||
## Design Decision
|
||||
|
||||
The project should not rely on a weak autonomous coder as the primary repair actor.
|
||||
|
||||
The chosen model is:
|
||||
|
||||
- Lead Codex remains the responsible repair brain.
|
||||
- A strong independent semantic/business audit layer reviews the replay from the user's business meaning first.
|
||||
- The stage-loop produces machine-readable artifacts and a lead-coder handoff instead of silently patching production code.
|
||||
- The loop can continue safely, but real code repair requires explicit execution mode and must be validated by rerun artifacts.
|
||||
- Human GUI confirmation remains the final high-signal reality check for accepted AGENT packs.
|
||||
|
||||
This preserves the user's desired automation pattern without delegating high-risk architecture repair to a low-context worker.
|
||||
|
||||
## Autorun Cyrillic Hygiene Cut
|
||||
|
||||
The GUI exposed an old saved-session failure where Cyrillic in autorun cards/questions displayed as replacement-character text.
|
||||
|
||||
Control examples after repair:
|
||||
|
||||
- `БОЛЬШОЙ ОБЩИЙ`
|
||||
- `АЛЬТЕРНАТИВА`
|
||||
|
||||
Root cause:
|
||||
|
||||
- old autorun history/runtime payloads contained double-decoded Cyrillic plus C1 controls such as `U+0098`;
|
||||
- the previous repair path encoded those controls as normal Windows-1251 text and lost the raw byte needed to reconstruct UTF-8;
|
||||
- the UI then displayed the remaining replacement character honestly.
|
||||
|
||||
Current fix:
|
||||
|
||||
- `addressTextRepair.ts` preserves C1 controls as raw bytes during UTF-8 reconstruction;
|
||||
- `autoRuns.ts` repairs autorun titles/questions before exposing cards or runtime materialization;
|
||||
- `eval.ts` and `assistantService.ts` now receive repaired scenario/runtime question text;
|
||||
- known already-lossy fragments with `U+FFFD` inside the organization name are repaired before they can poison organization clarification;
|
||||
- tests now cover the historical `БОЛЬШОЙ ОБЩИЙ` and `АЛЬТЕРНАТИВА` autorun cases.
|
||||
|
||||
Committed cut:
|
||||
|
||||
- `3be06b5 Починить восстановление кириллицы в автопрогонах`
|
||||
|
||||
Validation recorded during this cut:
|
||||
|
||||
- `npm.cmd test -- assistantOrganizationMatcher.test.ts addressTextRepair.test.ts autoRunsQuestionSplit.test.ts evalRuntimeQuestionSplit.test.ts` passed `20/20`;
|
||||
- `npm.cmd test -- assistantAddressFollowupContext.test.ts -t "continues the original inventory query after"` passed `2/2`;
|
||||
- `npm.cmd run build` passed;
|
||||
- graphify rebuilt to `6371` nodes, `14048` edges, `141` communities.
|
||||
|
||||
## Current Status
|
||||
|
||||
The current large module should be described as:
|
||||
|
||||
`Agentic Semantic Development Loop / Open-World Semantic Control operating system`
|
||||
|
||||
Status:
|
||||
|
||||
- implementation state: operational dogfood loop exists and has an accepted first loop artifact;
|
||||
- semantic status: accepted loop artifact is useful, but manual GUI confirmation remains required;
|
||||
- hygiene status: saved autorun/runtime Cyrillic repair is covered by code/tests and the GUI-side check is reported clean;
|
||||
- current autonomy status: `Open-World Route Candidate Promotion` is live-accepted at `5/5`; `Route-Candidate-Driven Enablement Loop` has accepted phase91/phase92 canaries; `accounting_profit_margin` is promoted into a reviewed 90/91/99 route by phase93; `debt_due_date_aging_quality` is promoted into a reviewed payment-term/open-balance route by phase94; `vendor_risk_procurement_quality` is promoted into reviewed procurement-concentration evidence by phase95; `inventory_reserve_liquidation_quality` is promoted into reviewed inventory quality-event evidence by phase96; all four reviewed proof-family routes are saved as accepted AGENT autoruns;
|
||||
- risk: medium, because the loop is now infrastructure for future acceptance decisions, not just a local route fix.
|
||||
|
||||
Recommended reporting line:
|
||||
|
||||
`Прогресс модуля: 99% (Agentic Semantic Development Loop, accepted dogfood loop + autorun hygiene; manual GUI confirmation still required)`
|
||||
|
||||
## What Is Not Closed
|
||||
|
||||
Do not treat the stage-loop as a replacement for business-answer review.
|
||||
|
||||
Still open:
|
||||
|
||||
- the first accepted dogfood loop proves the mechanism, not all future stage packs;
|
||||
- generated question quality still needs pressure from real GUI runs and user feedback;
|
||||
- broad arbitrary 1C autonomy is still bounded by reviewed routes, truth gates, and replay evidence;
|
||||
- route-candidate-driven enablement is now closed after the accepted phase93-phase96 proof-family routes, and should be treated as a regression gate rather than as an open promotion backlog;
|
||||
- manual GUI confirmation remains required before declaring a fat AGENT pack fully accepted.
|
||||
|
||||
## Next Work
|
||||
|
||||
Next operational pass:
|
||||
|
||||
1. Use phase91-phase96 as current candidate-driven enablement canaries.
|
||||
2. Continue dogfooding the stage-loop on real Open-World/agentic packs.
|
||||
3. Move the active autonomy work to broader schema/primitive discovery and keep the same live replay/save-after-acceptance discipline.
|
||||
4. Keep Post-F, phase83, inventory, business-overview, and mojibake autorun cases as regression canaries.
|
||||
|
||||
## Canonical Reading Order Update
|
||||
|
||||
For current planning, read:
|
||||
|
||||
1. `README.md`
|
||||
2. `21 - current_status_canon_2026-05-01.md`
|
||||
3. `31 - inventory_reserve_liquidation_quality_reviewed_route_2026-05-12.md`
|
||||
4. `30 - vendor_procurement_quality_reviewed_route_2026-05-12.md`
|
||||
5. `29 - debt_due_date_aging_reviewed_route_2026-05-10.md`
|
||||
6. `28 - accounting_profit_margin_reviewed_route_2026-05-10.md`
|
||||
7. `27 - proof_family_enablement_candidates_2026-05-10.md`
|
||||
8. `26 - route_candidate_driven_enablement_loop_2026-05-10.md`
|
||||
9. `25 - open_world_route_candidate_promotion_2026-05-10.md`
|
||||
10. this document
|
||||
11. `23 - current_execution_spine_and_semantic_control_gate_2026-05-05.md`
|
||||
12. `22 - open_world_bounded_autonomy_breadth_2026-05-01.md`
|
||||
13. `20 - planner_autonomy_consolidation_2026-05-01.md`
|
||||
14. `17 - post_f_semantic_integrity_hardening_2026-04-23.md`
|
||||
|
|
@ -1,132 +0,0 @@
|
|||
# 25 - Open-World Route Candidate Promotion (2026-05-10)
|
||||
|
||||
## Purpose
|
||||
|
||||
This note records the next autonomy slice after the agentic loop and autorun hygiene cut.
|
||||
|
||||
The target is not to let the assistant answer arbitrary 1C questions freely.
|
||||
|
||||
The target is to make the runtime leave a structured handoff whenever it almost understands an unfamiliar 1C ask:
|
||||
|
||||
`user ask -> data-need graph -> planner/catalog chain -> route candidate -> reviewed enablement or safe clarification`
|
||||
|
||||
That route candidate is the bridge between the current bounded MCP route fabric and the future autonomous route-growth loop.
|
||||
|
||||
## Why This Slice Exists
|
||||
|
||||
The project already has:
|
||||
|
||||
- data-need graph extraction;
|
||||
- reviewed catalog chain templates;
|
||||
- planner-vs-catalog alignment telemetry;
|
||||
- bounded pilot execution;
|
||||
- semantic replay and AGENT stage-loop artifacts.
|
||||
|
||||
The remaining gap is the materialization handoff for unfamiliar but meaningful asks.
|
||||
|
||||
Before this slice, a question could be recognized as "unsupported but understood" or could stop at clarification, but the repair loop still had to infer from scattered debug fields what route should be enabled next.
|
||||
|
||||
Now the runtime bridge exposes a single machine-readable `route_candidate` contract.
|
||||
|
||||
## Runtime Contract
|
||||
|
||||
`assistantMcpDiscoveryRuntimeBridge` now emits:
|
||||
|
||||
- `route_candidate.schema_version = assistant_mcp_route_candidate_v1`;
|
||||
- `candidate_status`:
|
||||
- `ready_for_reviewed_execution`;
|
||||
- `needs_user_scope`;
|
||||
- `needs_route_enablement`;
|
||||
- `blocked`;
|
||||
- selected reviewed chain id and selected chain summary;
|
||||
- nearest catalog chain template and catalog alignment status;
|
||||
- business fact family, action family, and proof expectation from the data-need graph;
|
||||
- required, provided, and missing axes;
|
||||
- `executable_now`;
|
||||
- enablement reason;
|
||||
- recommended next action;
|
||||
- forbidden overclaim flags.
|
||||
|
||||
This is intentionally internal orchestration state.
|
||||
|
||||
It must not leak into the user-facing answer. The answer adapter and response candidate layers still decide what the human sees.
|
||||
|
||||
## Safety Boundary
|
||||
|
||||
The route candidate is not proof.
|
||||
|
||||
Rules:
|
||||
|
||||
- `ready_for_reviewed_execution` means the selected reviewed bridge can run through the normal truth gate.
|
||||
- `needs_user_scope` means the assistant should ask for missing axes such as organization, period, item, or counterparty before execution.
|
||||
- `needs_route_enablement` means the system understood a likely family but still must add or wire a reviewed exact route before claiming the fact.
|
||||
- `blocked` means the route must not execute until the blocking reason is fixed.
|
||||
|
||||
This keeps the open-world direction bounded: the system may discover the next route candidate, but it may not promote unreviewed evidence into a confirmed business answer.
|
||||
|
||||
## First Implementation Cut
|
||||
|
||||
Implemented locally:
|
||||
|
||||
- `AssistantMcpRouteCandidateContract` in `assistantMcpDiscoveryRuntimeBridge.ts`;
|
||||
- route-candidate construction from planner, pilot, bridge status, data-need graph, and dry-run axes;
|
||||
- route-candidate reason codes in the runtime bridge;
|
||||
- focused runtime-bridge tests for clarification candidates, ready-for-execution candidates, metadata-scoped movement candidates, and internal-mechanics non-leakage;
|
||||
- debug attachment flattening for route-candidate status, fact/action family, proof expectation, missing/provided axes, executability, enablement reason, and next action;
|
||||
- domain-loop step-state, truth-review markdown, acceptance-matrix rows, source-catalog tags, and lead-coder handoff now preserve route-candidate fields.
|
||||
|
||||
Focused validation:
|
||||
|
||||
- `npm.cmd test -- --run tests/assistantMcpDiscoveryRuntimeBridge.test.ts` passed `17/17`.
|
||||
- `npm.cmd test -- --run tests/assistantMcpDiscoveryDebugAttachment.test.ts tests/assistantDebugPayloadAssembler.test.ts tests/assistantMcpDiscoveryRuntimeBridge.test.ts` passed `23/23`.
|
||||
- `python -m unittest scripts.test_domain_case_loop_step_state scripts.test_agent_semantic_pack_builder scripts.test_scenario_acceptance_policy` passed `30/30`.
|
||||
- `npm.cmd run build` passed.
|
||||
- graphify rebuilt after the artifact-propagation cut: `6379 nodes`, `14066 edges`, `143 communities`.
|
||||
|
||||
## Live Semantic Replay Closure
|
||||
|
||||
The declared route-candidate promotion slice is now accepted by live semantic replay:
|
||||
|
||||
- spec: `docs/orchestration/address_truth_harness_phase90_route_candidate_handoff_open_world.json`;
|
||||
- accepted run: `artifacts/domain_runs/address_truth_harness_phase90_route_candidate_handoff_open_world_live5_20260510`;
|
||||
- final status: `accepted`, `5/5` passed, `0` warnings, `0` failures;
|
||||
- accepted AGENT autorun: `AGENT | Phase 90 open-world route candidate handoff replay` (`gen-ag05100927-aec0ad`).
|
||||
|
||||
The replay proves:
|
||||
|
||||
- open counterparty money-flow ranking asks stop for organization scope instead of guessing;
|
||||
- a plain organization reply promotes the pending candidate into reviewed execution in the same session;
|
||||
- a counterparty lifecycle pivot to `Группа СВК` does not inherit the previous organization scope as stale counterparty context;
|
||||
- explicit VAT movement wording selects the metadata-scoped `movement_evidence` catalog chain while the exact VAT answer remains organization-bound and source-honest;
|
||||
- inventory reserve/liquidation wording remains bounded and does not claim a confirmed reserve without reviewed proof evidence.
|
||||
|
||||
Latest validation after the final VAT answer-shape cut:
|
||||
|
||||
- `npm.cmd test -- --run tests/assistantTransitionPolicy.test.ts tests/assistantRoutePolicy.test.ts tests/assistantMcpDiscoveryTurnInputAdapter.test.ts tests/assistantMcpDiscoveryAnswerAdapter.test.ts tests/assistantMcpDiscoveryResponseCandidate.test.ts tests/assistantMcpDiscoveryResponsePolicy.test.ts` passed `237/237` with `8` skipped;
|
||||
- `npm.cmd test -- --run tests/addressQueryRuntimeM23.test.ts -t "confirmed VAT tax-period reply"` passed `1/1`;
|
||||
- `npm.cmd run build` passed;
|
||||
- graphify rebuilt to `6383 nodes`, `14077 edges`, `143 communities`.
|
||||
|
||||
## Next Work
|
||||
|
||||
Next slices should use this contract instead of prose-only diagnosis:
|
||||
|
||||
1. Extend stage-loop analysis so accepted route-candidate fields become first-class repair targets, not just debug evidence.
|
||||
2. Let AGENT pack review group failures by candidate status, missing axes, selected chain, and catalog alignment.
|
||||
3. Only after a candidate is reviewed, wired, tested, and replay-accepted may it become a normal executable route.
|
||||
|
||||
The next active autonomy slice should move from "record a route candidate" to "use route candidates as repair-loop targets": the stage-loop should group replay failures by candidate status and produce a lead-coder handoff that names the next reviewed route/capability to enable, without letting unreviewed candidates become user-facing truth.
|
||||
|
||||
This continuation is recorded in [26 - route_candidate_driven_enablement_loop_2026-05-10.md](./26%20-%20route_candidate_driven_enablement_loop_2026-05-10.md).
|
||||
|
||||
## Status
|
||||
|
||||
This slice is the first accepted concrete step from "planner chooses reviewed routes" toward "the system can propose the next reviewed route to build."
|
||||
|
||||
Current module wording:
|
||||
|
||||
`Open-World Route Candidate Promotion, declared phase90 slice accepted`
|
||||
|
||||
Progress: `100%` for this declared route-candidate promotion slice.
|
||||
|
||||
This does not mean arbitrary open-world 1C autonomy is complete. It means the route-candidate handoff contract, artifact propagation, live semantic replay, and accepted autorun persistence are closed enough to become the next module's input contract.
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
# 26 - Route-Candidate-Driven Enablement Loop (2026-05-10)
|
||||
|
||||
## Purpose
|
||||
|
||||
This note records the next autonomy slice after route-candidate promotion.
|
||||
|
||||
Slice 25 made the runtime expose a structured `route_candidate`.
|
||||
|
||||
Slice 26 starts using that contract as a development-loop input:
|
||||
|
||||
`semantic replay -> route candidate fields -> repair target grouping -> Lead Codex handoff -> reviewed route enablement`
|
||||
|
||||
The goal is not to let unreviewed candidates become business truth.
|
||||
|
||||
The goal is to make the agentic development loop see what route should be reviewed or enabled next without asking a human to manually infer it from scattered debug payloads.
|
||||
|
||||
## Implementation Cut 1
|
||||
|
||||
Implemented locally:
|
||||
|
||||
- deterministic repair-target generation now treats `mcp_discovery_route_candidate_status = needs_route_enablement` as a first-class `route_candidate_enablement_gap`;
|
||||
- route-candidate repair targets preserve selected chain, fact/action family, missing axes, executability, enablement reason, and next action;
|
||||
- repair-target summaries now include `route_candidate_handoff_count`, status counts, and grouped route-candidate handoff rows;
|
||||
- Lead Codex handoff JSON/Markdown now includes:
|
||||
- `route_candidate_groups`;
|
||||
- `route_candidate_enablement_targets`;
|
||||
- explicit instructions to distinguish missing user scope from a real reviewed-route enablement gap before patching.
|
||||
|
||||
Safety boundary:
|
||||
|
||||
- `needs_user_scope` remains a valid clarification/continuation signal, not an automatic bug;
|
||||
- `ready_for_reviewed_execution` remains a proof that the reviewed route can execute through the normal truth gate;
|
||||
- `needs_route_enablement` becomes a repair target because the system understood the business family but still lacks a reviewed executable route;
|
||||
- unreviewed route candidates still must not leak into the user-facing answer.
|
||||
|
||||
## Validation
|
||||
|
||||
Local tooling validation:
|
||||
|
||||
- `python -m unittest scripts.test_domain_case_loop_step_state scripts.test_scenario_acceptance_policy scripts.test_agent_semantic_pack_builder` passed `33/33`.
|
||||
|
||||
Live semantic replay:
|
||||
|
||||
- spec: `docs/orchestration/address_truth_harness_phase91_route_candidate_driven_enablement_loop.json`;
|
||||
- accepted run: `artifacts/domain_runs/address_truth_harness_phase91_route_candidate_driven_enablement_loop_live1_20260510`;
|
||||
- final status: `accepted`, `5/5` passed, `0` warnings, `0` failures.
|
||||
- next accepted proof-family replay: `artifacts/domain_runs/address_truth_harness_phase92_proof_family_enablement_candidates_live5_20260510`, `6/6` passed, `0` warnings, `0` failures.
|
||||
- accepted user-runnable autorun for the proof-family replay: `AGENT | Phase 92 proof-family enablement candidates` (`gen-ag05101045-374169`).
|
||||
- first reviewed proof-family route replay: `artifacts/domain_runs/address_truth_harness_phase93_accounting_profit_margin_reviewed_route_live3_20260510`, `6/6` passed, `0` warnings, `0` failures.
|
||||
- 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`).
|
||||
- final proof-family route replay: `artifacts/domain_runs/phase96_inventory_reserve_liquidation_quality_rerun`, `2/2` passed, `0` warnings, `0` failures.
|
||||
- accepted user-runnable autorun for that route replay: `AGENT | Phase 96 inventory reserve/liquidation quality-events` (`gen-ag05122057-c9786e`).
|
||||
|
||||
The replay proves the user-facing route-candidate canary remains healthy while the development tooling starts treating route candidates as repair-loop input:
|
||||
|
||||
- open value-flow ranking asks for organization scope instead of guessing;
|
||||
- natural company clarification promotes the candidate to reviewed execution;
|
||||
- counterparty lifecycle pivot resets stale company scope;
|
||||
- VAT movement wording keeps metadata scope and company scope together;
|
||||
- reserve/liquidation proof wording remains bounded and does not overclaim.
|
||||
- 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.
|
||||
- exact reserve/write-off/liquidation wording now has a live-accepted reviewed inventory quality-events route that can move to `ready_for_reviewed_execution` when posted write-off, receipt-adjustment, stocktaking, or revaluation documents are reachable. It still does not prove market liquidation value, management reserve policy, or full warehouse health.
|
||||
|
||||
## Status
|
||||
|
||||
Current module wording:
|
||||
|
||||
`Route-Candidate-Driven Enablement Loop, active slice: final reviewed proof-family route accepted`
|
||||
|
||||
Progress: `100%`.
|
||||
|
||||
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 proved `vendor_risk_procurement_quality` as reviewed procurement-concentration evidence with accepted phase95 replay. The final cut proved `inventory_reserve_liquidation_quality` as reviewed inventory quality-event evidence with accepted phase96 replay. The declared route-candidate-driven enablement loop is now closed; future arbitrary 1C breadth work should treat these routes as regression gates, not as open blockers.
|
||||
|
||||
## Next Work
|
||||
|
||||
Next slices:
|
||||
|
||||
1. Treat this module as closed and keep phase91/92/93/94/95/96 as regression canaries.
|
||||
2. Start the next broader open-world autonomy slice: schema/primitive discovery beyond the selected phase92 proof families.
|
||||
3. Keep save-after-acceptance discipline for any new AGENT packs.
|
||||
|
||||
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)
|
||||
- [31 - inventory_reserve_liquidation_quality_reviewed_route_2026-05-12.md](./31%20-%20inventory_reserve_liquidation_quality_reviewed_route_2026-05-12.md)
|
||||
|
|
@ -1,123 +0,0 @@
|
|||
# 27 - Proof-Family Enablement Candidates (2026-05-10)
|
||||
|
||||
## Purpose
|
||||
|
||||
This note records the next `Route-Candidate-Driven Enablement Loop` cut after the repair-target handoff grouping in Slice 26.
|
||||
|
||||
The slice turns business-overview proof gaps into explicit route-candidate handoffs:
|
||||
|
||||
`valid exact business ask -> bounded proxy answer -> missing_proof_families -> route_candidate.needs_route_enablement -> accepted AGENT replay`
|
||||
|
||||
The goal is not to pretend the proxy is the final business truth.
|
||||
|
||||
The goal is to make the assistant and the development loop agree that the user asked a valid exact question, the current reviewed contour can only answer with a bounded proxy, and the missing reviewed proof route is now a concrete enablement target.
|
||||
|
||||
## Implementation Cut
|
||||
|
||||
Implemented locally:
|
||||
|
||||
- `assistantMcpDiscoveryRuntimeBridge` now promotes business-overview exact proof gaps to `route_candidate.candidate_status = needs_route_enablement` when the asked action maps to a missing reviewed proof family;
|
||||
- route candidates now preserve the missing proof-family reason, next required evidence, current proxy evidence, and the forbidden overclaim flag;
|
||||
- `assistantMcpDiscoveryTurnInputAdapter` now classifies organization-level exact proof asks into boundary action families:
|
||||
- `profit_margin_boundary`;
|
||||
- `debt_due_date_boundary`;
|
||||
- `inventory_reserve_boundary`;
|
||||
- `vendor_risk_procurement_boundary`;
|
||||
- legal-entity wording such as `по ООО ... чистую прибыль и маржу` is treated as organization-level P&L boundary wording, not as a generic money overview;
|
||||
- compact user-facing business-overview answers now keep exact proof-boundary turns direct:
|
||||
- clean profit/margin stays a P&L route gap;
|
||||
- overdue/due-date debt aging stays a payment-term route gap;
|
||||
- reserve/liquidation inventory quality stays an inventory-quality route gap;
|
||||
- vendor dependency/risk stays a procurement-quality route gap;
|
||||
- the answer surface may expose bounded proxy evidence, but it must not claim exact P&L, contractual overdue debt, confirmed reserve/liquidation value, or vendor-risk/procurement-quality conclusions.
|
||||
|
||||
## Validation
|
||||
|
||||
Local validation:
|
||||
|
||||
- `npm.cmd test -- --run tests/assistantMcpDiscoveryResponseCandidate.test.ts tests/assistantMcpDiscoveryAnswerAdapter.test.ts tests/assistantMcpDiscoveryTurnInputAdapter.test.ts tests/assistantMcpDiscoveryRuntimeBridge.test.ts` passed `178/178` with `8` skipped;
|
||||
- `npm.cmd run build` passed.
|
||||
|
||||
Live semantic replay:
|
||||
|
||||
- spec: `docs/orchestration/address_truth_harness_phase92_proof_family_enablement_candidates.json`;
|
||||
- accepted run: `artifacts/domain_runs/address_truth_harness_phase92_proof_family_enablement_candidates_live5_20260510`;
|
||||
- final status: `accepted`, `6/6` passed, `0` warnings, `0` failures.
|
||||
|
||||
Accepted autorun:
|
||||
|
||||
- title: `AGENT | Phase 92 proof-family enablement candidates`;
|
||||
- generation id: `gen-ag05101045-374169`;
|
||||
- saved session: `llm_normalizer/data/autorun_generators/saved_sessions/assistant_saved_session_20260510104553_gen-ag05101045-374169.json`;
|
||||
- eval case: `llm_normalizer/data/eval_cases/assistant_autogen_saved_user_sessions_20260510104553_gen-ag05101045-374169.json`.
|
||||
|
||||
The accepted replay proves:
|
||||
|
||||
- open ranked value-flow still asks for organization scope rather than guessing;
|
||||
- organization clarification still promotes the reviewed value-flow route to executable;
|
||||
- exact P&L/profit/margin wording remains user-safe and becomes `needs_route_enablement` for `accounting_profit_margin`;
|
||||
- exact overdue/due-date debt wording remains user-safe and becomes `needs_route_enablement` for `debt_due_date_aging_quality`;
|
||||
- exact vendor dependency/risk wording remains user-safe and becomes `needs_route_enablement` for `vendor_risk_procurement_quality`;
|
||||
- exact reserve/liquidation inventory wording remains user-safe and becomes `needs_route_enablement` for `inventory_reserve_liquidation_quality`.
|
||||
|
||||
## Phase93 Follow-Up
|
||||
|
||||
The first candidate from this slice has now been promoted:
|
||||
|
||||
- proof family: `accounting_profit_margin`;
|
||||
- reviewed route: `address_accounting_financial_result_for_organization_v1`;
|
||||
- evidence basis: confirmed 90/91/99 accounting rows with derived accounting result and margin basis;
|
||||
- accepted run: `artifacts/domain_runs/address_truth_harness_phase93_accounting_profit_margin_reviewed_route_live3_20260510`, `6/6` passed;
|
||||
- accepted autorun: `AGENT | Phase 93 accounting profit-margin reviewed route` (`gen-ag05101213-596d99`).
|
||||
|
||||
This means `accounting_profit_margin` is no longer only a proof-family candidate. It is now the first reviewed executable route produced by the candidate-driven enablement loop.
|
||||
|
||||
## Phase94 Follow-Up
|
||||
|
||||
The second candidate from this slice has now been promoted:
|
||||
|
||||
- proof family: `debt_due_date_aging_quality`;
|
||||
- reviewed route: `address_debt_due_date_aging_for_organization_v1`;
|
||||
- evidence basis: confirmed open-balance rows plus reachable contract payment-term fields;
|
||||
- accepted run: `artifacts/domain_runs/phase94_debt_due_date_aging_reviewed_route_live4`, `7/7` passed;
|
||||
- accepted autorun: `AGENT | Phase 94 debt due-date aging reviewed route` (`gen-ag05101319-c04f79`).
|
||||
|
||||
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, final reviewed proof-family route accepted; use as regression gate`
|
||||
|
||||
Progress: `100%`.
|
||||
|
||||
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 promoted vendor/procurement quality through reviewed procurement-concentration evidence. Phase96 promoted inventory reserve/liquidation quality through reviewed inventory quality-event documents. The declared route-candidate-driven enablement loop is now closed and should be used as a regression gate for broader autonomy work.
|
||||
|
||||
## Next Work
|
||||
|
||||
Next slices:
|
||||
|
||||
1. Use phase91-phase96 as regression canaries.
|
||||
2. Start the next broader open-world schema/primitive discovery module.
|
||||
3. Keep saving AGENT autoruns 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)
|
||||
- [31 - inventory_reserve_liquidation_quality_reviewed_route_2026-05-12.md](./31%20-%20inventory_reserve_liquidation_quality_reviewed_route_2026-05-12.md)
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
# 28 - Accounting Profit-Margin Reviewed Route (2026-05-10)
|
||||
|
||||
## Purpose
|
||||
|
||||
This note records the first route implementation inside the `Route-Candidate-Driven Enablement Loop`.
|
||||
|
||||
Phase 92 proved that exact organization-level profit, margin, and financial-result questions were valid business asks, but the runtime could only answer with bounded proxies and `route_candidate.needs_route_enablement`.
|
||||
|
||||
Phase 93 takes one proof family through the intended loop:
|
||||
|
||||
`needs_route_enablement -> reviewed accounting recipe -> confirmed 90/91/99 rows -> derived accounting result -> ready_for_reviewed_execution -> accepted live replay -> saved autorun`
|
||||
|
||||
The goal is still not to pretend this is a full financial-reporting engine.
|
||||
|
||||
The goal is to prove that the agentic route-candidate loop can promote one concrete missing proof family into a reviewed executable route without weakening the proxy boundaries for other proof families.
|
||||
|
||||
## Implementation Cut
|
||||
|
||||
Implemented now:
|
||||
|
||||
- new intent `accounting_financial_result_for_organization`;
|
||||
- new query template `accounting_financial_result_profile`;
|
||||
- reviewed recipe `address_accounting_financial_result_for_organization_v1`;
|
||||
- organization reference filtering through `Справочник.Организации` rather than stale counterparty or free-text scope;
|
||||
- 90/91/99/84 accounting markers for revenue, cost, commercial/admin expenses, operational/non-operational result transfers, and retained profit/loss transfer;
|
||||
- business-overview pilot execution for the accounting financial-result recipe;
|
||||
- derived `accounting_financial_result` overview from confirmed recipe rows;
|
||||
- answer-adapter and compact-reply support for direct accounting-result wording;
|
||||
- short follow-up continuity for profit/loss questions after a business overview;
|
||||
- explicit VAT wording bridge for "начислен или уплачен" period questions so VAT continuity remains protected during the same replay.
|
||||
|
||||
Safety boundaries:
|
||||
|
||||
- accounting result is stated as an inference from confirmed 1C accounting movements, not as audited statutory reporting;
|
||||
- margin is computed only when the 90.01 revenue basis is present;
|
||||
- remaining proof families stay bounded: due-date debt aging, vendor/procurement quality, and inventory reserve/liquidation evidence are not promoted by this slice;
|
||||
- route-candidate internals remain diagnostic and must not leak into the user-facing answer.
|
||||
|
||||
## Validation
|
||||
|
||||
Local validation:
|
||||
|
||||
- `npm.cmd test -- --run tests/addressIntentResolverRegression.test.ts tests/assistantMcpDiscoveryTurnInputAdapter.test.ts tests/assistantMcpDiscoveryRuntimeBridge.test.ts tests/assistantMcpDiscoveryAnswerAdapter.test.ts tests/assistantMcpDiscoveryResponseCandidate.test.ts` passed `194/194` with `8` skipped;
|
||||
- `npm.cmd test -- --run tests/addressQueryRuntimeM23.test.ts -t "confirmed VAT tax-period"` passed the targeted VAT regression;
|
||||
- `npm.cmd run build` passed.
|
||||
|
||||
Live semantic replay:
|
||||
|
||||
- spec: `docs/orchestration/address_truth_harness_phase93_accounting_profit_margin_reviewed_route.json`;
|
||||
- accepted run: `artifacts/domain_runs/address_truth_harness_phase93_accounting_profit_margin_reviewed_route_live3_20260510`;
|
||||
- final status: `accepted`, `6/6` passed, `0` warnings, `0` failures;
|
||||
- key invariants: direct answer, temporal honesty, selected-object continuity, truth gate, human-answer quality, meta-context integrity, catalog alignment, and route-candidate handoff all passed.
|
||||
|
||||
Accepted autorun:
|
||||
|
||||
- title: `AGENT | Phase 93 accounting profit-margin reviewed route`;
|
||||
- generation id: `gen-ag05101213-596d99`;
|
||||
- saved session: `llm_normalizer/data/autorun_generators/saved_sessions/assistant_saved_session_20260510121326_gen-ag05101213-596d99.json`;
|
||||
- eval case: `llm_normalizer/data/eval_cases/assistant_autogen_saved_user_sessions_20260510121326_gen-ag05101213-596d99.json`.
|
||||
|
||||
The accepted replay proves:
|
||||
|
||||
- exact profit/margin wording for `ООО Альтернатива Плюс` in 2020 no longer stops at proxy-only boundary wording;
|
||||
- the assistant can answer from confirmed 90/91/99 evidence and report the 2020 accounting loss and margin basis directly;
|
||||
- short follow-up wording such as "а это прибыль или убыток, коротко?" stays on the accounting result instead of drifting back into a generic business overview;
|
||||
- canonical VAT follow-up wording still reaches the exact VAT route and returns confirmed VAT payable for 2020;
|
||||
- value-flow ranking and inventory reserve boundary canaries still remain safe in the same mixed pack.
|
||||
|
||||
## Phase94 Follow-Up
|
||||
|
||||
The next proof-family route has now also been promoted:
|
||||
|
||||
- proof family: `debt_due_date_aging_quality`;
|
||||
- reviewed route: `address_debt_due_date_aging_for_organization_v1`;
|
||||
- accepted run: `artifacts/domain_runs/phase94_debt_due_date_aging_reviewed_route_live4`, `7/7` passed;
|
||||
- accepted autorun: `AGENT | Phase 94 debt due-date aging reviewed route` (`gen-ag05101319-c04f79`).
|
||||
|
||||
This keeps the phase93 accounting route as a canary while proving the candidate-driven loop is repeatable on a second proof family.
|
||||
|
||||
## Status
|
||||
|
||||
Current module wording:
|
||||
|
||||
`Route-Candidate-Driven Enablement Loop, final reviewed proof-family route accepted; use as regression gate`
|
||||
|
||||
Progress: `100%`.
|
||||
|
||||
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, phase95 accepted vendor/procurement quality through reviewed procurement-concentration evidence, and phase96 accepted inventory reserve/liquidation quality through reviewed inventory quality-event documents. The declared loop is now closed and this route remains a regression canary.
|
||||
|
||||
## Next Work
|
||||
|
||||
Next slices:
|
||||
|
||||
1. Use this route as a regression canary during broader autonomy work.
|
||||
2. Continue the next module through open-world schema/primitive discovery rather than more phase92 proof-family closure.
|
||||
3. Keep proof-specific wording bounded unless a reviewed route has live evidence.
|
||||
|
|
@ -1,92 +0,0 @@
|
|||
# 29 - Debt Due-Date Aging Reviewed Route (2026-05-10)
|
||||
|
||||
## Purpose
|
||||
|
||||
This note records the second reviewed proof-family promotion inside the `Route-Candidate-Driven Enablement Loop`.
|
||||
|
||||
Phase 92 marked organization-level overdue debt, debt quality, due-date, and debt-aging questions as valid business asks, but only proxy-safe until a reviewed payment-term route existed.
|
||||
|
||||
Phase 94 moves that proof family through the same intended loop:
|
||||
|
||||
`needs_route_enablement -> reviewed debt due-date recipe -> payment-term/open-balance evidence -> checked negative boundary -> ready_for_reviewed_execution -> accepted live replay -> saved autorun`
|
||||
|
||||
The goal is not to invent contractual delinquency from open balances.
|
||||
|
||||
The goal is to answer exactly what can be proven: whether overdue debt is confirmed from reachable 1C payment-term evidence, and why it cannot be claimed when payment terms are absent.
|
||||
|
||||
## Implementation Cut
|
||||
|
||||
Implemented now:
|
||||
|
||||
- new intent `debt_due_date_aging_for_organization`;
|
||||
- new query template `debt_due_date_aging_profile`;
|
||||
- reviewed recipe `address_debt_due_date_aging_for_organization_v1`;
|
||||
- open-balance evidence over accounting accounts `60`, `62`, and `76`;
|
||||
- contract fields for settlement context, including contract date, payment-term flag, payment-term days, settlement document, organization, counterparty, and balance direction;
|
||||
- organization reference filtering hardened against noisy raw tails, so a phrase such as "ООО Альтернатива Плюс на конец 2020 можно точно понять..." does not become the organization filter;
|
||||
- business-overview pilot execution for the due-date aging recipe;
|
||||
- derived `debt_due_date_aging` overview with explicit statuses:
|
||||
- `confirmed_overdue`;
|
||||
- `no_payment_terms_configured`;
|
||||
- `insufficient_due_date_basis`;
|
||||
- `no_overdue_found`;
|
||||
- compact user-facing answer support for checked negative overdue debt;
|
||||
- short boundary follow-up continuity, so "то есть просрочку доказать нельзя, коротко почему?" stays on the reviewed debt due-date route instead of falling into generic chat.
|
||||
|
||||
Safety boundaries:
|
||||
|
||||
- open balance alone is not presented as overdue debt;
|
||||
- absent payment terms produce a checked negative/boundary answer, not a fabricated delinquency;
|
||||
- missing settlement-date evidence is called out as insufficient basis;
|
||||
- route-candidate internals remain diagnostic and must not leak into the user-facing answer;
|
||||
- the exact buyer/debtor-list routes remain separate from organization-level due-date quality questions.
|
||||
|
||||
## Validation
|
||||
|
||||
Local validation:
|
||||
|
||||
- `npm.cmd test -- --run tests/assistantTransitionPolicy.test.ts` passed `38/38`;
|
||||
- `npm.cmd test -- --run tests/assistantMcpDiscoveryTurnInputAdapter.test.ts` passed `103/103` with `7` skipped;
|
||||
- `npm.cmd test -- --run tests/assistantMcpDiscoveryPilotExecutor.test.ts tests/assistantMcpDiscoveryAnswerAdapter.test.ts tests/assistantMcpDiscoveryResponseCandidate.test.ts tests/assistantMcpDiscoveryRuntimeBridge.test.ts` passed `113/113` with `1` skipped;
|
||||
- `npm.cmd run build` passed.
|
||||
|
||||
Live semantic replay:
|
||||
|
||||
- spec: `docs/orchestration/address_truth_harness_phase94_debt_due_date_aging_reviewed_route.json`;
|
||||
- accepted run: `artifacts/domain_runs/phase94_debt_due_date_aging_reviewed_route_live4`;
|
||||
- final status: `accepted`, `7/7` passed, `0` warnings, `0` failures;
|
||||
- key invariants: direct answer, temporal honesty, selected-object continuity, truth gate, human-answer quality, meta-context integrity, catalog alignment, and route-candidate handoff all passed;
|
||||
- the replay spec was repaired to real UTF-8 Russian before saving, and codepoint checks confirmed normal Cyrillic in the spec and live artifacts.
|
||||
|
||||
Accepted autorun:
|
||||
|
||||
- title: `AGENT | Phase 94 debt due-date aging reviewed route`;
|
||||
- generation id: `gen-ag05101319-c04f79`;
|
||||
- saved session: `llm_normalizer/data/autorun_generators/saved_sessions/assistant_saved_session_20260510131922_gen-ag05101319-c04f79.json`;
|
||||
- eval case: `llm_normalizer/data/eval_cases/assistant_autogen_saved_user_sessions_20260510131922_gen-ag05101319-c04f79.json`;
|
||||
- validation status: `accepted_live_replay`.
|
||||
|
||||
The accepted replay proves:
|
||||
|
||||
- organization-level overdue debt wording for `ООО Альтернатива Плюс` in 2020 no longer stops at proxy-only boundary wording;
|
||||
- the assistant can run the reviewed due-date route and answer that confirmed overdue debt is absent because the found contracts do not have payment terms configured;
|
||||
- the short explanation follow-up keeps the same organization, date, evidence basis, and `debt_due_date_boundary` action family;
|
||||
- profit/margin, VAT, value-flow ranking, inventory reserve boundary, and vendor-risk boundary canaries remain safe in the same mixed pack.
|
||||
|
||||
## Status
|
||||
|
||||
Current module wording:
|
||||
|
||||
`Route-Candidate-Driven Enablement Loop, final reviewed proof-family route accepted; use as regression gate`
|
||||
|
||||
Progress: `100%`.
|
||||
|
||||
This is the second live-accepted proof that the loop can turn a route candidate into an executable reviewed route. Phase95 accepted `vendor_risk_procurement_quality` through reviewed procurement-concentration evidence, and phase96 accepted `inventory_reserve_liquidation_quality` through reviewed inventory quality-event documents. The declared loop is now closed and this route remains a regression canary.
|
||||
|
||||
## Next Work
|
||||
|
||||
Next slices:
|
||||
|
||||
1. Use phase94, phase95, and phase96 as canaries for due-date, vendor, and inventory-quality continuity.
|
||||
2. Move the active plan to broader open-world schema/primitive discovery.
|
||||
3. Keep proxy-only evidence bounded when a reviewed route is not available.
|
||||
|
|
@ -1,90 +0,0 @@
|
|||
# 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, superseded by phase96 final reviewed proof-family acceptance`
|
||||
|
||||
Progress: `100%` in the parent loop after phase96.
|
||||
|
||||
This phase remains the vendor/procurement-quality proof. After this note, phase96 promoted the final remaining inventory reserve/liquidation proof family, so the parent Route-Candidate-Driven Enablement Loop is now closed.
|
||||
|
||||
The loop has now promoted four 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.
|
||||
- `inventory_reserve_liquidation_quality` accepted live through inventory quality-event evidence by phase96.
|
||||
|
||||
## Next Work
|
||||
|
||||
Next slices:
|
||||
|
||||
1. Use phase95 as a vendor/procurement regression canary during broader autonomy work.
|
||||
2. See phase96 for the final inventory reserve/liquidation route and module closure.
|
||||
|
||||
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)
|
||||
- [31 - inventory_reserve_liquidation_quality_reviewed_route_2026-05-12.md](./31%20-%20inventory_reserve_liquidation_quality_reviewed_route_2026-05-12.md)
|
||||
|
|
@ -1,100 +0,0 @@
|
|||
# 31 - Inventory Reserve/Liquidation Quality Reviewed Route (2026-05-12)
|
||||
|
||||
This note records the final phase92 proof-family promotion inside the `Route-Candidate-Driven Enablement Loop`.
|
||||
|
||||
## Why This Slice Exists
|
||||
|
||||
Phase92 exposed `inventory_reserve_liquidation_quality` as a real route-candidate gap:
|
||||
|
||||
- users can naturally ask whether warehouse stock has reserves, write-offs, obsolete goods, or liquidation value;
|
||||
- earlier business overview could show stock position, sales-to-stock proxy, and staleness-risk proxy;
|
||||
- those proxies were useful but still not reviewed evidence for write-offs, stocktaking, revaluation, reserve policy, or market liquidation value.
|
||||
|
||||
The safe implementation target was therefore not a fake "full inventory health" route. It was the narrowest reviewed 1C evidence route that can answer what is actually reachable.
|
||||
|
||||
## Implementation Cut
|
||||
|
||||
Implemented locally:
|
||||
|
||||
- added `inventory_quality_events_for_organization` as a reviewed address recipe intent;
|
||||
- added `inventory_quality_events_profile` over posted 1C documents:
|
||||
- `Документ.СписаниеТоваров`;
|
||||
- `Документ.ОприходованиеТоваров`;
|
||||
- `Документ.ИнвентаризацияТоваровНаСкладе`;
|
||||
- `Документ.ПереоценкаТоваровВРознице`;
|
||||
- `business_overview` now runs this probe only for reserve/liquidation/write-off/obsolete-stock boundary wording;
|
||||
- `derived_business_overview.inventory_quality_events` records:
|
||||
- checked period;
|
||||
- matched event rows;
|
||||
- write-off count and amount;
|
||||
- receipt-adjustment count and amount;
|
||||
- stocktaking count;
|
||||
- revaluation count;
|
||||
- first/latest event date when events exist;
|
||||
- evidence status: `reviewed_no_quality_events_found`, `reviewed_inventory_control_events_only`, or `reviewed_writeoff_or_adjustment_events_found`;
|
||||
- `inventory_reserve_liquidation_quality` is removed from `missing_proof_families` only when this reviewed route executed;
|
||||
- `inventory_reserve_boundary` route candidates can now promote to `ready_for_reviewed_execution`;
|
||||
- user-facing direct answers include the organization scope, period, checked document families, and explicit no-overclaim boundary.
|
||||
|
||||
## Boundaries
|
||||
|
||||
Still not claimed:
|
||||
|
||||
- market liquidation value;
|
||||
- management reserve amount or reserve policy;
|
||||
- confirmed obsolete stock classification;
|
||||
- complete warehouse health;
|
||||
- FIFO liquidation or sell-through quality;
|
||||
- accounting correctness of the source documents.
|
||||
|
||||
The route can honestly say "checked documents found / not found". It cannot turn absence of events into a legal or market conclusion that no obsolete stock exists.
|
||||
|
||||
## Validation
|
||||
|
||||
Local validation:
|
||||
|
||||
- `npm.cmd test -- assistantMcpDiscoveryAnswerAdapter.test.ts assistantMcpDiscoveryRuntimeBridge.test.ts assistantMcpDiscoveryResponseCandidate.test.ts` passed `84/84` with `1` skipped;
|
||||
- `npm.cmd test -- assistantMcpDiscoveryPilotExecutor.test.ts` passed `34/34`;
|
||||
- `npm.cmd run build` passed;
|
||||
- direct live MCP query for `address_inventory_quality_events_for_organization_v1` against `ООО Альтернатива Плюс / 2020` returned `fetched_rows=0`, `matched_rows=0`, `error=null`, proving the new union query is syntactically valid and yields a checked negative in the current base.
|
||||
|
||||
Semantic/live replay:
|
||||
|
||||
- spec: `docs/orchestration/address_truth_harness_phase96_inventory_reserve_liquidation_quality.json`;
|
||||
- first live run: `artifacts/domain_runs/phase96_inventory_reserve_liquidation_quality_live`, partial/fail because the answer did not repeat the explicit organization in the direct line;
|
||||
- rerun after organization anchoring and compact-candidate cleanup: `artifacts/domain_runs/phase96_inventory_reserve_liquidation_quality_rerun`;
|
||||
- final status: `accepted`, `2/2` passed, `0` warnings, `0` failures;
|
||||
- accepted autorun: `AGENT | Phase 96 inventory reserve/liquidation quality-events` (`gen-ag05122057-c9786e`).
|
||||
|
||||
## Status
|
||||
|
||||
Current module wording:
|
||||
|
||||
`Route-Candidate-Driven Enablement Loop, active slice: final reviewed proof-family route accepted`
|
||||
|
||||
Progress: `100%`.
|
||||
|
||||
The loop has now promoted all phase92 proof-family candidates that were selected for this module:
|
||||
|
||||
- `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;
|
||||
- `inventory_reserve_liquidation_quality` accepted live through reviewed inventory quality-event documents by phase96.
|
||||
|
||||
This closes the declared Route-Candidate-Driven Enablement Loop slice. It does not close arbitrary 1C autonomy: the next work should move to the next broader module, using these proof-family routes as regression gates.
|
||||
|
||||
## Next Work
|
||||
|
||||
Next slices:
|
||||
|
||||
1. Reclassify Route-Candidate-Driven Enablement Loop as closed and use it as a regression gate.
|
||||
2. Start the next open-world autonomy slice: broader schema/primitive discovery beyond the phase92 proof families.
|
||||
3. Preserve phase93/94/95/96 accepted autoruns as canaries before expanding new unknown 1C domains.
|
||||
|
||||
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)
|
||||
- [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)
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
# 32 - Financial Counterparty Flow Hints (2026-05-13)
|
||||
|
||||
This note opens the next broader autonomy slice after the closed `Route-Candidate-Driven Enablement Loop`.
|
||||
|
||||
The problem came from real business review: bank-like counterparties such as `СБЕРБАНК, ПАО` can dominate incoming or outgoing money-flow rankings. If the assistant treats that row as an ordinary customer or supplier, the business answer becomes misleading even when the amount itself is correct.
|
||||
|
||||
## Scope
|
||||
|
||||
The first cut adds reviewed document-field hints to bank-document money-flow routes:
|
||||
|
||||
- `bank_operations_by_counterparty` now keeps outgoing/incoming bank-document columns aligned across `ОБЪЕДИНИТЬ ВСЕ` while exposing operation type, payment purpose where available, and comment;
|
||||
- `customer_revenue_and_payments` now includes incoming bank document `ВидОперации` and `Комментарий`;
|
||||
- `supplier_payouts_profile` now includes outgoing bank document `ВидОперации`, `НазначениеПлатежа`, and `Комментарий`;
|
||||
- ranked value-flow buckets now carry `financial_flow_hint`;
|
||||
- current hints are `loan_or_credit` (including deposits/credit-like bank instruments), `bank_fee_or_service`, `tax_or_budget`, `payroll_or_social`, `supplier_payment`, and `unclear`;
|
||||
- business-overview evidence can now say that a bank-like leader is a bank fee/service, credit/loan, tax/budget, payroll/social, or ordinary supplier-payment signal instead of using name-only caution.
|
||||
|
||||
## Validation
|
||||
|
||||
Local checks:
|
||||
|
||||
- `npm.cmd test -- assistantMcpDiscoveryPilotExecutor.test.ts assistantMcpDiscoveryAnswerAdapter.test.ts assistantMcpDiscoveryResponseCandidate.test.ts`: `97/97` passed, `1` skipped;
|
||||
- `npm.cmd run build`: passed.
|
||||
- phase97 hardening rerun:
|
||||
- `npm.cmd test -- addressIntentResolverBidirectionalValueFlow.test.ts addressQueryRuntimeM23.test.ts assistantMcpDiscoveryTurnInputAdapter.test.ts assistantMcpDiscoveryResponseCandidate.test.ts`: `554/554` passed, `7` skipped;
|
||||
- `npm.cmd run build`: passed;
|
||||
- graphify rebuild: `6483` nodes, `14382` edges, `143` communities.
|
||||
|
||||
Live MCP checks:
|
||||
|
||||
- `bank_operations_by_counterparty` for `СБЕРБАНК` returned `fetched=593`, `matched=10`, `error=null`, proving the aligned bank-doc union executes with the enriched column set;
|
||||
- `supplier_payouts_profile` for `СБЕРБАНК` returned `fetched=5`, `matched=5`, `error=null`;
|
||||
- the live rows include `ВидОперации=Прочее списание` and `НазначениеПлатежа=Комиссия банка`;
|
||||
- `customer_revenue_and_payments` for `СБЕРБАНК` returned `fetched=5`, `matched=5`, `error=null`;
|
||||
- the live rows include incoming `ВидОперации`, including `Прочее поступление` and `Возврат от поставщика`.
|
||||
- accepted semantic replay:
|
||||
- spec: `docs/orchestration/address_truth_harness_phase97_financial_counterparty_flow_hints.json`;
|
||||
- accepted run: `artifacts/domain_runs/phase97_financial_counterparty_flow_hints_live4`;
|
||||
- final status: `accepted`, `4/4` passed, `0` warnings, `0` failures;
|
||||
- accepted user-runnable autorun: `AGENT | Phase 97 financial counterparty flow hints replay` (`gen-ag05122250-4451a8`).
|
||||
|
||||
The accepted replay proves the slice at user-facing level:
|
||||
|
||||
- explicit `СБЕРБАНК` wording no longer gets swallowed by supplier/customer comparison tails;
|
||||
- bank-operation answers state operation direction, operation type, payment purpose, and contract evidence where 1C exposes it;
|
||||
- incoming-bank questions are not overclaimed as ordinary customer revenue, credit, or deposit without proof;
|
||||
- business-overview ranking/overview answers keep the company scope and explicitly explain why bank-like leaders are not ordinary clients/suppliers;
|
||||
- normal counterparty net-flow follow-up for `Группа СВК` remains a regression canary after bank-flow pivots.
|
||||
|
||||
## Status
|
||||
|
||||
Current module wording:
|
||||
|
||||
`Open-World Schema/Primitive Discovery, completed slice: financial counterparty flow hints`
|
||||
|
||||
Slice progress: `100%`.
|
||||
|
||||
Broader module progress: `25%`.
|
||||
|
||||
This is not a full financial-purpose classifier yet. It is the first reviewed schema/primitive discovery slice that lets downstream business answers avoid the worst bank-as-supplier/customer overclaim. The broader module remains open because the next work is to generalize this pattern beyond bank-document purpose fields into new unfamiliar 1C asks and route-candidate discovery surfaces.
|
||||
|
||||
## Next Work
|
||||
|
||||
1. Treat phase97 as a regression canary together with phase91-phase96.
|
||||
2. Pick the next unfamiliar 1C ask from live/manual replay evidence, not from a synthetic domain wish list.
|
||||
3. Use the same pattern: data-need graph -> route candidate/schema surface -> reviewed primitive or bounded answer -> live replay -> save accepted autorun only after acceptance.
|
||||
4. Extend financial-purpose hints only when real 1C rows expose additional stable fields or when a replay proves a business answer still overclaims role/purpose.
|
||||
|
||||
See also:
|
||||
|
||||
- [21 - current_status_canon_2026-05-01.md](./21%20-%20current_status_canon_2026-05-01.md)
|
||||
- [26 - route_candidate_driven_enablement_loop_2026-05-10.md](./26%20-%20route_candidate_driven_enablement_loop_2026-05-10.md)
|
||||
- [31 - inventory_reserve_liquidation_quality_reviewed_route_2026-05-12.md](./31%20-%20inventory_reserve_liquidation_quality_reviewed_route_2026-05-12.md)
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
# 33 - Limit Honesty And Business Language Gate (2026-05-13)
|
||||
|
||||
This note records the second accepted cut in the broader `Open-World Schema/Primitive Discovery` module.
|
||||
|
||||
The trigger came from the real GUI review `assistant-stage1-v2qsm_R0fF`: several answers were factually bounded, but still felt like technical artifacts because they leaked `MCP`, `route`, `proxy`, or irrelevant row-limit wording into user-facing business answers.
|
||||
|
||||
## Scope
|
||||
|
||||
This cut keeps the data and routes unchanged, but hardens the answer surface:
|
||||
|
||||
- compact business-overview replies now pass through a shared business-language sanitizer before they are returned;
|
||||
- `procurement-concentration route`, `vendor-risk route`, `due-date route`, and `proxy` phrases are converted into business-readable wording;
|
||||
- debt due-date boundary answers keep the organization in the direct line and explain missing payment terms without route jargon;
|
||||
- inventory reserve answers no longer pull unrelated incoming/outgoing money-flow limit lines into a warehouse reserve question;
|
||||
- supplier dependency answers distinguish "checked concentration of outgoing payments" from a complete supplier-risk audit without naming it as a route;
|
||||
- accepted replay keeps VAT and bank-like counterparty boundary canaries in the same scenario.
|
||||
|
||||
## Validation
|
||||
|
||||
Local checks:
|
||||
|
||||
- `npm.cmd test -- assistantMcpDiscoveryResponseCandidate.test.ts`: `26/26` passed;
|
||||
- `npm.cmd test -- assistantMcpDiscoveryResponseCandidate.test.ts assistantMcpDiscoveryAnswerAdapter.test.ts assistantMcpDiscoveryPilotExecutor.test.ts addressQueryRuntimeM23.test.ts`: `519/519` passed, `1` skipped;
|
||||
- `npm.cmd run build`: passed;
|
||||
- graphify rebuild: `6484` nodes, `14385` edges, `142` communities.
|
||||
|
||||
Live semantic replay:
|
||||
|
||||
- spec: `docs/orchestration/address_truth_harness_phase98_limit_honesty_business_language.json`;
|
||||
- accepted run: `artifacts/domain_runs/phase98_limit_honesty_business_language_live3`;
|
||||
- final status: `accepted`, `6/6` passed, `0` warnings, `0` failures;
|
||||
- accepted user-runnable autorun: `AGENT | Phase 98 limit honesty and business-language replay` (`gen-ag05122315-f1e27c`).
|
||||
|
||||
The accepted replay proves:
|
||||
|
||||
- overdue receivables answers state the organization, date, checked open-settlement amount, and missing payment-term basis in business language;
|
||||
- short follow-up questions remain short and do not repeat technical limit text;
|
||||
- VAT remains direct and debug-free;
|
||||
- top incoming money keeps the bank boundary and does not overclaim bank inflows as ordinary customer revenue;
|
||||
- inventory reserve answers say the reserve cannot be confirmed exactly and do not drag unrelated money-flow limit warnings into the warehouse answer;
|
||||
- supplier dependency answers state that exact supplier-risk is not fully confirmed, show the checked concentration leader, and list what remains unproven.
|
||||
|
||||
## Status
|
||||
|
||||
Current module wording:
|
||||
|
||||
`Open-World Schema/Primitive Discovery, completed slice: limit honesty and business-language gate`
|
||||
|
||||
Slice progress: `100%`.
|
||||
|
||||
Broader module progress: `38%`.
|
||||
|
||||
This is not a new 1C primitive by itself. It is a necessary semantic gate for the broader primitive-discovery module: as coverage grows, bounded answers must stay business-readable and must not expose internal route/proxy/debug mechanics.
|
||||
|
||||
## Next Work
|
||||
|
||||
1. Treat phase98 as a regression canary together with phase91-phase97.
|
||||
2. Pick the next unfamiliar 1C ask from live/manual replay evidence.
|
||||
3. Prefer the same loop: business meaning -> route candidate/schema surface -> minimal reviewed primitive or answer-shape fix -> live replay -> save autorun only after accepted replay.
|
||||
4. Keep watching for cases where a technically correct bounded answer still feels like a system artifact rather than a competent business analyst.
|
||||
|
||||
See also:
|
||||
|
||||
- [21 - current_status_canon_2026-05-01.md](./21%20-%20current_status_canon_2026-05-01.md)
|
||||
- [32 - financial_counterparty_flow_hints_2026-05-13.md](./32%20-%20financial_counterparty_flow_hints_2026-05-13.md)
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
# 34 - Large Query Budget And Continuation Policy (2026-05-13)
|
||||
|
||||
## Purpose
|
||||
|
||||
This slice hardens explicit-year large business questions inside the broader `Open-World Schema/Primitive Discovery` module.
|
||||
|
||||
The triggering user concern was not only answer wording. It was the runtime behavior where a broad yearly business question could hit the MCP row cap and then answer as if coverage was materially limited, even though the value-flow runtime already had a safe monthly recovery mechanism.
|
||||
|
||||
## Runtime Change
|
||||
|
||||
The planner now grants the existing chunked coverage budget to explicit-year `business_overview` routes, not only to direct value-flow routes.
|
||||
|
||||
Key boundary:
|
||||
|
||||
- this is not a global unlimited query mode;
|
||||
- this does not remove evidence gates;
|
||||
- this does not turn all-time broad analysis into a full accounting audit;
|
||||
- it enables bounded monthly recovery for explicit-year business overview money-flow evidence when the broad money probe reaches the row cap.
|
||||
|
||||
The pilot executor now also records successful business-overview monthly recovery in reason codes:
|
||||
|
||||
- `pilot_business_overview_incoming_monthly_period_chunking_recovered_coverage`
|
||||
- `pilot_business_overview_outgoing_monthly_period_chunking_recovered_coverage`
|
||||
|
||||
The answer layer was tightened so profit follow-ups answer direct-first:
|
||||
|
||||
- money-flow net is not clean profit;
|
||||
- if accounting close evidence exists, the answer may add the checked 90/91/99 result;
|
||||
- user-facing text avoids route jargon such as "бухгалтерский маршрут" and account-specific margin phrasing like `90.01`.
|
||||
|
||||
## Validation
|
||||
|
||||
Local validation:
|
||||
|
||||
- `npm.cmd test -- assistantMcpDiscoveryPlanner.test.ts assistantMcpDiscoveryPilotExecutor.test.ts assistantMcpDiscoveryAnswerAdapter.test.ts assistantMcpDiscoveryResponseCandidate.test.ts`
|
||||
- result: `141 passed, 1 skipped`
|
||||
- `npm.cmd run build`
|
||||
- result: passed
|
||||
|
||||
Live semantic replay:
|
||||
|
||||
- spec: `docs/orchestration/address_truth_harness_phase99_large_query_budget_continuation.json`
|
||||
- accepted run: `artifacts/domain_runs/phase99_large_query_budget_continuation_live2`
|
||||
- result: `4/4`, `final_status=accepted`
|
||||
|
||||
Saved user-runnable AGENT autorun:
|
||||
|
||||
- title: `AGENT | Phase 99 large-query budget and continuation policy replay`
|
||||
- generation id: `gen-ag05131009-f08174`
|
||||
- validation status: `accepted_live_replay`
|
||||
|
||||
## Acceptance Meaning
|
||||
|
||||
Accepted for this slice:
|
||||
|
||||
- explicit-year company business overview can recover yearly money-flow coverage through monthly probes when the broad money query hits the row cap;
|
||||
- yearly overview answers remain direct-first and preserve bank/counterparty role boundaries;
|
||||
- profit follow-up after money overview does not equate cash-flow net to clean profit;
|
||||
- if accounting-result evidence exists, it is framed as checked 1C period-close evidence, not external audit or legal reporting.
|
||||
|
||||
Still not accepted as universal:
|
||||
|
||||
- arbitrary all-time large 1C scans;
|
||||
- automatic long-running user confirmation UX;
|
||||
- complete accounting audit, statutory P&L, or legal reporting;
|
||||
- unlimited primitive exploration outside reviewed route candidates.
|
||||
|
||||
## Current Status
|
||||
|
||||
`Open-World Schema/Primitive Discovery` moves to `52%` after this slice.
|
||||
|
||||
Why not higher:
|
||||
|
||||
- phase97, phase98, and phase99 prove important runtime breadth/support behavior;
|
||||
- but broader dynamic schema/primitive discovery still needs more unfamiliar 1C asks, more primitive descriptors where live evidence demands them, and continuation UX for genuinely large all-time questions.
|
||||
|
|
@ -1,65 +0,0 @@
|
|||
# 35 - Large Query Continuation UX (2026-05-13)
|
||||
|
||||
## Purpose
|
||||
|
||||
This slice hardens the user-facing behavior after genuinely broad all-time business questions.
|
||||
|
||||
The previous phase99 slice made explicit-year `business_overview` safe by giving it the existing monthly recovery budget. Phase100 covers the adjacent UX seam: when an all-time or very wide question still reaches row caps, the assistant must not pretend that the checked slice is a complete accounting answer, and it must not leave the user at a dead end.
|
||||
|
||||
## Runtime Change
|
||||
|
||||
The compact business-overview answer now turns a row-limit disclosure into an actionable continuation policy:
|
||||
|
||||
- state that the current result is an expanded checked slice, not a guaranteed full accounting turnover;
|
||||
- tell the user to choose a concrete year or quarter for a safe follow-up;
|
||||
- avoid promising unlimited scans or unreviewed full exports;
|
||||
- keep the answer business-readable and free of MCP/route/probe jargon.
|
||||
|
||||
The bidirectional value-flow compact answer also now respects organization scope when no counterparty is selected. If the turn is scoped to `ООО Альтернатива Плюс` and the derived value-flow has no explicit counterparty, the answer says `по компании ООО Альтернатива Плюс`, not `по контрагенту запрошенному контрагенту`.
|
||||
|
||||
## Validation
|
||||
|
||||
Local validation:
|
||||
|
||||
- `npm.cmd test -- assistantMcpDiscoveryResponseCandidate.test.ts`
|
||||
- result: `28 passed`
|
||||
- `npm.cmd run build`
|
||||
- result: passed
|
||||
|
||||
Live semantic replay:
|
||||
|
||||
- spec: `docs/orchestration/address_truth_harness_phase100_large_query_continuation_ux.json`
|
||||
- first run: `artifacts/domain_runs/phase100_large_query_continuation_ux_live1`, partial because the initial spec over-forbade a correct negative profit-boundary phrase and exposed the company-vs-counterparty wording defect in step 2;
|
||||
- accepted run: `artifacts/domain_runs/phase100_large_query_continuation_ux_live2`
|
||||
- result: `3/3`, `final_status=accepted`
|
||||
|
||||
Saved user-runnable AGENT autorun:
|
||||
|
||||
- title: `AGENT | Phase 100 large-query continuation UX replay`
|
||||
- generation id: `gen-ag05131028-234e5e`
|
||||
- validation status: `accepted_live_replay`
|
||||
|
||||
## Acceptance Meaning
|
||||
|
||||
Accepted for this slice:
|
||||
|
||||
- all-time company business overview can answer from the checked slice while honestly warning about row caps;
|
||||
- the answer includes a safe continuation path through a concrete year or quarter instead of a technical dead end;
|
||||
- explicit-year follow-up after the all-time answer recovers the 2020 money-flow numbers and labels the scope as company scope;
|
||||
- profit follow-up still separates operating cash-flow net from clean profit/accounting financial result.
|
||||
|
||||
Still not accepted as universal:
|
||||
|
||||
- automatic long-running confirmation UX;
|
||||
- arbitrary all-time full accounting export;
|
||||
- unlimited primitive exploration outside reviewed route candidates;
|
||||
- legal or external-audit profit claims.
|
||||
|
||||
## Current Status
|
||||
|
||||
`Open-World Schema/Primitive Discovery` moves to `60%` after this slice.
|
||||
|
||||
Why not higher:
|
||||
|
||||
- phase97 through phase100 now cover financial counterparty hints, language/limit hygiene, explicit-year large-query recovery, and all-time continuation UX;
|
||||
- but broader dynamic schema/primitive discovery still needs more unfamiliar 1C asks, more primitive descriptors where live evidence proves real gaps, and more continuation behavior for non-money domains.
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
# 36 - Inventory Root Scope Without Warehouse Clarification (2026-05-13)
|
||||
|
||||
## Purpose
|
||||
|
||||
This slice hardens a real manual-GUI signal from `assistant-stage1-hyh1A1WR3j`.
|
||||
|
||||
The problematic seam was not a missing inventory route. The route already knew how to answer stock-on-hand snapshots. The semantic risk was narrower:
|
||||
|
||||
- user asks a broad root question such as `что там на складе по остаткам?`;
|
||||
- assistant correctly asks for company because several organizations exist;
|
||||
- user selects `АЛЬТЕРНАТИВА`;
|
||||
- assistant must resume the broad inventory snapshot for the selected company instead of inventing a new requirement for a concrete warehouse, item, category, or material.
|
||||
|
||||
For root inventory, `склад` in user language means the stock contour, not necessarily a warehouse filter. A warehouse filter is valid only when the user names or asks for a specific warehouse.
|
||||
|
||||
## Runtime Meaning
|
||||
|
||||
No new code patch was required in this slice.
|
||||
|
||||
The live replay proved the current runtime already behaves correctly after the latest scope/continuation work:
|
||||
|
||||
- ambiguous broad inventory root asks only for organization;
|
||||
- bare company selection resumes the pending inventory intent;
|
||||
- extracted filters contain organization and date, but not `warehouse`, `item`, or `category`;
|
||||
- historical capability follow-up stays human and does not ask for a warehouse;
|
||||
- month-only follow-ups keep the inventory contour and the selected organization.
|
||||
|
||||
This is still important because it converts a manual concern into a saved regression gate rather than leaving it as a vague GUI memory.
|
||||
|
||||
## Validation
|
||||
|
||||
Live semantic replay:
|
||||
|
||||
- spec: `docs/orchestration/address_truth_harness_phase101_inventory_root_scope_no_warehouse_clarification.json`
|
||||
- accepted run: `artifacts/domain_runs/phase101_inventory_root_scope_no_warehouse_clarification_live1`
|
||||
- result: `7/7`, `final_status=accepted`
|
||||
|
||||
Critical step evidence:
|
||||
|
||||
- `step_03_inventory_root_requires_company_only`: asks for organization only.
|
||||
- `step_04_company_choice_resumes_inventory_without_warehouse`: answers `На 13.05.2026 на складе подтверждено 11 позиций...`, with `organization=ООО Альтернатива Плюс` and no warehouse/item/category filters.
|
||||
- `step_06_inventory_june_2017_after_capability`: answers the June 2017 snapshot with organization/date carryover and no warehouse filter.
|
||||
- `step_07_inventory_march_2016_stays_root`: answers the March 2016 snapshot with organization/date carryover and no warehouse filter.
|
||||
|
||||
Saved user-runnable AGENT autorun:
|
||||
|
||||
- title: `AGENT | Phase 101 inventory root scope without warehouse clarification replay`
|
||||
- generation id: `gen-ag05131044-cbe2ff`
|
||||
- validation status: `accepted_live_replay`
|
||||
|
||||
## Acceptance Meaning
|
||||
|
||||
Accepted for this slice:
|
||||
|
||||
- broad inventory root wording may ask for company when organization scope is ambiguous;
|
||||
- after company choice, the assistant answers across available company stock evidence instead of asking for a warehouse;
|
||||
- historical inventory follow-ups keep company/date carryover;
|
||||
- root inventory does not promote natural Russian `на складе` into a literal warehouse anchor by itself.
|
||||
|
||||
Still not accepted as universal:
|
||||
|
||||
- arbitrary warehouse-specific analytics without a named warehouse;
|
||||
- inventory turnover/FIFO/liquidity beyond already reviewed proxy contours;
|
||||
- item-level provenance unless an item or selected object is present;
|
||||
- external audit-grade stock valuation outside confirmed 1C evidence.
|
||||
|
||||
## Current Status
|
||||
|
||||
`Open-World Schema/Primitive Discovery` moves to `68%` after this slice.
|
||||
|
||||
Why not higher:
|
||||
|
||||
- this was a control/semantic-proof slice rather than new primitive expansion;
|
||||
- the live replay covers the manual warehouse-clarification seam, but broader unfamiliar inventory asks can still reveal new primitive gaps;
|
||||
- the next slice should continue from real manual/live evidence, especially where a broad business noun could be misread as a required technical axis.
|
||||
|
|
@ -1,83 +0,0 @@
|
|||
# 37 - Debt Mirror Clean-Scope Polarity (2026-05-13)
|
||||
|
||||
## Purpose
|
||||
|
||||
This slice hardens a real debt-polarity question from manual replay `assistant-stage1-87gHJCwTI9`.
|
||||
|
||||
The dangerous symptom was semantic, not just routing noise:
|
||||
|
||||
- the same `Комитет государственных услуг` amount could appear in both "мы должны" and "нам должны";
|
||||
- mirrored financial-security rows on account `76.09` could be mistaken for clean payable or receivable debt;
|
||||
- a short follow-up such as `а мы кому?` after receivables could be expanded by LLM predecompose into an open-items route instead of the clean payables route;
|
||||
- a fresh-session bare company name such as `Альтернатива Плюс` could fall into generic living chat instead of becoming the selected organization scope.
|
||||
|
||||
The target behavior is direct and conservative: clean debt answers may disclose mirrored/offset evidence, but must not count it as real debt in both directions.
|
||||
|
||||
## Runtime Changes
|
||||
|
||||
Implemented:
|
||||
|
||||
- `assistantTransitionPolicy` now lets a detected debt-role swap override an LLM open-items expansion for short mirror follow-ups.
|
||||
- `assistantLivingChatRuntimeAdapter` now probes live data scope for short bare organization-name turns when no organization is active yet.
|
||||
- If the probe resolves a known organization, living chat emits the deterministic organization-selection reply and stores the selected/active organization before subsequent address turns.
|
||||
- The debug contract now records `living_chat_bare_scope_probe_attempted` and `living_chat_bare_scope_probe_matched_organization`.
|
||||
|
||||
This keeps the fix bounded:
|
||||
|
||||
- normal data/meta/capability/safety questions are not reinterpreted as organization selection;
|
||||
- unmatched bare text still falls back to the previous living-chat path;
|
||||
- clean payables/receivables still run through reviewed exact debt recipes.
|
||||
|
||||
## Validation
|
||||
|
||||
Live semantic replay:
|
||||
|
||||
- spec: `docs/orchestration/address_truth_harness_phase102_debt_mirror_clean_scope_polarity.json`
|
||||
- accepted run: `artifacts/domain_runs/phase102_debt_mirror_clean_scope_polarity_live3`
|
||||
- result: `6/6`, `final_status=accepted`
|
||||
|
||||
Critical step evidence:
|
||||
|
||||
- `step_01_choose_company_scope`: bare `Альтернатива Плюс` selects `ООО Альтернатива Плюс` through deterministic data-scope selection, not LLM chat.
|
||||
- `step_02_clean_payables_today`: clean payables keep `organization=ООО Альтернатива Плюс`.
|
||||
- `step_03_committee_payable_is_not_clean_debt`: payable debt to the Committee is confirmed as `0,00`.
|
||||
- `step_04_committee_receivable_is_not_clean_debt`: confirmed receivable debt from the Committee is not found.
|
||||
- `step_05_clean_receivables_today`: clean receivables keep the selected organization.
|
||||
- `step_06_payables_mirror_followup_keeps_clean_scope`: `а мы кому?` resolves to `payables_confirmed_as_of_date`, not open-items, and keeps the organization scope.
|
||||
|
||||
Targeted verification:
|
||||
|
||||
- `npm.cmd test -- assistantLivingChatRuntimeAdapter.test.ts assistantLivingChatAttemptInputBuilder.test.ts assistantLivingChatAttemptRuntimeInputBuilder.test.ts assistantTransitionPolicy.test.ts assistantAddressFollowupContext.test.ts`
|
||||
- `npm.cmd run build`
|
||||
|
||||
Saved user-runnable AGENT autorun:
|
||||
|
||||
- title: `AGENT | Phase 102 debt mirror clean-scope polarity replay`
|
||||
- generation id: `gen-ag05131121-8c41ab`
|
||||
- validation status: `accepted_live_replay`
|
||||
|
||||
## Acceptance Meaning
|
||||
|
||||
Accepted for this slice:
|
||||
|
||||
- fresh bare organization labels can bind organization scope from live known organizations;
|
||||
- clean debt root questions keep selected organization scope;
|
||||
- short receivables-to-payables mirror follow-ups stay in the confirmed clean-debt route;
|
||||
- mirrored financial-security evidence may be disclosed separately, but is not counted as clean debt in both directions.
|
||||
|
||||
Still not accepted as universal:
|
||||
|
||||
- arbitrary legal/commercial classification of every account `76.*` without reviewed semantics;
|
||||
- external audit-grade netting rules outside confirmed 1C evidence;
|
||||
- full contractual delinquency analysis beyond the reviewed due-date aging route;
|
||||
- organization/counterparty names that cannot be matched by the live data-scope probe.
|
||||
|
||||
## Current Status
|
||||
|
||||
`Open-World Schema/Primitive Discovery` moves to `72%` after this slice.
|
||||
|
||||
Why not higher:
|
||||
|
||||
- this slice fixes an important polarity/scope integrity seam, but does not expand the primitive catalog to arbitrary 1C facts;
|
||||
- account-role semantics beyond the reviewed debt recipes still need evidence-driven expansion;
|
||||
- the next slice should again be selected from real manual/live replay evidence where user business wording exposes an unsupported schema primitive or wrong semantic arbitration.
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
# 38 - Financial Role/Purpose Arbitration (2026-05-13)
|
||||
|
||||
## Purpose
|
||||
|
||||
This slice hardens a real schema/semantic seam around bank-like counterparties.
|
||||
|
||||
The dangerous symptom was not that `СБЕРБАНК` could be found. It was that the same evidence could be misread through an ordinary counterparty lens:
|
||||
|
||||
- incoming bank rows could be overclaimed as customer revenue;
|
||||
- outgoing bank rows could be overclaimed as supplier payments;
|
||||
- generic `value_flow` discovery could replace the more precise exact `bank_operations_*` route;
|
||||
- long bank operation lists could bury the business answer in raw rows.
|
||||
|
||||
The target behavior is conservative: bank-like counterparties are first treated as financial-operation evidence, and ordinary customer/supplier/revenue meaning must be proven from operation kind, payment purpose, contract, and checked rows rather than inferred from the name or movement direction alone.
|
||||
|
||||
## Runtime Changes
|
||||
|
||||
Implemented:
|
||||
|
||||
- `assistantMcpDiscoveryResponsePolicy` now keeps confirmed exact `bank_operations_by_counterparty` / `bank_operations_by_contract` replies over generic value-flow discovery candidates.
|
||||
- The guard is intentionally narrow: it requires an exact bank operation intent, matching bank recipe, address-lane source, and grounded/matched/exact runtime evidence.
|
||||
- Bank operation answer composition now includes a compact direction summary over incoming/outgoing/unknown rows.
|
||||
- Bank role boundary wording now says directly when a bank is not proven as an ordinary customer, supplier, or client revenue source.
|
||||
- Bank operation drilldown answers now show a bounded first-page sample instead of dumping every row into direct business answers.
|
||||
- Evidence selection prefers the direction implied by the user question when showing the short 1C basis line.
|
||||
|
||||
This keeps the fix bounded:
|
||||
|
||||
- ordinary customer/supplier value-flow routes are unchanged;
|
||||
- generic value-flow candidates can still replace stale or unsupported replies outside exact bank-operation evidence;
|
||||
- the assistant does not classify bank flows as credit, deposit, commission, or revenue without operation/purpose/contract evidence.
|
||||
|
||||
## Validation
|
||||
|
||||
Live semantic replay:
|
||||
|
||||
- spec: `docs/orchestration/address_truth_harness_phase103_financial_role_purpose_arbitration.json`
|
||||
- accepted run: `artifacts/domain_runs/phase103_financial_role_purpose_arbitration_live3`
|
||||
- result: `6/6`, `final_status=accepted`
|
||||
|
||||
Critical step evidence:
|
||||
|
||||
- `step_01_choose_company_scope`: fresh `Альтернатива Плюс` binds `ООО Альтернатива Плюс`.
|
||||
- `step_02_sberbank_role_purpose_summary`: exact bank route wins over generic value-flow and answers the role/purpose question.
|
||||
- `step_03_sberbank_incoming_customer_boundary`: incoming bank evidence is not overclaimed as customer revenue.
|
||||
- `step_04_sberbank_outgoing_supplier_boundary`: outgoing bank evidence is not overclaimed as ordinary supplier dependency.
|
||||
- `step_05_business_overview_excludes_bank_role_overclaim`: broad company overview keeps bank role boundaries in top-flow discussion.
|
||||
- `step_06_svk_value_flow_canary_after_bank_context`: normal non-bank counterparty value-flow canary remains healthy after the bank context.
|
||||
|
||||
Targeted verification:
|
||||
|
||||
- `npm.cmd test -- assistantMcpDiscoveryResponsePolicy.test.ts`
|
||||
- `npm.cmd test -- addressQueryRuntimeM23.test.ts assistantMcpDiscoveryResponsePolicy.test.ts`
|
||||
- `npm.cmd run build`
|
||||
|
||||
Saved user-runnable AGENT autorun:
|
||||
|
||||
- title: `AGENT | Phase 103 financial role and purpose arbitration replay`
|
||||
- generation id: `gen-ag05131200-0ed59a`
|
||||
- validation status: `accepted_live_replay`
|
||||
|
||||
## Acceptance Meaning
|
||||
|
||||
Accepted for this slice:
|
||||
|
||||
- exact bank-operation evidence is preferred over generic value-flow when the exact route is grounded;
|
||||
- bank-like counterparties are not classified as ordinary customers, suppliers, or revenue sources without purpose/operation/contract evidence;
|
||||
- direct bank boundary answers stay compact enough for business review;
|
||||
- the fix preserves normal counterparty value-flow behavior.
|
||||
|
||||
Still not accepted as universal:
|
||||
|
||||
- full bank-product accounting classification across every bank and account family;
|
||||
- external audit-grade loan/deposit/commission categorization without reviewed 1C document semantics;
|
||||
- arbitrary financial-instrument semantics outside the currently materialized operation/purpose/contract fields;
|
||||
- replacement of all value-flow logic with bank-specific heuristics.
|
||||
|
||||
## Current Status
|
||||
|
||||
`Open-World Schema/Primitive Discovery` moves to `78%` after this slice.
|
||||
|
||||
Why not higher:
|
||||
|
||||
- the slice closes a high-value role/purpose arbitration gap, but it is still one family of financial-operation semantics;
|
||||
- broader schema/primitive discovery still needs additional real evidence around unfamiliar 1C asks;
|
||||
- the next slice should come from another live/manual replay seam where the assistant can reach evidence but still lacks the right primitive, role, or answer shape.
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
# 39 - Generic Role-Tail Anchor Hygiene (2026-05-13)
|
||||
|
||||
## Purpose
|
||||
|
||||
This slice hardens a subtle semantic-integrity seam found after the financial role/purpose work.
|
||||
|
||||
The user may describe ordinary business roles inside a broad company question, for example `не обычный клиент или поставщик`. Those words are business semantics, not entity anchors. The dangerous failure mode was that the tail `или поставщик` could become a fake `counterparty` in exact filters, selected objects, and MCP discovery input.
|
||||
|
||||
The target behavior is narrow:
|
||||
|
||||
- role words such as `клиент`, `поставщик`, or `или поставщик` must not become counterparty anchors by themselves;
|
||||
- real explicit role-prefixed counterparties such as `по поставщику Группа СВК` must still keep the actual counterparty;
|
||||
- broad business overview can discuss customer/supplier/bank roles without poisoning later bank or value-flow drilldowns.
|
||||
|
||||
## Runtime Changes
|
||||
|
||||
Implemented:
|
||||
|
||||
- `addressFilterExtractor` now treats standalone generic role tails as low-quality counterparty anchors.
|
||||
- `address_runtime/decomposeStage` clears inherited low-quality role-tail anchors from follow-up state before recipe selection.
|
||||
- `assistantMcpDiscoveryTurnInputAdapter` rejects generic role tails from LLM predecompose before they become discovery entity candidates.
|
||||
- `addressIntentResolver` now recognizes `сколько мы ему заплатили` / `заплатили` supplier-payment wording as `supplier_payouts_profile`, preventing a supplier payment question from first falling into `counterparty_population_and_roles`.
|
||||
|
||||
The fix is bounded:
|
||||
|
||||
- `по поставщику Альфа` and `по поставщику Группа СВК` remain valid explicit counterparty anchors;
|
||||
- supplier count / role split questions such as `скока поставщиков` remain in counterparty population;
|
||||
- MCP discovery can still recover unsupported value-flow asks, but no longer receives a fake role-tail entity from the accepted replay seam.
|
||||
|
||||
## Validation
|
||||
|
||||
Live semantic replay:
|
||||
|
||||
- spec: `docs/orchestration/address_truth_harness_phase104_generic_role_tail_anchor_hygiene.json`
|
||||
- accepted run: `artifacts/domain_runs/phase104_generic_role_tail_anchor_hygiene_live2`
|
||||
- result: `4/4`, `final_status=accepted`
|
||||
|
||||
Critical step evidence:
|
||||
|
||||
- `step_01_choose_company_scope`: fresh `Альтернатива Плюс` binds `ООО Альтернатива Плюс`.
|
||||
- `step_02_business_overview_role_tail_not_counterparty`: broad company overview has no `counterparty` filter while still explaining bank/customer/supplier boundaries.
|
||||
- `step_03_sberbank_role_after_role_tail_overview`: SBERBANK bank-role drilldown remains grounded after the role-tail overview.
|
||||
- `step_04_real_supplier_prefix_still_keeps_counterparty`: real supplier-prefixed `Группа СВК` stays a counterparty and routes through `supplier_payouts_profile` instead of population.
|
||||
|
||||
Targeted verification:
|
||||
|
||||
- `npm.cmd test -- addressQueryRuntimeM23.test.ts assistantMcpDiscoveryTurnInputAdapter.test.ts`
|
||||
- `npm.cmd run build`
|
||||
|
||||
Saved user-runnable AGENT autorun:
|
||||
|
||||
- title: `AGENT | Phase 104 generic role-tail anchor hygiene replay`
|
||||
- generation id: `gen-ag05131226-630ddf`
|
||||
- validation status: `accepted_live_replay`
|
||||
|
||||
## Acceptance Meaning
|
||||
|
||||
Accepted for this slice:
|
||||
|
||||
- generic role-tail anchors are filtered across exact extraction, follow-up merge, and discovery predecompose input;
|
||||
- bank role/purpose context does not contaminate later normal counterparty value-flow questions;
|
||||
- explicit supplier-payment wording is routed as supplier payout, not as a population/role count question;
|
||||
- the accepted replay checks both user-facing answer quality and machine-level forbidden-filter hygiene.
|
||||
|
||||
Still not accepted as universal:
|
||||
|
||||
- every possible Russian business-role paraphrase outside the covered role-tail family;
|
||||
- full natural-language role taxonomy;
|
||||
- automatic proof that every role word in every sentence is entity-free;
|
||||
- broad arbitrary 1C schema discovery outside the currently accepted phase97-phase104 seams.
|
||||
|
||||
## Current Status
|
||||
|
||||
`Open-World Schema/Primitive Discovery` moves to `84%` after this slice.
|
||||
|
||||
Why not higher:
|
||||
|
||||
- the slice closes a real semantic poisoning seam, but it is still a targeted role-tail/intent-arbitration hardening cut;
|
||||
- the module still needs at least one broader mixed replay pass over unfamiliar asks before a `95%+` claim is honest;
|
||||
- future slices should continue coming from live/manual replay evidence where the assistant can reach evidence but still risks wrong primitive, stale state, wrong role, or noisy answer shape.
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
# 40 - Mixed Schema/Primitive Closure Replay (2026-05-13)
|
||||
|
||||
## Status
|
||||
|
||||
Accepted.
|
||||
|
||||
This slice closes the current `Open-World Schema/Primitive Discovery` pass to manual-review readiness. It is intentionally not a new proof-family expansion: it is a mixed semantic closure replay across the routes and integrity seams already hardened in phases 97-104.
|
||||
|
||||
Validated replay:
|
||||
|
||||
- spec: `docs/orchestration/address_truth_harness_phase105_mixed_schema_primitive_closure.json`
|
||||
- accepted run: `artifacts/domain_runs/phase105_mixed_schema_primitive_closure_live3`
|
||||
- result: `13/13`, `final_status=accepted`
|
||||
- saved autorun: `AGENT | Phase 105 mixed schema/primitive closure replay`
|
||||
- generation id: `gen-ag05131312-2d0445`
|
||||
|
||||
## What It Proves
|
||||
|
||||
- Broad inventory root wording such as `что там на складе по остаткам` answers from the resolved organization scope without inventing a warehouse, item, category, or material clarification.
|
||||
- Historical inventory follow-ups keep organization and temporal continuity.
|
||||
- Business-overview wording with generic role tails does not leak `или поставщик` or similar role text into counterparty scope.
|
||||
- Bank-like counterparties such as `СБЕРБАНК` are classified from bank-operation evidence and are not overclaimed as ordinary clients or suppliers.
|
||||
- Explicit supplier-payment wording for `Группа СВК` routes to supplier payout evidence.
|
||||
- Bidirectional counterparty value-flow keeps the full multi-token subject `Группа СВК` through raw-scope discovery and answers incoming, outgoing, and net money flow directly.
|
||||
- Broad debt-polarity questions switch cleanly between payables and receivables without inheriting stale counterparty scope.
|
||||
- VAT tax-period questions survive after inventory, role, value-flow, and debt pivots and keep the quarter tax-period basis explicit.
|
||||
- Earnings/cash-flow wording routes to business overview and does not equate operational cash net with clean accounting profit.
|
||||
|
||||
## Code-Level Corrections From The Replay
|
||||
|
||||
- `address_runtime/decomposeStage.ts` suppresses stale counterparty carryover for broad debt-polarity questions such as `кому мы должны` and `а нам кто должен`, while preserving explicit referential counterparty follow-ups such as `по нему`.
|
||||
- `addressFilterExtractor.ts` treats generic inventory wording like `по остаткам` as a low-quality warehouse pseudo-anchor.
|
||||
- `assistantMcpDiscoveryTurnInputAdapter.ts` preserves multi-token raw scoped counterparties in discovery input, so `по Группа СВК за 2020` does not degrade to `Группа`.
|
||||
|
||||
## Verification
|
||||
|
||||
- `npm.cmd test -- addressQueryRuntimeM23.test.ts assistantMcpDiscoveryTurnInputAdapter.test.ts --testTimeout=45000` passed: `537 passed`, `7 skipped`.
|
||||
- `npm.cmd run build` passed.
|
||||
- `phase105_mixed_schema_primitive_closure_live3` passed `13/13`.
|
||||
|
||||
## Remaining Risk
|
||||
|
||||
The module is now ready for a fat manual GUI check rather than more blind implementation. Remaining risk is not that phases 97-105 are absent; the risk is that a human mixed run may still expose a new unfamiliar 1C phrasing, another entity alias edge, or an answer-shape expectation outside the current replay.
|
||||
|
|
@ -41,34 +41,13 @@ This package answers the next question:
|
|||
21. [21 - current_status_canon_2026-05-01.md](./21%20-%20current_status_canon_2026-05-01.md)
|
||||
22. [22 - open_world_bounded_autonomy_breadth_2026-05-01.md](./22%20-%20open_world_bounded_autonomy_breadth_2026-05-01.md)
|
||||
23. [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)
|
||||
24. [24 - agentic_semantic_development_loop_and_autorun_hygiene_2026-05-10.md](./24%20-%20agentic_semantic_development_loop_and_autorun_hygiene_2026-05-10.md)
|
||||
25. [25 - open_world_route_candidate_promotion_2026-05-10.md](./25%20-%20open_world_route_candidate_promotion_2026-05-10.md)
|
||||
26. [26 - route_candidate_driven_enablement_loop_2026-05-10.md](./26%20-%20route_candidate_driven_enablement_loop_2026-05-10.md)
|
||||
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)
|
||||
31. [31 - inventory_reserve_liquidation_quality_reviewed_route_2026-05-12.md](./31%20-%20inventory_reserve_liquidation_quality_reviewed_route_2026-05-12.md)
|
||||
32. [32 - financial_counterparty_flow_hints_2026-05-13.md](./32%20-%20financial_counterparty_flow_hints_2026-05-13.md)
|
||||
33. [33 - limit_honesty_business_language_2026-05-13.md](./33%20-%20limit_honesty_business_language_2026-05-13.md)
|
||||
34. [34 - large_query_budget_continuation_2026-05-13.md](./34%20-%20large_query_budget_continuation_2026-05-13.md)
|
||||
35. [35 - large_query_continuation_ux_2026-05-13.md](./35%20-%20large_query_continuation_ux_2026-05-13.md)
|
||||
36. [36 - inventory_root_scope_no_warehouse_clarification_2026-05-13.md](./36%20-%20inventory_root_scope_no_warehouse_clarification_2026-05-13.md)
|
||||
37. [37 - debt_mirror_clean_scope_polarity_2026-05-13.md](./37%20-%20debt_mirror_clean_scope_polarity_2026-05-13.md)
|
||||
38. [38 - financial_role_purpose_arbitration_2026-05-13.md](./38%20-%20financial_role_purpose_arbitration_2026-05-13.md)
|
||||
39. [39 - generic_role_tail_anchor_hygiene_2026-05-13.md](./39%20-%20generic_role_tail_anchor_hygiene_2026-05-13.md)
|
||||
40. [40 - mixed_schema_primitive_closure_replay_2026-05-13.md](./40%20-%20mixed_schema_primitive_closure_replay_2026-05-13.md)
|
||||
|
||||
## Current Status Snapshot (2026-05-13)
|
||||
## Current Status Snapshot (2026-05-05)
|
||||
|
||||
This package is no longer planning-only.
|
||||
|
||||
Status canon for planning:
|
||||
|
||||
- The current operational overlay is now [24 - agentic_semantic_development_loop_and_autorun_hygiene_2026-05-10.md](./24%20-%20agentic_semantic_development_loop_and_autorun_hygiene_2026-05-10.md).
|
||||
- The active engineering surface is no longer only individual route hardening; it is the repo-native AGENT/stage-loop operating system that should generate/review/replay/audit/repair/rerun current-stage packs before saving accepted autoruns.
|
||||
- The first dogfood stage loop for `agentic_semantic_development_loop` is accepted in artifacts, but manual GUI confirmation remains required before treating a fat AGENT pack as fully human-accepted.
|
||||
- Autorun/runtime Cyrillic hygiene is now a current regression gate: old saved-session mojibake with C1 controls must be repaired before cards, questions, and runtime jobs reach the GUI or assistant lane.
|
||||
- Post-F Semantic Integrity Hardening is operationally closed at `99%` and should now be used as a regression gate, not as the active module denominator.
|
||||
- Planner Autonomy Consolidation is closed at `100%` for the declared phase83 planner-brain slice.
|
||||
- The active next module is now `Open-World Bounded Autonomy Breadth` over unfamiliar 1C asks, with Post-F and phase83 retained as semantic canaries.
|
||||
|
|
@ -99,60 +78,8 @@ Status canon for planning:
|
|||
- The `assistant-stage1-EHMOy3lNFt` manual GUI replay opened the next acceptance gate: `Open-World Semantic Control Gate`.
|
||||
- The `~99%` Open-World number now means implementation breadth through Slice 25, not accepted semantic closure under broad human dialogue pressure.
|
||||
- The active breadth slice is semantic control rather than new proof-family expansion: garbage-anchor protection, business-overview continuation, intent dominance, frame hygiene, counterparty/organization arbitration, and final-summary answer shape.
|
||||
- The current accepted dogfood infrastructure slice is `Agentic Semantic Development Loop`: stage manifest, stage pack, loop wrapper, status/continue safety, strong business-audit handoff, and save-after-acceptance gating are wired and validated by the `asl` accepted loop artifact.
|
||||
- The latest hygiene slice is `Autorun Cyrillic C1 Repair`: `addressTextRepair`, `autoRuns`, `eval`, and `assistantService` now preserve C1 bytes while repairing old saved-session Russian text, preventing replacement-character autorun cards or runtime turns from leaking into the user path after backend refresh.
|
||||
- The GUI-side autorun Cyrillic check is now reported clean by the user after the backend-side C1 repair cut.
|
||||
- The completed autonomy slice is now `Open-World Route Candidate Promotion`: runtime bridge emits a versioned `route_candidate` contract that records the selected reviewed chain, catalog alignment, fact/action family, required/provided/missing axes, execution readiness, and safe next action; debug/truth-harness/acceptance/lead-handoff artifacts preserve that handoff, and the phase90 live replay is accepted at `5/5`.
|
||||
- The active autonomy slice is `Route-Candidate-Driven Enablement Loop`: `needs_route_enablement` route candidates now become first-class repair targets and Lead Codex handoff groups, while `needs_user_scope` remains a valid clarification signal rather than a false bug.
|
||||
- The current live canary for this slice is accepted: `address_truth_harness_phase91_route_candidate_driven_enablement_loop_live1_20260510` passed `5/5`.
|
||||
- The current proof-family candidate slice is accepted: `address_truth_harness_phase92_proof_family_enablement_candidates_live5_20260510` passed `6/6`, proving exact profit/margin, debt due-date aging, inventory reserve/liquidation, and vendor-risk/procurement-quality asks remain user-safe while `route_candidate` marks the missing reviewed proof families as `needs_route_enablement`.
|
||||
- The accepted user-runnable autorun for that slice is `AGENT | Phase 92 proof-family enablement candidates` (`gen-ag05101045-374169`).
|
||||
- The first proof-family route is now implemented and accepted: `accounting_profit_margin` moved from `needs_route_enablement` to reviewed execution through the 90/91/99 accounting-result recipe; `address_truth_harness_phase93_accounting_profit_margin_reviewed_route_live3_20260510` passed `6/6`.
|
||||
- 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 fourth/final proof-family route is now implemented and accepted: `inventory_reserve_liquidation_quality` moves from missing proof-family gap into reviewed inventory quality-event evidence over posted write-off, receipt-adjustment, stocktaking, and revaluation documents; `phase96_inventory_reserve_liquidation_quality_rerun` passed `2/2`.
|
||||
- The accepted user-runnable autorun for that slice is `AGENT | Phase 96 inventory reserve/liquidation quality-events` (`gen-ag05122057-c9786e`).
|
||||
- The first broader schema/primitive discovery slice is now accepted: `financial counterparty flow hints` adds bank-document purpose/operation/comment hints, protects bank-like counterparties from ordinary supplier/customer overclaim, and keeps normal counterparty net-flow canaries healthy; `phase97_financial_counterparty_flow_hints_live4` passed `4/4`.
|
||||
- The accepted user-runnable autorun for that slice is `AGENT | Phase 97 financial counterparty flow hints replay` (`gen-ag05122250-4451a8`).
|
||||
- The second broader schema/primitive discovery support slice is now accepted: `limit honesty and business-language gate` sanitizes route/proxy/MCP-style answer wording, keeps row-limit disclosure relevant to the asked business contour, and preserves debt/VAT/bank/inventory/supplier canaries; `phase98_limit_honesty_business_language_live3` passed `6/6`.
|
||||
- The accepted user-runnable autorun for that slice is `AGENT | Phase 98 limit honesty and business-language replay` (`gen-ag05122315-f1e27c`).
|
||||
- The third broader schema/primitive discovery support slice is now accepted: `large-query budget and continuation policy` grants explicit-year `business_overview` the existing monthly recovery budget, avoids artificial row-limit refusal when yearly money-flow coverage can be chunked safely, and fixes the profit follow-up answer shape so cash-flow net is not equated with clean profit; `phase99_large_query_budget_continuation_live2` passed `4/4`.
|
||||
- The accepted user-runnable autorun for that slice is `AGENT | Phase 99 large-query budget and continuation policy replay` (`gen-ag05131009-f08174`).
|
||||
- The fourth broader schema/primitive discovery support slice is now accepted: `large-query continuation UX` turns all-time row-cap disclosure into a safe year/quarter continuation path, keeps broad answers honest about checked-slice coverage, and fixes organization-scoped bidirectional value-flow wording after continuation; `phase100_large_query_continuation_ux_live2` passed `3/3`.
|
||||
- The accepted user-runnable autorun for that slice is `AGENT | Phase 100 large-query continuation UX replay` (`gen-ag05131028-234e5e`).
|
||||
- The fifth broader schema/primitive discovery support slice is now accepted: `inventory root scope without warehouse clarification` proves a broad stock-on-hand root query resumes after company clarification as an all-warehouse company snapshot instead of asking for a warehouse, item, category, or material; `phase101_inventory_root_scope_no_warehouse_clarification_live1` passed `7/7`.
|
||||
- The accepted user-runnable autorun for that slice is `AGENT | Phase 101 inventory root scope without warehouse clarification replay` (`gen-ag05131044-cbe2ff`).
|
||||
- The sixth broader schema/primitive discovery support slice is now accepted: `debt mirror clean-scope polarity` proves a bare company-name turn in a fresh session can bind organization scope from live data-scope probe, confirmed payables/receivables keep the selected organization, short mirror follow-ups such as `а мы кому?` stay in the clean debt route instead of drifting into open-items, and mirrored 76.09 financial-security rows are disclosed as offset/mirror evidence rather than counted as debt in both directions; `phase102_debt_mirror_clean_scope_polarity_live3` passed `6/6`.
|
||||
- The accepted user-runnable autorun for that slice is `AGENT | Phase 102 debt mirror clean-scope polarity replay` (`gen-ag05131121-8c41ab`).
|
||||
- The seventh broader schema/primitive discovery support slice is now accepted: `financial role/purpose arbitration` keeps grounded exact `bank_operations_*` evidence over generic value-flow candidates, summarizes incoming/outgoing bank rows compactly, and prevents bank-like counterparties from being classified as ordinary customer revenue or supplier dependency without operation/purpose/contract evidence; `phase103_financial_role_purpose_arbitration_live3` passed `6/6`.
|
||||
- The accepted user-runnable autorun for that slice is `AGENT | Phase 103 financial role and purpose arbitration replay` (`gen-ag05131200-0ed59a`).
|
||||
- The eighth broader schema/primitive discovery support slice is now accepted: `generic role-tail anchor hygiene` prevents wording such as `или поставщик` from becoming a fake counterparty in exact filters, selected objects, or discovery predecompose input, while preserving real role-prefixed counterparties such as `по поставщику Группа СВК`; `phase104_generic_role_tail_anchor_hygiene_live2` passed `4/4`.
|
||||
- The accepted user-runnable autorun for that slice is `AGENT | Phase 104 generic role-tail anchor hygiene replay` (`gen-ag05131226-630ddf`).
|
||||
- The ninth broader schema/primitive discovery closure slice is now accepted: `mixed schema/primitive closure replay` validates inventory scope, historical inventory carryover, role-tail hygiene, bank role/purpose, supplier payout wording, bidirectional SVK value-flow, clean payables/receivables polarity, VAT tax-period continuity, and cash-flow-vs-profit answer shape together; `phase105_mixed_schema_primitive_closure_live3` passed `13/13`.
|
||||
- The accepted user-runnable autorun for that slice is `AGENT | Phase 105 mixed schema/primitive closure replay` (`gen-ag05131312-2d0445`).
|
||||
- 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).
|
||||
- The current stage-loop/hygiene overlay after the AGENT dogfood cut is [24 - agentic_semantic_development_loop_and_autorun_hygiene_2026-05-10.md](./24%20-%20agentic_semantic_development_loop_and_autorun_hygiene_2026-05-10.md).
|
||||
- The current route-candidate autonomy slice is [25 - open_world_route_candidate_promotion_2026-05-10.md](./25%20-%20open_world_route_candidate_promotion_2026-05-10.md).
|
||||
- The closed route-candidate enablement-loop slice is [26 - route_candidate_driven_enablement_loop_2026-05-10.md](./26%20-%20route_candidate_driven_enablement_loop_2026-05-10.md), now used as a regression gate.
|
||||
- The closed 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 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 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 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).
|
||||
- The fourth/final reviewed proof-family route slice is [31 - inventory_reserve_liquidation_quality_reviewed_route_2026-05-12.md](./31%20-%20inventory_reserve_liquidation_quality_reviewed_route_2026-05-12.md).
|
||||
- The first broader schema/primitive discovery slice is [32 - financial_counterparty_flow_hints_2026-05-13.md](./32%20-%20financial_counterparty_flow_hints_2026-05-13.md), now accepted live and saved as a user-runnable AGENT autorun.
|
||||
- The second broader schema/primitive discovery support slice is [33 - limit_honesty_business_language_2026-05-13.md](./33%20-%20limit_honesty_business_language_2026-05-13.md), now accepted live and saved as a user-runnable AGENT autorun.
|
||||
- The third broader schema/primitive discovery support slice is [34 - large_query_budget_continuation_2026-05-13.md](./34%20-%20large_query_budget_continuation_2026-05-13.md), now accepted live and saved as a user-runnable AGENT autorun.
|
||||
- The fourth broader schema/primitive discovery support slice is [35 - large_query_continuation_ux_2026-05-13.md](./35%20-%20large_query_continuation_ux_2026-05-13.md), now accepted live and saved as a user-runnable AGENT autorun.
|
||||
- The fifth broader schema/primitive discovery support slice is [36 - inventory_root_scope_no_warehouse_clarification_2026-05-13.md](./36%20-%20inventory_root_scope_no_warehouse_clarification_2026-05-13.md), now accepted live and saved as a user-runnable AGENT autorun.
|
||||
- The sixth broader schema/primitive discovery support slice is [37 - debt_mirror_clean_scope_polarity_2026-05-13.md](./37%20-%20debt_mirror_clean_scope_polarity_2026-05-13.md), now accepted live and saved as a user-runnable AGENT autorun.
|
||||
- The seventh broader schema/primitive discovery support slice is [38 - financial_role_purpose_arbitration_2026-05-13.md](./38%20-%20financial_role_purpose_arbitration_2026-05-13.md), now accepted live and saved as a user-runnable AGENT autorun.
|
||||
- The eighth broader schema/primitive discovery support slice is [39 - generic_role_tail_anchor_hygiene_2026-05-13.md](./39%20-%20generic_role_tail_anchor_hygiene_2026-05-13.md), now accepted live and saved as a user-runnable AGENT autorun.
|
||||
- The mixed schema/primitive closure replay is [40 - mixed_schema_primitive_closure_replay_2026-05-13.md](./40%20-%20mixed_schema_primitive_closure_replay_2026-05-13.md), now accepted live and saved as a user-runnable AGENT autorun.
|
||||
|
||||
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:
|
||||
|
||||
|
|
@ -215,16 +142,12 @@ Current honest status:
|
|||
- exit-from-danger-zone readiness: `~97%`
|
||||
- pre-multidomain readiness: `~90%`
|
||||
- bounded-autonomy foundation readiness: `~89%`
|
||||
- legacy open-world bounded-autonomy parent-readiness snapshot: `~87%` before the later route-candidate/schema-primitive closure; use the active module line below for the current `95%` schema/primitive discovery status
|
||||
- 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, vendor/procurement quality promoted into reviewed procurement-concentration evidence by phase95, inventory reserve/write-off/liquidation quality promoted into reviewed inventory quality-event evidence by phase96, and bank-like financial counterparty role/purpose hints accepted by phase97
|
||||
- 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, and missing proof families recorded as runtime evidence ledger; exact accounting profit/margin, true due-date debt aging/overdue, confirmed vendor-risk/procurement-quality analysis, and confirmed reserve/write-off/liquidation inventory evidence are still pending
|
||||
- active Open-World Bounded Autonomy Breadth 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: `100%`, 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, `vendor_risk_procurement_quality` promoted into reviewed procurement-concentration evidence by phase95 live replay, and `inventory_reserve_liquidation_quality` promoted into reviewed inventory quality-event evidence by phase96 live replay; the declared route-candidate-driven enablement loop is now closed and should be used as a regression gate for the next broader autonomy slice
|
||||
- Open-World Schema/Primitive Discovery progress: `95%`, with phases97-105 accepted live and saved as user-runnable AGENT autoruns; the latest closure replay `phase105_mixed_schema_primitive_closure_live3` passed `13/13` across inventory scope, historical inventory carryover, business overview role-tail hygiene, bank role/purpose, supplier payout wording, bidirectional SVK value-flow, clean payables/receivables polarity, VAT tax-period continuity, and cash-flow-vs-profit answer shape.
|
||||
- Current manual checkpoint for this module: run `AGENT | Phase 105 mixed schema/primitive closure replay` (`gen-ag05131312-2d0445`) from GUI autoruns before moving the module from `95%` to final closure.
|
||||
- graph snapshot after latest rebuild: see `graphify-out/GRAPH_REPORT.md`
|
||||
- current regression-gate breakpoint:
|
||||
- the validated hot paths are no longer structurally broken;
|
||||
|
|
@ -308,20 +231,6 @@ Latest live proof now includes:
|
|||
- guarded phase83 acceptance: `phase83_planner_brain_alignment_live_20260501_readygate_rerun3` accepted `20/20`, with `0` warnings, `0` failures, `catalog_alignment_ok=true`, `direct_answer_ok=true`, `temporal_honesty_ok=true`, `selected_object_continuity_ok=true`, `truth_gate_ok=true`, `human_answer_quality_ok=true`, and `meta_context_integrity_ok=true`;
|
||||
- surface-grounded catalog promotion accepted locally: targeted planner/response-policy/pilot/continuity slice passed `109/109`, build passed, and graphify rebuilt to `5973 nodes`, `12971 edges`, `138 communities`.
|
||||
- accepted phase83 is saved as an autorun canary: `AGENT | Planner Autonomy phase83: мозг маршрутов, pivots и legacy continuity` (`gen-ag05011759-6f85fc`).
|
||||
- route-candidate promotion accepted live: `address_truth_harness_phase90_route_candidate_handoff_open_world_live5_20260510` accepted `5/5`, proving open-scope value-flow clarification, organization-scope continuation, counterparty lifecycle stale-scope reset, metadata-scoped VAT movement arbitration, and bounded reserve/no-overclaim behavior; the accepted autorun is `AGENT | Phase 90 open-world route candidate handoff replay` (`gen-ag05100927-aec0ad`).
|
||||
- route-candidate-driven enablement loop cut 1 accepted locally/live: Python tooling tests passed `33/33`; `address_truth_harness_phase91_route_candidate_driven_enablement_loop_live1_20260510` accepted `5/5`, proving the user-facing canary remains clean while `needs_route_enablement` can now become a Lead Codex repair target in deterministic loop artifacts.
|
||||
- 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`).
|
||||
- inventory reserve/liquidation quality reviewed route accepted locally/live: answer/runtime/candidate tests passed `84/84` with `1` skipped, pilot-executor tests passed `34/34`, build passed; direct MCP query for `address_inventory_quality_events_for_organization_v1` returned `fetched_rows=0`, `matched_rows=0`, `error=null`; `phase96_inventory_reserve_liquidation_quality_rerun` accepted `2/2`; `inventory_reserve_liquidation_quality` now derives reviewed evidence from posted write-off, receipt-adjustment, stocktaking, and revaluation documents, removes the proof family from `missing_proof_families` when this reviewed route executes, anchors the organization in the direct answer, and can promote `inventory_reserve_boundary` route candidates to `ready_for_reviewed_execution`; the accepted autorun is `AGENT | Phase 96 inventory reserve/liquidation quality-events` (`gen-ag05122057-c9786e`).
|
||||
- financial counterparty flow hints accepted locally/live: targeted bank-flow/intent/turn-input/answer tests passed `554/554` with `7` skipped, build passed, graphify rebuilt to `6483` nodes, `14382` edges, `143` communities; `phase97_financial_counterparty_flow_hints_live4` accepted `4/4`, proving explicit `СБЕРБАНК` wording, bank-operation purpose/direction disclosure, incoming-bank no-overclaim, business-overview bank boundaries, and `Группа СВК` net-flow canary continuity; the accepted autorun is `AGENT | Phase 97 financial counterparty flow hints replay` (`gen-ag05122250-4451a8`).
|
||||
- limit honesty and business-language gate accepted locally/live: response-candidate/answer-adapter/pilot-executor/M23 tests passed `519/519` with `1` skipped, build passed, graphify rebuilt to `6484` nodes, `14385` edges, `142` communities; `phase98_limit_honesty_business_language_live3` accepted `6/6`, proving debt due-date boundary, short follow-up directness, VAT debug hygiene, top incoming bank boundary, inventory reserve boundary language, and supplier dependency language together; the accepted autorun is `AGENT | Phase 98 limit honesty and business-language replay` (`gen-ag05122315-f1e27c`).
|
||||
- large-query budget/continuation policy accepted locally/live: targeted planner/pilot/answer/candidate tests passed `141/141` with `1` skipped, build passed; `phase99_large_query_budget_continuation_live2` accepted `4/4`, proving explicit-year business overview can recover money-flow coverage through monthly probes, cash-flow net is not treated as clean profit, bank-like incoming leaders stay bounded, and supplier-dependency answers remain concentration-only unless stronger evidence exists; the accepted autorun is `AGENT | Phase 99 large-query budget and continuation policy replay` (`gen-ag05131009-f08174`).
|
||||
- large-query continuation UX accepted locally/live: response-candidate tests passed `28/28`, build passed; `phase100_large_query_continuation_ux_live2` accepted `3/3`, proving all-time row-cap disclosure becomes a safe year/quarter continuation path, the 2020 follow-up recovers checked incoming/outgoing/net numbers under company scope, and profit follow-up remains cash-flow-vs-profit honest; the accepted autorun is `AGENT | Phase 100 large-query continuation UX replay` (`gen-ag05131028-234e5e`).
|
||||
- inventory root scope without warehouse clarification accepted live: `phase101_inventory_root_scope_no_warehouse_clarification_live1` accepted `7/7`, proving the manual `assistant-stage1-hyh1A1WR3j` stock-root seam now asks only for company when organization scope is ambiguous, resumes the root stock snapshot after `АЛЬТЕРНАТИВА`, preserves organization/date carryover for June 2017 and March 2016, and does not invent warehouse/item/category/material clarification requirements; the accepted autorun is `AGENT | Phase 101 inventory root scope without warehouse clarification replay` (`gen-ag05131044-cbe2ff`).
|
||||
|
||||
- debt mirror clean-scope polarity accepted locally/live: targeted living-chat/transition/follow-up tests passed `95/95` with `1` skipped, build passed; `phase102_debt_mirror_clean_scope_polarity_live3` accepted `6/6`, proving a fresh bare company-name turn binds `ООО Альтернатива Плюс` through data-scope probe, clean payables/receivables keep organization scope, Committee 76.09 mirror rows are disclosed as offset evidence rather than double-counted debt, and short `а мы кому?` follow-up stays in `payables_confirmed_as_of_date` instead of drifting into open-items; the accepted autorun is `AGENT | Phase 102 debt mirror clean-scope polarity replay` (`gen-ag05131121-8c41ab`).
|
||||
|
||||
Current architectural reading:
|
||||
|
||||
|
|
@ -347,24 +256,6 @@ For the detailed audit, current percentages, and remaining debt, read:
|
|||
- [20 - planner_autonomy_consolidation_2026-05-01.md](./20%20-%20planner_autonomy_consolidation_2026-05-01.md)
|
||||
- [21 - current_status_canon_2026-05-01.md](./21%20-%20current_status_canon_2026-05-01.md)
|
||||
- [22 - open_world_bounded_autonomy_breadth_2026-05-01.md](./22%20-%20open_world_bounded_autonomy_breadth_2026-05-01.md)
|
||||
- [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)
|
||||
- [24 - agentic_semantic_development_loop_and_autorun_hygiene_2026-05-10.md](./24%20-%20agentic_semantic_development_loop_and_autorun_hygiene_2026-05-10.md)
|
||||
- [25 - open_world_route_candidate_promotion_2026-05-10.md](./25%20-%20open_world_route_candidate_promotion_2026-05-10.md)
|
||||
- [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)
|
||||
- [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)
|
||||
- [31 - inventory_reserve_liquidation_quality_reviewed_route_2026-05-12.md](./31%20-%20inventory_reserve_liquidation_quality_reviewed_route_2026-05-12.md)
|
||||
- [32 - financial_counterparty_flow_hints_2026-05-13.md](./32%20-%20financial_counterparty_flow_hints_2026-05-13.md)
|
||||
- [33 - limit_honesty_business_language_2026-05-13.md](./33%20-%20limit_honesty_business_language_2026-05-13.md)
|
||||
- [34 - large_query_budget_continuation_2026-05-13.md](./34%20-%20large_query_budget_continuation_2026-05-13.md)
|
||||
- [35 - large_query_continuation_ux_2026-05-13.md](./35%20-%20large_query_continuation_ux_2026-05-13.md)
|
||||
- [36 - inventory_root_scope_no_warehouse_clarification_2026-05-13.md](./36%20-%20inventory_root_scope_no_warehouse_clarification_2026-05-13.md)
|
||||
- [37 - debt_mirror_clean_scope_polarity_2026-05-13.md](./37%20-%20debt_mirror_clean_scope_polarity_2026-05-13.md)
|
||||
- [38 - financial_role_purpose_arbitration_2026-05-13.md](./38%20-%20financial_role_purpose_arbitration_2026-05-13.md)
|
||||
- [39 - generic_role_tail_anchor_hygiene_2026-05-13.md](./39%20-%20generic_role_tail_anchor_hygiene_2026-05-13.md)
|
||||
- [40 - mixed_schema_primitive_closure_replay_2026-05-13.md](./40%20-%20mixed_schema_primitive_closure_replay_2026-05-13.md)
|
||||
|
||||
## Architectural Objects Of Planning
|
||||
|
||||
|
|
@ -405,24 +296,6 @@ Read in this order:
|
|||
21. `20 - planner_autonomy_consolidation_2026-05-01.md`
|
||||
22. `21 - current_status_canon_2026-05-01.md`
|
||||
23. `22 - open_world_bounded_autonomy_breadth_2026-05-01.md`
|
||||
24. `23 - current_execution_spine_and_semantic_control_gate_2026-05-05.md`
|
||||
25. `24 - agentic_semantic_development_loop_and_autorun_hygiene_2026-05-10.md`
|
||||
26. `25 - open_world_route_candidate_promotion_2026-05-10.md`
|
||||
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`
|
||||
32. `31 - inventory_reserve_liquidation_quality_reviewed_route_2026-05-12.md`
|
||||
33. `32 - financial_counterparty_flow_hints_2026-05-13.md`
|
||||
34. `33 - limit_honesty_business_language_2026-05-13.md`
|
||||
35. `34 - large_query_budget_continuation_2026-05-13.md`
|
||||
36. `35 - large_query_continuation_ux_2026-05-13.md`
|
||||
37. `36 - inventory_root_scope_no_warehouse_clarification_2026-05-13.md`
|
||||
38. `37 - debt_mirror_clean_scope_polarity_2026-05-13.md`
|
||||
39. `38 - financial_role_purpose_arbitration_2026-05-13.md`
|
||||
40. `39 - generic_role_tail_anchor_hygiene_2026-05-13.md`
|
||||
41. `40 - mixed_schema_primitive_closure_replay_2026-05-13.md`
|
||||
|
||||
## Planning Rules
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
Документ описывает практический контур, который используется оператором в интерфейсе
|
||||
`История автопрогонов`: генерация вопросов, запуск прогонов, разметка ответов, закрытие кейсов и пост-анализ.
|
||||
|
||||
Дата актуализации: `2026-05-10`
|
||||
Дата актуализации: `2026-04-09`
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -213,13 +213,6 @@ Queue mapping:
|
|||
6. Фильтр "скрыть выполненные" корректно исключает `resolved=true`.
|
||||
7. Пост-анализ показывает очереди и кандидатов.
|
||||
8. Текст в интерфейсе читается без mojibake.
|
||||
9. Старые сохраненные автопрогоны с C1-control mojibake в `history.json` и runtime job payload должны отдаваться через backend уже в восстановленной кириллице; контрольные примеры: `БОЛЬШОЙ ОБЩИЙ`, `АЛЬТЕРНАТИВА`.
|
||||
|
||||
Важно после правок encoding/autorun:
|
||||
|
||||
- перезапустить backend, чтобы UI получил новый repair-слой;
|
||||
- обновить список автопрогонов в браузере;
|
||||
- если replacement-character `U+FFFD` остается видимым, сравнить API payload `GET /api/autoruns/autogen/history` с состоянием frontend/browser cache.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -228,3 +221,4 @@ Queue mapping:
|
|||
1. Async run ограничен `assistant_stage1`.
|
||||
2. Качество live-данных зависит от заполнения session-файлов на стороне рантайма.
|
||||
3. Пост-анализ основан на фактической ручной разметке; без нее очереди пустые.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,112 +0,0 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase100_large_query_continuation_ux",
|
||||
"domain": "address_phase100_large_query_continuation_ux",
|
||||
"title": "Phase 100 large-query continuation UX replay",
|
||||
"description": "Focused semantic replay for all-time or very broad business overview questions: the assistant should answer from checked evidence, disclose row-limit coverage honestly, offer a safe period-narrowing continuation path, and then recover the explicit-year follow-up through chunked evidence without leaking technical mechanics.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_all_time_overview_limit_becomes_continuation_path",
|
||||
"title": "All-time overview gives checked partial answer plus safe continuation path",
|
||||
"question": "Дай общий бизнес-обзор ООО Альтернатива Плюс за весь доступный период: входящие, исходящие, нетто, лучший год. Если срез слишком широкий, не выдумывай полный итог, а скажи как безопасно дособрать.",
|
||||
"allowed_reply_types": [
|
||||
"factual",
|
||||
"factual_with_explanation",
|
||||
"partial_coverage"
|
||||
],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)альтернатива",
|
||||
"(?i)входящ|поступлен",
|
||||
"(?i)исходящ|списан|платеж",
|
||||
"(?i)нетто|денежн",
|
||||
"(?i)год|период",
|
||||
"(?i)лимит строк|проверенн.*срез|не гарантия полного|не полный",
|
||||
"(?i)конкретн.*год|год или квартал|дозапрос|собрать.*част"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)\\bMCP\\b",
|
||||
"(?i)route_candidate",
|
||||
"(?i)primitive",
|
||||
"(?i)planner_",
|
||||
"(?i)catalog_",
|
||||
"(?i)snapshot_items",
|
||||
"(?i)answer_object"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"business_overview",
|
||||
"large_query_continuation",
|
||||
"limit_honesty"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_user_narrows_to_2020_and_gets_recovered_year",
|
||||
"title": "Explicit-year continuation recovers yearly money evidence",
|
||||
"question": "Ок, тогда дособери конкретно 2020: входящие, исходящие и расчетное денежное нетто.",
|
||||
"allowed_reply_types": [
|
||||
"factual",
|
||||
"factual_with_explanation",
|
||||
"partial_coverage"
|
||||
],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)2020",
|
||||
"(?i)входящ|поступлен",
|
||||
"(?i)исходящ|списан|платеж",
|
||||
"(?i)нетто|денежн",
|
||||
"(?i)47\\s*628\\s*853|47,?6",
|
||||
"(?i)43\\s*763\\s*351|43,?7|43,?8"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уп[её]р.*лимит",
|
||||
"(?i)лимит выборки",
|
||||
"(?i)лимит строк",
|
||||
"(?i)\\bMCP\\b",
|
||||
"(?i)route_candidate",
|
||||
"(?i)primitive",
|
||||
"(?i)planner_",
|
||||
"(?i)catalog_",
|
||||
"(?i)snapshot_items",
|
||||
"(?i)answer_object"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"business_overview",
|
||||
"large_query_budget",
|
||||
"followup_continuation"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_profit_followup_keeps_boundary_after_continuation",
|
||||
"title": "Profit follow-up keeps cash-flow boundary after continuation",
|
||||
"question": "Это можно считать прибылью за 2020 или нет? Ответь коротко и по делу.",
|
||||
"allowed_reply_types": [
|
||||
"factual",
|
||||
"factual_with_explanation",
|
||||
"partial_coverage"
|
||||
],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)нет|нельзя|не стоит",
|
||||
"(?i)прибыл",
|
||||
"(?i)денежн|поток|нетто|поступлен",
|
||||
"(?i)90/91/99|закрыт|финрезультат|себестоим|расход"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)\\bMCP\\b",
|
||||
"(?i)route_candidate",
|
||||
"(?i)primitive",
|
||||
"(?i)planner_",
|
||||
"(?i)catalog_",
|
||||
"(?i)snapshot_items",
|
||||
"(?i)answer_object",
|
||||
"(?i)бухгалтерскому маршруту"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"profit_boundary",
|
||||
"followup_directness",
|
||||
"business_language"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,267 +0,0 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase101_inventory_root_scope_no_warehouse_clarification",
|
||||
"domain": "address_phase101_inventory_root_scope_no_warehouse_clarification",
|
||||
"title": "Phase 101 inventory root scope without warehouse clarification replay",
|
||||
"description": "Focused semantic replay for the manual assistant-stage1-hyh1A1WR3j signal: after a broad stock-on-hand question and organization clarification, the assistant must resume the root inventory snapshot for the selected company across available warehouses instead of asking the user to name a warehouse, item, category, or material.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_smalltalk_entry",
|
||||
"title": "Smalltalk entry stays human",
|
||||
"question": "приветик - че как там дела",
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)привет|дела|помочь|могу"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)\\bMCP\\b",
|
||||
"(?i)route_candidate",
|
||||
"(?i)primitive",
|
||||
"(?i)planner_",
|
||||
"(?i)snapshot_items",
|
||||
"(?i)answer_object"
|
||||
],
|
||||
"criticality": "important",
|
||||
"semantic_tags": [
|
||||
"smalltalk_entry",
|
||||
"human_answer_quality"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_capability_meta_entry",
|
||||
"title": "Capability meta entry stays human and business-oriented",
|
||||
"question": "расскажи что можешь интересного",
|
||||
"allowed_reply_types": [
|
||||
"factual",
|
||||
"factual_with_explanation"
|
||||
],
|
||||
"required_direct_answer_patterns_any": [
|
||||
"(?i)могу|умею",
|
||||
"(?i)ндс|документ|контрагент|долг|склад|остат"
|
||||
],
|
||||
"forbidden_direct_answer_patterns": [
|
||||
"(?i)\\bMCP\\b",
|
||||
"(?i)route_candidate",
|
||||
"(?i)primitive",
|
||||
"(?i)planner_",
|
||||
"(?i)snapshot_items",
|
||||
"(?i)answer_object"
|
||||
],
|
||||
"criticality": "important",
|
||||
"semantic_tags": [
|
||||
"capability_meta",
|
||||
"human_answer_quality"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_inventory_root_requires_company_only",
|
||||
"title": "Broad inventory root asks only for organization when company scope is ambiguous",
|
||||
"question": "кайф - что там на складе по остаткам?",
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)уточни|уточнить|выбери|какую компанию|какая компания|организац",
|
||||
"(?i)альтернатива плюс|лайсвуд|райм"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)\\bMCP\\b",
|
||||
"(?i)route_candidate",
|
||||
"(?i)primitive",
|
||||
"(?i)planner_",
|
||||
"(?i)snapshot_items",
|
||||
"(?i)answer_object",
|
||||
"(?i)какой\\s+конкретн.*склад",
|
||||
"(?i)какой\\s+склад",
|
||||
"(?i)конкретн.*товар",
|
||||
"(?i)названи[ея]\\s+товар",
|
||||
"(?i)категор",
|
||||
"(?i)материал"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"inventory_root",
|
||||
"clarification_required",
|
||||
"warehouse_not_required"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_04_company_choice_resumes_inventory_without_warehouse",
|
||||
"title": "Company choice resumes root inventory snapshot without asking for warehouse or item",
|
||||
"question": "АЛЬТЕРНАТИВА",
|
||||
"allowed_reply_types": [
|
||||
"factual",
|
||||
"factual_with_explanation",
|
||||
"partial_coverage"
|
||||
],
|
||||
"expected_intents": [
|
||||
"inventory_on_hand_as_of_date"
|
||||
],
|
||||
"required_filters": {
|
||||
"as_of_date": "{{runtime.today_iso}}",
|
||||
"organization": "ООО Альтернатива Плюс"
|
||||
},
|
||||
"forbidden_filter_keys": [
|
||||
"warehouse",
|
||||
"item",
|
||||
"category"
|
||||
],
|
||||
"required_direct_answer_patterns_any": [
|
||||
"(?i)на складе|остат",
|
||||
"{{runtime.today_dot_regex}}"
|
||||
],
|
||||
"forbidden_direct_answer_patterns": [
|
||||
"(?i)^отлично, фиксирую рабочую организацию",
|
||||
"(?i)^фиксирую рабочую организацию",
|
||||
"(?i)уточните организацию",
|
||||
"(?i)какой\\s+конкретн.*склад",
|
||||
"(?i)какой\\s+склад",
|
||||
"(?i)конкретн.*товар",
|
||||
"(?i)названи[ея]\\s+товар",
|
||||
"(?i)категор",
|
||||
"(?i)материал",
|
||||
"(?i)\\bMCP\\b",
|
||||
"(?i)route_candidate",
|
||||
"(?i)primitive",
|
||||
"(?i)planner_",
|
||||
"(?i)snapshot_items",
|
||||
"(?i)answer_object"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"clarification_resume",
|
||||
"inventory_root",
|
||||
"warehouse_not_required"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_05_historical_inventory_capability_no_warehouse_reask",
|
||||
"title": "Historical inventory capability follow-up stays human and does not ask for warehouse",
|
||||
"question": "а исторические остатки на другие даты умеешь?",
|
||||
"allowed_reply_types": [
|
||||
"factual",
|
||||
"factual_with_explanation"
|
||||
],
|
||||
"required_answer_patterns_any": [
|
||||
"(?i)историческ|история",
|
||||
"(?i)могу|умею"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)\\bMCP\\b",
|
||||
"(?i)route_candidate",
|
||||
"(?i)primitive",
|
||||
"(?i)planner_",
|
||||
"(?i)snapshot_items",
|
||||
"(?i)answer_object",
|
||||
"(?i)какой\\s+конкретн.*склад",
|
||||
"(?i)какой\\s+склад",
|
||||
"(?i)конкретн.*товар",
|
||||
"(?i)названи[ея]\\s+товар",
|
||||
"(?i)категор",
|
||||
"(?i)материал"
|
||||
],
|
||||
"criticality": "important",
|
||||
"semantic_tags": [
|
||||
"inventory_capability_meta",
|
||||
"warehouse_not_required"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_06_inventory_june_2017_after_capability",
|
||||
"title": "Month-only follow-up keeps inventory contour and selected organization",
|
||||
"question": "давай на июнь 2017",
|
||||
"allowed_reply_types": [
|
||||
"factual",
|
||||
"factual_with_explanation",
|
||||
"partial_coverage"
|
||||
],
|
||||
"expected_intents": [
|
||||
"inventory_on_hand_as_of_date"
|
||||
],
|
||||
"required_filters": {
|
||||
"as_of_date": "2017-06-30",
|
||||
"period_from": "2017-06-01",
|
||||
"period_to": "2017-06-30",
|
||||
"organization": "ООО Альтернатива Плюс"
|
||||
},
|
||||
"forbidden_filter_keys": [
|
||||
"warehouse",
|
||||
"item",
|
||||
"category"
|
||||
],
|
||||
"required_direct_answer_patterns_any": [
|
||||
"30\\.06\\.2017",
|
||||
"(?i)на складе|остат"
|
||||
],
|
||||
"forbidden_direct_answer_patterns": [
|
||||
"(?i)^отлично, фиксирую рабочую организацию",
|
||||
"(?i)уточните организацию",
|
||||
"(?i)какой\\s+конкретн.*склад",
|
||||
"(?i)какой\\s+склад",
|
||||
"(?i)конкретн.*товар",
|
||||
"(?i)названи[ея]\\s+товар",
|
||||
"(?i)категор",
|
||||
"(?i)материал",
|
||||
"(?i)\\bMCP\\b",
|
||||
"(?i)route_candidate",
|
||||
"(?i)primitive",
|
||||
"(?i)planner_",
|
||||
"(?i)snapshot_items",
|
||||
"(?i)answer_object"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"historical_inventory",
|
||||
"date_followup",
|
||||
"warehouse_not_required"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_07_inventory_march_2016_stays_root",
|
||||
"title": "Another short month stays in inventory root and organization scope",
|
||||
"question": "март 2016",
|
||||
"allowed_reply_types": [
|
||||
"factual",
|
||||
"factual_with_explanation",
|
||||
"partial_coverage"
|
||||
],
|
||||
"expected_intents": [
|
||||
"inventory_on_hand_as_of_date"
|
||||
],
|
||||
"required_filters": {
|
||||
"as_of_date": "2016-03-31",
|
||||
"period_from": "2016-03-01",
|
||||
"period_to": "2016-03-31",
|
||||
"organization": "ООО Альтернатива Плюс"
|
||||
},
|
||||
"forbidden_filter_keys": [
|
||||
"warehouse",
|
||||
"item",
|
||||
"category"
|
||||
],
|
||||
"required_direct_answer_patterns_any": [
|
||||
"31\\.03\\.2016",
|
||||
"(?i)на складе|остат"
|
||||
],
|
||||
"forbidden_direct_answer_patterns": [
|
||||
"(?i)^отлично, фиксирую рабочую организацию",
|
||||
"(?i)уточните организацию",
|
||||
"(?i)какой\\s+конкретн.*склад",
|
||||
"(?i)какой\\s+склад",
|
||||
"(?i)конкретн.*товар",
|
||||
"(?i)названи[ея]\\s+товар",
|
||||
"(?i)категор",
|
||||
"(?i)материал",
|
||||
"(?i)\\bMCP\\b",
|
||||
"(?i)route_candidate",
|
||||
"(?i)primitive",
|
||||
"(?i)planner_",
|
||||
"(?i)snapshot_items",
|
||||
"(?i)answer_object"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"historical_inventory",
|
||||
"date_followup",
|
||||
"warehouse_not_required"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,207 +0,0 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase102_debt_mirror_clean_scope_polarity",
|
||||
"domain": "address_phase102_debt_mirror_clean_scope_polarity",
|
||||
"title": "Phase 102 debt mirror clean-scope polarity replay",
|
||||
"description": "Focused semantic replay for the assistant-stage1-87gHJCwTI9 concern: mirrored 76.* settlement rows around Комитет государственных услуг must not look like the same clean debt in both directions. The assistant may disclose the mirrored/offset part, but the direct business answer must keep payables and receivables polarity clean.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_choose_company_scope",
|
||||
"title": "Select Альтернатива Плюс as active organization",
|
||||
"question": "Альтернатива Плюс",
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)альтернатива плюс",
|
||||
"(?i)зафиксир|работаем|организац"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)\\bMCP\\b",
|
||||
"(?i)route_candidate",
|
||||
"(?i)primitive",
|
||||
"(?i)planner_",
|
||||
"(?i)snapshot_items",
|
||||
"(?i)answer_object"
|
||||
],
|
||||
"criticality": "important",
|
||||
"semantic_tags": [
|
||||
"company_selected",
|
||||
"organization_scope"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_clean_payables_today",
|
||||
"title": "Clean payables root excludes mirrored settlement offset from direct answer",
|
||||
"question": "мы кому реально должны денег на сегодня? коротко, чистый долг к оплате, без встречных обеспечений как основного долга",
|
||||
"allowed_reply_types": [
|
||||
"factual",
|
||||
"factual_with_explanation"
|
||||
],
|
||||
"expected_intents": [
|
||||
"payables_confirmed_as_of_date"
|
||||
],
|
||||
"required_filters": {
|
||||
"as_of_date": "{{runtime.today_iso}}",
|
||||
"organization": "ООО Альтернатива Плюс"
|
||||
},
|
||||
"required_direct_answer_patterns_any": [
|
||||
"(?i)долг к оплате|должны|кредитор|к оплате"
|
||||
],
|
||||
"forbidden_direct_answer_patterns": [
|
||||
"(?i)комитет.*3\\s*[\\.,]?\\s*677\\s*[\\.,]?\\s*454",
|
||||
"(?i)3\\s*[\\.,]?\\s*677\\s*[\\.,]?\\s*454.*комитет"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)\\bMCP\\b",
|
||||
"(?i)route_candidate",
|
||||
"(?i)primitive",
|
||||
"(?i)planner_",
|
||||
"(?i)snapshot_items",
|
||||
"(?i)answer_object"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"payables_snapshot",
|
||||
"debt_mirror_clean_scope",
|
||||
"direct_answer_first"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_committee_payable_is_not_clean_debt",
|
||||
"title": "Committee payable check explains mirrored/offset nature instead of clean debt overclaim",
|
||||
"question": "а Комитету государственных услуг мы реально должны эти 3,6 млн или это встречное обеспечение/зачет? объясни именно по смыслу долга",
|
||||
"allowed_reply_types": [
|
||||
"factual",
|
||||
"factual_with_explanation",
|
||||
"partial_coverage"
|
||||
],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)комитет",
|
||||
"(?i)встречн|обеспеч|зач[её]т|исключ|не.*чист|не.*основн"
|
||||
],
|
||||
"forbidden_direct_answer_patterns": [
|
||||
"(?i)^.*мы\\s+должны.*комитет.*3\\s*[\\.,]?\\s*677\\s*[\\.,]?\\s*454",
|
||||
"(?i)^.*комитет.*чист.*долг.*3\\s*[\\.,]?\\s*677\\s*[\\.,]?\\s*454"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)\\bMCP\\b",
|
||||
"(?i)route_candidate",
|
||||
"(?i)primitive",
|
||||
"(?i)planner_",
|
||||
"(?i)snapshot_items",
|
||||
"(?i)answer_object"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"payables_counterparty_check",
|
||||
"debt_mirror_clean_scope",
|
||||
"polarity_honesty"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_04_committee_receivable_is_not_clean_debt",
|
||||
"title": "Mirrored receivable question does not present the same amount as clean customer debt",
|
||||
"question": "а нам Комитет государственных услуг тоже должен 3,6 млн? это же та же сумма — скажи честно, это дебиторка или встречная часть?",
|
||||
"allowed_reply_types": [
|
||||
"factual",
|
||||
"factual_with_explanation",
|
||||
"partial_coverage"
|
||||
],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)комитет",
|
||||
"(?i)встречн|обеспеч|зач[её]т|исключ|не.*чист|не.*основн|не.*дебитор"
|
||||
],
|
||||
"forbidden_direct_answer_patterns": [
|
||||
"(?i)^.*нам\\s+долж.*комитет.*3\\s*[\\.,]?\\s*677\\s*[\\.,]?\\s*454",
|
||||
"(?i)^.*комитет.*чист.*дебитор.*3\\s*[\\.,]?\\s*677\\s*[\\.,]?\\s*454"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)\\bMCP\\b",
|
||||
"(?i)route_candidate",
|
||||
"(?i)primitive",
|
||||
"(?i)planner_",
|
||||
"(?i)snapshot_items",
|
||||
"(?i)answer_object"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"receivables_counterparty_check",
|
||||
"debt_mirror_clean_scope",
|
||||
"polarity_honesty"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_05_clean_receivables_today",
|
||||
"title": "Clean receivables root excludes mirrored settlement offset from direct answer",
|
||||
"question": "тогда кто нам реально должен денег на сегодня? именно чистая дебиторка",
|
||||
"allowed_reply_types": [
|
||||
"factual",
|
||||
"factual_with_explanation"
|
||||
],
|
||||
"expected_intents": [
|
||||
"receivables_confirmed_as_of_date"
|
||||
],
|
||||
"required_filters": {
|
||||
"as_of_date": "{{runtime.today_iso}}",
|
||||
"organization": "ООО Альтернатива Плюс"
|
||||
},
|
||||
"required_direct_answer_patterns_any": [
|
||||
"(?i)нам должны|дебитор|задолж|долг"
|
||||
],
|
||||
"forbidden_direct_answer_patterns": [
|
||||
"(?i)комитет.*3\\s*[\\.,]?\\s*677\\s*[\\.,]?\\s*454",
|
||||
"(?i)3\\s*[\\.,]?\\s*677\\s*[\\.,]?\\s*454.*комитет"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)\\bMCP\\b",
|
||||
"(?i)route_candidate",
|
||||
"(?i)primitive",
|
||||
"(?i)planner_",
|
||||
"(?i)snapshot_items",
|
||||
"(?i)answer_object"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"receivables_snapshot",
|
||||
"debt_mirror_clean_scope",
|
||||
"direct_answer_first"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_06_payables_mirror_followup_keeps_clean_scope",
|
||||
"title": "Short mirror follow-up returns clean payables again, not the mirrored Committee amount",
|
||||
"question": "а мы кому?",
|
||||
"allowed_reply_types": [
|
||||
"factual",
|
||||
"factual_with_explanation"
|
||||
],
|
||||
"expected_intents": [
|
||||
"payables_confirmed_as_of_date"
|
||||
],
|
||||
"required_filters": {
|
||||
"as_of_date": "{{runtime.today_iso}}",
|
||||
"organization": "ООО Альтернатива Плюс"
|
||||
},
|
||||
"required_direct_answer_patterns_any": [
|
||||
"(?i)долг к оплате|должны|кредитор|к оплате"
|
||||
],
|
||||
"forbidden_direct_answer_patterns": [
|
||||
"(?i)комитет.*3\\s*[\\.,]?\\s*677\\s*[\\.,]?\\s*454",
|
||||
"(?i)3\\s*[\\.,]?\\s*677\\s*[\\.,]?\\s*454.*комитет"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)\\bMCP\\b",
|
||||
"(?i)route_candidate",
|
||||
"(?i)primitive",
|
||||
"(?i)planner_",
|
||||
"(?i)snapshot_items",
|
||||
"(?i)answer_object"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"settlements_mirror_followup",
|
||||
"payables_snapshot",
|
||||
"debt_mirror_clean_scope"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,176 +0,0 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase103_financial_role_purpose_arbitration",
|
||||
"domain": "address_phase103_financial_role_purpose_arbitration",
|
||||
"title": "Phase 103 financial role and purpose arbitration replay",
|
||||
"description": "Focused semantic replay for the Open-World Schema/Primitive Discovery slice: bank-like counterparties must be explained through confirmed 1C operation/purpose evidence and must not contaminate ordinary customer, supplier, revenue, or procurement semantics. The replay starts with a bare organization choice and ends with a normal counterparty value-flow canary.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_choose_company_scope",
|
||||
"title": "Bare organization choice binds active company scope",
|
||||
"question": "Альтернатива Плюс",
|
||||
"allowed_reply_types": [
|
||||
"factual_with_explanation",
|
||||
"partial_coverage"
|
||||
],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)фиксир|рабоч.*организац|контур",
|
||||
"(?i)альтернатива"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)программ|продукт|услуг.*компан",
|
||||
"(?i)не могу определить",
|
||||
"(?i)route_candidate|primitive|planner_|catalog_|snapshot_items|answer_object"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"bare_org_scope",
|
||||
"phase102_canary"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_sberbank_role_purpose_summary",
|
||||
"title": "Sberbank role is answered by operation and purpose evidence",
|
||||
"question": "По СБЕРБАНКУ за 2020 покажи коротко: сколько денег входило и уходило, и что это по смыслу в 1С — клиентская выручка, поставщик, комиссия, кредит или другой финансовый поток?",
|
||||
"allowed_reply_types": [
|
||||
"factual",
|
||||
"factual_with_explanation",
|
||||
"partial_coverage"
|
||||
],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)сбербанк|банк|финансов",
|
||||
"(?i)вход|поступ|исход|списан|ушл",
|
||||
"(?i)комисс|кредит|возврат|вид операц|назначен|договор",
|
||||
"(?i)не.*обычн.*клиент|не.*обычн.*поставщик|не.*клиентск.*выручк|нельзя.*читать"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)обычный поставщик.*сбербанк",
|
||||
"(?i)обычный клиент.*сбербанк",
|
||||
"(?i)главный поставщик.*сбербанк",
|
||||
"(?i)главный клиент.*сбербанк",
|
||||
"(?i)route_candidate|primitive|planner_|catalog_|snapshot_items|answer_object"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"financial_role_purpose",
|
||||
"bank_like_counterparty",
|
||||
"bank_operations_by_counterparty"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_sberbank_incoming_customer_boundary",
|
||||
"title": "Incoming Sberbank money is not overclaimed as customer revenue",
|
||||
"question": "Если СБЕРБАНК есть во входящих поступлениях, можно ли считать его нашим клиентом и выручкой? Скажи по подтвержденным строкам, без притягивания.",
|
||||
"allowed_reply_types": [
|
||||
"factual",
|
||||
"factual_with_explanation",
|
||||
"partial_coverage"
|
||||
],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)сбербанк|банк",
|
||||
"(?i)не.*клиент|не.*выручк|нельзя.*считать|не подтвержд",
|
||||
"(?i)поступ|вид операц|договор|возврат|кредит|депозит|финансов"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)сбербанк.*обычный клиент",
|
||||
"(?i)сбербанк.*клиентская выручка",
|
||||
"(?i)точно.*выручка",
|
||||
"(?i)route_candidate|primitive|planner_|catalog_|snapshot_items|answer_object"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"bank_like_customer_boundary",
|
||||
"customer_revenue_and_payments"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_04_sberbank_outgoing_supplier_boundary",
|
||||
"title": "Outgoing Sberbank money is not overclaimed as supplier dependency",
|
||||
"question": "А если деньги уходили в СБЕРБАНК, это наш поставщик или финансовые списания? Раздели по смыслу и покажи основание.",
|
||||
"allowed_reply_types": [
|
||||
"factual",
|
||||
"factual_with_explanation",
|
||||
"partial_coverage"
|
||||
],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)сбербанк|банк",
|
||||
"(?i)не.*поставщик|не.*обычн.*поставщик|финансов|банковск",
|
||||
"(?i)комисс|назначен|вид операц|списан|договор"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)сбербанк.*обычный поставщик",
|
||||
"(?i)главный поставщик.*сбербанк",
|
||||
"(?i)точно.*закупк",
|
||||
"(?i)route_candidate|primitive|planner_|catalog_|snapshot_items|answer_object"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"bank_like_supplier_boundary",
|
||||
"supplier_payouts_profile"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_05_business_overview_excludes_bank_role_overclaim",
|
||||
"title": "Business overview keeps bank leaders out of ordinary operating role overclaim",
|
||||
"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,
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)2020|компан|организац",
|
||||
"(?i)вход|поступ|исход|списан|нетто",
|
||||
"(?i)банк|сбербанк|финансов",
|
||||
"(?i)не.*обычн.*клиент|не.*обычн.*поставщик|не.*выручк|не.*закупк|назначен"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)сбербанк.*обычный поставщик",
|
||||
"(?i)сбербанк.*обычный клиент",
|
||||
"(?i)чистая прибыль.*точно",
|
||||
"(?i)route_candidate|primitive|planner_|catalog_|snapshot_items|answer_object"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"business_overview",
|
||||
"financial_role_purpose",
|
||||
"bank_like_counterparty"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_06_svk_value_flow_canary_after_bank_context",
|
||||
"title": "Normal counterparty net flow survives after bank-role 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_comparison",
|
||||
"expected_catalog_selected_matches_top": true,
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)свк|группа",
|
||||
"(?i)2020",
|
||||
"(?i)получ|вход|поступ",
|
||||
"(?i)заплат|исход|списан",
|
||||
"(?i)нетто|сальдо|разниц"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)сбербанк",
|
||||
"(?i)уточните организац|какую компанию",
|
||||
"(?i)route_candidate|primitive|planner_|catalog_|snapshot_items|answer_object"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"counterparty_net_cash_flow",
|
||||
"stale_scope_guard",
|
||||
"canary"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,138 +0,0 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase104_generic_role_tail_anchor_hygiene",
|
||||
"domain": "address_phase104_generic_role_tail_anchor_hygiene",
|
||||
"title": "Phase 104 generic role-tail anchor hygiene replay",
|
||||
"description": "Focused semantic replay for the Open-World Schema/Primitive Discovery slice: broad company overview wording may mention ordinary customer/supplier roles as business semantics, but generic role tails such as 'или поставщик' must not become a counterparty anchor or poison later bank/counterparty drilldowns.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_choose_company_scope",
|
||||
"title": "Bare organization choice binds active company scope",
|
||||
"question": "Альтернатива Плюс",
|
||||
"allowed_reply_types": [
|
||||
"factual_with_explanation",
|
||||
"partial_coverage"
|
||||
],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)фиксир|рабоч.*организац|контур",
|
||||
"(?i)альтернатива"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)не могу определить",
|
||||
"(?i)route_candidate|primitive|planner_|catalog_|snapshot_items|answer_object"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"bare_org_scope",
|
||||
"phase102_canary"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_business_overview_role_tail_not_counterparty",
|
||||
"title": "Business overview role words do not become counterparty anchors",
|
||||
"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,
|
||||
"forbidden_filter_keys": [
|
||||
"counterparty"
|
||||
],
|
||||
"forbidden_filter_values": {
|
||||
"counterparty": [
|
||||
"или поставщик",
|
||||
"поставщик",
|
||||
"клиент",
|
||||
"обычный клиент",
|
||||
"обычный клиент или поставщик"
|
||||
]
|
||||
},
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)2020|компан|организац",
|
||||
"(?i)вход|поступ|исход|списан|нетто",
|
||||
"(?i)банк|сбербанк|финансов",
|
||||
"(?i)не.*обычн.*клиент|не.*обычн.*поставщик|не.*выручк|не.*закупк|назначен"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)сбербанк.*обычный поставщик",
|
||||
"(?i)сбербанк.*обычный клиент",
|
||||
"(?i)чистая прибыль.*точно",
|
||||
"(?i)route_candidate|primitive|planner_|catalog_|snapshot_items|answer_object"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"business_overview",
|
||||
"generic_role_tail_anchor_hygiene",
|
||||
"financial_role_purpose"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_sberbank_role_after_role_tail_overview",
|
||||
"title": "Bank-role drilldown still resolves Sberbank after role-tail overview",
|
||||
"question": "А отдельно по СБЕРБАНКУ: он для нас клиент, поставщик или финансовый поток? Дай коротко по подтвержденным строкам.",
|
||||
"allowed_reply_types": [
|
||||
"factual",
|
||||
"factual_with_explanation",
|
||||
"partial_coverage"
|
||||
],
|
||||
"required_filters": {
|
||||
"counterparty": "СБЕРБАНК"
|
||||
},
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)сбербанк|банк",
|
||||
"(?i)финансов|банковск|кредит|комисс|возврат|назначен|вид операц",
|
||||
"(?i)не.*обычн.*клиент|не.*обычн.*поставщик|не.*клиентск.*выручк|нельзя.*читать"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)или поставщик",
|
||||
"(?i)сбербанк.*обычный поставщик",
|
||||
"(?i)сбербанк.*обычный клиент",
|
||||
"(?i)route_candidate|primitive|planner_|catalog_|snapshot_items|answer_object"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"bank_like_counterparty",
|
||||
"financial_role_purpose",
|
||||
"post_overview_anchor_integrity"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_04_real_supplier_prefix_still_keeps_counterparty",
|
||||
"title": "Explicit supplier prefix still preserves a real counterparty",
|
||||
"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",
|
||||
"expected_catalog_selected_matches_top": true,
|
||||
"required_filters": {
|
||||
"counterparty": "Группа СВК"
|
||||
},
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)свк|группа",
|
||||
"(?i)2020",
|
||||
"(?i)заплат|исход|списан|получ|поступ|нетто|смысл"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)или поставщик",
|
||||
"(?i)сбербанк",
|
||||
"(?i)уточните организац|какую компанию",
|
||||
"(?i)route_candidate|primitive|planner_|catalog_|snapshot_items|answer_object"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"counterparty_net_cash_flow",
|
||||
"supplier_prefix_canary",
|
||||
"stale_scope_guard"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,429 +0,0 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase105_mixed_schema_primitive_closure",
|
||||
"domain": "address_phase105_mixed_schema_primitive_closure",
|
||||
"title": "Phase 105 mixed schema/primitive closure replay",
|
||||
"description": "A broad live semantic replay for the Open-World Schema/Primitive Discovery module. It intentionally crosses old and new seams: inventory root clarification, historical inventory carryover, business overview role-tail hygiene, bank role/purpose boundaries, supplier payout wording, bidirectional value-flow, clean debt polarity, VAT continuity, and cash-flow-vs-profit answer shape.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_inventory_root_requires_company_only",
|
||||
"title": "Broad inventory root uses resolved organization and does not ask for warehouse",
|
||||
"question": "кайф - что там на складе по остаткам?",
|
||||
"allowed_reply_types": [
|
||||
"factual",
|
||||
"factual_with_explanation",
|
||||
"partial_coverage"
|
||||
],
|
||||
"expected_intents": [
|
||||
"inventory_on_hand_as_of_date"
|
||||
],
|
||||
"required_filters": {
|
||||
"as_of_date": "{{runtime.today_iso}}",
|
||||
"organization": "ООО Альтернатива Плюс"
|
||||
},
|
||||
"forbidden_filter_keys": [
|
||||
"warehouse",
|
||||
"item",
|
||||
"category",
|
||||
"counterparty"
|
||||
],
|
||||
"required_direct_answer_patterns_any": [
|
||||
"(?i)на складе|остат",
|
||||
"{{runtime.today_dot_regex}}"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)какой\\s+конкретн.*склад",
|
||||
"(?i)какой\\s+склад",
|
||||
"(?i)конкретн.*товар",
|
||||
"(?i)названи[ея]\\s+товар",
|
||||
"(?i)категор",
|
||||
"(?i)материал",
|
||||
"(?i)route_candidate|primitive|planner_|snapshot_items|answer_object"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"inventory_root",
|
||||
"resolved_organization_scope",
|
||||
"warehouse_not_required"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_redundant_company_choice_scope_ack",
|
||||
"title": "Redundant company choice only updates organization scope",
|
||||
"question": "АЛЬТЕРНАТИВА",
|
||||
"allowed_reply_types": [
|
||||
"factual_with_explanation",
|
||||
"partial_coverage"
|
||||
],
|
||||
"forbidden_filter_keys": [
|
||||
"warehouse",
|
||||
"item",
|
||||
"counterparty",
|
||||
"category"
|
||||
],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)альтернатива плюс",
|
||||
"(?i)фиксир|организац|контур|компан"
|
||||
],
|
||||
"forbidden_direct_answer_patterns": [
|
||||
"(?i)уточните организацию",
|
||||
"(?i)какой\\s+конкретн.*склад",
|
||||
"(?i)какой\\s+склад",
|
||||
"(?i)конкретн.*товар",
|
||||
"(?i)названи[ея]\\s+товар",
|
||||
"(?i)категор",
|
||||
"(?i)материал",
|
||||
"(?i)route_candidate|primitive|planner_|snapshot_items|answer_object"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"redundant_scope_selection",
|
||||
"organization_scope",
|
||||
"warehouse_not_required"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_historical_inventory_meta",
|
||||
"title": "Historical inventory capability follow-up stays human",
|
||||
"question": "а исторические остатки на другие даты умеешь?",
|
||||
"allowed_reply_types": [
|
||||
"factual",
|
||||
"factual_with_explanation"
|
||||
],
|
||||
"required_answer_patterns_any": [
|
||||
"(?i)историческ|история",
|
||||
"(?i)могу|умею"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)какой\\s+конкретн.*склад",
|
||||
"(?i)какой\\s+склад",
|
||||
"(?i)конкретн.*товар",
|
||||
"(?i)названи[ея]\\s+товар",
|
||||
"(?i)категор",
|
||||
"(?i)материал",
|
||||
"(?i)route_candidate|primitive|planner_|snapshot_items|answer_object"
|
||||
],
|
||||
"criticality": "important",
|
||||
"semantic_tags": [
|
||||
"inventory_capability_meta",
|
||||
"warehouse_not_required"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_04_inventory_june_2017_followup",
|
||||
"title": "Month-only follow-up keeps inventory contour and selected organization",
|
||||
"question": "давай на июнь 2017",
|
||||
"allowed_reply_types": [
|
||||
"factual",
|
||||
"factual_with_explanation",
|
||||
"partial_coverage"
|
||||
],
|
||||
"expected_intents": [
|
||||
"inventory_on_hand_as_of_date"
|
||||
],
|
||||
"required_filters": {
|
||||
"as_of_date": "2017-06-30",
|
||||
"period_from": "2017-06-01",
|
||||
"period_to": "2017-06-30",
|
||||
"organization": "ООО Альтернатива Плюс"
|
||||
},
|
||||
"forbidden_filter_keys": [
|
||||
"warehouse",
|
||||
"item",
|
||||
"category"
|
||||
],
|
||||
"required_direct_answer_patterns_any": [
|
||||
"30\\.06\\.2017",
|
||||
"(?i)на складе|остат"
|
||||
],
|
||||
"forbidden_direct_answer_patterns": [
|
||||
"(?i)уточните организацию",
|
||||
"(?i)какой\\s+конкретн.*склад",
|
||||
"(?i)какой\\s+склад",
|
||||
"(?i)конкретн.*товар",
|
||||
"(?i)названи[ея]\\s+товар",
|
||||
"(?i)категор",
|
||||
"(?i)материал",
|
||||
"(?i)route_candidate|primitive|planner_|snapshot_items|answer_object"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"historical_inventory",
|
||||
"date_followup",
|
||||
"warehouse_not_required"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_05_business_overview_role_tail_hygiene",
|
||||
"title": "Business overview keeps role-tail words out of counterparty filters",
|
||||
"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,
|
||||
"forbidden_filter_keys": [
|
||||
"counterparty"
|
||||
],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)2020|компан|организац",
|
||||
"(?i)вход|поступ|исход|списан|нетто",
|
||||
"(?i)банк|сбербанк|финансов",
|
||||
"(?i)не.*обычн.*клиент|не.*обычн.*поставщик|не.*выручк|не.*закупк|назначен"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)сбербанк.*обычный поставщик",
|
||||
"(?i)сбербанк.*обычный клиент",
|
||||
"(?i)route_candidate|primitive|planner_|snapshot_items|answer_object"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"business_overview",
|
||||
"generic_role_tail_anchor_hygiene"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_06_sberbank_role_after_overview",
|
||||
"title": "Sberbank role drilldown stays on bank-operation evidence",
|
||||
"question": "А отдельно по СБЕРБАНКУ: он для нас клиент, поставщик или финансовый поток? Дай коротко по подтвержденным строкам.",
|
||||
"allowed_reply_types": [
|
||||
"factual",
|
||||
"factual_with_explanation",
|
||||
"partial_coverage"
|
||||
],
|
||||
"required_filters": {
|
||||
"counterparty": "СБЕРБАНК"
|
||||
},
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)сбербанк|банк",
|
||||
"(?i)финансов|банковск|кредит|комисс|возврат|назначен|вид операц",
|
||||
"(?i)не.*обычн.*клиент|не.*обычн.*поставщик|не.*клиентск.*выручк|нельзя.*читать"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)или поставщик",
|
||||
"(?i)сбербанк.*обычный поставщик",
|
||||
"(?i)сбербанк.*обычный клиент",
|
||||
"(?i)route_candidate|primitive|planner_|snapshot_items|answer_object"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"bank_like_counterparty",
|
||||
"financial_role_purpose"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_07_supplier_payment_wording",
|
||||
"title": "Supplier payment wording routes as supplier payout, not population",
|
||||
"question": "А теперь по поставщику Группа СВК за 2020: сколько мы ему заплатили и какой общий денежный смысл?",
|
||||
"allowed_reply_types": [
|
||||
"factual",
|
||||
"factual_with_explanation",
|
||||
"partial_coverage"
|
||||
],
|
||||
"expected_intents": [
|
||||
"supplier_payouts_profile"
|
||||
],
|
||||
"expected_catalog_alignment_status": "selected_matches_top",
|
||||
"expected_catalog_chain_top_match": "value_flow",
|
||||
"expected_catalog_selected_matches_top": true,
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)свк|группа",
|
||||
"(?i)2020",
|
||||
"(?i)заплат|исход|списан|платеж|платёж|денежн|смысл"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)или поставщик",
|
||||
"(?i)сбербанк",
|
||||
"(?i)уточните организац|какую компанию",
|
||||
"(?i)route_candidate|primitive|planner_|snapshot_items|answer_object"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"supplier_prefix_canary",
|
||||
"value_flow"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_08_svk_bidirectional_value_flow",
|
||||
"title": "Normal counterparty bidirectional value flow survives after supplier-only ask",
|
||||
"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_comparison",
|
||||
"expected_catalog_selected_matches_top": true,
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)свк",
|
||||
"(?i)2020",
|
||||
"(?i)получ|вход|поступ",
|
||||
"(?i)заплат|исход|списан",
|
||||
"(?i)нетто|сальдо|разниц"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)сбербанк",
|
||||
"(?i)уточните организац|какую компанию",
|
||||
"(?i)route_candidate|primitive|planner_|snapshot_items|answer_object"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"counterparty_net_cash_flow",
|
||||
"stale_scope_guard"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_09_clean_payables_scope",
|
||||
"title": "Clean payables question keeps organization scope and debt polarity",
|
||||
"question": "кому мы должны на конец 2020?",
|
||||
"allowed_reply_types": [
|
||||
"factual",
|
||||
"factual_with_explanation",
|
||||
"partial_coverage"
|
||||
],
|
||||
"expected_intents": [
|
||||
"payables_confirmed_as_of_date"
|
||||
],
|
||||
"required_filters": {
|
||||
"as_of_date": "2020-12-31",
|
||||
"organization": "ООО Альтернатива Плюс"
|
||||
},
|
||||
"forbidden_filter_keys": [
|
||||
"counterparty"
|
||||
],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)должн|кредитор|кредиторск|поставщик|обязательств",
|
||||
"(?i)2020|31\\.12\\.2020|конец"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)нам должны.*мы должны",
|
||||
"(?i)route_candidate|primitive|planner_|snapshot_items|answer_object"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"debt_polarity",
|
||||
"payables"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_10_clean_receivables_scope",
|
||||
"title": "Mirror receivables follow-up switches polarity cleanly",
|
||||
"question": "а нам кто должен на конец 2020?",
|
||||
"allowed_reply_types": [
|
||||
"factual",
|
||||
"factual_with_explanation",
|
||||
"partial_coverage"
|
||||
],
|
||||
"expected_intents": [
|
||||
"receivables_confirmed_as_of_date"
|
||||
],
|
||||
"required_filters": {
|
||||
"as_of_date": "2020-12-31",
|
||||
"organization": "ООО Альтернатива Плюс"
|
||||
},
|
||||
"forbidden_filter_keys": [
|
||||
"counterparty"
|
||||
],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)нам.*долж|дебитор|дебиторск|покупател|заказчик",
|
||||
"(?i)2020|31\\.12\\.2020|конец"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)мы должны.*нам должны",
|
||||
"(?i)route_candidate|primitive|planner_|snapshot_items|answer_object"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"debt_polarity",
|
||||
"receivables"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_11_vat_tax_period_continuity",
|
||||
"title": "VAT tax-period exact question survives after debt/value-flow pivots",
|
||||
"question": "сколько НДС надо заплатить в налоговую за декабрь 2019?",
|
||||
"allowed_reply_types": [
|
||||
"factual",
|
||||
"factual_with_explanation",
|
||||
"partial_coverage"
|
||||
],
|
||||
"expected_intents": [
|
||||
"vat_liability_confirmed_for_tax_period"
|
||||
],
|
||||
"required_filters": {
|
||||
"period_from": "2019-10-01",
|
||||
"period_to": "2019-12-31",
|
||||
"organization": "ООО Альтернатива Плюс"
|
||||
},
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)ндс",
|
||||
"(?i)декабр|2019",
|
||||
"(?i)налог|заплат|уплат|к уплате|не найден"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)route_candidate|primitive|planner_|snapshot_items|answer_object"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"vat_continuity",
|
||||
"tax_period"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_12_earnings_wording_to_business_overview",
|
||||
"title": "Organization earnings slang routes to business overview, not customer ranking",
|
||||
"question": "скока денег альтернатива заработала за 20 год?",
|
||||
"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,
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)альтернатива|компан|организац",
|
||||
"(?i)2020",
|
||||
"(?i)поступ|вход|денег|заработ",
|
||||
"(?i)не.*прибыл|не.*чист"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)route_candidate|primitive|planner_|snapshot_items|answer_object"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"business_overview",
|
||||
"earnings_wording"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_13_profit_followup_boundary",
|
||||
"title": "Profit follow-up distinguishes cash-flow net from clean profit",
|
||||
"question": "а это чистая прибыль?",
|
||||
"allowed_reply_types": [
|
||||
"factual",
|
||||
"factual_with_explanation",
|
||||
"partial_coverage"
|
||||
],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)не.*чист.*прибыл|не.*является.*прибыл|это.*не.*прибыл",
|
||||
"(?i)поступ|денежн|cash|нетто|расход|себестоим|закрытие"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)это чистая прибыль",
|
||||
"(?i)точно.*прибыл",
|
||||
"(?i)route_candidate|primitive|planner_|snapshot_items|answer_object"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"profit_boundary",
|
||||
"business_overview_followup"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,190 +0,0 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase90_route_candidate_handoff_open_world",
|
||||
"domain": "address_phase90_route_candidate_handoff",
|
||||
"title": "Phase 90 open-world route candidate handoff replay",
|
||||
"description": "Focused semantic replay for Open-World Route Candidate Promotion. The scenario checks that unfamiliar-but-meaningful 1C asks produce a structured internal route candidate, distinguish missing user scope from executable reviewed routes, preserve catalog alignment, and keep unreviewed proof families honest without leaking route/debug mechanics to the user.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_open_ranking_needs_organization",
|
||||
"title": "Open ranked money-flow ask needs organization scope before execution",
|
||||
"question": "какой контрагент принес больше всего денег за 2020 год?",
|
||||
"allowed_reply_types": [
|
||||
"clarification_required",
|
||||
"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": "needs_user_scope",
|
||||
"expected_route_candidate_executable_now": false,
|
||||
"expected_route_candidate_missing_axes": [
|
||||
"organization"
|
||||
],
|
||||
"required_answer_patterns_any": [
|
||||
"(?i)организац|компан|контур"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)runtime_",
|
||||
"(?i)route_candidate",
|
||||
"(?i)query_movements",
|
||||
"(?i)primitive",
|
||||
"(?i)planner_",
|
||||
"(?i)catalog_"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"route_candidate_handoff",
|
||||
"needs_user_scope",
|
||||
"value_flow_ranking",
|
||||
"open_organization_scope"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_company_scope_promotes_ranking_execution",
|
||||
"title": "Natural company clarification promotes the ranking candidate to reviewed execution",
|
||||
"question": "ООО Альтернатива Плюс",
|
||||
"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)runtime_",
|
||||
"(?i)route_candidate",
|
||||
"(?i)query_movements",
|
||||
"(?i)primitive",
|
||||
"(?i)planner_",
|
||||
"(?i)catalog_",
|
||||
"(?i)прибыль.*точно"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"route_candidate_handoff",
|
||||
"ready_for_reviewed_execution",
|
||||
"value_flow_ranking",
|
||||
"clarification_recovery"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_counterparty_lifecycle_not_polluted_by_company_scope",
|
||||
"title": "Counterparty lifecycle pivot has its own executable route candidate",
|
||||
"question": "а теперь сколько лет мы работаем с Группа СВК?",
|
||||
"allowed_reply_types": [
|
||||
"factual",
|
||||
"factual_with_explanation",
|
||||
"partial_coverage"
|
||||
],
|
||||
"expected_catalog_alignment_status": "selected_matches_top",
|
||||
"expected_catalog_chain_top_match": "lifecycle",
|
||||
"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)активност|1С|подтвержд|проверен",
|
||||
"(?i)лет|год|месяц",
|
||||
"(?i)юридическ|регистрац|не подтвержд|не доказ"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)Альтернатива Плюс.*контрагент",
|
||||
"(?i)runtime_",
|
||||
"(?i)route_candidate",
|
||||
"(?i)query_documents",
|
||||
"(?i)primitive",
|
||||
"(?i)planner_",
|
||||
"(?i)catalog_"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"route_candidate_handoff",
|
||||
"counterparty_lifecycle",
|
||||
"stale_scope_guard"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_04_vat_movements_are_metadata_scoped_not_stale_counterparty",
|
||||
"title": "VAT movement ask uses metadata-scoped movement candidate instead of stale counterparty focus",
|
||||
"question": "покажи движения по НДС за 2020 по ООО Альтернатива Плюс",
|
||||
"allowed_reply_types": [
|
||||
"factual",
|
||||
"factual_with_explanation",
|
||||
"partial_coverage",
|
||||
"checked_sources_only"
|
||||
],
|
||||
"expected_catalog_alignment_status": "selected_matches_top",
|
||||
"expected_catalog_chain_top_match": "movement_evidence",
|
||||
"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",
|
||||
"(?i)Альтернатива Плюс|организац",
|
||||
"(?i)движен|строк|проверен|подтвержд|источник"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)Группа СВК.*как контрагент",
|
||||
"(?i)runtime_",
|
||||
"(?i)route_candidate",
|
||||
"(?i)query_movements",
|
||||
"(?i)primitive",
|
||||
"(?i)planner_",
|
||||
"(?i)catalog_"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"route_candidate_handoff",
|
||||
"movement_evidence",
|
||||
"metadata_scope",
|
||||
"vat",
|
||||
"stale_scope_guard"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_05_inventory_reserve_boundary_stays_honest",
|
||||
"title": "Unreviewed inventory reserve proof family remains bounded and does not overclaim",
|
||||
"question": "можно ли по этим данным точно подтвердить резерв под неликвиды на складе?",
|
||||
"allowed_reply_types": [
|
||||
"factual",
|
||||
"factual_with_explanation",
|
||||
"partial_coverage",
|
||||
"no_grounded_answer",
|
||||
"out_of_scope"
|
||||
],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)резерв|неликвид|склад|товар",
|
||||
"(?i)не подтвержд|не доказ|нельзя точно|нет точн",
|
||||
"(?i)провер|нужн|треб"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)точно подтверждаю",
|
||||
"(?i)резерв.*подтвержден",
|
||||
"(?i)runtime_",
|
||||
"(?i)route_candidate",
|
||||
"(?i)primitive",
|
||||
"(?i)planner_",
|
||||
"(?i)catalog_"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"route_candidate_handoff",
|
||||
"no_overclaim",
|
||||
"inventory_reserve_boundary",
|
||||
"unreviewed_proof_family"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,190 +0,0 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase91_route_candidate_driven_enablement_loop",
|
||||
"domain": "address_phase91_route_candidate_driven_enablement_loop",
|
||||
"title": "Phase 91 route-candidate-driven enablement loop canary",
|
||||
"description": "Focused semantic replay for the route-candidate-driven enablement loop. The scenario keeps the phase90 user-facing route-candidate handoff behavior as a canary while the development tooling starts grouping route candidates into repair targets and lead-coder handoff artifacts.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_open_ranking_needs_organization",
|
||||
"title": "Open ranked money-flow ask needs organization scope before execution",
|
||||
"question": "какой контрагент принес больше всего денег за 2020 год?",
|
||||
"allowed_reply_types": [
|
||||
"clarification_required",
|
||||
"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": "needs_user_scope",
|
||||
"expected_route_candidate_executable_now": false,
|
||||
"expected_route_candidate_missing_axes": [
|
||||
"organization"
|
||||
],
|
||||
"required_answer_patterns_any": [
|
||||
"(?i)организац|компан|контур"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)runtime_",
|
||||
"(?i)route_candidate",
|
||||
"(?i)query_movements",
|
||||
"(?i)primitive",
|
||||
"(?i)planner_",
|
||||
"(?i)catalog_"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"route_candidate_handoff",
|
||||
"needs_user_scope",
|
||||
"value_flow_ranking",
|
||||
"open_organization_scope"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_company_scope_promotes_ranking_execution",
|
||||
"title": "Natural company clarification promotes the ranking candidate to reviewed execution",
|
||||
"question": "ООО Альтернатива Плюс",
|
||||
"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)runtime_",
|
||||
"(?i)route_candidate",
|
||||
"(?i)query_movements",
|
||||
"(?i)primitive",
|
||||
"(?i)planner_",
|
||||
"(?i)catalog_",
|
||||
"(?i)прибыль.*точно"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"route_candidate_handoff",
|
||||
"ready_for_reviewed_execution",
|
||||
"value_flow_ranking",
|
||||
"clarification_recovery"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_counterparty_lifecycle_stale_scope_reset",
|
||||
"title": "Counterparty lifecycle pivot does not inherit the previous company scope",
|
||||
"question": "а теперь сколько лет мы работаем с Группа СВК?",
|
||||
"allowed_reply_types": [
|
||||
"factual",
|
||||
"factual_with_explanation",
|
||||
"partial_coverage"
|
||||
],
|
||||
"expected_catalog_alignment_status": "selected_matches_top",
|
||||
"expected_catalog_chain_top_match": "lifecycle",
|
||||
"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)активност|1С|подтвержд|проверен",
|
||||
"(?i)лет|год|месяц",
|
||||
"(?i)юридическ|регистрац|не подтвержд|не доказ"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)Альтернатива Плюс.*контрагент",
|
||||
"(?i)runtime_",
|
||||
"(?i)route_candidate",
|
||||
"(?i)query_documents",
|
||||
"(?i)primitive",
|
||||
"(?i)planner_",
|
||||
"(?i)catalog_"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"route_candidate_handoff",
|
||||
"counterparty_lifecycle",
|
||||
"stale_scope_guard"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_04_vat_movement_candidate_keeps_company_scope",
|
||||
"title": "VAT movement ask keeps metadata scope and company scope together",
|
||||
"question": "покажи движения по НДС за 2020 по ООО Альтернатива Плюс",
|
||||
"allowed_reply_types": [
|
||||
"factual",
|
||||
"factual_with_explanation",
|
||||
"partial_coverage",
|
||||
"checked_sources_only"
|
||||
],
|
||||
"expected_catalog_alignment_status": "selected_matches_top",
|
||||
"expected_catalog_chain_top_match": "movement_evidence",
|
||||
"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",
|
||||
"(?i)Альтернатива Плюс|организац",
|
||||
"(?i)движен|строк|проверен|подтвержд|источник"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)Группа СВК.*как контрагент",
|
||||
"(?i)runtime_",
|
||||
"(?i)route_candidate",
|
||||
"(?i)query_movements",
|
||||
"(?i)primitive",
|
||||
"(?i)planner_",
|
||||
"(?i)catalog_"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"route_candidate_handoff",
|
||||
"movement_evidence",
|
||||
"metadata_scope",
|
||||
"vat",
|
||||
"stale_scope_guard"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_05_inventory_reserve_boundary_no_overclaim",
|
||||
"title": "Reserve proof request stays bounded instead of pretending enablement is proof",
|
||||
"question": "можно ли по этим данным точно подтвердить резерв под неликвиды на складе?",
|
||||
"allowed_reply_types": [
|
||||
"factual",
|
||||
"factual_with_explanation",
|
||||
"partial_coverage",
|
||||
"no_grounded_answer",
|
||||
"out_of_scope"
|
||||
],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)резерв|неликвид|склад|товар",
|
||||
"(?i)не подтвержд|не доказ|нельзя точно|нет точн",
|
||||
"(?i)провер|нужн|треб"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)точно подтверждаю",
|
||||
"(?i)резерв.*подтвержден",
|
||||
"(?i)runtime_",
|
||||
"(?i)route_candidate",
|
||||
"(?i)primitive",
|
||||
"(?i)planner_",
|
||||
"(?i)catalog_"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"route_candidate_handoff",
|
||||
"no_overclaim",
|
||||
"inventory_reserve_boundary",
|
||||
"unreviewed_proof_family"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,227 +0,0 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase92_proof_family_enablement_candidates",
|
||||
"domain": "address_phase92_proof_family_enablement_candidates",
|
||||
"title": "Phase 92 proof-family enablement candidates",
|
||||
"description": "Focused semantic replay for route-candidate autonomy: exact proof-family asks must stay user-safe while route_candidate marks missing reviewed proof routes as needs_route_enablement.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_open_ranking_still_needs_company",
|
||||
"title": "Open ranked money-flow ask still needs organization scope",
|
||||
"question": "какой контрагент принес больше всего денег за 2020 год?",
|
||||
"allowed_reply_types": [
|
||||
"clarification_required",
|
||||
"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": "needs_user_scope",
|
||||
"expected_route_candidate_executable_now": false,
|
||||
"expected_route_candidate_missing_axes": [
|
||||
"organization"
|
||||
],
|
||||
"required_answer_patterns_any": [
|
||||
"(?i)организац|компан|контур"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)runtime_",
|
||||
"(?i)route_candidate",
|
||||
"(?i)query_movements",
|
||||
"(?i)primitive",
|
||||
"(?i)planner_",
|
||||
"(?i)catalog_"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"route_candidate_handoff",
|
||||
"needs_user_scope",
|
||||
"value_flow_ranking"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_company_clarification_keeps_reviewed_execution",
|
||||
"title": "Company clarification keeps the reviewed value-flow route executable",
|
||||
"question": "ООО Альтернатива Плюс",
|
||||
"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)runtime_",
|
||||
"(?i)route_candidate",
|
||||
"(?i)query_movements",
|
||||
"(?i)primitive",
|
||||
"(?i)planner_",
|
||||
"(?i)catalog_"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"route_candidate_handoff",
|
||||
"ready_for_reviewed_execution",
|
||||
"value_flow_ranking"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_profit_margin_needs_reviewed_pnl_route",
|
||||
"title": "Exact profit/margin ask becomes a proof-family enablement candidate",
|
||||
"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)финансов|p&l|расход|себестоим|закрыт"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)точно подтверждаю",
|
||||
"(?i)чистая прибыль.*подтвержден",
|
||||
"(?i)runtime_",
|
||||
"(?i)route_candidate",
|
||||
"(?i)primitive",
|
||||
"(?i)planner_",
|
||||
"(?i)catalog_"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"business_overview",
|
||||
"profit_margin_boundary",
|
||||
"missing_proof_families",
|
||||
"needs_route_enablement"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_04_debt_due_date_needs_payment_terms_route",
|
||||
"title": "Exact overdue/debt-aging ask becomes a proof-family enablement candidate",
|
||||
"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)не due-date|не точн|нельзя точно|не подтвержд|не доказ|нет срок",
|
||||
"(?i)договор|срок оплат|расчет|провер"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)точно подтверждаю",
|
||||
"(?i)просроч.*подтвержден",
|
||||
"(?i)runtime_",
|
||||
"(?i)route_candidate",
|
||||
"(?i)primitive",
|
||||
"(?i)planner_",
|
||||
"(?i)catalog_"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"business_overview",
|
||||
"debt_due_date_boundary",
|
||||
"missing_proof_families",
|
||||
"needs_route_enablement"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_05_vendor_risk_needs_procurement_quality_route",
|
||||
"title": "Exact vendor-risk ask becomes a proof-family enablement candidate",
|
||||
"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)proxy|не полный|не точн|нельзя точно|не подтвержд|не доказ"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)точно подтверждаю",
|
||||
"(?i)vendor.*risk.*подтвержден",
|
||||
"(?i)runtime_",
|
||||
"(?i)route_candidate",
|
||||
"(?i)primitive",
|
||||
"(?i)planner_",
|
||||
"(?i)catalog_"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"business_overview",
|
||||
"vendor_risk_procurement_boundary",
|
||||
"missing_proof_families",
|
||||
"needs_route_enablement"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_06_inventory_reserve_needs_quality_route",
|
||||
"title": "Exact reserve/liquidation ask becomes a proof-family enablement candidate",
|
||||
"question": "можно ли по ООО Альтернатива Плюс точно подтвердить резерв под неликвиды на складе?",
|
||||
"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)резерв.*подтвержден",
|
||||
"(?i)runtime_",
|
||||
"(?i)route_candidate",
|
||||
"(?i)primitive",
|
||||
"(?i)planner_",
|
||||
"(?i)catalog_"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"business_overview",
|
||||
"inventory_reserve_boundary",
|
||||
"missing_proof_families",
|
||||
"needs_route_enablement"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,221 +0,0 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase93_accounting_profit_margin_reviewed_route",
|
||||
"domain": "address_phase93_accounting_profit_margin_reviewed_route",
|
||||
"title": "Phase 93 accounting profit-margin reviewed route",
|
||||
"description": "Focused semantic replay for promoting accounting_profit_margin from needs_route_enablement to a reviewed 90/91/99 accounting financial-result route while preserving neighbouring proof-family boundaries.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_profit_margin_uses_accounting_result",
|
||||
"title": "Exact profit and margin ask uses reviewed 90/91/99 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)90/91/99|90\\.01|99",
|
||||
"(?i)учетн|финрезульт|прибыл|убыт",
|
||||
"(?i)марж|рентаб",
|
||||
"(?i)2020",
|
||||
"(?i)1С|проверенн|найденн|подтвержд"
|
||||
],
|
||||
"required_answer_patterns_any": [
|
||||
"7[\\s\\u00a0]*136[\\s\\u00a0]*815",
|
||||
"(?i)минус|убыт"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)только bounded operating-flow/trading-margin proxy",
|
||||
"(?i)route_candidate",
|
||||
"(?i)query_movements",
|
||||
"(?i)primitive",
|
||||
"(?i)planner_",
|
||||
"(?i)catalog_"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"business_overview",
|
||||
"profit_margin_boundary",
|
||||
"accounting_profit_margin",
|
||||
"ready_for_reviewed_execution"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_short_followup_keeps_accounting_context",
|
||||
"title": "Short follow-up keeps company, year, and accounting result context",
|
||||
"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)2020",
|
||||
"(?i)90/91/99|90\\.01|99|учетн"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните организац",
|
||||
"(?i)какую компанию",
|
||||
"(?i)route_candidate",
|
||||
"(?i)primitive",
|
||||
"(?i)planner_",
|
||||
"(?i)catalog_"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"business_overview",
|
||||
"context_carryover",
|
||||
"profit_margin_boundary",
|
||||
"accounting_profit_margin"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_debt_due_date_boundary_still_needs_route",
|
||||
"title": "Debt due-date boundary stays an enablement candidate",
|
||||
"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)не due-date|не точн|нельзя точно|не подтвержд|не доказ|нет срок",
|
||||
"(?i)договор|срок оплат|расчет|провер"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)точно подтверждаю",
|
||||
"(?i)просроч.*подтвержден",
|
||||
"(?i)route_candidate",
|
||||
"(?i)primitive",
|
||||
"(?i)planner_",
|
||||
"(?i)catalog_"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"business_overview",
|
||||
"debt_due_date_boundary",
|
||||
"missing_proof_families",
|
||||
"needs_route_enablement"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_04_vat_continuity_still_answers",
|
||||
"title": "VAT continuity still answers from the 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",
|
||||
"ready_for_reviewed_execution"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_05_value_flow_ranking_context_still_works",
|
||||
"title": "Value-flow ranking still uses carried 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",
|
||||
"ready_for_reviewed_execution"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_06_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",
|
||||
"needs_route_enablement"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,254 +0,0 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase94_debt_due_date_aging_reviewed_route",
|
||||
"domain": "address_phase94_debt_due_date_aging_reviewed_route",
|
||||
"title": "Phase 94 debt due-date aging reviewed route",
|
||||
"description": "Focused semantic replay for promoting debt_due_date_aging_quality from proxy-only enablement to a reviewed payment-term/open-balance route while preserving profit, VAT, inventory, and vendor-risk boundaries.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_debt_due_date_checked_negative",
|
||||
"title": "Debt due-date ask 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)не установлен|не подтвержд|не доказ|нет подтвержденной просроч",
|
||||
"(?i)1С|проверенн|найденн|подтвержд"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)точно подтверждаю.*просроч",
|
||||
"(?i)просроч.*подтвержден.*без.*срок",
|
||||
"(?i)route_candidate",
|
||||
"(?i)query_movements",
|
||||
"(?i)primitive",
|
||||
"(?i)planner_",
|
||||
"(?i)catalog_"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"business_overview",
|
||||
"debt_due_date_boundary",
|
||||
"debt_due_date_aging_quality",
|
||||
"ready_for_reviewed_execution",
|
||||
"checked_negative"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_short_followup_preserves_due_date_boundary",
|
||||
"title": "Short follow-up keeps debt due-date context",
|
||||
"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)срок.*оплат|due[- ]?date|договор",
|
||||
"(?i)не установлен|не хватает|не подтвержд|не доказ",
|
||||
"(?i)просроч",
|
||||
"(?i)2020|2020-12-31|конец 2020"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уточните организац",
|
||||
"(?i)какую компанию",
|
||||
"(?i)route_candidate",
|
||||
"(?i)primitive",
|
||||
"(?i)planner_",
|
||||
"(?i)catalog_"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"business_overview",
|
||||
"context_carryover",
|
||||
"debt_due_date_boundary",
|
||||
"checked_negative"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_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_04_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_05_value_flow_ranking_context_still_works",
|
||||
"title": "Value-flow ranking still uses carried 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_06_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"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_07_vendor_risk_boundary_still_needs_route",
|
||||
"title": "Vendor-risk boundary stays 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)поставщик|vendor|supplier|закуп",
|
||||
"(?i)не подтвержд|не доказ|нельзя точно|proxy|сигнал",
|
||||
"(?i)концентрац|зависим"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)точно подтверждаю",
|
||||
"(?i)route_candidate",
|
||||
"(?i)primitive",
|
||||
"(?i)planner_",
|
||||
"(?i)catalog_"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"business_overview",
|
||||
"vendor_risk_procurement_boundary",
|
||||
"missing_proof_families",
|
||||
"canary"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,249 +0,0 @@
|
|||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase96_inventory_reserve_liquidation_quality",
|
||||
"domain": "address_phase96_inventory_reserve_liquidation_quality",
|
||||
"title": "Phase 96 inventory reserve/liquidation quality-events replay",
|
||||
"description": "Targeted replay for the reviewed inventory quality-events route: direct reserve/write-off/liquidation questions should trigger checked 1C document evidence for write-offs, receipt adjustments, stocktaking, and retail revaluation, while still refusing to invent market liquidation value or management reserve policy.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_direct_inventory_reserve_liquidation_boundary",
|
||||
"title": "Direct reserve/liquidation boundary uses reviewed inventory quality events",
|
||||
"question": "По ООО Альтернатива Плюс за 2020 год проверь склад: были ли списания товаров, оприходования/корректировки, инвентаризации, переоценки, резервы под неликвиды или ликвидационная стоимость? Скажи коротко и честно, что подтверждено 1С, а что нельзя утверждать.",
|
||||
"expected_catalog_alignment_status": "selected_matches_top",
|
||||
"expected_catalog_chain_top_match": "business_overview",
|
||||
"expected_catalog_selected_matches_top": true,
|
||||
"allowed_reply_types": [
|
||||
"partial_coverage",
|
||||
"factual_with_explanation"
|
||||
],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)альтернатива|организац|компани",
|
||||
"(?i)2020|2020 год",
|
||||
"(?i)склад|товар",
|
||||
"(?i)списан|оприход|инвентаризац|переоцен",
|
||||
"(?i)провер|подтвержд|1с",
|
||||
"(?i)не рыночн|не ликвидационн|не управленческ|не резерв"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)business_overview_route_template_v1|inventory_quality_events_profile",
|
||||
"(?i)query_movements|query_documents|primitive|planner_|runtime_|pilot_",
|
||||
"(?i)ликвидационная стоимость составляет|резерв составляет|подтвержденный резерв под неликвид"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"inventory_reserve_liquidation_quality",
|
||||
"reviewed_inventory_quality_events",
|
||||
"direct_answer_first",
|
||||
"no_market_liquidation_overclaim"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_followup_inventory_liquidity_boundary",
|
||||
"title": "Follow-up does not turn checked quality events into full liquidity claim",
|
||||
"question": "А по этим же данным можно сказать, что склад ликвидный и неликвидов нет?",
|
||||
"expected_catalog_alignment_status": "selected_matches_top",
|
||||
"expected_catalog_chain_top_match": "business_overview",
|
||||
"expected_catalog_selected_matches_top": true,
|
||||
"allowed_reply_types": [
|
||||
"partial_coverage",
|
||||
"factual_with_explanation"
|
||||
],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)склад|товар|остат",
|
||||
"(?i)ликвид|неликвид",
|
||||
"(?i)не подтвержд|нельзя|не значит|отдельн",
|
||||
"(?i)списан|инвентаризац|переоцен|quality|событ"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)business_overview_route_template_v1|inventory_quality_events_profile",
|
||||
"(?i)query_movements|query_documents|primitive|planner_|runtime_|pilot_",
|
||||
"(?i)склад ликвидный$|неликвидов нет$|ликвидность подтверждена"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"inventory_liquidity_boundary",
|
||||
"followup_context_carryover",
|
||||
"no_inventory_health_overclaim"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,145 +0,0 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase97_financial_counterparty_flow_hints",
|
||||
"domain": "address_phase97_financial_counterparty_flow_hints",
|
||||
"title": "Phase 97 financial counterparty flow hints replay",
|
||||
"description": "Focused semantic replay for the Open-World Schema/Primitive Discovery slice: bank-like counterparties must not be described as ordinary suppliers/customers when operation, payment purpose, contract, or comment fields indicate banking commission, credit, deposit, tax/budget, or payroll-like flows. The replay also keeps a normal counterparty value-flow canary.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_sberbank_outgoing_is_not_plain_supplier",
|
||||
"title": "Sberbank outgoing money is explained as bank flow, not ordinary supplier dependency",
|
||||
"question": "По ООО Альтернатива Плюс за 2020 отдельно посмотри платежи в СБЕРБАНК: это обычный поставщик или банковский/финансовый поток? Дай коротко и по проверенным данным 1С.",
|
||||
"allowed_reply_types": [
|
||||
"factual",
|
||||
"factual_with_explanation",
|
||||
"partial_coverage"
|
||||
],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)сбербанк|банк|финансов",
|
||||
"(?i)не.*обычн.*поставщик|не.*поставщик|банковск|финансов",
|
||||
"(?i)комисс|назначени|вид операции|платеж|списан",
|
||||
"(?i)2020|1с|провер"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)главный поставщик.*сбербанк",
|
||||
"(?i)обычный поставщик.*сбербанк",
|
||||
"(?i)route_candidate",
|
||||
"(?i)primitive",
|
||||
"(?i)planner_",
|
||||
"(?i)catalog_",
|
||||
"(?i)snapshot_items",
|
||||
"(?i)answer_object"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"financial_counterparty_flow_hint",
|
||||
"bank_like_supplier_boundary",
|
||||
"supplier_payouts_profile"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_sberbank_incoming_is_not_plain_customer",
|
||||
"title": "Sberbank incoming money is not overclaimed as normal customer revenue",
|
||||
"question": "А если по этой же компании СБЕРБАНК встречается во входящих поступлениях, это клиентская выручка или там может быть кредитный/депозитный банковский смысл? Не притягивай, скажи что подтверждено.",
|
||||
"allowed_reply_types": [
|
||||
"factual",
|
||||
"factual_with_explanation",
|
||||
"partial_coverage"
|
||||
],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)сбербанк|банк|финансов",
|
||||
"(?i)не.*клиентск|не.*обычн.*клиент|не.*выручк|кредит|депозит|возврат",
|
||||
"(?i)вид операции|договор|поступлен|1с|провер",
|
||||
"(?i)подтвержд|не подтвержд|не доказ"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)сбербанк.*главный клиент",
|
||||
"(?i)сбербанк.*обычный клиент",
|
||||
"(?i)route_candidate",
|
||||
"(?i)primitive",
|
||||
"(?i)planner_",
|
||||
"(?i)catalog_",
|
||||
"(?i)snapshot_items",
|
||||
"(?i)answer_object"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"financial_counterparty_flow_hint",
|
||||
"bank_like_customer_boundary",
|
||||
"customer_revenue_and_payments"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_business_overview_keeps_bank_boundary",
|
||||
"title": "Business overview keeps bank-like leaders bounded by flow meaning",
|
||||
"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,
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)альтернатива|компан|организац",
|
||||
"(?i)входящ|поступлен|исходящ|списан|нетто",
|
||||
"(?i)банк|финансов|сбербанк|не.*обычн.*клиент|не.*обычн.*поставщик",
|
||||
"(?i)прибыл|марж|не подтвержд|не доказ|не является"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)сбербанк.*обычный поставщик",
|
||||
"(?i)сбербанк.*обычный клиент",
|
||||
"(?i)чистая прибыль.*точно",
|
||||
"(?i)route_candidate",
|
||||
"(?i)primitive",
|
||||
"(?i)planner_",
|
||||
"(?i)catalog_",
|
||||
"(?i)snapshot_items",
|
||||
"(?i)answer_object"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"business_overview",
|
||||
"financial_counterparty_flow_hint",
|
||||
"profit_margin_boundary"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_04_normal_counterparty_value_flow_canary",
|
||||
"title": "Normal counterparty value-flow still works after bank-flow questions",
|
||||
"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_comparison",
|
||||
"expected_catalog_selected_matches_top": true,
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)свк|группа",
|
||||
"(?i)2020",
|
||||
"(?i)получил|входящ|поступлен",
|
||||
"(?i)заплат|исходящ|списан",
|
||||
"(?i)нетто|сальдо|разниц"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)сбербанк",
|
||||
"(?i)уточните организац",
|
||||
"(?i)какую компанию",
|
||||
"(?i)route_candidate",
|
||||
"(?i)primitive",
|
||||
"(?i)planner_",
|
||||
"(?i)catalog_"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"counterparty_net_cash_flow",
|
||||
"stale_scope_guard",
|
||||
"canary"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,205 +0,0 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase98_limit_honesty_business_language",
|
||||
"domain": "address_phase98_limit_honesty_business_language",
|
||||
"title": "Phase 98 limit honesty and business-language replay",
|
||||
"description": "Focused semantic replay from assistant-stage1-v2qsm_R0fF: answers may be bounded, but they must stay business-readable, direct-first, and must not leak MCP/proxy/route/debug wording when explaining row limits, incomplete coverage, debt due-date proof, inventory reserve proof, supplier dependency, VAT, or bank-like counterparties.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_debt_overdue_exactness_boundary",
|
||||
"title": "Overdue receivables answer explains exactness boundary in business language",
|
||||
"question": "По ООО Альтернатива Плюс на конец 2020 можно точно понять, какая дебиторка просрочена?",
|
||||
"allowed_reply_types": [
|
||||
"factual",
|
||||
"factual_with_explanation",
|
||||
"partial_coverage"
|
||||
],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)альтернатива",
|
||||
"(?i)2020|31\\.12\\.2020|конец 2020",
|
||||
"(?i)дебитор|расчет|долг",
|
||||
"(?i)срок оплат|просроч|не подтвержд|нельзя точно"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)\\bMCP\\b",
|
||||
"(?i)proxy",
|
||||
"(?i)route_candidate",
|
||||
"(?i)primitive",
|
||||
"(?i)planner_",
|
||||
"(?i)catalog_",
|
||||
"(?i)уперл.*лимит",
|
||||
"(?i)лимит выборки"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"debt_due_date_aging_quality",
|
||||
"limit_honesty",
|
||||
"business_language"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_short_followup_keeps_short_business_answer",
|
||||
"title": "Short why follow-up stays short and avoids repeated technical limitation text",
|
||||
"question": "То есть просрочку доказать нельзя, коротко почему?",
|
||||
"allowed_reply_types": [
|
||||
"factual",
|
||||
"factual_with_explanation",
|
||||
"partial_coverage"
|
||||
],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)коротко|нельзя|не подтвержд|не доказ",
|
||||
"(?i)срок оплат|дата оплат|услови",
|
||||
"(?i)просроч"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)\\bMCP\\b",
|
||||
"(?i)proxy",
|
||||
"(?i)route_candidate",
|
||||
"(?i)primitive",
|
||||
"(?i)planner_",
|
||||
"(?i)catalog_",
|
||||
"(?i)уперл.*лимит",
|
||||
"(?i)лимит выборки"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"followup_directness",
|
||||
"debt_due_date_aging_quality",
|
||||
"business_language"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_vat_direct_answer_without_debug_leak",
|
||||
"title": "VAT answer remains direct and does not leak debug mechanics",
|
||||
"question": "НДС за 2020 по ООО Альтернатива Плюс какой?",
|
||||
"allowed_reply_types": [
|
||||
"factual",
|
||||
"factual_with_explanation",
|
||||
"partial_coverage"
|
||||
],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)ндс",
|
||||
"(?i)2020",
|
||||
"(?i)альтернатива",
|
||||
"(?i)к уплат|нетто|книга продаж|книга покупок|вычет"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)\\bMCP\\b",
|
||||
"(?i)proxy",
|
||||
"(?i)route_candidate",
|
||||
"(?i)primitive",
|
||||
"(?i)planner_",
|
||||
"(?i)catalog_",
|
||||
"(?i)snapshot_items",
|
||||
"(?i)answer_object"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"vat",
|
||||
"business_language",
|
||||
"debug_leak_guard"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_04_top_incoming_money_keeps_bank_boundary",
|
||||
"title": "Top incoming money answer keeps bank boundary and avoids ordinary-customer overclaim",
|
||||
"question": "А кто принес больше всего денег за 2020?",
|
||||
"allowed_reply_types": [
|
||||
"factual",
|
||||
"factual_with_explanation",
|
||||
"partial_coverage"
|
||||
],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)2020",
|
||||
"(?i)деньг|поступлен|входящ",
|
||||
"(?i)сбербанк|свк|контрагент",
|
||||
"(?i)банк|не.*обычн|не.*клиент|финансов|провер"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)сбербанк.*обычн.*клиент",
|
||||
"(?i)сбербанк.*главн.*клиент",
|
||||
"(?i)\\bMCP\\b",
|
||||
"(?i)proxy",
|
||||
"(?i)route_candidate",
|
||||
"(?i)primitive",
|
||||
"(?i)planner_",
|
||||
"(?i)catalog_",
|
||||
"(?i)уперл.*лимит",
|
||||
"(?i)лимит выборки"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"financial_counterparty_flow_hint",
|
||||
"customer_revenue_and_payments",
|
||||
"limit_honesty"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_05_inventory_reserve_boundary_business_language",
|
||||
"title": "Inventory reserve boundary uses business terms, not proxy jargon",
|
||||
"question": "По ООО Альтернатива Плюс на конец 2020 можно точно подтвердить резерв под неликвиды на складе?",
|
||||
"allowed_reply_types": [
|
||||
"factual",
|
||||
"factual_with_explanation",
|
||||
"partial_coverage"
|
||||
],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)резерв|неликвид|склад",
|
||||
"(?i)не подтвержд|нельзя точно|точно.*нельзя|нет подтвержден",
|
||||
"(?i)списан|резерв|ликвидац|залежал|стар"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)\\bMCP\\b",
|
||||
"(?i)proxy",
|
||||
"(?i)route_candidate",
|
||||
"(?i)primitive",
|
||||
"(?i)planner_",
|
||||
"(?i)catalog_",
|
||||
"(?i)уперл.*лимит",
|
||||
"(?i)лимит выборки"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"inventory_reserve_liquidation_quality",
|
||||
"business_language",
|
||||
"limit_honesty"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_06_supplier_dependency_bank_boundary",
|
||||
"title": "Supplier dependency answer separates bank-like recipient from supplier dependency",
|
||||
"question": "А зависимость от одного поставщика за 2020 можно точно оценить?",
|
||||
"allowed_reply_types": [
|
||||
"factual",
|
||||
"factual_with_explanation",
|
||||
"partial_coverage"
|
||||
],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)поставщик|зависим|закуп|исходящ",
|
||||
"(?i)2020",
|
||||
"(?i)крупнейш|получател|концентрац",
|
||||
"(?i)точн|не подтвержд|не доказ|не полностью",
|
||||
"(?i)надежност|качество|договор|полная структура"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)сбербанк.*обычн.*поставщик",
|
||||
"(?i)сбербанк.*главн.*поставщик",
|
||||
"(?i)\\bMCP\\b",
|
||||
"(?i)proxy",
|
||||
"(?i)route_candidate",
|
||||
"(?i)primitive",
|
||||
"(?i)planner_",
|
||||
"(?i)catalog_",
|
||||
"(?i)уперл.*лимит",
|
||||
"(?i)лимит выборки"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"vendor_risk_procurement_quality",
|
||||
"financial_counterparty_flow_hint",
|
||||
"business_language"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,144 +0,0 @@
|
|||
{
|
||||
"schema_version": "domain_truth_harness_spec_v1",
|
||||
"scenario_id": "address_truth_harness_phase99_large_query_budget_continuation",
|
||||
"domain": "address_phase99_large_query_budget_continuation",
|
||||
"title": "Phase 99 large-query budget and continuation policy replay",
|
||||
"description": "Focused semantic replay for explicit-year large business questions: the assistant should use chunked 1C evidence where available, answer direct-first, avoid fake profit claims, keep bank/counterparty boundaries, and not collapse into row-limit refusal wording when yearly money coverage can be recovered.",
|
||||
"bindings": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_year_business_overview_uses_chunked_money_evidence",
|
||||
"title": "Explicit-year business overview gives direct money summary without row-limit refusal",
|
||||
"question": "Дай взрослый бизнес-обзор ООО Альтернатива Плюс за 2020: входящие, исходящие, нетто, кто основные источники денег и где важные ограничения.",
|
||||
"allowed_reply_types": [
|
||||
"factual",
|
||||
"factual_with_explanation",
|
||||
"partial_coverage"
|
||||
],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)альтернатива",
|
||||
"(?i)2020",
|
||||
"(?i)входящ|поступлен",
|
||||
"(?i)исходящ|списан|платеж",
|
||||
"(?i)нетто|денежн",
|
||||
"(?i)прибыл|не.*прибыл|не.*бухгалтерск"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)уп[её]р.*лимит",
|
||||
"(?i)лимит выборки",
|
||||
"(?i)лимит строк",
|
||||
"(?i)\\bMCP\\b",
|
||||
"(?i)route_candidate",
|
||||
"(?i)primitive",
|
||||
"(?i)planner_",
|
||||
"(?i)catalog_",
|
||||
"(?i)snapshot_items",
|
||||
"(?i)answer_object"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"business_overview",
|
||||
"large_query_budget",
|
||||
"direct_answer_first"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_followup_profit_boundary_stays_short",
|
||||
"title": "Follow-up keeps cash-flow versus profit boundary",
|
||||
"question": "То есть это можно считать прибылью за 2020 или нет? Коротко.",
|
||||
"allowed_reply_types": [
|
||||
"factual",
|
||||
"factual_with_explanation",
|
||||
"partial_coverage"
|
||||
],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)нет|нельзя|не стоит",
|
||||
"(?i)прибыл",
|
||||
"(?i)денежн|поступлен|поток",
|
||||
"(?i)себестоим|расход|финрезультат|бухгалтерск"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)\\bMCP\\b",
|
||||
"(?i)route_candidate",
|
||||
"(?i)primitive",
|
||||
"(?i)planner_",
|
||||
"(?i)catalog_",
|
||||
"(?i)snapshot_items",
|
||||
"(?i)answer_object"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"profit_boundary",
|
||||
"followup_directness",
|
||||
"business_language"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_top_money_source_keeps_bank_boundary",
|
||||
"title": "Top money source keeps bank boundary after broad overview",
|
||||
"question": "А кто за 2020 принес больше всего денег, и если там банк, не называй его обычным клиентом.",
|
||||
"allowed_reply_types": [
|
||||
"factual",
|
||||
"factual_with_explanation",
|
||||
"partial_coverage"
|
||||
],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)2020",
|
||||
"(?i)деньг|поступлен|входящ",
|
||||
"(?i)сбербанк|банк|финансов|не.*обычн|не.*клиент"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)сбербанк.*обычн.*клиент",
|
||||
"(?i)сбербанк.*главн.*клиент",
|
||||
"(?i)уп[её]р.*лимит",
|
||||
"(?i)лимит выборки",
|
||||
"(?i)лимит строк",
|
||||
"(?i)\\bMCP\\b",
|
||||
"(?i)route_candidate",
|
||||
"(?i)primitive",
|
||||
"(?i)planner_",
|
||||
"(?i)catalog_"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"financial_counterparty_flow_hint",
|
||||
"large_query_budget",
|
||||
"context_continuity"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_04_supplier_dependency_uses_checked_outgoing_scope",
|
||||
"title": "Supplier-dependency question uses checked outgoing scope and avoids hard audit overclaim",
|
||||
"question": "По исходящим за 2020 есть зависимость от одного поставщика или это только денежная концентрация?",
|
||||
"allowed_reply_types": [
|
||||
"factual",
|
||||
"factual_with_explanation",
|
||||
"partial_coverage"
|
||||
],
|
||||
"required_answer_patterns_all": [
|
||||
"(?i)2020",
|
||||
"(?i)исходящ|платеж|списан",
|
||||
"(?i)поставщик|получател|концентрац",
|
||||
"(?i)не.*аудит|не.*полный|не.*надежност|не.*качество"
|
||||
],
|
||||
"forbidden_answer_patterns": [
|
||||
"(?i)сбербанк.*обычн.*поставщик",
|
||||
"(?i)сбербанк.*главн.*поставщик",
|
||||
"(?i)уп[её]р.*лимит",
|
||||
"(?i)лимит выборки",
|
||||
"(?i)лимит строк",
|
||||
"(?i)\\bMCP\\b",
|
||||
"(?i)route_candidate",
|
||||
"(?i)primitive",
|
||||
"(?i)planner_",
|
||||
"(?i)catalog_"
|
||||
],
|
||||
"criticality": "critical",
|
||||
"semantic_tags": [
|
||||
"vendor_risk_procurement_quality",
|
||||
"large_query_budget",
|
||||
"business_language"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -77,7 +77,7 @@ exports.FEATURE_ASSISTANT_LIVING_CHAT_ROUTER_V1 = toBooleanFlag(process.env.FEAT
|
|||
exports.ASSISTANT_MCP_PROXY_URL = (process.env.ASSISTANT_MCP_PROXY_URL ?? "http://127.0.0.1:6003").replace(/\/+$/, "");
|
||||
exports.ASSISTANT_MCP_CHANNEL = process.env.ASSISTANT_MCP_CHANNEL ?? "default";
|
||||
exports.ASSISTANT_MCP_TIMEOUT_MS = toNumberFlag(process.env.ASSISTANT_MCP_TIMEOUT_MS, 6000);
|
||||
exports.ASSISTANT_MCP_LIVE_LIMIT = Math.max(1, Math.trunc(toNumberFlag(process.env.ASSISTANT_MCP_LIVE_LIMIT, 128)));
|
||||
exports.ASSISTANT_MCP_LIVE_LIMIT = Math.max(1, Math.trunc(toNumberFlag(process.env.ASSISTANT_MCP_LIVE_LIMIT, 24)));
|
||||
exports.VAT_PAYABLE_68_PREFIXES = toStringListFlag(process.env.VAT_PAYABLE_68_PREFIXES, ["68.02"]);
|
||||
exports.VAT_PAYABLE_19_PREFIXES = toStringListFlag(process.env.VAT_PAYABLE_19_PREFIXES, ["19"]);
|
||||
exports.DATA_DIR = process.env.DATA_DIR ?? path_1.default.resolve(exports.MODULE_ROOT, "data");
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ function parseAutoGenTitle(value) {
|
|||
if (!title) {
|
||||
return null;
|
||||
}
|
||||
return repairAutogenMojibake(title).slice(0, 160);
|
||||
return title.slice(0, 160);
|
||||
}
|
||||
function parseManualCaseDecision(value, fallback = "needs_dialog_policy_fix") {
|
||||
const normalized = toStringSafe(value);
|
||||
|
|
@ -1186,9 +1186,7 @@ function textMojibakeScore(value) {
|
|||
const hardMarkers = (source.match(/[Ѓѓ‚„…†‡€‰‹ЉЊЌЋЏ‘’“”•–—™љ›њќћџ]/g) ?? []).length;
|
||||
const pairMarkers = (source.match(/(?:Р.|С.|Ð.|Ñ.)/g) ?? []).length;
|
||||
const doubleEncodedMarkers = (source.match(/(?:Г[Ђ-џ]|В[Ђ-џ]|Ã.|Â.)/gu) ?? []).length;
|
||||
const replacement = (source.match(/\uFFFD/g) ?? []).length;
|
||||
const c1Controls = (source.match(/[\u0080-\u009f]/g) ?? []).length;
|
||||
return cyrillic + latin - replacement * 8 - c1Controls * 5 - hardMarkers * 3 - pairMarkers * 2 - doubleEncodedMarkers * 2;
|
||||
return cyrillic + latin - hardMarkers * 3 - pairMarkers * 2 - doubleEncodedMarkers * 2;
|
||||
}
|
||||
function looksLikeMojibake(value) {
|
||||
const source = String(value ?? "");
|
||||
|
|
@ -1203,36 +1201,16 @@ function looksLikeMojibake(value) {
|
|||
}
|
||||
return (source.match(/(?:Г[Ђ-џ]|В[Ђ-џ]|Ã.|Â.)/gu) ?? []).length >= 2;
|
||||
}
|
||||
function encodeWin1251MojibakeBytes(value) {
|
||||
const chunks = [];
|
||||
for (const char of String(value ?? "")) {
|
||||
const code = char.codePointAt(0) ?? 0;
|
||||
if (code >= 0x80 && code <= 0x9f) {
|
||||
chunks.push(Buffer.from([code]));
|
||||
continue;
|
||||
}
|
||||
chunks.push(iconv_lite_1.default.encode(char, "win1251"));
|
||||
}
|
||||
return Buffer.concat(chunks);
|
||||
}
|
||||
function decodeUtf8FromWin1251Mojibake(value) {
|
||||
return encodeWin1251MojibakeBytes(value).toString("utf8");
|
||||
}
|
||||
function repairKnownReplacementDamagedAutogenText(value) {
|
||||
return String(value ?? "")
|
||||
.replace(/\u0410\u041b\u042c\u0422\u0415\u0420\u041d\u0410\u0422[\uFFFD?]+\u0412\u0410/giu, "\u0410\u041b\u042c\u0422\u0415\u0420\u041d\u0410\u0422\u0418\u0412\u0410")
|
||||
.replace(/\u041e\u0411\u0429[\uFFFD?]+\u0419/giu, "\u041e\u0411\u0429\u0418\u0419");
|
||||
}
|
||||
function repairAutogenMojibake(value) {
|
||||
const source = repairKnownReplacementDamagedAutogenText(String(value ?? ""));
|
||||
if (!looksLikeMojibake(source) && !/[\u0080-\u009f\uFFFD]/.test(source)) {
|
||||
const source = String(value ?? "");
|
||||
if (!looksLikeMojibake(source)) {
|
||||
return source;
|
||||
}
|
||||
let candidate = source;
|
||||
for (let pass = 0; pass < 3; pass += 1) {
|
||||
let improved = false;
|
||||
try {
|
||||
const fromWin1251 = decodeUtf8FromWin1251Mojibake(candidate);
|
||||
const fromWin1251 = iconv_lite_1.default.encode(candidate, "win1251").toString("utf8");
|
||||
if (textMojibakeScore(fromWin1251) > textMojibakeScore(candidate)) {
|
||||
candidate = fromWin1251;
|
||||
improved = true;
|
||||
|
|
@ -1251,16 +1229,11 @@ function repairAutogenMojibake(value) {
|
|||
catch {
|
||||
// ignore
|
||||
}
|
||||
const repairedKnownText = repairKnownReplacementDamagedAutogenText(candidate);
|
||||
if (repairedKnownText !== candidate) {
|
||||
candidate = repairedKnownText;
|
||||
improved = true;
|
||||
}
|
||||
if (!improved) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return repairKnownReplacementDamagedAutogenText(candidate);
|
||||
return candidate;
|
||||
}
|
||||
function sanitizeGeneratedQuestion(value) {
|
||||
return repairAutogenMojibake(String(value ?? ""))
|
||||
|
|
@ -1459,8 +1432,7 @@ function extractQuestionsFromAutogenOutput(rawText) {
|
|||
}
|
||||
exports.__autoRunsQuestionTestUtils = {
|
||||
splitQuestionCandidates,
|
||||
extractQuestionsFromAutogenOutput,
|
||||
repairAutogenMojibake
|
||||
extractQuestionsFromAutogenOutput
|
||||
};
|
||||
async function generateQwenSeedQuestionsLive(input) {
|
||||
const seedExamples = collectCanonicalQuestions(40);
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ const nanoid_1 = require("nanoid");
|
|||
const express_1 = require("express");
|
||||
const config_1 = require("../config");
|
||||
const http_1 = require("../utils/http");
|
||||
const addressTextRepair_1 = require("../services/addressTextRepair");
|
||||
const ASYNC_JOBS = new Map();
|
||||
const MAX_ASYNC_JOBS = 80;
|
||||
function toRecord(value) {
|
||||
|
|
@ -31,7 +30,7 @@ function toArray(value) {
|
|||
return Array.isArray(value) ? value : [];
|
||||
}
|
||||
function normalizeQuestionChunk(value) {
|
||||
return (0, addressTextRepair_1.repairAddressMojibakeText)(String(value ?? ""))
|
||||
return String(value ?? "")
|
||||
.replace(/\r/g, " ")
|
||||
.replace(/\t/g, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
|
|
@ -93,7 +92,7 @@ function normalizeRuntimeQuestionList(items) {
|
|||
return normalized.filter((item) => item.length > 0);
|
||||
}
|
||||
function splitQuestionCandidate(raw) {
|
||||
const normalized = (0, addressTextRepair_1.repairAddressMojibakeText)(String(raw ?? "")).replace(/\r/g, "\n").trim();
|
||||
const normalized = String(raw ?? "").replace(/\r/g, "\n").trim();
|
||||
if (!normalized) {
|
||||
return [];
|
||||
}
|
||||
|
|
@ -456,8 +455,7 @@ function buildEvalRouter(services) {
|
|||
}
|
||||
const questions = normalizeRuntimeQuestions(body.questions);
|
||||
const scenarioQuestions = normalizeRuntimeQuestions(body.scenarioQuestions, { dedupe: false, splitCandidates: false });
|
||||
const scenarioTitleRaw = toStringSafe(body.scenarioTitle);
|
||||
const scenarioTitle = scenarioTitleRaw ? (0, addressTextRepair_1.repairAddressMojibakeText)(scenarioTitleRaw) : null;
|
||||
const scenarioTitle = toStringSafe(body.scenarioTitle);
|
||||
const jobId = `job-${(0, nanoid_1.nanoid)(10)}`;
|
||||
const runId = `assistant-stage1-${(0, nanoid_1.nanoid)(10)}`;
|
||||
const runtimeCaseSetFile = scenarioQuestions.length > 0
|
||||
|
|
|
|||
|
|
@ -13,24 +13,6 @@ const ACCOUNT_REVERSE_PATTERN = /(?:^|[\s,.;:!?()\-])(\d{2}(?:[.,]\d{1,2})?)(?=\
|
|||
const LIMIT_PATTERN = /(?:\btop\b|\blimit\b|первые|топ)[\s\-–—_:№#]*?(\d{1,3})/iu;
|
||||
const VALUE_ANALYTICS_SAMPLE_LIMIT = 1000;
|
||||
const COUNTERPARTY_PATTERN = /(?:по\s+контрагенту|контрагент(?:у|а)?|по\s+контре|контра|по\s+компан(?:ии|ию|ия)|компан(?:ия|ии|ию)|по\s+организац(?:ии|ию|ия)|организац(?:ия|ии|ию)|по\s+поставщик(?:у|а)?|поставщик(?:у|а)?|по\s+клиент(?:у|а)?|клиент(?:у|а)?|по\s+покупател(?:ю|я)|покупател(?:ю|я)|по\s+партнер(?:у|а)?|партнер(?:у|а)?|by\s+counterparty|counterparty|by\s+company|company|by\s+supplier|supplier|by\s+vendor|vendor|by\s+customer|customer|by\s+client|client|by\s+partner|partner)\s+([^\r\n,.;:]+)/iu;
|
||||
const KNOWN_FINANCIAL_COUNTERPARTY_ANCHORS = [
|
||||
{
|
||||
pattern: /(?<![\p{L}\p{N}])(?:\u043f\u0430\u043e\s+|\u0430\u043e\s+)?\u0441\u0431\u0435\u0440\u0431\u0430\u043d\u043a(?:\s*,?\s*(?:\u043f\u0430\u043e|\u0430\u043e))?(?![\p{L}\p{N}])/iu,
|
||||
value: "\u0421\u0411\u0415\u0420\u0411\u0410\u041d\u041a"
|
||||
},
|
||||
{
|
||||
pattern: /(?<![\p{L}\p{N}])(?:\u0431\u0430\u043d\u043a\s+)?\u0432\u0442\u0431(?![\p{L}\p{N}])/iu,
|
||||
value: "\u0412\u0422\u0411"
|
||||
},
|
||||
{
|
||||
pattern: /(?<![\p{L}\p{N}])\u0430\u043b\u044c\u0444\u0430[-\s]?\u0431\u0430\u043d\u043a(?![\p{L}\p{N}])/iu,
|
||||
value: "\u0410\u041b\u042c\u0424\u0410-\u0411\u0410\u041d\u041a"
|
||||
},
|
||||
{
|
||||
pattern: /(?<![\p{L}\p{N}])(?:\u0442\u0438\u043d\u044c\u043a\u043e\u0444\u0444|\u0442[-\s]?\u0431\u0430\u043d\u043a)(?![\p{L}\p{N}])/iu,
|
||||
value: "\u0422-\u0411\u0410\u041d\u041a"
|
||||
}
|
||||
];
|
||||
const CONTRACT_PATTERN = /(?:по\s+(?:договору|контракту)|(?:договор|контракт)(?:у|а)?\s*(?:№|#|n)?|by\s+contract|contract(?:\s*(?:no|number|#|n))?)\s+([^\r\n,.;:]+)/i;
|
||||
const DATE_DMY_PATTERN = /\b(\d{1,2})[.\/-](\d{1,2})[.\/-](\d{2,4})\b/;
|
||||
const DATE_YMD_PATTERN = /\b(20\d{2})[.\/-](\d{1,2})[.\/-](\d{1,2})\b/;
|
||||
|
|
@ -626,18 +608,6 @@ function isLikelyCounterpartyToken(rawToken) {
|
|||
}
|
||||
return !isCounterpartyNoiseToken(lowered);
|
||||
}
|
||||
function extractKnownFinancialCounterpartyAnchor(text) {
|
||||
const source = String(text ?? "");
|
||||
if (!source.trim()) {
|
||||
return undefined;
|
||||
}
|
||||
for (const anchor of KNOWN_FINANCIAL_COUNTERPARTY_ANCHORS) {
|
||||
if (anchor.pattern.test(source)) {
|
||||
return anchor.value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
function isLowQualityCounterpartyAnchorValue(rawValue) {
|
||||
const value = String(rawValue ?? "")
|
||||
.trim()
|
||||
|
|
@ -649,9 +619,6 @@ function isLowQualityCounterpartyAnchorValue(rawValue) {
|
|||
if (/(?:за\s+вс[её]\s+время|за\s+всю\s+истори(?:ю|и)|all\s+time|entire\s+period|full\s+history)/iu.test(value)) {
|
||||
return true;
|
||||
}
|
||||
if (/^(?:или|это|там|может|можно|обычн\p{L}*|клиентск\p{L}*|банковск\p{L}*(?:\/|\s+и\s+)?финансов\p{L}*)\b/iu.test(value)) {
|
||||
return true;
|
||||
}
|
||||
const tokens = value
|
||||
.split(/[^a-zа-я0-9]+/iu)
|
||||
.map((token) => token.trim())
|
||||
|
|
@ -715,33 +682,6 @@ function isLowQualityCounterpartyAnchorValue(rawValue) {
|
|||
const isLowQualityTimeToken = (token) => lowQualityTimeTokens.has(token) ||
|
||||
/^(?:январ|феврал|март|апрел|ма(?:й|я|е)|июн|июл|август|сентябр|октябр|ноябр|декабр)/iu.test(token);
|
||||
const lowQualityGenericTokens = new Set([
|
||||
"или",
|
||||
"обычный",
|
||||
"обычная",
|
||||
"обычное",
|
||||
"обычные",
|
||||
"обычного",
|
||||
"обычному",
|
||||
"обычным",
|
||||
"контрагент",
|
||||
"контрагента",
|
||||
"контрагенту",
|
||||
"клиент",
|
||||
"клиента",
|
||||
"клиенту",
|
||||
"клиентом",
|
||||
"клиенты",
|
||||
"поставщик",
|
||||
"поставщика",
|
||||
"поставщику",
|
||||
"поставщиком",
|
||||
"поставщики",
|
||||
"покупатель",
|
||||
"покупателя",
|
||||
"покупателю",
|
||||
"заказчик",
|
||||
"заказчика",
|
||||
"заказчику",
|
||||
"деньги",
|
||||
"денег",
|
||||
"деньгам",
|
||||
|
|
@ -1267,10 +1207,6 @@ function isLowQualityWarehouseAnchorValue(rawValue) {
|
|||
"лежали",
|
||||
"на",
|
||||
"по",
|
||||
"остатка",
|
||||
"остаткам",
|
||||
"остатками",
|
||||
"остатков",
|
||||
"компания",
|
||||
"компании",
|
||||
"компанию",
|
||||
|
|
@ -1351,7 +1287,7 @@ function extractInventoryWarehouseAnchor(text) {
|
|||
isLowQualityWarehouseAnchorValue(candidate) ||
|
||||
normalizedCandidate.startsWith("по состоянию") ||
|
||||
isTemporalWarehousePhrase(candidate) ||
|
||||
/^(?:сейчас|на|дату|дате|остат(?:ок|ки|ка|кам|ками|ков)|по\s+остат(?:кам|ки|ку|ка|ков))$/iu.test(candidate)) {
|
||||
/^(?:сейчас|на|дату|дате|остаток|остатки)$/iu.test(candidate)) {
|
||||
continue;
|
||||
}
|
||||
return candidate;
|
||||
|
|
@ -1559,13 +1495,6 @@ function shouldExpandSampleForValueAnalytics(intent) {
|
|||
intent === "supplier_payouts_profile" ||
|
||||
intent === "contract_usage_and_value");
|
||||
}
|
||||
function shouldPreferKnownFinancialCounterpartyAnchor(intent) {
|
||||
return (intent === "bank_operations_by_counterparty" ||
|
||||
intent === "customer_revenue_and_payments" ||
|
||||
intent === "supplier_payouts_profile" ||
|
||||
intent === "list_documents_by_counterparty" ||
|
||||
intent === "list_contracts_by_counterparty");
|
||||
}
|
||||
function extractAddressFilters(userMessage, intent) {
|
||||
const rawText = String(userMessage ?? "").trim();
|
||||
const text = normalizeMojibakeString(rawText);
|
||||
|
|
@ -1644,13 +1573,6 @@ function extractAddressFilters(userMessage, intent) {
|
|||
}
|
||||
}
|
||||
const allowGenericCounterpartyAnchor = !isInventoryTraceIntent(intent);
|
||||
const knownFinancialCounterparty = allowGenericCounterpartyAnchor && shouldPreferKnownFinancialCounterpartyAnchor(intent)
|
||||
? extractKnownFinancialCounterpartyAnchor(text)
|
||||
: undefined;
|
||||
if (knownFinancialCounterparty && !filters.counterparty) {
|
||||
filters.counterparty = knownFinancialCounterparty;
|
||||
warnings.push("counterparty_anchor_derived_from_known_financial_name");
|
||||
}
|
||||
const counterpartyMatch = allowGenericCounterpartyAnchor ? text.match(COUNTERPARTY_PATTERN) : null;
|
||||
if (counterpartyMatch && !filters.counterparty) {
|
||||
filters.counterparty = cleanupAnchorValue(String(counterpartyMatch[1]));
|
||||
|
|
@ -1811,7 +1733,8 @@ function extractAddressFilters(userMessage, intent) {
|
|||
const periodWasDerivedHeuristically = warnings.includes("period_derived_from_month_phrase") ||
|
||||
warnings.includes("period_derived_from_year_range_phrase") ||
|
||||
warnings.includes("period_derived_from_year_phrase");
|
||||
const preserveDerivedPeriodWindow = intent === "inventory_on_hand_as_of_date" ||
|
||||
const preserveDerivedPeriodWindow = usesAsOfPrimaryWindow(intent) ||
|
||||
intent === "inventory_on_hand_as_of_date" ||
|
||||
intent === "inventory_supplier_stock_overlap_as_of_date";
|
||||
if (periodWasDerivedHeuristically && !warnings.includes("exact_historical_period_window_requested")) {
|
||||
warnings.push("exact_historical_period_window_requested");
|
||||
|
|
|
|||
|
|
@ -1658,9 +1658,9 @@ function hasBidirectionalValueFlowComparisonSignal(text) {
|
|||
return false;
|
||||
}
|
||||
const hasIncomingCue = /(?:\u0432\u0445\u043e\u0434\u044f\u0449|\u043f\u043e\u0441\u0442\u0443\u043f|\u043f\u043e\u043b\u0443\u0447|inflow|incoming)/iu.test(normalized);
|
||||
const hasOutgoingCue = /(?:\u0438\u0441\u0445\u043e\u0434\u044f\u0449|\u0441\u043f\u0438\u0441\u0430\u043d|\u0437\u0430\u043f\u043b\u0430\u0442|\u043f\u043b\u0430\u0442\u0438\u043b|\u0432\u044b\u043f\u043b\u0430\u0442|\u0432\u044b\u043f\u043b\u0430\u0447|\u0443\u043f\u043b\u0430\u0442|\u043e\u043f\u043b\u0430\u0442|outflow|outgoing|payout|paid)/iu.test(normalized);
|
||||
const hasOutgoingCue = /(?:\u0438\u0441\u0445\u043e\u0434\u044f\u0449|\u0441\u043f\u0438\u0441\u0430\u043d|\u0437\u0430\u043f\u043b\u0430\u0442|\u043f\u043b\u0430\u0442\u0438\u043b|\u043e\u043f\u043b\u0430\u0442|outflow|outgoing|payout)/iu.test(normalized);
|
||||
const hasComparisonCue = /(?:\u0431\u043e\u043b\u044c\u0448|\u043c\u0435\u043d\u044c\u0448|\u0441\u0440\u0430\u0432|\u0438\u043b\u0438|\u043d\u0435\u0442\u0442\u043e|\u0441\u0430\u043b\u044c\u0434\u043e|vs|versus)/iu.test(normalized);
|
||||
const hasValueFlowCue = /(?:\u0434\u0435\u043d\u044c\u0433|\u0434\u0435\u043d\u0435\u0433|\u0434\u0435\u043d\u0435\u0436|\u0441\u0440\u0435\u0434\u0441\u0442\u0432|\u043f\u043e\u0442\u043e\u043a|\u043e\u0431\u043e\u0440\u043e\u0442|money|cash|funds|flow)/iu.test(normalized);
|
||||
const hasValueFlowCue = /(?:\u0434\u0435\u043d\u044c\u0433|\u0434\u0435\u043d\u0435\u0433|\u0434\u0435\u043d\u0435\u0436|\u043f\u043e\u0442\u043e\u043a|\u043e\u0431\u043e\u0440\u043e\u0442|money|cash|flow)/iu.test(normalized);
|
||||
const hasNetAmountCue = /(?:сколько|сумм|итог|нетто|сальдо|минус|net|total|sum)/iu.test(normalized);
|
||||
return hasIncomingCue && hasOutgoingCue && hasComparisonCue && (hasValueFlowCue || hasNetAmountCue);
|
||||
}
|
||||
|
|
@ -1672,8 +1672,7 @@ function hasVatPeriodInspectionBridgeSignal(text) {
|
|||
const hasPeriodCue = /(?:\b(?:19|20)\d{2}\b|за\s+(?:\d{4}|год|период|квартал|месяц|январ|феврал|март|апрел|ма[йя]|июн|июл|август|сентябр|октябр|ноябр|декабр)|\b[1-4]\s*(?:кв|квартал))/iu.test(normalized);
|
||||
const hasInspectionCue = /(?:что\s+с|позици|основан|не\s+хватает|налогов[а-яё]*\s+вывод|вывод|декларац|книга\s+(?:продаж|покупок)|расшифр|разбор)/iu.test(normalized);
|
||||
const forecastOnlyCue = /(?:прогноз|план|примерн|ориентировочн)/iu.test(normalized) && !hasInspectionCue;
|
||||
const hasVatMovementInspectionCue = /(?:покаж|движен|операц|по\s+сч(?:е|ё)т|покаж|движен|операц|РїРѕ\s+СЃС‡(?:Рµ|С‘)С‚|show|movement|movements|operation|operations|account)/iu.test(normalized);
|
||||
return hasPeriodCue && (hasInspectionCue || hasVatMovementInspectionCue) && !forecastOnlyCue;
|
||||
return hasPeriodCue && hasInspectionCue && !forecastOnlyCue;
|
||||
}
|
||||
function resolveUnicodeAddressIntentBridge(text) {
|
||||
const normalized = String(text ?? "").trim().toLowerCase();
|
||||
|
|
@ -1758,10 +1757,6 @@ 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");
|
||||
}
|
||||
|
|
@ -1865,9 +1860,7 @@ function resolveUnicodeAddressIntentBridge(text) {
|
|||
}
|
||||
if (/(?:поставщик|vendor|supplier|кому\s+(?:ушло|платили|заплатили)|выплат|исходящ|списан|сгрузил)/iu.test(normalized) &&
|
||||
!/(?:аванс.*(?:не\s+)?закрыт|закрыт.*аванс)/iu.test(normalized) &&
|
||||
(hasMoneyCue ||
|
||||
hasRankingCue ||
|
||||
/заплат|платил|платили|уплат|плат[её]ж|оплат|выплат|outflow|payout|хвост|задержк|проблем/iu.test(normalized))) {
|
||||
(hasMoneyCue || hasRankingCue || /плат[её]ж|оплат|выплат|outflow|payout|хвост|задержк|проблем/iu.test(normalized))) {
|
||||
return unicodeBridgeResolution(/(?:хвост|задержк|проблем)/iu.test(normalized) ? "list_payables_counterparties" : "supplier_payouts_profile", "high", /(?:хвост|задержк|проблем)/iu.test(normalized)
|
||||
? "supplier_tail_risk_signal_detected"
|
||||
: "unicode_supplier_payouts_bridge_signal_detected");
|
||||
|
|
@ -2005,7 +1998,7 @@ function resolveUnicodeAddressIntentBridge(text) {
|
|||
return unicodeBridgeResolution("contract_usage_and_value", "high", "unicode_contract_usage_value_bridge_signal_detected");
|
||||
}
|
||||
if (/(?:поставщик|vendor|supplier|кому\s+(?:ушло|платили|заплатили)|выплат|исходящ|списан|сгрузил)/iu.test(normalized) &&
|
||||
(hasMoneyCue || hasRankingCue || /заплат|платил|платили|уплат|плат[её]ж|оплат|выплат|outflow|payout/iu.test(normalized))) {
|
||||
(hasMoneyCue || hasRankingCue || /плат[её]ж|оплат|выплат|outflow|payout/iu.test(normalized))) {
|
||||
return unicodeBridgeResolution("supplier_payouts_profile", "high", "unicode_supplier_payouts_bridge_signal_detected");
|
||||
}
|
||||
if ((/(?:клиент|покупател|заказчик|контрагент|альтернатива|свк)/iu.test(normalized) || hasRankingCue) &&
|
||||
|
|
@ -2027,52 +2020,14 @@ 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_])намс(?=$|[^\p{L}0-9_])/giu, "$1нам")
|
||||
.replace(/(^|[^\p{L}0-9_])какиек(?=$|[^\p{L}0-9_])/giu, "$1какие");
|
||||
.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");
|
||||
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];
|
||||
|
|
@ -2089,16 +2044,6 @@ function resolveAddressIntent(userMessage) {
|
|||
reasons
|
||||
};
|
||||
}
|
||||
const hasExplicitVatLiabilityPeriodBridge = /(?:\u043d\u0434\u0441|vat)/iu.test(text) &&
|
||||
/(?:\b(?:19|20)\d{2}\b|\u0437\u0430\s+(?:\d{4}|\u0433\u043e\u0434|\u043f\u0435\u0440\u0438\u043e\u0434|\u043a\u0432\u0430\u0440\u0442\u0430\u043b|\u043c\u0435\u0441\u044f\u0446))/iu.test(text) &&
|
||||
/(?:\u043a\u0430\u043a\u043e\u0439|\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u043d\u0430\u0447\u0438\u0441\u043b|\u0443\u043f\u043b\u0430\u0447|\u0443\u043f\u043b\u0430\u0442|\u043f\u0440\u043e\u0434\u0430\u0436|\u043f\u043e\u043a\u0443\u043f|\u0432\u044b\u0447\u0435\u0442|\u043a\s+\u0443\u043f\u043b\u0430\u0442|\u043a\s+\u0432\u043e\u0437\u043c\u0435\u0449|\u043f\u043e\u0437\u0438\u0446|liability|payable|charged|paid|sales|purchase|deduction|position)/iu.test(text);
|
||||
if (hasExplicitVatLiabilityPeriodBridge) {
|
||||
return {
|
||||
intent: "vat_liability_confirmed_for_tax_period",
|
||||
confidence: "high",
|
||||
reasons: ["vat_liability_explicit_period_bridge_signal_detected"]
|
||||
};
|
||||
}
|
||||
const hasLooseVatPayableBridge = /(?:\u043d\u0434\u0441|vat)/iu.test(text) &&
|
||||
/(?:\u043a\u0430\u043a\u043e\u0439\s+\u043d\u0434\u0441\s+(?:(?:\u043d\u0430\u043c|(?:\u043c\u044b\s+)?\u0434\u043e\u043b\u0436\u043d\u044b)\s+)?(?:\u043d\u0430\u0434\u043e|\u043d\u0443\u0436\u043d\u043e|\u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e)|(?:\u043d\u0430\u043c|\u043c\u044b\s+)?\u043d\u0430\u0434\u043e\s+(?:\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u0442\u044c|\u0441\u0433\u0440\u0443\u0437\u0438\u0442\u044c)|(?:\u043d\u0430\u043c|\u043c\u044b\s+)?\u043d\u0443\u0436\u043d\u043e\s+(?:\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u0442\u044c|\u0441\u0433\u0440\u0443\u0437\u0438\u0442\u044c)|\u043c\u044b\s+\u0434\u043e\u043b\u0436\u043d\u044b\s+(?:\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u0442\u044c|\u0441\u0433\u0440\u0443\u0437\u0438\u0442\u044c)|\u043d\u0434\u0441\s+\u043a\s+\u0443\u043f\u043b\u0430\u0442\u0435)/iu.test(text) &&
|
||||
/(?:\u0437\u0430\s+(?:\d{4}|(?:\u044f\u043d\u0432\u0430\u0440|\u0444\u0435\u0432\u0440\u0430\u043b|\u043c\u0430\u0440\u0442|\u0430\u043f\u0440\u0435\u043b|\u043c\u0430[\u0439\u044f]|\u0438\u044e\u043d|\u0438\u044e\u043b|\u0430\u0432\u0433\u0443\u0441\u0442|\u0441\u0435\u043d\u0442\u044f\u0431\u0440|\u043e\u043a\u0442\u044f\u0431\u0440|\u043d\u043e\u044f\u0431\u0440|\u0434\u0435\u043a\u0430\u0431\u0440)\S*(?:\s+(?:19|20)\d{2})?)|\u043d\u0430\s+(?:\u044f\u043d\u0432\u0430\u0440|\u0444\u0435\u0432\u0440\u0430\u043b|\u043c\u0430\u0440\u0442|\u0430\u043f\u0440\u0435\u043b|\u043c\u0430[\u0439\u044f]|\u0438\u044e\u043d|\u0438\u044e\u043b|\u0430\u0432\u0433\u0443\u0441\u0442|\u0441\u0435\u043d\u0442\u044f\u0431\u0440|\u043e\u043a\u0442\u044f\u0431\u0440|\u043d\u043e\u044f\u0431\u0440|\u0434\u0435\u043a\u0430\u0431\u0440)\S*(?:\s+(?:19|20)\d{2})?|\u0432\s+(?:\u044f\u043d\u0432\u0430\u0440|\u0444\u0435\u0432\u0440\u0430\u043b|\u043c\u0430\u0440\u0442|\u0430\u043f\u0440\u0435\u043b|\u043c\u0430[\u0439\u044f]|\u0438\u044e\u043d|\u0438\u044e\u043b|\u0430\u0432\u0433\u0443\u0441\u0442|\u0441\u0435\u043d\u0442\u044f\u0431\u0440|\u043e\u043a\u0442\u044f\u0431\u0440|\u043d\u043e\u044f\u0431\u0440|\u0434\u0435\u043a\u0430\u0431\u0440)\S*(?:\s+(?:19|20)\d{2})?|\b[1-4]\s*(?:\u043a\u0432\u0430\u0440\u0442\u0430\u043b|\u043a\u0432\.?)\b)/iu.test(text);
|
||||
|
|
|
|||
|
|
@ -1189,9 +1189,6 @@ function toNormalizedRows(rows) {
|
|||
const organization = firstNonEmptyString(row.Организация, row.Organization, row.organization, row.organization_name, row.ОрганизацияПредставление);
|
||||
const counterparty = firstNonEmptyString(row.Контрагент, row.Counterparty, row.counterparty);
|
||||
const contract = firstNonEmptyString(row.Договор, row.Contract, row.contract);
|
||||
const operationKind = firstNonEmptyString(row.ВидОперации, row.OperationKind, row.operation_kind, row.operationType);
|
||||
const paymentPurpose = firstNonEmptyString(row.НазначениеПлатежа, row.PaymentPurpose, row.payment_purpose, row.paymentPurpose);
|
||||
const comment = firstNonEmptyString(row.Комментарий, row.Comment, row.comment);
|
||||
const analytics = collectAnalyticsStrings(row);
|
||||
return {
|
||||
period,
|
||||
|
|
@ -1205,10 +1202,7 @@ function toNormalizedRows(rows) {
|
|||
warehouse,
|
||||
organization,
|
||||
counterparty,
|
||||
contract,
|
||||
operation_kind: operationKind,
|
||||
payment_purpose: paymentPurpose,
|
||||
comment
|
||||
contract
|
||||
};
|
||||
})
|
||||
.filter((item) => Boolean(item.period || item.registrator));
|
||||
|
|
|
|||
|
|
@ -197,51 +197,6 @@ const OPEN_CONTRACTS_CONFIRMED_AS_OF_QUERY_TEMPLATE = `
|
|||
УПОРЯДОЧИТЬ ПО
|
||||
Сумма __ORDER_DIRECTION__
|
||||
`;
|
||||
const DEBT_DUE_DATE_AGING_QUERY_TEMPLATE = `
|
||||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
||||
__AS_OF_EXPR__ КАК Период,
|
||||
"DUE_DATE_OPEN_BALANCE" КАК Регистратор,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Счет) КАК СчетДт,
|
||||
"" КАК СчетКт,
|
||||
Остатки.СуммаРазвернутыйОстатокДт КАК Сумма,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто1) КАК СубконтоДт1,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто2) КАК СубконтоДт2,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто3) КАК СубконтоДт3,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто1) КАК Контрагент,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто2) КАК Договор,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто3) КАК ДокументРасчетов,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Организация) КАК Организация,
|
||||
ВЫРАЗИТЬ(Остатки.Субконто2 КАК Справочник.ДоговорыКонтрагентов).Дата КАК ДатаДоговора,
|
||||
ВЫРАЗИТЬ(Остатки.Субконто2 КАК Справочник.ДоговорыКонтрагентов).УстановленСрокОплаты КАК УстановленСрокОплаты,
|
||||
ВЫРАЗИТЬ(Остатки.Субконто2 КАК Справочник.ДоговорыКонтрагентов).СрокОплаты КАК СрокОплаты,
|
||||
"debit_open_balance" КАК НаправлениеОстатка
|
||||
ИЗ
|
||||
РегистрБухгалтерии.Хозрасчетный.Остатки(__AS_OF_EXPR__, , , ) КАК Остатки
|
||||
__WHERE_DT__
|
||||
ОБЪЕДИНИТЬ ВСЕ
|
||||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
||||
__AS_OF_EXPR__ КАК Период,
|
||||
"DUE_DATE_OPEN_BALANCE" КАК Регистратор,
|
||||
"" КАК СчетДт,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Счет) КАК СчетКт,
|
||||
Остатки.СуммаРазвернутыйОстатокКт КАК Сумма,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто1) КАК СубконтоДт1,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто2) КАК СубконтоДт2,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто3) КАК СубконтоДт3,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто1) КАК Контрагент,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто2) КАК Договор,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто3) КАК ДокументРасчетов,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Организация) КАК Организация,
|
||||
ВЫРАЗИТЬ(Остатки.Субконто2 КАК Справочник.ДоговорыКонтрагентов).Дата КАК ДатаДоговора,
|
||||
ВЫРАЗИТЬ(Остатки.Субконто2 КАК Справочник.ДоговорыКонтрагентов).УстановленСрокОплаты КАК УстановленСрокОплаты,
|
||||
ВЫРАЗИТЬ(Остатки.Субконто2 КАК Справочник.ДоговорыКонтрагентов).СрокОплаты КАК СрокОплаты,
|
||||
"credit_open_balance" КАК НаправлениеОстатка
|
||||
ИЗ
|
||||
РегистрБухгалтерии.Хозрасчетный.Остатки(__AS_OF_EXPR__, , , ) КАК Остатки
|
||||
__WHERE_KT__
|
||||
УПОРЯДОЧИТЬ ПО
|
||||
Сумма __ORDER_DIRECTION__
|
||||
`;
|
||||
const VAT_PAYABLE_CONFIRMED_AS_OF_QUERY_TEMPLATE = `
|
||||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
||||
__AS_OF_EXPR__ КАК Период,
|
||||
|
|
@ -283,61 +238,6 @@ const INVENTORY_ON_HAND_AS_OF_QUERY_TEMPLATE = `
|
|||
УПОРЯДОЧИТЬ ПО
|
||||
Количество __ORDER_DIRECTION__
|
||||
`;
|
||||
const INVENTORY_QUALITY_EVENTS_QUERY_TEMPLATE = `
|
||||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
||||
Списание.Дата КАК Период,
|
||||
ПРЕДСТАВЛЕНИЕ(Списание.Ссылка) КАК Регистратор,
|
||||
"Списание товаров" КАК ТипСобытия,
|
||||
ПРЕДСТАВЛЕНИЕ(Списание.Организация) КАК Организация,
|
||||
ПРЕДСТАВЛЕНИЕ(Списание.Склад) КАК Склад,
|
||||
Списание.СуммаДокумента КАК Сумма,
|
||||
Списание.Основание КАК Основание,
|
||||
Списание.Комментарий КАК Комментарий
|
||||
ИЗ
|
||||
Документ.СписаниеТоваров КАК Списание
|
||||
__WHERE_WRITE_OFF__
|
||||
ОБЪЕДИНИТЬ ВСЕ
|
||||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
||||
Оприходование.Дата КАК Период,
|
||||
ПРЕДСТАВЛЕНИЕ(Оприходование.Ссылка) КАК Регистратор,
|
||||
"Оприходование товаров" КАК ТипСобытия,
|
||||
ПРЕДСТАВЛЕНИЕ(Оприходование.Организация) КАК Организация,
|
||||
ПРЕДСТАВЛЕНИЕ(Оприходование.Склад) КАК Склад,
|
||||
Оприходование.СуммаДокумента КАК Сумма,
|
||||
Оприходование.Основание КАК Основание,
|
||||
Оприходование.Комментарий КАК Комментарий
|
||||
ИЗ
|
||||
Документ.ОприходованиеТоваров КАК Оприходование
|
||||
__WHERE_RECEIPT__
|
||||
ОБЪЕДИНИТЬ ВСЕ
|
||||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
||||
Инвентаризация.Дата КАК Период,
|
||||
ПРЕДСТАВЛЕНИЕ(Инвентаризация.Ссылка) КАК Регистратор,
|
||||
"Инвентаризация товаров на складе" КАК ТипСобытия,
|
||||
ПРЕДСТАВЛЕНИЕ(Инвентаризация.Организация) КАК Организация,
|
||||
ПРЕДСТАВЛЕНИЕ(Инвентаризация.Склад) КАК Склад,
|
||||
0 КАК Сумма,
|
||||
Инвентаризация.ПричинаПроведенияИнвентаризации КАК Основание,
|
||||
Инвентаризация.Комментарий КАК Комментарий
|
||||
ИЗ
|
||||
Документ.ИнвентаризацияТоваровНаСкладе КАК Инвентаризация
|
||||
__WHERE_INVENTORY_COUNT__
|
||||
ОБЪЕДИНИТЬ ВСЕ
|
||||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
||||
Переоценка.Дата КАК Период,
|
||||
ПРЕДСТАВЛЕНИЕ(Переоценка.Ссылка) КАК Регистратор,
|
||||
"Переоценка товаров в рознице" КАК ТипСобытия,
|
||||
ПРЕДСТАВЛЕНИЕ(Переоценка.Организация) КАК Организация,
|
||||
ПРЕДСТАВЛЕНИЕ(Переоценка.Склад) КАК Склад,
|
||||
0 КАК Сумма,
|
||||
"" КАК Основание,
|
||||
Переоценка.Комментарий КАК Комментарий
|
||||
ИЗ
|
||||
Документ.ПереоценкаТоваровВРознице КАК Переоценка
|
||||
__WHERE_REVALUATION__
|
||||
УПОРЯДОЧИТЬ ПО
|
||||
Период __ORDER_DIRECTION__
|
||||
`;
|
||||
const BANK_DOCS_QUERY_TEMPLATE = `
|
||||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
||||
БанкСписание.Дата КАК Период,
|
||||
|
|
@ -346,10 +246,7 @@ const BANK_DOCS_QUERY_TEMPLATE = `
|
|||
"" КАК СчетКт,
|
||||
БанкСписание.СуммаДокумента КАК Сумма,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкСписание.Контрагент) КАК Контрагент,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкСписание.ДоговорКонтрагента) КАК Договор,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкСписание.ВидОперации) КАК ВидОперации,
|
||||
БанкСписание.НазначениеПлатежа КАК НазначениеПлатежа,
|
||||
БанкСписание.Комментарий КАК Комментарий
|
||||
ПРЕДСТАВЛЕНИЕ(БанкСписание.ДоговорКонтрагента) КАК Договор
|
||||
ИЗ
|
||||
Документ.СписаниеСРасчетногоСчета КАК БанкСписание
|
||||
__WHERE_OUT__
|
||||
|
|
@ -361,10 +258,7 @@ __WHERE_OUT__
|
|||
"" КАК СчетКт,
|
||||
БанкПоступление.СуммаДокумента КАК Сумма,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкПоступление.Контрагент) КАК Контрагент,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкПоступление.ДоговорКонтрагента) КАК Договор,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкПоступление.ВидОперации) КАК ВидОперации,
|
||||
"" КАК НазначениеПлатежа,
|
||||
БанкПоступление.Комментарий КАК Комментарий
|
||||
ПРЕДСТАВЛЕНИЕ(БанкПоступление.ДоговорКонтрагента) КАК Договор
|
||||
ИЗ
|
||||
Документ.ПоступлениеНаРасчетныйСчет КАК БанкПоступление
|
||||
__WHERE_IN__
|
||||
|
|
@ -630,9 +524,7 @@ const CUSTOMER_REVENUE_PROFILE_QUERY_TEMPLATE = `
|
|||
"" КАК СчетКт,
|
||||
БанкПоступление.СуммаДокумента КАК Сумма,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкПоступление.Контрагент) КАК Контрагент,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкПоступление.ДоговорКонтрагента) КАК Договор,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкПоступление.ВидОперации) КАК ВидОперации,
|
||||
БанкПоступление.Комментарий КАК Комментарий
|
||||
ПРЕДСТАВЛЕНИЕ(БанкПоступление.ДоговорКонтрагента) КАК Договор
|
||||
ИЗ
|
||||
Документ.ПоступлениеНаРасчетныйСчет КАК БанкПоступление
|
||||
__WHERE_IN__
|
||||
|
|
@ -647,10 +539,7 @@ const SUPPLIER_PAYOUT_PROFILE_QUERY_TEMPLATE = `
|
|||
"" КАК СчетКт,
|
||||
БанкСписание.СуммаДокумента КАК Сумма,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкСписание.Контрагент) КАК Контрагент,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкСписание.ДоговорКонтрагента) КАК Договор,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкСписание.ВидОперации) КАК ВидОперации,
|
||||
БанкСписание.НазначениеПлатежа КАК НазначениеПлатежа,
|
||||
БанкСписание.Комментарий КАК Комментарий
|
||||
ПРЕДСТАВЛЕНИЕ(БанкСписание.ДоговорКонтрагента) КАК Договор
|
||||
ИЗ
|
||||
Документ.СписаниеСРасчетногоСчета КАК БанкСписание
|
||||
__WHERE_OUT__
|
||||
|
|
@ -834,7 +723,7 @@ const BASE_RECIPES = [
|
|||
purpose: "Build customer value ranking and incoming deal profile from bank inflow docs",
|
||||
required_filters: [],
|
||||
optional_filters: ["period_from", "period_to", "organization", "counterparty", "contract", "limit", "sort"],
|
||||
default_limit: 200,
|
||||
default_limit: 20,
|
||||
account_scope_mode: "preferred",
|
||||
query_template: "customer_revenue_profile"
|
||||
},
|
||||
|
|
@ -844,7 +733,7 @@ const BASE_RECIPES = [
|
|||
purpose: "Build supplier payout ranking and outgoing deal profile from bank outflow docs",
|
||||
required_filters: [],
|
||||
optional_filters: ["period_from", "period_to", "organization", "counterparty", "contract", "limit", "sort"],
|
||||
default_limit: 200,
|
||||
default_limit: 20,
|
||||
account_scope_mode: "preferred",
|
||||
query_template: "supplier_payout_profile"
|
||||
},
|
||||
|
|
@ -889,17 +778,6 @@ const BASE_RECIPES = [
|
|||
account_scope_mode: "preferred",
|
||||
query_template: "vat_liability_confirmed_tax_period_profile"
|
||||
},
|
||||
{
|
||||
recipe_id: "address_accounting_financial_result_for_organization_v1",
|
||||
intent: "accounting_financial_result_for_organization",
|
||||
purpose: "Build reviewed accounting financial-result aggregate from 90/91/99 period-close movements",
|
||||
required_filters: ["period_from", "period_to"],
|
||||
optional_filters: ["organization", "limit", "sort"],
|
||||
default_limit: 32,
|
||||
account_scope: ["90", "91", "99"],
|
||||
account_scope_mode: "strict",
|
||||
query_template: "accounting_financial_result_profile"
|
||||
},
|
||||
{
|
||||
recipe_id: "address_inventory_on_hand_as_of_date_v1",
|
||||
intent: "inventory_on_hand_as_of_date",
|
||||
|
|
@ -999,16 +877,6 @@ const BASE_RECIPES = [
|
|||
account_scope_mode: "strict",
|
||||
query_template: "inventory_aging_by_purchase_date_profile"
|
||||
},
|
||||
{
|
||||
recipe_id: "address_inventory_quality_events_for_organization_v1",
|
||||
intent: "inventory_quality_events_for_organization",
|
||||
purpose: "Check posted inventory quality event documents: write-offs, stocktaking, receipt adjustments, and retail revaluation",
|
||||
required_filters: [],
|
||||
optional_filters: ["as_of_date", "period_from", "period_to", "organization", "warehouse", "limit", "sort"],
|
||||
default_limit: 400,
|
||||
account_scope_mode: "preferred",
|
||||
query_template: "inventory_quality_events_profile"
|
||||
},
|
||||
{
|
||||
recipe_id: "address_open_contracts_confirmed_as_of_date_v1",
|
||||
intent: "open_contracts_confirmed_as_of_date",
|
||||
|
|
@ -1020,17 +888,6 @@ const BASE_RECIPES = [
|
|||
account_scope_mode: "strict",
|
||||
query_template: "open_contracts_confirmed_as_of_balance_profile"
|
||||
},
|
||||
{
|
||||
recipe_id: "address_debt_due_date_aging_for_organization_v1",
|
||||
intent: "debt_due_date_aging_for_organization",
|
||||
purpose: "Check open 60/62/76 settlements against contract payment-term fields and settlement document dates before claiming overdue debt",
|
||||
required_filters: ["as_of_date"],
|
||||
optional_filters: ["period_from", "period_to", "organization", "counterparty", "contract", "limit", "sort"],
|
||||
default_limit: 400,
|
||||
account_scope: ["60", "62", "76"],
|
||||
account_scope_mode: "strict",
|
||||
query_template: "debt_due_date_aging_profile"
|
||||
},
|
||||
{
|
||||
recipe_id: "address_contracts_by_counterparty_v1",
|
||||
intent: "list_contracts_by_counterparty",
|
||||
|
|
@ -1236,32 +1093,6 @@ function buildContractValueWhereClause(filters, fieldPath, contractFieldPath) {
|
|||
`${contractFieldPath} <> ЗНАЧЕНИЕ(Справочник.ДоговорыКонтрагентов.ПустаяСсылка)`
|
||||
]);
|
||||
}
|
||||
function buildContractReferenceCondition(filters, fieldPaths) {
|
||||
const contract = typeof filters.contract === "string" ? filters.contract.trim() : "";
|
||||
if (!contract) {
|
||||
return null;
|
||||
}
|
||||
const contractTokens = Array.from(new Set(contract
|
||||
.split(/[^A-Za-zА-Яа-яЁё0-9]+/u)
|
||||
.map((token) => token.trim())
|
||||
.filter((token) => token.length >= 3)
|
||||
.filter((token) => !["договор", "дог"].includes(token.toLowerCase()))));
|
||||
const tokens = contractTokens.length > 0 ? contractTokens : [contract];
|
||||
const clauses = fieldPaths
|
||||
.map((fieldPath) => String(fieldPath ?? "").trim())
|
||||
.filter((fieldPath) => fieldPath.length > 0)
|
||||
.map((fieldPath) => {
|
||||
const tokenConditions = tokens.map((token) => {
|
||||
const escapedToken = toQueryStringLiteral(token);
|
||||
return `${fieldPath}.Наименование ПОДОБНО "%${escapedToken}%"`;
|
||||
});
|
||||
return tokenConditions.length === 1 ? tokenConditions[0] : `(${tokenConditions.join(" И ")})`;
|
||||
});
|
||||
if (clauses.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return clauses.length === 1 ? clauses[0] : `(${clauses.join(" ИЛИ ")})`;
|
||||
}
|
||||
function normalizeAccountTokenForQuery(value) {
|
||||
const source = String(value ?? "").trim().replace(",", ".");
|
||||
const match = source.match(/^(\d{2})(?:\.(\d{1,2}))?/);
|
||||
|
|
@ -1347,35 +1178,6 @@ function buildAccountPrefixPredicate(fieldPath, prefixes) {
|
|||
const clauses = normalizedPrefixes.map((prefix) => `ПОДСТРОКА(ЕСТЬNULL(${fieldPath}.Код, ""), 1, ${prefix.length}) = "${prefix}"`);
|
||||
return clauses.length === 1 ? clauses[0] : `(${clauses.join(" ИЛИ ")})`;
|
||||
}
|
||||
function buildDebtDueDateAgingWhereClause(filters, amountFieldPath, accountPredicate) {
|
||||
const conditions = [
|
||||
`${amountFieldPath} > 0`,
|
||||
`(${accountPredicate})`,
|
||||
buildOrganizationReferenceCondition(filters, ["Остатки.Организация"]),
|
||||
buildCounterpartyReferenceCondition(filters, ["Остатки.Субконто1"]),
|
||||
buildContractReferenceCondition(filters, ["Остатки.Субконто2"])
|
||||
].filter((item) => Boolean(item));
|
||||
return `ГДЕ\n ${conditions.join("\n И ")}`;
|
||||
}
|
||||
function buildDebtDueDateAgingQuery(filters, resolvedLimit) {
|
||||
const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
|
||||
? toDateTimeExpr(filters.as_of_date, true)
|
||||
: null) ??
|
||||
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0
|
||||
? toDateTimeExpr(filters.period_to, true)
|
||||
: null) ??
|
||||
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0
|
||||
? toDateTimeExpr(filters.period_from, true)
|
||||
: null) ??
|
||||
"ТЕКУЩАЯДАТА()";
|
||||
const accountPredicate = buildAccountPrefixPredicate("Остатки.Счет", ["60", "62", "76"]);
|
||||
return DEBT_DUE_DATE_AGING_QUERY_TEMPLATE
|
||||
.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||
.replaceAll("__AS_OF_EXPR__", asOfExpr)
|
||||
.replaceAll("__WHERE_DT__", buildDebtDueDateAgingWhereClause(filters, "Остатки.СуммаРазвернутыйОстатокДт", accountPredicate))
|
||||
.replaceAll("__WHERE_KT__", buildDebtDueDateAgingWhereClause(filters, "Остатки.СуммаРазвернутыйОстатокКт", accountPredicate))
|
||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||
}
|
||||
function buildInventoryMovementQuery(filters, resolvedLimit, side) {
|
||||
const debitPredicate = buildAccountPrefixPredicate("Движения.СчетДт", ["41.01"]);
|
||||
const creditPredicate = buildAccountPrefixPredicate("Движения.СчетКт", ["41.01"]);
|
||||
|
|
@ -1402,48 +1204,6 @@ function buildInventoryMovementQuery(filters, resolvedLimit, side) {
|
|||
.replace("__WHERE_CLAUSE__", buildWhereClause(filters, "Движения.Период", [inventoryCondition, itemCondition].filter((item) => Boolean(item))))
|
||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||
}
|
||||
function buildWarehouseReferenceCondition(filters, fieldPaths) {
|
||||
const warehouse = typeof filters.warehouse === "string" ? filters.warehouse.trim() : "";
|
||||
if (!warehouse) {
|
||||
return null;
|
||||
}
|
||||
const tokens = Array.from(new Set(warehouse
|
||||
.split(/[^A-Za-zА-Яа-яЁё0-9]+/u)
|
||||
.map((token) => token.trim())
|
||||
.filter((token) => token.length >= 3)
|
||||
.filter((token) => !["склад", "warehouse"].includes(token.toLowerCase()))));
|
||||
const effectiveTokens = tokens.length > 0 ? tokens : [warehouse];
|
||||
const clauses = fieldPaths
|
||||
.map((fieldPath) => String(fieldPath ?? "").trim())
|
||||
.filter((fieldPath) => fieldPath.length > 0)
|
||||
.map((fieldPath) => {
|
||||
const tokenConditions = effectiveTokens.map((token) => {
|
||||
const escapedToken = toQueryStringLiteral(token);
|
||||
return `${fieldPath}.Наименование ПОДОБНО "%${escapedToken}%"`;
|
||||
});
|
||||
return tokenConditions.length === 1 ? tokenConditions[0] : `(${tokenConditions.join(" И ")})`;
|
||||
});
|
||||
if (clauses.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return clauses.length === 1 ? clauses[0] : `(${clauses.join(" ИЛИ ")})`;
|
||||
}
|
||||
function buildInventoryQualityDocumentWhereClause(filters, dateFieldPath, organizationFieldPath, warehouseFieldPath) {
|
||||
return buildWhereClause(filters, dateFieldPath, [
|
||||
`${dateFieldPath.replace(/\.Дата$/u, ".Проведен")} = ИСТИНА`,
|
||||
buildOrganizationReferenceCondition(filters, [organizationFieldPath]),
|
||||
buildWarehouseReferenceCondition(filters, [warehouseFieldPath])
|
||||
].filter((item) => Boolean(item)));
|
||||
}
|
||||
function buildInventoryQualityEventsQuery(filters, resolvedLimit) {
|
||||
return INVENTORY_QUALITY_EVENTS_QUERY_TEMPLATE
|
||||
.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||
.replace("__WHERE_WRITE_OFF__", buildInventoryQualityDocumentWhereClause(filters, "Списание.Дата", "Списание.Организация", "Списание.Склад"))
|
||||
.replace("__WHERE_RECEIPT__", buildInventoryQualityDocumentWhereClause(filters, "Оприходование.Дата", "Оприходование.Организация", "Оприходование.Склад"))
|
||||
.replace("__WHERE_INVENTORY_COUNT__", buildInventoryQualityDocumentWhereClause(filters, "Инвентаризация.Дата", "Инвентаризация.Организация", "Инвентаризация.Склад"))
|
||||
.replace("__WHERE_REVALUATION__", buildInventoryQualityDocumentWhereClause(filters, "Переоценка.Дата", "Переоценка.Организация", "Переоценка.Склад"))
|
||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||
}
|
||||
function buildInventoryItemReferenceCondition(filters, fieldPaths) {
|
||||
const item = typeof filters.item === "string" ? filters.item.trim() : "";
|
||||
if (!item) {
|
||||
|
|
@ -1486,148 +1246,6 @@ function buildCounterpartyReferenceCondition(filters, fieldPaths) {
|
|||
}
|
||||
return clauses.length === 1 ? clauses[0] : `(${clauses.join(" ИЛИ ")})`;
|
||||
}
|
||||
const ORGANIZATION_REFERENCE_STOP_WORDS = new Set([
|
||||
"ооо",
|
||||
"зао",
|
||||
"оао",
|
||||
"ао",
|
||||
"пао",
|
||||
"ип",
|
||||
"на",
|
||||
"за",
|
||||
"по",
|
||||
"конец",
|
||||
"начало",
|
||||
"год",
|
||||
"года",
|
||||
"период",
|
||||
"можно",
|
||||
"точно",
|
||||
"понять",
|
||||
"какая",
|
||||
"какой",
|
||||
"какие",
|
||||
"какую",
|
||||
"компания",
|
||||
"компании",
|
||||
"организация",
|
||||
"организации",
|
||||
"дебиторка",
|
||||
"дебиторки",
|
||||
"кредиторка",
|
||||
"кредиторки",
|
||||
"просрочена",
|
||||
"просроченные",
|
||||
"просрочка",
|
||||
"срок",
|
||||
"оплаты",
|
||||
"прибыль",
|
||||
"маржа",
|
||||
"ндс"
|
||||
]);
|
||||
const ORGANIZATION_REFERENCE_BOUNDARY_WORDS = new Set([
|
||||
"на",
|
||||
"за",
|
||||
"конец",
|
||||
"начало",
|
||||
"можно",
|
||||
"точно",
|
||||
"понять",
|
||||
"какая",
|
||||
"какой",
|
||||
"какие",
|
||||
"какую",
|
||||
"дебиторка",
|
||||
"дебиторки",
|
||||
"кредиторка",
|
||||
"кредиторки",
|
||||
"просрочена",
|
||||
"просроченные",
|
||||
"просрочка",
|
||||
"прибыль",
|
||||
"маржа",
|
||||
"ндс"
|
||||
]);
|
||||
function organizationReferenceTokens(organization) {
|
||||
const rawTokens = organization
|
||||
.split(/[^A-Za-zА-Яа-яЁё0-9]+/u)
|
||||
.map((token) => token.trim())
|
||||
.filter((token) => token.length > 0);
|
||||
const boundaryIndex = rawTokens.findIndex((token) => {
|
||||
const lower = token.toLowerCase();
|
||||
return /^\d+$/.test(token) || ORGANIZATION_REFERENCE_BOUNDARY_WORDS.has(lower);
|
||||
});
|
||||
const scopedTokens = boundaryIndex > 0 ? rawTokens.slice(0, boundaryIndex) : rawTokens;
|
||||
return Array.from(new Set(scopedTokens
|
||||
.filter((token) => token.length >= 3)
|
||||
.filter((token) => !/^\d+$/.test(token))
|
||||
.filter((token) => !ORGANIZATION_REFERENCE_STOP_WORDS.has(token.toLowerCase())))).slice(0, 4);
|
||||
}
|
||||
function buildOrganizationReferenceCondition(filters, fieldPaths) {
|
||||
const organization = typeof filters.organization === "string" ? filters.organization.trim() : "";
|
||||
if (!organization) {
|
||||
return null;
|
||||
}
|
||||
const organizationTokens = organizationReferenceTokens(organization);
|
||||
const tokens = organizationTokens.length > 0 ? organizationTokens : [organization];
|
||||
const clauses = fieldPaths
|
||||
.map((fieldPath) => String(fieldPath ?? "").trim())
|
||||
.filter((fieldPath) => fieldPath.length > 0)
|
||||
.map((fieldPath) => {
|
||||
const tokenConditions = tokens.map((token) => {
|
||||
const escapedToken = toQueryStringLiteral(token);
|
||||
return `(Организации.Наименование ПОДОБНО "%${escapedToken}%" ИЛИ Организации.НаименованиеПолное ПОДОБНО "%${escapedToken}%")`;
|
||||
});
|
||||
const referenceSubquery = `(ВЫБРАТЬ Организации.Ссылка ИЗ Справочник.Организации КАК Организации ` +
|
||||
`ГДЕ ${tokenConditions.length === 1 ? tokenConditions[0] : tokenConditions.join(" И ")})`;
|
||||
return `${fieldPath} В ${referenceSubquery}`;
|
||||
});
|
||||
if (clauses.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return clauses.length === 1 ? clauses[0] : `(${clauses.join(" ИЛИ ")})`;
|
||||
}
|
||||
function buildAccountingFinancialResultAggregateSelect(filters, marker, debitLabel, creditLabel, debitPrefixes, creditPrefixes) {
|
||||
const whereClause = buildWhereClause(filters, "Движения.Период", [
|
||||
debitPrefixes.length > 0 ? buildAccountPrefixPredicate("Движения.СчетДт", debitPrefixes) : null,
|
||||
creditPrefixes.length > 0 ? buildAccountPrefixPredicate("Движения.СчетКт", creditPrefixes) : null,
|
||||
buildOrganizationReferenceCondition(filters, ["Движения.Организация"])
|
||||
].filter((item) => Boolean(item)));
|
||||
return `
|
||||
ВЫБРАТЬ
|
||||
ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0) КАК Период,
|
||||
"${marker}" КАК Регистратор,
|
||||
"${debitLabel}" КАК СчетДт,
|
||||
"${creditLabel}" КАК СчетКт,
|
||||
ЕСТЬNULL(СУММА(Движения.Сумма), 0) КАК Сумма,
|
||||
"" КАК СубконтоДт1,
|
||||
"" КАК СубконтоДт2,
|
||||
"" КАК СубконтоДт3,
|
||||
"" КАК СубконтоКт1,
|
||||
"" КАК СубконтоКт2,
|
||||
"" КАК СубконтоКт3,
|
||||
"" КАК Организация
|
||||
ИЗ
|
||||
РегистрБухгалтерии.Хозрасчетный.ДвиженияССубконто КАК Движения
|
||||
${whereClause}`;
|
||||
}
|
||||
function buildAccountingFinancialResultQuery(filters) {
|
||||
const rows = [
|
||||
["ACC90_REVENUE_KT", "ANY", "90.01", [], ["90.01"]],
|
||||
["ACC90_COST_DT", "90.02", "ANY", ["90.02"], []],
|
||||
["ACC90_SELLING_DT", "90.07", "ANY", ["90.07"], []],
|
||||
["ACC90_ADMIN_DT", "90.08", "ANY", ["90.08"], []],
|
||||
["ACC90_RESULT_TO_99_PROFIT", "90.09", "99", ["90.09"], ["99"]],
|
||||
["ACC90_RESULT_FROM_99_LOSS", "99", "90.09", ["99"], ["90.09"]],
|
||||
["ACC91_RESULT_TO_99_PROFIT", "91.09", "99", ["91.09"], ["99"]],
|
||||
["ACC91_RESULT_FROM_99_LOSS", "99", "91.09", ["99"], ["91.09"]],
|
||||
["ACC99_TO84_PROFIT_TRANSFER", "99", "84", ["99"], ["84"]],
|
||||
["ACC84_TO99_LOSS_TRANSFER", "84", "99", ["84"], ["99"]]
|
||||
];
|
||||
return rows
|
||||
.map(([marker, debitLabel, creditLabel, debitPrefixes, creditPrefixes]) => buildAccountingFinancialResultAggregateSelect(filters, marker, debitLabel, creditLabel, [...debitPrefixes], [...creditPrefixes]).trim())
|
||||
.join("\nОБЪЕДИНИТЬ ВСЕ\n");
|
||||
}
|
||||
function buildInventorySaleDocumentQuery(filters, resolvedLimit) {
|
||||
const itemCondition = buildInventoryItemReferenceCondition(filters, ["Товары.Номенклатура"]);
|
||||
return INVENTORY_SALE_DOCUMENTS_QUERY_TEMPLATE
|
||||
|
|
@ -1706,8 +1324,6 @@ function maxLimitForIntent(intent) {
|
|||
intent === "contract_usage_and_value" ||
|
||||
intent === "vat_payable_forecast" ||
|
||||
intent === "vat_liability_confirmed_for_tax_period" ||
|
||||
intent === "accounting_financial_result_for_organization" ||
|
||||
intent === "debt_due_date_aging_for_organization" ||
|
||||
intent === "inventory_on_hand_as_of_date" ||
|
||||
intent === "inventory_purchase_provenance_for_item" ||
|
||||
intent === "inventory_purchase_documents_for_item" ||
|
||||
|
|
@ -1717,7 +1333,6 @@ function maxLimitForIntent(intent) {
|
|||
intent === "inventory_profitability_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date" ||
|
||||
intent === "inventory_quality_events_for_organization" ||
|
||||
intent === "open_contracts_confirmed_as_of_date" ||
|
||||
intent === "list_contracts_by_counterparty" ||
|
||||
intent === "list_documents_by_counterparty" ||
|
||||
|
|
@ -1759,8 +1374,7 @@ function buildAddressRecipePlan(recipe, filters) {
|
|||
recipe.query_template === "counterparty_roles_profile" ||
|
||||
recipe.query_template === "contract_usage_profile" ||
|
||||
recipe.query_template === "vat_payable_forecast_profile" ||
|
||||
recipe.query_template === "vat_liability_confirmed_tax_period_profile" ||
|
||||
recipe.query_template === "accounting_financial_result_profile";
|
||||
recipe.query_template === "vat_liability_confirmed_tax_period_profile";
|
||||
const baseLimit = typeof filters.limit === "number" && Number.isFinite(filters.limit)
|
||||
? Math.max(1, Math.min(maxLimit, Math.trunc(filters.limit)))
|
||||
: recipe.default_limit;
|
||||
|
|
@ -1853,131 +1467,125 @@ function buildAddressRecipePlan(recipe, filters) {
|
|||
.replaceAll("__WHERE_CLAUSE__", buildManagementWhereClause(filters, "Движения.Период"))
|
||||
.replaceAll("__PERIOD_TO_EXPR__", periodToExpr);
|
||||
})()
|
||||
: recipe.query_template === "accounting_financial_result_profile"
|
||||
? buildAccountingFinancialResultQuery(filters)
|
||||
: recipe.query_template === "debt_due_date_aging_profile"
|
||||
? buildDebtDueDateAgingQuery(filters, resolvedLimit)
|
||||
: recipe.query_template === "vat_payable_confirmed_as_of_balance_profile"
|
||||
? (() => {
|
||||
const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
|
||||
? toDateTimeExpr(filters.as_of_date, true)
|
||||
: recipe.query_template === "vat_payable_confirmed_as_of_balance_profile"
|
||||
? (() => {
|
||||
const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
|
||||
? toDateTimeExpr(filters.as_of_date, true)
|
||||
: null) ??
|
||||
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0
|
||||
? toDateTimeExpr(filters.period_to, true)
|
||||
: null) ??
|
||||
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0
|
||||
? toDateTimeExpr(filters.period_from, true)
|
||||
: null) ??
|
||||
"ТЕКУЩАЯДАТА()";
|
||||
return VAT_PAYABLE_CONFIRMED_AS_OF_QUERY_TEMPLATE
|
||||
.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||
.replaceAll("__AS_OF_EXPR__", asOfExpr)
|
||||
.replaceAll("__VAT_PAYABLE_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", config_1.VAT_PAYABLE_68_PREFIXES))
|
||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||
})()
|
||||
: recipe.query_template === "inventory_on_hand_as_of_balance_profile"
|
||||
? (() => {
|
||||
const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
|
||||
? toDateTimeExpr(filters.as_of_date, true)
|
||||
: null) ??
|
||||
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0
|
||||
? toDateTimeExpr(filters.period_to, true)
|
||||
: null) ??
|
||||
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0
|
||||
? toDateTimeExpr(filters.period_to, true)
|
||||
: null) ??
|
||||
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0
|
||||
? toDateTimeExpr(filters.period_from, true)
|
||||
: null) ??
|
||||
"ТЕКУЩАЯДАТА()";
|
||||
return VAT_PAYABLE_CONFIRMED_AS_OF_QUERY_TEMPLATE
|
||||
.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||
.replaceAll("__AS_OF_EXPR__", asOfExpr)
|
||||
.replaceAll("__VAT_PAYABLE_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", config_1.VAT_PAYABLE_68_PREFIXES))
|
||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||
})()
|
||||
: recipe.query_template === "inventory_on_hand_as_of_balance_profile"
|
||||
? (() => {
|
||||
const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
|
||||
? toDateTimeExpr(filters.as_of_date, true)
|
||||
: null) ??
|
||||
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0
|
||||
? toDateTimeExpr(filters.period_to, true)
|
||||
: null) ??
|
||||
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0
|
||||
? toDateTimeExpr(filters.period_from, true)
|
||||
: null) ??
|
||||
"ТЕКУЩАЯДАТА()";
|
||||
return INVENTORY_ON_HAND_AS_OF_QUERY_TEMPLATE
|
||||
.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||
.replaceAll("__AS_OF_EXPR__", asOfExpr)
|
||||
.replaceAll("__INVENTORY_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["41.01"]))
|
||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||
})()
|
||||
: recipe.query_template === "inventory_purchase_provenance_profile"
|
||||
? buildInventoryPurchaseDocumentQuery(filters, resolvedLimit)
|
||||
: recipe.query_template === "inventory_purchase_documents_profile"
|
||||
? buildInventoryMovementQuery(filters, resolvedLimit, "dt")
|
||||
: recipe.query_template === "inventory_supplier_stock_overlap_profile"
|
||||
? buildInventoryMovementQuery(filters, resolvedLimit, "dt")
|
||||
: recipe.query_template === "inventory_sale_trace_profile"
|
||||
? buildInventorySaleDocumentQuery(filters, resolvedLimit)
|
||||
: recipe.query_template === "inventory_profitability_profile"
|
||||
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0
|
||||
? toDateTimeExpr(filters.period_from, true)
|
||||
: null) ??
|
||||
"ТЕКУЩАЯДАТА()";
|
||||
return INVENTORY_ON_HAND_AS_OF_QUERY_TEMPLATE
|
||||
.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||
.replaceAll("__AS_OF_EXPR__", asOfExpr)
|
||||
.replaceAll("__INVENTORY_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["41.01"]))
|
||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||
})()
|
||||
: recipe.query_template === "inventory_purchase_provenance_profile"
|
||||
? buildInventoryPurchaseDocumentQuery(filters, resolvedLimit)
|
||||
: recipe.query_template === "inventory_purchase_documents_profile"
|
||||
? buildInventoryMovementQuery(filters, resolvedLimit, "dt")
|
||||
: recipe.query_template === "inventory_supplier_stock_overlap_profile"
|
||||
? buildInventoryMovementQuery(filters, resolvedLimit, "dt")
|
||||
: recipe.query_template === "inventory_sale_trace_profile"
|
||||
? buildInventorySaleDocumentQuery(filters, resolvedLimit)
|
||||
: recipe.query_template === "inventory_profitability_profile"
|
||||
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
||||
: recipe.query_template === "inventory_trading_margin_proxy_profile"
|
||||
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
||||
: recipe.query_template === "inventory_purchase_to_sale_chain_profile"
|
||||
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
||||
: recipe.query_template === "inventory_trading_margin_proxy_profile"
|
||||
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
||||
: recipe.query_template === "inventory_purchase_to_sale_chain_profile"
|
||||
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
||||
: recipe.query_template === "inventory_aging_by_purchase_date_profile"
|
||||
? buildInventoryMovementQuery(filters, resolvedLimit, "dt")
|
||||
: recipe.query_template === "inventory_quality_events_profile"
|
||||
? buildInventoryQualityEventsQuery(filters, resolvedLimit)
|
||||
: recipe.query_template === "contracts_by_counterparty_profile"
|
||||
? CONTRACTS_BY_COUNTERPARTY_QUERY_TEMPLATE.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||
: recipe.query_template === "open_contracts_confirmed_as_of_balance_profile"
|
||||
? (() => {
|
||||
const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
|
||||
? toDateTimeExpr(filters.as_of_date, true)
|
||||
: recipe.query_template === "inventory_aging_by_purchase_date_profile"
|
||||
? buildInventoryMovementQuery(filters, resolvedLimit, "dt")
|
||||
: recipe.query_template === "contracts_by_counterparty_profile"
|
||||
? CONTRACTS_BY_COUNTERPARTY_QUERY_TEMPLATE.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||
: recipe.query_template === "open_contracts_confirmed_as_of_balance_profile"
|
||||
? (() => {
|
||||
const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
|
||||
? toDateTimeExpr(filters.as_of_date, true)
|
||||
: null) ??
|
||||
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0
|
||||
? toDateTimeExpr(filters.period_to, true)
|
||||
: null) ??
|
||||
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0
|
||||
? toDateTimeExpr(filters.period_from, true)
|
||||
: null) ??
|
||||
"ТЕКУЩАЯДАТА()";
|
||||
return OPEN_CONTRACTS_CONFIRMED_AS_OF_QUERY_TEMPLATE
|
||||
.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||
.replaceAll("__AS_OF_EXPR__", asOfExpr)
|
||||
.replaceAll("__OPEN_CONTRACT_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["60", "62", "76"]))
|
||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||
})()
|
||||
: recipe.query_template === "payables_confirmed_as_of_balance_profile"
|
||||
? (() => {
|
||||
const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
|
||||
? toDateTimeExpr(filters.as_of_date, true)
|
||||
: null) ??
|
||||
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0
|
||||
? toDateTimeExpr(filters.period_to, true)
|
||||
: null) ??
|
||||
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0
|
||||
? toDateTimeExpr(filters.period_from, true)
|
||||
: null) ??
|
||||
"ТЕКУЩАЯДАТА()";
|
||||
return PAYABLES_CONFIRMED_AS_OF_QUERY_TEMPLATE
|
||||
.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||
.replaceAll("__AS_OF_EXPR__", asOfExpr)
|
||||
.replaceAll("__PAYABLE_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["60", "76"]))
|
||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||
})()
|
||||
: recipe.query_template === "receivables_confirmed_as_of_balance_profile"
|
||||
? (() => {
|
||||
const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
|
||||
? toDateTimeExpr(filters.as_of_date, true)
|
||||
: null) ??
|
||||
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0
|
||||
? toDateTimeExpr(filters.period_to, true)
|
||||
: null) ??
|
||||
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0
|
||||
? toDateTimeExpr(filters.period_to, true)
|
||||
: null) ??
|
||||
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0
|
||||
? toDateTimeExpr(filters.period_from, true)
|
||||
: null) ??
|
||||
"ТЕКУЩАЯДАТА()";
|
||||
return OPEN_CONTRACTS_CONFIRMED_AS_OF_QUERY_TEMPLATE
|
||||
.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||
.replaceAll("__AS_OF_EXPR__", asOfExpr)
|
||||
.replaceAll("__OPEN_CONTRACT_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["60", "62", "76"]))
|
||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||
})()
|
||||
: recipe.query_template === "payables_confirmed_as_of_balance_profile"
|
||||
? (() => {
|
||||
const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
|
||||
? toDateTimeExpr(filters.as_of_date, true)
|
||||
: null) ??
|
||||
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0
|
||||
? toDateTimeExpr(filters.period_to, true)
|
||||
: null) ??
|
||||
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0
|
||||
? toDateTimeExpr(filters.period_from, true)
|
||||
: null) ??
|
||||
"ТЕКУЩАЯДАТА()";
|
||||
return OPEN_CONTRACTS_CONFIRMED_AS_OF_QUERY_TEMPLATE
|
||||
.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||
.replaceAll("__AS_OF_EXPR__", asOfExpr)
|
||||
.replaceAll("__OPEN_CONTRACT_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["60", "76"]))
|
||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||
})()
|
||||
: recipe.query_template === "receivables_confirmed_as_of_balance_profile"
|
||||
? (() => {
|
||||
const asOfExpr = (typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
|
||||
? toDateTimeExpr(filters.as_of_date, true)
|
||||
: null) ??
|
||||
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0
|
||||
? toDateTimeExpr(filters.period_to, true)
|
||||
: null) ??
|
||||
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0
|
||||
? toDateTimeExpr(filters.period_from, true)
|
||||
: null) ??
|
||||
"ТЕКУЩАЯДАТА()";
|
||||
return OPEN_CONTRACTS_CONFIRMED_AS_OF_QUERY_TEMPLATE
|
||||
.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||
.replaceAll("__AS_OF_EXPR__", asOfExpr)
|
||||
.replaceAll("__OPEN_CONTRACT_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["62", "76"]))
|
||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||
})()
|
||||
: MOVEMENTS_QUERY_TEMPLATE
|
||||
.replace("__LIMIT__", String(resolvedLimit))
|
||||
.replace("__WHERE_CLAUSE__", (() => {
|
||||
const extraConditions = [];
|
||||
const accountCondition = buildMovementAccountCondition(filters);
|
||||
if (accountCondition) {
|
||||
extraConditions.push(accountCondition);
|
||||
}
|
||||
return buildWhereClause(filters, "Движения.Период", extraConditions);
|
||||
})())
|
||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0
|
||||
? toDateTimeExpr(filters.period_from, true)
|
||||
: null) ??
|
||||
"ТЕКУЩАЯДАТА()";
|
||||
return RECEIVABLES_CONFIRMED_AS_OF_QUERY_TEMPLATE
|
||||
.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||
.replaceAll("__AS_OF_EXPR__", asOfExpr)
|
||||
.replaceAll("__RECEIVABLE_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["62", "76"]))
|
||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||
})()
|
||||
: MOVEMENTS_QUERY_TEMPLATE
|
||||
.replace("__LIMIT__", String(resolvedLimit))
|
||||
.replace("__WHERE_CLAUSE__", (() => {
|
||||
const extraConditions = [];
|
||||
const accountCondition = buildMovementAccountCondition(filters);
|
||||
if (accountCondition) {
|
||||
extraConditions.push(accountCondition);
|
||||
}
|
||||
return buildWhereClause(filters, "Движения.Период", extraConditions);
|
||||
})())
|
||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||
return {
|
||||
recipe,
|
||||
query,
|
||||
|
|
|
|||
|
|
@ -6,68 +6,44 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|||
exports.repairAddressMojibakeText = repairAddressMojibakeText;
|
||||
exports.normalizeRussianComparableText = normalizeRussianComparableText;
|
||||
const iconv_lite_1 = __importDefault(require("iconv-lite"));
|
||||
const MOJIBAKE_CONTINUATION_CLASS = "[\\u0080-\\u00bf\\u0401-\\u040f\\u0451-\\u045f\\u2018-\\u201e\\u2020-\\u2022\\u2013-\\u2014\\u2122\\u20ac]";
|
||||
const MOJIBAKE_PAIR_PATTERN = new RegExp(`(?:[\\u0420\\u0421]${MOJIBAKE_CONTINUATION_CLASS})`, "gu");
|
||||
function compactWhitespace(value) {
|
||||
return value.replace(/\s+/g, " ").trim();
|
||||
}
|
||||
function countMatches(value, pattern) {
|
||||
return (String(value ?? "").match(pattern) ?? []).length;
|
||||
}
|
||||
function textMojibakeScore(value) {
|
||||
const source = String(value ?? "");
|
||||
const cyrillic = countMatches(source, /[\u0400-\u04ff]/g);
|
||||
const latin = countMatches(source, /[A-Za-z]/g);
|
||||
const replacement = countMatches(source, /\uFFFD/g);
|
||||
const c1Controls = countMatches(source, /[\u0080-\u009f]/g);
|
||||
const pairMarkers = countMatches(source, MOJIBAKE_PAIR_PATTERN);
|
||||
const doubleEncodedMarkers = countMatches(source, /(?:\u0420[\u00a0-\u00bf]\u0421|\u0413[\u0080-\u00bf]|\u00c3.|\u00c2.)/gu);
|
||||
return cyrillic + latin - replacement * 8 - c1Controls * 5 - pairMarkers * 3 - doubleEncodedMarkers * 2;
|
||||
const cyrillic = (source.match(/[\u0400-\u04ff]/g) ?? []).length;
|
||||
const latin = (source.match(/[A-Za-z]/g) ?? []).length;
|
||||
const replacement = (source.match(/[<5B>]/g) ?? []).length;
|
||||
const pairMarkers = (source.match(/(?:Р.|С.|Ð.|Ñ.)/g) ?? []).length;
|
||||
const doubleEncodedMarkers = (source.match(/(?:Р“[Р-џ]|Р’[Р-џ]|Ã.|Â.)/gu) ?? []).length;
|
||||
return cyrillic + latin - replacement * 3 - pairMarkers * 2 - doubleEncodedMarkers * 2;
|
||||
}
|
||||
function looksLikeAddressMojibake(value) {
|
||||
const source = String(value ?? "");
|
||||
if (!source.trim()) {
|
||||
return false;
|
||||
}
|
||||
if (/[\u0080-\u009f\uFFFD]/.test(source)) {
|
||||
if (/[<EFBFBD>]/.test(source)) {
|
||||
return true;
|
||||
}
|
||||
if (countMatches(source, MOJIBAKE_PAIR_PATTERN) >= 2) {
|
||||
if ((source.match(/(?:Р.|С.|Ð.|Ñ.)/g) ?? []).length >= 2) {
|
||||
return true;
|
||||
}
|
||||
return countMatches(source, /(?:\u0420[\u00a0-\u00bf]\u0421|\u0413[\u0080-\u00bf]|\u00c3.|\u00c2.)/gu) >= 2;
|
||||
}
|
||||
function encodeWin1251MojibakeBytes(value) {
|
||||
const chunks = [];
|
||||
for (const char of String(value ?? "")) {
|
||||
const code = char.codePointAt(0) ?? 0;
|
||||
if (code >= 0x80 && code <= 0x9f) {
|
||||
chunks.push(Buffer.from([code]));
|
||||
continue;
|
||||
}
|
||||
chunks.push(iconv_lite_1.default.encode(char, "win1251"));
|
||||
if ((source.match(/(?:Р“[Р-џ]|Р’[Р-џ]|Ã.|Â.)/gu) ?? []).length >= 2) {
|
||||
return true;
|
||||
}
|
||||
return Buffer.concat(chunks);
|
||||
}
|
||||
function decodeUtf8FromWin1251Mojibake(value) {
|
||||
return encodeWin1251MojibakeBytes(value).toString("utf8");
|
||||
}
|
||||
function repairKnownReplacementDamagedRussianText(value) {
|
||||
return String(value ?? "")
|
||||
.replace(/\u0410\u041b\u042c\u0422\u0415\u0420\u041d\u0410\u0422[\uFFFD?]+\u0412\u0410/giu, "\u0410\u041b\u042c\u0422\u0415\u0420\u041d\u0410\u0422\u0418\u0412\u0410")
|
||||
.replace(/\u041e\u0411\u0429[\uFFFD?]+\u0419/giu, "\u041e\u0411\u0429\u0418\u0419");
|
||||
return false;
|
||||
}
|
||||
function repairAddressMojibakeText(value) {
|
||||
const source = String(value ?? "");
|
||||
const sourceWithKnownRepairs = repairKnownReplacementDamagedRussianText(source);
|
||||
if (!looksLikeAddressMojibake(sourceWithKnownRepairs)) {
|
||||
return sourceWithKnownRepairs;
|
||||
if (!looksLikeAddressMojibake(source)) {
|
||||
return source;
|
||||
}
|
||||
let candidate = sourceWithKnownRepairs;
|
||||
let candidate = source;
|
||||
for (let pass = 0; pass < 3; pass += 1) {
|
||||
let improved = false;
|
||||
try {
|
||||
const fromWin1251 = decodeUtf8FromWin1251Mojibake(candidate);
|
||||
const fromWin1251 = iconv_lite_1.default.encode(candidate, "win1251").toString("utf8");
|
||||
if (textMojibakeScore(fromWin1251) > textMojibakeScore(candidate)) {
|
||||
candidate = fromWin1251;
|
||||
improved = true;
|
||||
|
|
@ -86,17 +62,12 @@ function repairAddressMojibakeText(value) {
|
|||
catch {
|
||||
// Ignore decode failures and keep the current candidate.
|
||||
}
|
||||
const repairedKnownText = repairKnownReplacementDamagedRussianText(candidate);
|
||||
if (repairedKnownText !== candidate) {
|
||||
candidate = repairedKnownText;
|
||||
improved = true;
|
||||
}
|
||||
if (!improved) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return repairKnownReplacementDamagedRussianText(candidate);
|
||||
return candidate;
|
||||
}
|
||||
function normalizeRussianComparableText(value) {
|
||||
return compactWhitespace(repairAddressMojibakeText(String(value ?? "")).toLowerCase()).replace(/\u0451/g, "\u0435");
|
||||
return compactWhitespace(repairAddressMojibakeText(String(value ?? "")).toLowerCase()).replace(/ё/g, "е");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|||
exports.contractCandidatesFromRows = contractCandidatesFromRows;
|
||||
exports.composeFactualReply = composeFactualReply;
|
||||
exports.inferReplyType = inferReplyType;
|
||||
const assistantOrganizationMatcher_1 = require("../assistantOrganizationMatcher");
|
||||
const replyPackaging_1 = require("./replyPackaging");
|
||||
const counterpartyAnalyticsReplyBuilders_1 = require("./counterpartyAnalyticsReplyBuilders");
|
||||
const inventoryReplyBuilders_1 = require("./inventoryReplyBuilders");
|
||||
|
|
@ -266,123 +265,6 @@ 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 hasBankIncomingRoleBoundaryQuestion(userMessage) {
|
||||
const text = normalizeQuestionText(userMessage);
|
||||
return (/(?:входящ|поступлен|клиентск|выручк|кредит|депозит|возврат)/iu.test(text) &&
|
||||
/(?:банк|сбербанк|финанс)/iu.test(text));
|
||||
}
|
||||
function hasBankOutgoingRoleBoundaryQuestion(userMessage) {
|
||||
const text = normalizeQuestionText(userMessage);
|
||||
return (/(?:исходящ|списан|платеж|поставщик|закуп|выплат)/iu.test(text) &&
|
||||
/(?:банк|сбербанк|финанс)/iu.test(text));
|
||||
}
|
||||
function bankOperationDirection(row) {
|
||||
const text = normalizeQuestionText(`${row.registrator} ${row.operation_kind ?? ""}`);
|
||||
if (/(?:поступлени[ея]\s+на\s+расчетн|bank\s+receipt|incoming)/iu.test(text)) {
|
||||
return "incoming";
|
||||
}
|
||||
if (/(?:списани[ея]\s+с\s+расчетн|bank\s+payment|outgoing|write[-\s]?off)/iu.test(text)) {
|
||||
return "outgoing";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
function bankOperationDirectionLabel(direction) {
|
||||
if (direction === "incoming") {
|
||||
return "входящее поступление";
|
||||
}
|
||||
if (direction === "outgoing") {
|
||||
return "исходящее списание";
|
||||
}
|
||||
return "банковская операция без надежно распознанного направления";
|
||||
}
|
||||
function summarizeBankOperationDirections(rows) {
|
||||
const summary = {
|
||||
incoming: { count: 0, amount: 0 },
|
||||
outgoing: { count: 0, amount: 0 },
|
||||
unknown: { count: 0, amount: 0 }
|
||||
};
|
||||
for (const row of rows) {
|
||||
const direction = bankOperationDirection(row);
|
||||
const amount = typeof row.amount === "number" && Number.isFinite(row.amount) ? Math.abs(row.amount) : 0;
|
||||
summary[direction].count += 1;
|
||||
summary[direction].amount += amount;
|
||||
}
|
||||
const parts = [];
|
||||
if (summary.incoming.count > 0) {
|
||||
parts.push(`входящие: ${formatMoneyRub(summary.incoming.amount)} (${summary.incoming.count} строк)`);
|
||||
}
|
||||
if (summary.outgoing.count > 0) {
|
||||
parts.push(`исходящие: ${formatMoneyRub(summary.outgoing.amount)} (${summary.outgoing.count} строк)`);
|
||||
}
|
||||
if (summary.unknown.count > 0) {
|
||||
parts.push(`без распознанного направления: ${formatMoneyRub(summary.unknown.amount)} (${summary.unknown.count} строк)`);
|
||||
}
|
||||
return parts.length > 0
|
||||
? `Сводка по направлению: ${parts.join("; ")}.`
|
||||
: "Сводка по направлению: подтвержденные строки не найдены.";
|
||||
}
|
||||
function preferredBankEvidenceDirection(userMessage) {
|
||||
if (hasBankIncomingRoleBoundaryQuestion(userMessage)) {
|
||||
return "incoming";
|
||||
}
|
||||
if (hasBankOutgoingRoleBoundaryQuestion(userMessage)) {
|
||||
return "outgoing";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function bankOperationEvidenceLine(rows, preferredDirection = null) {
|
||||
const sample = (preferredDirection ? rows.find((row) => bankOperationDirection(row) === preferredDirection) : null) ?? rows[0];
|
||||
if (!sample) {
|
||||
return "Проверенная строка 1С не найдена.";
|
||||
}
|
||||
const direction = bankOperationDirection(sample);
|
||||
const parts = [`тип по документу: ${bankOperationDirectionLabel(direction)}`];
|
||||
const operationKind = String(sample.operation_kind ?? "").trim();
|
||||
const paymentPurpose = String(sample.payment_purpose ?? "").trim();
|
||||
const contract = String(sample.contract ?? "").trim();
|
||||
if (operationKind) {
|
||||
parts.push(`вид операции: ${operationKind}`);
|
||||
}
|
||||
if (paymentPurpose) {
|
||||
parts.push(`назначение платежа: ${paymentPurpose}`);
|
||||
}
|
||||
if (contract) {
|
||||
parts.push(`договор: ${contract}`);
|
||||
}
|
||||
if (!operationKind && !paymentPurpose && !contract) {
|
||||
parts.push("вид операции/назначение платежа/договор в материализованной строке не заполнены");
|
||||
}
|
||||
return `Основание 1С: ${parts.join("; ")}.`;
|
||||
}
|
||||
function bankRoleBoundaryLine(userMessage, rows) {
|
||||
const incomingBoundary = hasBankIncomingRoleBoundaryQuestion(userMessage);
|
||||
const outgoingBoundary = hasBankOutgoingRoleBoundaryQuestion(userMessage);
|
||||
if (!incomingBoundary && !outgoingBoundary) {
|
||||
return null;
|
||||
}
|
||||
const directions = rows.map(bankOperationDirection);
|
||||
const hasIncomingRow = directions.includes("incoming");
|
||||
const hasOutgoingRow = directions.includes("outgoing");
|
||||
if (incomingBoundary) {
|
||||
return hasIncomingRow
|
||||
? "Это не обычный клиент и не клиентская выручка автоматически: для банка/финорганизации нужен вид операции, назначение платежа и договор; кредитный, депозитный или возвратный смысл без этих полей не исключаю и не притягиваю."
|
||||
: hasOutgoingRow
|
||||
? "В найденных строках по банку подтверждено исходящее списание, а входящее поступление от банка в этом срезе не подтверждено; это не подтвержденная клиентская выручка, кредит или депозит."
|
||||
: "Входящее поступление от банка в найденных строках не подтверждено; это не подтвержденная клиентская выручка, кредитный или депозитный смысл.";
|
||||
}
|
||||
return "Это не обычный поставщик автоматически: для банка/финорганизации нужен вид операции, назначение платежа и договор; текущий срез подтверждает банковский платежный контур, а не бизнес-роль поставщика.";
|
||||
}
|
||||
function hasInventoryPurchaseDateActionFocus(userMessage) {
|
||||
const text = normalizeQuestionText(userMessage);
|
||||
if (!text) {
|
||||
|
|
@ -633,12 +515,10 @@ function detectValueRankingFocus(userMessage) {
|
|||
if (asksTotalMoneyEarned) {
|
||||
return "total_flow";
|
||||
}
|
||||
const hasCounterpartyRankingSubject = /(?:клиент|заказчик|покупател|контрагент|customer|client|counterpart|\u043a\u043b\u0438\u0435\u043d\u0442|\u0437\u0430\u043a\u0430\u0437\u0447\u0438\u043a|\u043f\u043e\u043a\u0443\u043f\u0430\u0442\u0435\u043b|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442)/iu.test(text);
|
||||
const asksExplicitYearBreakdown = /(?:по\s+годам|за\s+какие\s+годы|динамик\w*\s+по\s+год|yearly\s+breakdown|by\s+year|\u043f\u043e\s+\u0433\u043e\u0434\u0430\u043c|\u0437\u0430\s+\u043a\u0430\u043a\u0438\u0435\s+\u0433\u043e\u0434\u044b|\u0434\u0438\u043d\u0430\u043c\u0438\u043a\w*\s+\u043f\u043e\s+\u0433\u043e\u0434)/iu.test(text);
|
||||
const asksYearlyRevenueRanking = /(?:доходн|выручк|оборот|прибыл|деньг|денег|revenue|turnover|income)/iu.test(text) &&
|
||||
/(?:год|года|годы|year|years|по\s+годам)/iu.test(text) &&
|
||||
/(?:сам(?:ый|ая|ое|ые)|топ|луч|best|max|наибольш|больше)/iu.test(text);
|
||||
if (asksYearlyRevenueRanking && (!hasCounterpartyRankingSubject || asksExplicitYearBreakdown)) {
|
||||
if (asksYearlyRevenueRanking) {
|
||||
return "top_years_by_total";
|
||||
}
|
||||
if (/(?:сам(?:ый|ая|ое|ые)\s+высок[а-яё]*|highest|largest)\s+чек|(?:max\s+check|чек\s+макс)/iu.test(text)) {
|
||||
|
|
@ -696,42 +576,25 @@ function extractRequestedYearFromQuestion(userMessage) {
|
|||
return 2000 + shortYear;
|
||||
}
|
||||
function extractCounterpartyName(row) {
|
||||
const isCounterpartyLikeToken = (value, skipPattern) => {
|
||||
const normalized = String(value ?? "").trim();
|
||||
const skipTokenPattern = /(?:^0$|^<пусто>$|^пустая ссылка$|договор|contract|документ|операц|счет[-\s]?фактур|накладн|акт|поступлен|списани|плат[её]ж|перевод|банк|касса|расчетн|проводк|movement|invoice|payment)/iu;
|
||||
for (const token of row.analytics) {
|
||||
const normalized = String(token ?? "").trim();
|
||||
if (!normalized) {
|
||||
return null;
|
||||
continue;
|
||||
}
|
||||
if (/^\d{4}-\d{2}-\d{2}/.test(normalized)) {
|
||||
return null;
|
||||
continue;
|
||||
}
|
||||
if (/^\d+(?:[./-]\d+)*$/.test(normalized)) {
|
||||
return null;
|
||||
continue;
|
||||
}
|
||||
if (!/[a-zа-я]/iu.test(normalized)) {
|
||||
return null;
|
||||
continue;
|
||||
}
|
||||
if (skipPattern.test(normalized)) {
|
||||
return null;
|
||||
if (skipTokenPattern.test(normalized)) {
|
||||
continue;
|
||||
}
|
||||
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();
|
||||
|
|
@ -1285,16 +1148,6 @@ 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) {
|
||||
|
|
@ -1574,211 +1427,6 @@ 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) {
|
||||
|
|
@ -1952,45 +1600,6 @@ 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()
|
||||
|
|
@ -1998,12 +1607,6 @@ 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);
|
||||
|
|
@ -2050,41 +1653,6 @@ 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);
|
||||
|
|
@ -2948,7 +2516,7 @@ function composeFactualReplyBody(intent, rows, options = {}) {
|
|||
const nonErrorProbeRows = orderedProbeRows.filter((item) => item.status !== "error");
|
||||
const visibleProbeRows = (nonErrorProbeRows.length > 0 ? nonErrorProbeRows : orderedProbeRows).slice(0, 6);
|
||||
const erroredSources = vatProbe.probedSources.filter((item) => item.status === "error").length;
|
||||
lines.push("", "Покрытие VAT-источников в 1С:", `- Найдено VAT-объектов: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`, `- Прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`, `- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.`, `- Источников с ошибкой запроса: ${formatNumberWithDots(erroredSources)}.`);
|
||||
lines.push("", "Покрытие VAT-источников через MCP:", `- Найдено VAT-объектов: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`, `- Прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`, `- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.`, `- Источников с ошибкой запроса: ${formatNumberWithDots(erroredSources)}.`);
|
||||
if (visibleProbeRows.length > 0) {
|
||||
lines.push(...visibleProbeRows.map((item, index) => {
|
||||
const name = item.synonym ? `${item.fullName} (${item.synonym})` : item.fullName;
|
||||
|
|
@ -2968,7 +2536,7 @@ function composeFactualReplyBody(intent, rows, options = {}) {
|
|||
lines.push("- Сумма прогноза выше рассчитана строго по оборотам 68.02*/19*; прямые VAT-источники показаны для проверки покрытия.");
|
||||
}
|
||||
else if (vatProbe && vatProbe.status === "error") {
|
||||
lines.push("", "Покрытие VAT-источников в 1С: дополнительная проверка завершилась ошибкой, поэтому использован только базовый контур 68.02*/19*.");
|
||||
lines.push("", "Покрытие VAT-источников через MCP: probe завершился ошибкой, поэтому использован только базовый контур 68.02*/19*.");
|
||||
}
|
||||
if (!vatActivityDetected) {
|
||||
lines.push(`В выбранном окне не найдено движений по НДС-субсчетам 68.02*/19*; поэтому оперативный прогноз к уплате равен ${formatForecastMoney(0)}.`);
|
||||
|
|
@ -3020,15 +2588,12 @@ function composeFactualReplyBody(intent, rows, options = {}) {
|
|||
const periodWindowLabel = options.periodFrom && options.periodTo ? `${formatDateRu(options.periodFrom)}..${formatDateRu(options.periodTo)}` : null;
|
||||
const formatConfirmedMoney = (value) => (options.useRubCurrency ? formatMoneyRub(value) : formatMoney(value));
|
||||
const vatProbe = options.vatDirectSourceProbe ?? null;
|
||||
const organizationLabel = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeValue)(options.organizationHint);
|
||||
const organizationScopeLabel = organizationLabel ? ` по организации ${organizationLabel}` : "";
|
||||
const lines = [
|
||||
`Коротко: подтвержденный НДС к уплате за налоговый период${organizationScopeLabel} — ${formatConfirmedMoney(vatToPay)}.`,
|
||||
`Коротко: подтвержденный НДС к уплате за налоговый период — ${formatConfirmedMoney(vatToPay)}.`,
|
||||
`Если смотреть на возможный перенос или переплату, получается ${formatConfirmedMoney(carryoverOrOverpayment)}.`,
|
||||
"Это подтвержденный расчет по регистрам книг продаж и покупок, без surrogate-формулы 68/19.",
|
||||
"",
|
||||
"Что вошло в расчет:",
|
||||
...(organizationLabel ? [`- Организация: ${organizationLabel}.`] : []),
|
||||
`- Налоговый период расчета: ${periodWindowLabel ?? "не задан (нужен явный период)"}.`,
|
||||
`- НДС по книге продаж: ${formatConfirmedMoney(salesVat)}.`,
|
||||
`- НДС по книге покупок (вычеты): ${formatConfirmedMoney(purchaseVat)}.`,
|
||||
|
|
@ -3037,14 +2602,14 @@ function composeFactualReplyBody(intent, rows, options = {}) {
|
|||
if (vatProbe && vatProbe.status === "ok") {
|
||||
const nonEmptySources = vatProbe.probedSources.filter((item) => item.status === "ok").length;
|
||||
const erroredSources = vatProbe.probedSources.filter((item) => item.status === "error").length;
|
||||
lines.push("", "Покрытие VAT-источников в 1С:", `- Найдено VAT-объектов: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`, `- Прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`, `- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.`, `- Источников с ошибкой запроса: ${formatNumberWithDots(erroredSources)}.`);
|
||||
lines.push("", "Покрытие VAT-источников через MCP:", `- Найдено VAT-объектов: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`, `- Прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`, `- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.`, `- Источников с ошибкой запроса: ${formatNumberWithDots(erroredSources)}.`);
|
||||
if (vatProbe.errors.length > 0) {
|
||||
lines.push(`- Ограничения probe: ${vatProbe.errors.slice(0, 2).join("; ")}.`);
|
||||
}
|
||||
lines.push("- Сумма расчета выше получена по книгам продаж/покупок; дополнительная проверка использована для контроля полноты VAT-источников.");
|
||||
lines.push("- Сумма расчета выше получена по книгам продаж/покупок; probe использован для контроля полноты VAT-источников.");
|
||||
}
|
||||
else if (vatProbe && vatProbe.status === "error") {
|
||||
lines.push("", "Покрытие VAT-источников в 1С: дополнительная проверка недоступна, поэтому использован основной бухгалтерский срез.", "Итоговая сумма НДС выше рассчитана по основному маршруту книг продаж/покупок; probe влияет только на диагностику покрытия.");
|
||||
lines.push("", "Покрытие VAT-источников через MCP: дополнительный probe недоступен (например, timeout metadata).", "Итоговая сумма НДС выше рассчитана по основному маршруту книг продаж/покупок; probe влияет только на диагностику покрытия.");
|
||||
if (vatProbe.errors.length > 0) {
|
||||
lines.push(`- Детали probe: ${vatProbe.errors.slice(0, 2).join("; ")}.`);
|
||||
}
|
||||
|
|
@ -3114,7 +2679,7 @@ function composeFactualReplyBody(intent, rows, options = {}) {
|
|||
const vatProbe = options.vatDirectSourceProbe ?? null;
|
||||
if (vatProbe && vatProbe.status === "ok") {
|
||||
const nonEmptySources = vatProbe.probedSources.filter((item) => item.status === "ok").length;
|
||||
lines.push("", "Блок 2.1. Проверка VAT-источников в 1С", `- VAT-объектов в метаданных 1С: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`, `- Пробных прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`, `- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.`);
|
||||
lines.push("", "Блок 2.1. MCP-проверка VAT-источников", `- VAT-объектов в метаданных 1С: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`, `- Пробных прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`, `- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.`);
|
||||
if (vatProbe.probedSources.length > 0) {
|
||||
lines.push(...vatProbe.probedSources.slice(0, 4).map((item, index) => {
|
||||
const name = item.synonym ? `${item.fullName} (${item.synonym})` : item.fullName;
|
||||
|
|
@ -3131,7 +2696,7 @@ function composeFactualReplyBody(intent, rows, options = {}) {
|
|||
}
|
||||
}
|
||||
else if (vatProbe && vatProbe.status === "error") {
|
||||
lines.push("", "Блок 2.1. Проверка VAT-источников в 1С", "- Дополнительная проверка VAT-источников завершилась ошибкой, поэтому срез подтвержден по доступному бухгалтерскому источнику (68*).");
|
||||
lines.push("", "Блок 2.1. MCP-проверка VAT-источников", "- Probe VAT-источников завершился ошибкой, поэтому срез подтвержден по доступному бухгалтерскому источнику (68*).");
|
||||
}
|
||||
lines.push("", "Блок 3. Сводка", `- Строк в выборке: ${formatNumberWithDots(rows.length)}.`, `- Подтвержденных позиций по НДС: ${formatNumberWithDots(accountRows.length)}.`, "", "Блок 4. Подтвержденные позиции");
|
||||
if (accountRows.length > 0) {
|
||||
|
|
@ -3384,8 +2949,7 @@ function composeFactualReplyBody(intent, rows, options = {}) {
|
|||
}
|
||||
if (intent === "payables_confirmed_as_of_date") {
|
||||
const payablesAsOfDate = resolvePayablesAsOfDate(options);
|
||||
const balanceSnapshot = buildPayablesConfirmedBalanceSnapshot(rows, payablesAsOfDate);
|
||||
const confirmedBalances = balanceSnapshot.balances;
|
||||
const confirmedBalances = buildPayablesConfirmedBalanceAggregate(rows, payablesAsOfDate);
|
||||
const asOfDate = normalizeIsoDateOnly(options.asOfDate);
|
||||
const periodFrom = normalizeIsoDateOnly(options.periodFrom);
|
||||
const periodTo = normalizeIsoDateOnly(options.periodTo);
|
||||
|
|
@ -3400,35 +2964,6 @@ 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)}.`,
|
||||
"Это подтвержденный срез обязательств к оплате по точному остатку."
|
||||
|
|
@ -3447,7 +2982,6 @@ 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)}.`);
|
||||
|
|
@ -3480,8 +3014,7 @@ function composeFactualReplyBody(intent, rows, options = {}) {
|
|||
}
|
||||
if (intent === "receivables_confirmed_as_of_date") {
|
||||
const receivablesAsOfDate = resolveReceivablesAsOfDate(options);
|
||||
const balanceSnapshot = buildReceivablesConfirmedBalanceSnapshot(rows, receivablesAsOfDate);
|
||||
const confirmedBalances = balanceSnapshot.balances;
|
||||
const confirmedBalances = buildReceivablesConfirmedBalanceAggregate(rows, receivablesAsOfDate);
|
||||
const asOfDate = normalizeIsoDateOnly(options.asOfDate);
|
||||
const periodFrom = normalizeIsoDateOnly(options.periodFrom);
|
||||
const periodTo = normalizeIsoDateOnly(options.periodTo);
|
||||
|
|
@ -3496,35 +3029,6 @@ 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."
|
||||
|
|
@ -3543,7 +3047,6 @@ 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)}.`);
|
||||
|
|
@ -3651,8 +3154,7 @@ function composeFactualReplyBody(intent, rows, options = {}) {
|
|||
return lines;
|
||||
};
|
||||
if (options.requestedResultMode === "confirmed_balance") {
|
||||
const balanceSnapshot = buildPayablesConfirmedBalanceSnapshot(rows, payablesAsOfDate);
|
||||
const confirmedBalances = balanceSnapshot.balances;
|
||||
const confirmedBalances = buildPayablesConfirmedBalanceAggregate(rows, payablesAsOfDate);
|
||||
if (confirmedBalances.length > 0) {
|
||||
const categoryCounts = confirmedBalances.reduce((acc, item) => {
|
||||
acc[item.category] += 1;
|
||||
|
|
@ -3687,7 +3189,6 @@ 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),
|
||||
|
|
@ -3926,39 +3427,22 @@ function composeFactualReplyBody(intent, rows, options = {}) {
|
|||
};
|
||||
}
|
||||
if (intent === "bank_operations_by_counterparty") {
|
||||
const rowCounterparties = uniqueStrings(rows
|
||||
.map((row) => extractCounterpartyName(row))
|
||||
.filter((item) => Boolean(item)));
|
||||
const counterparty = resolvePreferredCounterpartyDisplayLabel(options.counterpartyHint, rowCounterparties);
|
||||
const roleBoundary = bankRoleBoundaryLine(options.userMessage, rows);
|
||||
const visibleRows = rows.slice(0, Math.min(rows.length, 5));
|
||||
const lines = [
|
||||
`Коротко: найдено банковских операций${counterparty ? ` по ${counterparty}` : " по контрагенту"} — ${rows.length}.`,
|
||||
summarizeBankOperationDirections(rows),
|
||||
roleBoundary ?? "Показываю подтвержденные банковские операции из текущего среза.",
|
||||
bankOperationEvidenceLine(rows, preferredBankEvidenceDirection(options.userMessage)),
|
||||
...formatTopRows(visibleRows, visibleRows.length)
|
||||
`Коротко: найдено банковских операций по контрагенту — ${rows.length}.`,
|
||||
"Показываю подтвержденные банковские операции из текущего среза.",
|
||||
...formatTopRows(rows, rows.length)
|
||||
];
|
||||
if (rows.length > visibleRows.length) {
|
||||
lines.push(`Показаны первые ${visibleRows.length} из ${rows.length}; полный список остается в подтвержденном срезе.`);
|
||||
}
|
||||
return {
|
||||
responseType: "FACTUAL_LIST",
|
||||
text: lines.join("\n")
|
||||
};
|
||||
}
|
||||
if (intent === "bank_operations_by_contract") {
|
||||
const visibleRows = rows.slice(0, Math.min(rows.length, 5));
|
||||
const lines = [
|
||||
`Коротко: найдено банковских операций по договору — ${rows.length}.`,
|
||||
summarizeBankOperationDirections(rows),
|
||||
"Показываю подтвержденные банковские операции из текущего среза.",
|
||||
bankOperationEvidenceLine(rows),
|
||||
...formatTopRows(visibleRows, visibleRows.length)
|
||||
...formatTopRows(rows, rows.length)
|
||||
];
|
||||
if (rows.length > visibleRows.length) {
|
||||
lines.push(`Показаны первые ${visibleRows.length} из ${rows.length}; полный список остается в подтвержденном срезе.`);
|
||||
}
|
||||
return {
|
||||
responseType: "FACTUAL_LIST",
|
||||
text: lines.join("\n")
|
||||
|
|
|
|||
|
|
@ -48,9 +48,9 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
|
|||
const includeTotal = focus === "full_profile" || focus === "total_only";
|
||||
const includeRoles = focus === "full_profile" || focus === "roles_only";
|
||||
const directLead = focus === "suppliers_only"
|
||||
? `Поставщиков с ролью поставщика: ${supplierOnly}.`
|
||||
? `Поставщиков (только supplier-роль): ${supplierOnly}.`
|
||||
: focus === "customers_only"
|
||||
? `Заказчиков с ролью покупателя: ${customerOnly}.`
|
||||
? `Заказчиков (только customer-роль): ${customerOnly}.`
|
||||
: focus === "mixed_only"
|
||||
? `Контрагентов со смешанной ролью: ${mixedActive}.`
|
||||
: includeTotal && totalCounterparties > 0
|
||||
|
|
@ -74,9 +74,9 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
|
|||
}
|
||||
if (includeRoles) {
|
||||
if (resolvedActive > 0 || activeCounterparties > 0) {
|
||||
lines.push("Распределение ролей по активности:");
|
||||
lines.push(`Заказчики с ролью покупателя: ${customerOnly}.`);
|
||||
lines.push(`Поставщики с ролью поставщика: ${supplierOnly}.`);
|
||||
lines.push("Роли контрагентов по активности:");
|
||||
lines.push(`Заказчики (только customer-роль): ${customerOnly}.`);
|
||||
lines.push(`Поставщики (только supplier-роль): ${supplierOnly}.`);
|
||||
lines.push(`Смешанные (и покупатель, и поставщик): ${mixedActive}.`);
|
||||
lines.push(`4. Всего активных контрагентов: ${activeCounterparties}.`);
|
||||
if (otherCounterparties !== null) {
|
||||
|
|
@ -88,10 +88,10 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
|
|||
}
|
||||
}
|
||||
if (focus === "suppliers_only") {
|
||||
lines.push(`Поставщиков с ролью поставщика: ${supplierOnly}.`);
|
||||
lines.push(`Поставщиков (только supplier-роль): ${supplierOnly}.`);
|
||||
}
|
||||
if (focus === "customers_only") {
|
||||
lines.push(`Заказчиков с ролью покупателя: ${customerOnly}.`);
|
||||
lines.push(`Заказчиков (только customer-роль): ${customerOnly}.`);
|
||||
}
|
||||
if (focus === "mixed_only") {
|
||||
lines.push(`Контрагентов со смешанной ролью: ${mixedActive}.`);
|
||||
|
|
@ -387,15 +387,6 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
|
|||
const limit = deps.detectRankingLimit(options.userMessage, 20);
|
||||
const minOpsForAvgCheck = deps.detectMinOpsForAvgCheck(options.userMessage);
|
||||
const normalizedQuestion = deps.normalizeQuestionText(options.userMessage);
|
||||
const asksSingleBestCounterparty = focus === "top_by_total" &&
|
||||
/(?:какой|кто|which|who|какой|кто)/iu.test(normalizedQuestion) &&
|
||||
/(?:больше\s+всего|сам(?:ый|ая|ое|ые)|наибольш|прин[её]с|highest|most|больше\s+всего|сам(?:ый|ая|РѕРµ|ые)|наибол|РїСЂРёРЅ[её]СЃ)/iu.test(normalizedQuestion) &&
|
||||
!/(?:\btop\b|топ|рейтинг|список|первые|покажи\s+топ|дай\s+топ|покаж\w*\s+топ|дай\s+топ)/iu.test(normalizedQuestion);
|
||||
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 = [];
|
||||
|
|
@ -563,7 +554,7 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
|
|||
? `Топ-${visible.length} поставщиков по максимальной разовой выплате:`
|
||||
: `Топ-${visible.length} заказчиков по максимальной сумме одной входящей операции:`;
|
||||
lines.unshift(heading);
|
||||
lines.push(...visible.map((item, index) => `${index + 1}. ${item.name} | максимальная разовая сумма: ${deps.formatMoneyRub(item.maxSingle)} | сумма: ${deps.formatMoneyRub(item.total)} | операций: ${item.ops}`));
|
||||
lines.push(...visible.map((item, index) => `${index + 1}. ${item.name} | max single: ${item.maxSingle} | максимальная разовая сумма: ${deps.formatMoneyRub(item.maxSingle)} | сумма: ${deps.formatMoneyRub(item.total)} | операций: ${item.ops}`));
|
||||
return (0, replyContracts_1.buildFactualListReply)(lines);
|
||||
}
|
||||
if (focus === "top_by_avg_check_min_ops") {
|
||||
|
|
@ -601,11 +592,8 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
|
|||
lines.push(...visible.map((item, index) => `${index + 1}. ${formatOptionalDate(item.period, deps.formatDateRu)} | ${item.counterparty} | ${item.registrator} | ${deps.formatMoneyRub(item.amount)}`));
|
||||
return (0, replyContracts_1.buildFactualListReply)(lines);
|
||||
}
|
||||
const visible = rankedByTotal.slice(0, effectiveLimit);
|
||||
const visible = rankedByTotal.slice(0, limit);
|
||||
const singleCandidateOnly = rankedByTotal.length === 1;
|
||||
const rankingPeriodLabel = options.periodFrom && options.periodTo
|
||||
? `за период ${deps.formatDateRu(options.periodFrom)}..${deps.formatDateRu(options.periodTo)}`
|
||||
: "за доступное время";
|
||||
const heading = singleCandidateOnly
|
||||
? isSupplier
|
||||
? "Найденный поставщик по сумме выплат:"
|
||||
|
|
@ -615,17 +603,14 @@ function composeCounterpartyAnalyticsReply(intent, rows, options = {}, deps) {
|
|||
: `Топ-${visible.length} заказчиков по сумме поступлений:`;
|
||||
const leadingCounterparty = visible[0] ?? null;
|
||||
lines.unshift(heading);
|
||||
if (options.periodFrom && options.periodTo) {
|
||||
lines.push(`Период рейтинга: ${rankingPeriodLabel}.`);
|
||||
}
|
||||
if (leadingCounterparty) {
|
||||
const directAnswerLine = singleCandidateOnly
|
||||
? isSupplier
|
||||
? `В выбранном срезе найден один поставщик: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг.`
|
||||
: `В выбранном срезе найден один клиент: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг; сумма является денежным потоком, а не чистой прибылью.`
|
||||
: isSupplier
|
||||
? `Крупнейший поставщик по подтвержденным выплатам ${rankingPeriodLabel}: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).`
|
||||
: `Самый доходный клиент ${rankingPeriodLabel} по подтвержденным поступлениям: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это денежный поток, а не чистая прибыль.`;
|
||||
? `Крупнейший поставщик по подтвержденным выплатам за доступное время: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).`
|
||||
: `Самый доходный клиент за доступное время по подтвержденным поступлениям: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это денежный поток, а не чистая прибыль.`;
|
||||
lines.unshift(directAnswerLine);
|
||||
}
|
||||
lines.push(...visible.map((item, index) => {
|
||||
|
|
|
|||
|
|
@ -126,20 +126,11 @@ function hasExplicitLooseByAnchorToken(text) {
|
|||
return !pronounTokens.has(token) && !genericTokens.has(token);
|
||||
}
|
||||
const FOLLOWUP_LOW_QUALITY_COUNTERPARTY_TOKENS = new Set([
|
||||
"мы",
|
||||
"нам",
|
||||
"нас",
|
||||
"наш",
|
||||
"наша",
|
||||
"наше",
|
||||
"наши",
|
||||
"унас",
|
||||
"есть",
|
||||
"же",
|
||||
"что",
|
||||
"все",
|
||||
"всё",
|
||||
"или",
|
||||
"кроме",
|
||||
"помимо",
|
||||
"этого",
|
||||
|
|
@ -158,30 +149,6 @@ const FOLLOWUP_LOW_QUALITY_COUNTERPARTY_TOKENS = new Set([
|
|||
"договора",
|
||||
"контрагент",
|
||||
"контрагента",
|
||||
"контрагенту",
|
||||
"клиент",
|
||||
"клиента",
|
||||
"клиенту",
|
||||
"клиентом",
|
||||
"клиенты",
|
||||
"поставщик",
|
||||
"поставщика",
|
||||
"поставщику",
|
||||
"поставщиком",
|
||||
"поставщики",
|
||||
"покупатель",
|
||||
"покупателя",
|
||||
"покупателю",
|
||||
"заказчик",
|
||||
"заказчика",
|
||||
"заказчику",
|
||||
"обычный",
|
||||
"обычная",
|
||||
"обычное",
|
||||
"обычные",
|
||||
"обычного",
|
||||
"обычному",
|
||||
"обычным",
|
||||
"еще",
|
||||
"ещё",
|
||||
"другие",
|
||||
|
|
@ -679,19 +646,6 @@ function hasBroadCounterpartyRankingCue(text) {
|
|||
}
|
||||
return /(?:\bкто\b|\bкакие\b|\bкакой\b|\bтоп\b|\bсписок\b|\bвсе\b|\bвсех\b|\bвсего\b|\bclients?\b|\bcounterpart(?:y|ies)\b|контрагент|клиент|заказчик)/iu.test(normalized);
|
||||
}
|
||||
function isBroadDebtPolarityQuestion(intent, text) {
|
||||
if (intent !== "payables_confirmed_as_of_date" && intent !== "receivables_confirmed_as_of_date") {
|
||||
return false;
|
||||
}
|
||||
const normalized = textWithRepairedVariant(String(text ?? "")).toLowerCase().replace(/ё/g, "е");
|
||||
if (!/(?:долж|задолж|дебитор|кредитор|обязательств)/iu.test(normalized)) {
|
||||
return false;
|
||||
}
|
||||
if (/(?:по\s+(?:нему|ней|ним|этому|этой|этому\s+контрагенту|этой\s+компании|поставщику|клиенту|покупателю|заказчику)|\bон\b|\bона\b)/iu.test(normalized)) {
|
||||
return false;
|
||||
}
|
||||
return /(?:^|[\s,.;:!?()\-])(?:кто|кому|какие|какой|список|топ|все|всех|всего)(?=$|[\s,.;:!?()\-])/iu.test(normalized);
|
||||
}
|
||||
function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||
const merged = { ...current };
|
||||
const reasons = [];
|
||||
|
|
@ -861,15 +815,10 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
|||
const inheritedCounterparty = previousCounterparty ??
|
||||
(followupContext.previous_anchor_type === "counterparty" ? previousAnchorValue : null);
|
||||
const currentCounterparty = toNonEmptyString(merged.counterparty);
|
||||
const suppressCounterpartyForBroadDebtQuestion = isBroadDebtPolarityQuestion(intent, userMessage) && !currentCounterparty;
|
||||
const shouldInheritCounterparty = !suppressCounterpartyForBroadDebtQuestion &&
|
||||
(!currentCounterparty ||
|
||||
(Boolean(inheritedCounterparty) &&
|
||||
isLowQualityCounterpartyAnchor(currentCounterparty) &&
|
||||
!isLowQualityCounterpartyAnchor(inheritedCounterparty)));
|
||||
if (inheritedCounterparty && suppressCounterpartyForBroadDebtQuestion) {
|
||||
reasons.push("counterparty_carryover_suppressed_for_broad_debt_polarity_question");
|
||||
}
|
||||
const shouldInheritCounterparty = !currentCounterparty ||
|
||||
(Boolean(inheritedCounterparty) &&
|
||||
isLowQualityCounterpartyAnchor(currentCounterparty) &&
|
||||
!isLowQualityCounterpartyAnchor(inheritedCounterparty));
|
||||
if (inheritedCounterparty && shouldInheritCounterparty) {
|
||||
merged.counterparty = inheritedCounterparty;
|
||||
reasons.push(currentCounterparty ? "counterparty_replaced_from_followup_context" : "counterparty_from_followup_context");
|
||||
|
|
@ -1199,12 +1148,6 @@ 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")) {
|
||||
|
|
|
|||
|
|
@ -69,7 +69,6 @@ async function runAssistantAddressAttemptRuntime(input) {
|
|||
hasLivingChatSignal: input.hasLivingChatSignal,
|
||||
shouldEmitOrganizationSelectionReply: input.shouldEmitOrganizationSelectionReply,
|
||||
hasAssistantCapabilityQuestionSignal: input.hasAssistantCapabilityQuestionSignal,
|
||||
resolveOrganizationSelectionFromMessage: input.resolveOrganizationSelectionFromMessage,
|
||||
resolveDataScopeProbe: input.resolveDataScopeProbe,
|
||||
applyScriptGuard: input.applyScriptGuard,
|
||||
applyGroundingGuard: input.applyGroundingGuard,
|
||||
|
|
|
|||
|
|
@ -371,14 +371,6 @@ function isReferentialCounterpartyPlaceholder(value) {
|
|||
return false;
|
||||
}
|
||||
return new Set([
|
||||
"мы",
|
||||
"нам",
|
||||
"нас",
|
||||
"наш",
|
||||
"наша",
|
||||
"наше",
|
||||
"наши",
|
||||
"унас",
|
||||
"он",
|
||||
"она",
|
||||
"оно",
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ function buildAssistantLivingChatAttemptRuntimeInput(input) {
|
|||
hasLivingChatSignal: input.hasLivingChatSignal,
|
||||
shouldEmitOrganizationSelectionReply: input.shouldEmitOrganizationSelectionReply,
|
||||
hasAssistantCapabilityQuestionSignal: input.hasAssistantCapabilityQuestionSignal,
|
||||
resolveOrganizationSelectionFromMessage: input.resolveOrganizationSelectionFromMessage,
|
||||
resolveDataScopeProbe: input.resolveDataScopeProbe,
|
||||
applyScriptGuard: input.applyScriptGuard,
|
||||
applyGroundingGuard: input.applyGroundingGuard,
|
||||
|
|
|
|||
|
|
@ -41,7 +41,6 @@ async function runAssistantLivingChatAttemptRuntime(input) {
|
|||
hasLivingChatSignal: input.hasLivingChatSignal,
|
||||
shouldEmitOrganizationSelectionReply: input.shouldEmitOrganizationSelectionReply,
|
||||
hasAssistantCapabilityQuestionSignal: input.hasAssistantCapabilityQuestionSignal,
|
||||
resolveOrganizationSelectionFromMessage: input.resolveOrganizationSelectionFromMessage,
|
||||
resolveDataScopeProbe: input.resolveDataScopeProbe,
|
||||
executeLlmChat,
|
||||
applyScriptGuard: input.applyScriptGuard,
|
||||
|
|
|
|||
|
|
@ -36,7 +36,6 @@ function buildAssistantLivingChatHandlerRuntimeInput(input) {
|
|||
hasLivingChatSignal: input.hasLivingChatSignal,
|
||||
shouldEmitOrganizationSelectionReply: input.shouldEmitOrganizationSelectionReply,
|
||||
hasAssistantCapabilityQuestionSignal: input.hasAssistantCapabilityQuestionSignal,
|
||||
resolveOrganizationSelectionFromMessage: input.resolveOrganizationSelectionFromMessage,
|
||||
resolveDataScopeProbe: input.resolveDataScopeProbe,
|
||||
executeLlmChat: input.executeLlmChat,
|
||||
applyScriptGuard: input.applyScriptGuard,
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ async function tryHandleAssistantLivingChatRuntime(input) {
|
|||
hasLivingChatSignal: input.hasLivingChatSignal,
|
||||
shouldEmitOrganizationSelectionReply: input.shouldEmitOrganizationSelectionReply,
|
||||
hasAssistantCapabilityQuestionSignal: input.hasAssistantCapabilityQuestionSignal,
|
||||
resolveOrganizationSelectionFromMessage: input.resolveOrganizationSelectionFromMessage,
|
||||
resolveDataScopeProbe: input.resolveDataScopeProbe,
|
||||
executeLlmChat: input.executeLlmChat,
|
||||
applyScriptGuard: input.applyScriptGuard,
|
||||
|
|
|
|||
|
|
@ -11,34 +11,6 @@ function hasPriorAssistantTurn(items) {
|
|||
}
|
||||
return items.some((item) => item && typeof item === "object" && item.role === "assistant");
|
||||
}
|
||||
function shouldProbeBareOrganizationScopeCandidate(input) {
|
||||
if (input.selectedOrganization ||
|
||||
input.activeOrganization ||
|
||||
input.dataScopeMetaQuery ||
|
||||
input.capabilityMetaQuery ||
|
||||
input.destructiveSignal ||
|
||||
input.dangerSignal ||
|
||||
input.operationalSignal) {
|
||||
return false;
|
||||
}
|
||||
const raw = String(input.userMessage ?? "").trim();
|
||||
if (!raw || raw.length > 80 || /[?!]/u.test(raw) || /\d/u.test(raw) || !/\p{L}/u.test(raw)) {
|
||||
return false;
|
||||
}
|
||||
const tokenCount = raw.split(/\s+/u).filter(Boolean).length;
|
||||
if (tokenCount < 1 || tokenCount > 5) {
|
||||
return false;
|
||||
}
|
||||
const normalized = raw
|
||||
.toLowerCase()
|
||||
.replace(/\u0451/gu, "\u0435")
|
||||
.replace(/\s+/gu, " ")
|
||||
.trim();
|
||||
if (/^(?:\u043f\u0440\u0438\u0432\u0435\u0442|\u0437\u0434\u0440\u0430\u0432\u0441\u0442\u0432\u0443\u0439|\u0437\u0434\u0440\u0430\u0432\u0441\u0442\u0432\u0443\u0439\u0442\u0435|\u0434\u0430|\u043d\u0435\u0442|\u043e\u043a|\u043e\u043a\u0435\u0439|\u0441\u043f\u0430\u0441\u0438\u0431\u043e|\u043f\u043e\u043a\u0430|\u0433\u043e|\u0434\u0430\u043b\u044c\u0448\u0435|\u043f\u043e\u043d\u044f\u043b|\u043f\u043e\u043d\u044f\u043b\u0430)(?:\s|$)/iu.test(normalized)) {
|
||||
return false;
|
||||
}
|
||||
return !/(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u043f\u043e\u043a\u0430\u0436\u0438|\u0434\u0430\u0439|\u0440\u0430\u0441\u0441\u043a\u0430\u0436\u0438|\u0447\u0442\u043e|\u043a\u0430\u043a|\u0433\u0434\u0435|\u043a\u043e\u0433\u0434\u0430|\u043f\u043e\u0447\u0435\u043c\u0443|\u0437\u0430\u0447\u0435\u043c|\u043c\u043e\u0436\u0435\u0448\u044c|\u0443\u043c\u0435\u0435\u0448\u044c|\u043d\u0430\u0434\u043e|\u043d\u0443\u0436\u043d\u043e|\u0445\u043e\u0447\u0443|\u043e\u0441\u0442\u0430\u0442\u043a|\u043d\u0434\u0441|\u0434\u043e\u043b\u0433|\u0434\u0435\u0431\u0438\u0442\u043e\u0440|\u043a\u0440\u0435\u0434\u0438\u0442\u043e\u0440|\u0441\u043a\u043b\u0430\u0434|\u0442\u043e\u0432\u0430\u0440|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u043e\u0431\u043e\u0440\u043e\u0442|\u0432\u044b\u0440\u0443\u0447\u043a|\u043f\u0440\u0438\u0431\u044b\u043b)/iu.test(normalized);
|
||||
}
|
||||
function buildDeterministicSmalltalkLeadReply() {
|
||||
return "\u041f\u0440\u0438\u0432\u0435\u0442! \u0412\u0441\u0451 \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u043e.";
|
||||
}
|
||||
|
|
@ -108,8 +80,6 @@ async function runAssistantLivingChatRuntime(input) {
|
|||
let livingChatGroundingGuardApplied = false;
|
||||
let livingChatGroundingGuardReason = null;
|
||||
let livingChatProactiveScopeOfferApplied = false;
|
||||
let livingChatBareScopeProbeAttempted = false;
|
||||
let livingChatBareScopeProbeMatchedOrganization = null;
|
||||
const continuityActiveOrganization = organizationAuthority.continuityActiveOrganization;
|
||||
let knownOrganizations = [...organizationAuthority.knownOrganizations];
|
||||
let selectedOrganization = organizationAuthority.selectedOrganization;
|
||||
|
|
@ -131,29 +101,6 @@ async function runAssistantLivingChatRuntime(input) {
|
|||
const lastGroundedInventoryAddressDebug = memoryRecapContext.lastGroundedInventoryAddressDebug;
|
||||
const lastMemoryAddressDebug = memoryRecapContext.lastMemoryAddressDebug;
|
||||
const lastAnswerInspectionAddressDebug = memoryRecapContext.lastAnswerInspectionAddressDebug;
|
||||
if (shouldProbeBareOrganizationScopeCandidate({
|
||||
userMessage,
|
||||
selectedOrganization,
|
||||
activeOrganization,
|
||||
dataScopeMetaQuery,
|
||||
capabilityMetaQuery,
|
||||
destructiveSignal,
|
||||
dangerSignal,
|
||||
operationalSignal
|
||||
})) {
|
||||
dataScopeProbe = await input.resolveDataScopeProbe();
|
||||
livingChatBareScopeProbeAttempted = true;
|
||||
knownOrganizations = input.mergeKnownOrganizations([
|
||||
...knownOrganizations,
|
||||
...(Array.isArray(dataScopeProbe?.organizations) ? dataScopeProbe.organizations : [])
|
||||
]);
|
||||
const probedOrganization = input.resolveOrganizationSelectionFromMessage(userMessage, knownOrganizations);
|
||||
if (probedOrganization) {
|
||||
selectedOrganization = probedOrganization;
|
||||
activeOrganization = probedOrganization;
|
||||
livingChatBareScopeProbeMatchedOrganization = probedOrganization;
|
||||
}
|
||||
}
|
||||
if (capabilityMetaQuery && (destructiveSignal || dangerSignal)) {
|
||||
chatText = input.buildAssistantSafetyRefusalReply();
|
||||
livingChatSource = "deterministic_safety_refusal";
|
||||
|
|
@ -355,8 +302,6 @@ async function runAssistantLivingChatRuntime(input) {
|
|||
living_chat_grounding_guard_applied: livingChatGroundingGuardApplied,
|
||||
living_chat_grounding_guard_reason: livingChatGroundingGuardReason,
|
||||
living_chat_proactive_scope_offer_applied: livingChatProactiveScopeOfferApplied,
|
||||
living_chat_bare_scope_probe_attempted: livingChatBareScopeProbeAttempted,
|
||||
living_chat_bare_scope_probe_matched_organization: livingChatBareScopeProbeMatchedOrganization,
|
||||
living_chat_data_scope_probe_status: dataScopeProbe?.status ?? null,
|
||||
living_chat_data_scope_probe_channel: dataScopeProbe?.channel ?? null,
|
||||
living_chat_data_scope_probe_org_count: Array.isArray(dataScopeProbe?.organizations)
|
||||
|
|
|
|||
|
|
@ -209,10 +209,7 @@ function createAssistantLivingModePolicy(deps) {
|
|||
if (hasAffectiveReactionCue) {
|
||||
return false;
|
||||
}
|
||||
const rawQuestionProbe = String(userMessage ?? "")
|
||||
.replace(/\uFFFD\?/g, "\uFFFD")
|
||||
.replace(/пїЅ\?/giu, "пїЅ");
|
||||
return normalized.length <= 36 && !/[?]/.test(rawQuestionProbe);
|
||||
return normalized.length <= 36 && !/[?]/.test(String(userMessage ?? ""));
|
||||
}
|
||||
function hasAssistantDataScopeMetaQuestionSignal(text) {
|
||||
const repaired = repairAddressMojibake(String(text ?? ""));
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION = void 0;
|
||||
exports.buildAssistantMcpDiscoveryAnswerDraft = buildAssistantMcpDiscoveryAnswerDraft;
|
||||
const counterpartyRoleHeuristics_1 = require("./counterpartyRoleHeuristics");
|
||||
exports.ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION = "assistant_mcp_discovery_answer_draft_v1";
|
||||
function normalizeReasonCode(value) {
|
||||
const normalized = value
|
||||
|
|
@ -372,30 +371,7 @@ function metadataRouteFamilyLabelRu(routeFamily) {
|
|||
}
|
||||
return null;
|
||||
}
|
||||
function isInventoryReserveBoundaryTurn(pilot) {
|
||||
const action = pilot.evidence.query_plan.turn_meaning_ref?.asked_action_family;
|
||||
const unsupported = pilot.evidence.query_plan.turn_meaning_ref?.unsupported_but_understood_family;
|
||||
return action === "inventory_reserve_boundary" || unsupported === "inventory_reserve_liquidation_boundary";
|
||||
}
|
||||
function isProfitMarginBoundaryTurn(pilot) {
|
||||
const action = pilot.evidence.query_plan.turn_meaning_ref?.asked_action_family;
|
||||
const unsupported = pilot.evidence.query_plan.turn_meaning_ref?.unsupported_but_understood_family;
|
||||
return action === "profit_margin_boundary" || unsupported === "profit_margin_boundary";
|
||||
}
|
||||
function isDebtDueDateBoundaryTurn(pilot) {
|
||||
const action = pilot.evidence.query_plan.turn_meaning_ref?.asked_action_family;
|
||||
const unsupported = pilot.evidence.query_plan.turn_meaning_ref?.unsupported_but_understood_family;
|
||||
return action === "debt_due_date_boundary" || unsupported === "debt_due_date_boundary";
|
||||
}
|
||||
function isVendorRiskBoundaryTurn(pilot) {
|
||||
const action = pilot.evidence.query_plan.turn_meaning_ref?.asked_action_family;
|
||||
const unsupported = pilot.evidence.query_plan.turn_meaning_ref?.unsupported_but_understood_family;
|
||||
return action === "vendor_risk_procurement_boundary" || unsupported === "vendor_risk_procurement_boundary";
|
||||
}
|
||||
function businessOverviewInventoryUnknownLabel(overview) {
|
||||
if (overview.inventory_quality_events) {
|
||||
return "рыночная ликвидационная стоимость и управленческий резерв склада";
|
||||
}
|
||||
if (overview.inventory_staleness_risk_proxy) {
|
||||
return "резервы/списания/ликвидационная стоимость склада";
|
||||
}
|
||||
|
|
@ -457,67 +433,6 @@ function inlineBusinessOverviewAmount(value) {
|
|||
.replace(/\s*руб\.$/u, " рублей")
|
||||
.replace(/[\s.]+$/u, "");
|
||||
}
|
||||
function isFinancialInstitutionBucket(bucket) {
|
||||
if (!bucket) {
|
||||
return false;
|
||||
}
|
||||
return (bucket.counterparty_role_hint === "bank_or_financial_institution" ||
|
||||
(0, counterpartyRoleHeuristics_1.isLikelyFinancialInstitutionCounterparty)(bucket.axis_value));
|
||||
}
|
||||
function firstNonFinancialInstitutionBucket(buckets) {
|
||||
return (buckets ?? []).find((bucket) => !isFinancialInstitutionBucket(bucket)) ?? null;
|
||||
}
|
||||
function rankedBucketAmountLabel(bucket) {
|
||||
return `${bucket.axis_value} — ${bucket.total_amount_human_ru}`;
|
||||
}
|
||||
function businessOverviewIncomingLeaderLine(overview) {
|
||||
const leader = overview.top_customers[0];
|
||||
if (!leader) {
|
||||
return null;
|
||||
}
|
||||
if (!isFinancialInstitutionBucket(leader)) {
|
||||
return `Самый крупный подтвержденный клиент в проверенном срезе: ${rankedBucketAmountLabel(leader)}.`;
|
||||
}
|
||||
const nonFinancial = firstNonFinancialInstitutionBucket(overview.top_customers.slice(1));
|
||||
const nonFinancialText = nonFinancial
|
||||
? ` Крупнейший небанковский входящий контрагент в этом же срезе: ${rankedBucketAmountLabel(nonFinancial)}.`
|
||||
: "";
|
||||
return (`Крупнейший входящий денежный источник в проверенном срезе: ${rankedBucketAmountLabel(leader)}. ` +
|
||||
"По названию это банк/финансовая организация, поэтому без проверки назначения платежа не называю это клиентской выручкой или бизнес-заказчиком." +
|
||||
nonFinancialText);
|
||||
}
|
||||
function businessOverviewOutgoingLeaderLine(overview) {
|
||||
const leader = overview.top_suppliers?.[0];
|
||||
if (!leader) {
|
||||
return null;
|
||||
}
|
||||
if (!isFinancialInstitutionBucket(leader)) {
|
||||
return `Самый крупный подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${rankedBucketAmountLabel(leader)}.`;
|
||||
}
|
||||
const nonFinancial = firstNonFinancialInstitutionBucket(overview.top_suppliers.slice(1));
|
||||
const nonFinancialText = nonFinancial
|
||||
? ` Крупнейший небанковский получатель исходящих денег в этом же срезе: ${rankedBucketAmountLabel(nonFinancial)}.`
|
||||
: "";
|
||||
return (`Крупнейший получатель исходящих денег в проверенном срезе: ${rankedBucketAmountLabel(leader)}. ` +
|
||||
"По названию это банк/финансовая организация, поэтому без назначения платежа/договора не считаю это обычным поставщиком." +
|
||||
nonFinancialText);
|
||||
}
|
||||
function businessOverviewSupplierBoundaryBasis(overview) {
|
||||
const leader = overview.top_suppliers?.[0] ?? null;
|
||||
if (!leader) {
|
||||
return "есть только общий срез исходящих платежей без надежного профиля поставщицкого риска";
|
||||
}
|
||||
const share = percentText(leader.total_amount, overview.outgoing_supplier_payout.total_amount);
|
||||
if (isFinancialInstitutionBucket(leader)) {
|
||||
const base = share
|
||||
? `крупнейший получатель исходящих денег ${leader.axis_value} держит около ${share} проверенного исходящего потока (${leader.total_amount_human_ru})`
|
||||
: `крупнейший получатель исходящих денег: ${rankedBucketAmountLabel(leader)}`;
|
||||
return `${base}; по названию это банк/финансовая организация, поэтому этот факт нельзя считать доказанной зависимостью от одного обычного поставщика`;
|
||||
}
|
||||
return share
|
||||
? `крупнейший подтвержденный поставщик/получатель исходящих платежей ${leader.axis_value} держит около ${share} проверенного исходящего потока (${leader.total_amount_human_ru})`
|
||||
: `крупнейший подтвержденный поставщик/получатель исходящих платежей: ${rankedBucketAmountLabel(leader)}`;
|
||||
}
|
||||
function businessOverviewHeadlineMetricsLine(overview) {
|
||||
const parts = [];
|
||||
if (overview.incoming_customer_revenue.rows_with_amount > 0) {
|
||||
|
|
@ -529,139 +444,14 @@ function businessOverviewHeadlineMetricsLine(overview) {
|
|||
if (overview.incoming_customer_revenue.rows_with_amount > 0 || overview.outgoing_supplier_payout.rows_with_amount > 0) {
|
||||
parts.push(`расчетное операционное нетто ${inlineBusinessOverviewAmount(overview.net_amount_human_ru)}`);
|
||||
}
|
||||
if (overview.accounting_financial_result) {
|
||||
const result = overview.accounting_financial_result;
|
||||
const direction = result.final_result_direction === "profit"
|
||||
? "учетная прибыль"
|
||||
: result.final_result_direction === "loss"
|
||||
? "учетный убыток"
|
||||
: "нулевой учетный финрезультат";
|
||||
const amount = result.final_result_direction === "loss"
|
||||
? `минус ${inlineBusinessOverviewAmount(result.final_result_amount_human_ru)}`
|
||||
: inlineBusinessOverviewAmount(result.final_result_amount_human_ru);
|
||||
const margin = result.net_margin_to_revenue_pct === null
|
||||
? "маржа к подтвержденной выручке не рассчитана"
|
||||
: `маржа к подтвержденной выручке ${result.net_margin_to_revenue_pct}%`;
|
||||
parts.push(`${direction} по закрытию счетов 90/91/99 ${amount}; ${margin}`);
|
||||
}
|
||||
const strongestIncomingYear = businessOverviewStrongestIncomingYear(overview);
|
||||
if (strongestIncomingYear) {
|
||||
parts.push(`самый сильный год по подтвержденным входящим поступлениям ${strongestIncomingYear.year_bucket}: ${inlineBusinessOverviewAmount(strongestIncomingYear.incoming_total_amount_human_ru)}`);
|
||||
}
|
||||
return parts.length > 0
|
||||
? overview.accounting_financial_result
|
||||
? `${parts.join("; ")}. Финрезультат ограничен найденными строками 1С и не является внешним аудитом или юридически подтвержденной отчетностью`
|
||||
: `${parts.join("; ")}. Это операционный денежный сигнал по найденным строкам, не бухгалтерская прибыль и не финрезультат`
|
||||
? `${parts.join("; ")}. Это operating-flow proxy по найденным строкам, не бухгалтерская прибыль и не финрезультат`
|
||||
: null;
|
||||
}
|
||||
function businessOverviewAccountingFinancialResultText(overview) {
|
||||
const result = overview.accounting_financial_result;
|
||||
if (!result) {
|
||||
return null;
|
||||
}
|
||||
const direction = result.final_result_direction === "profit"
|
||||
? "учетная прибыль"
|
||||
: result.final_result_direction === "loss"
|
||||
? "учетный убыток"
|
||||
: "нулевой учетный финрезультат";
|
||||
const signedAmount = result.final_result_direction === "loss"
|
||||
? `минус ${result.final_result_amount_human_ru}`
|
||||
: result.final_result_amount_human_ru;
|
||||
const marginText = result.net_margin_to_revenue_pct === null
|
||||
? "маржа к подтвержденной выручке не рассчитана"
|
||||
: `маржа к подтвержденной выручке ${result.net_margin_to_revenue_pct}%`;
|
||||
const basis = result.final_transfer_basis === "account_99_to_84_period_close"
|
||||
? "по закрытию 99 на 84"
|
||||
: "по закрытию 90/91 на 99";
|
||||
return `Нет: денежное операционное нетто не стоит считать чистой прибылью. Отдельно по закрытию счетов 90/91/99 в 1С за ${result.period_scope} подтвержден ${direction}: ${signedAmount}; ${marginText}. Основа: ${basis}, ${result.period_close_rows_with_amount} строк(и) закрытия периода с суммой. Это учетный финрезультат по найденным строкам 1С, не внешний аудит и не юридически подтвержденная отчетность.`;
|
||||
}
|
||||
function businessOverviewDebtDueDateAgingText(overview) {
|
||||
const aging = overview.debt_due_date_aging;
|
||||
if (!aging) {
|
||||
return null;
|
||||
}
|
||||
if (aging.evidence_status === "confirmed_overdue") {
|
||||
const top = aging.top_overdue_items?.[0] ?? null;
|
||||
const topText = top
|
||||
? ` Самая старая строка: due date ${top.due_date}, просрочка ${top.overdue_days} дн., ${top.amount_human_ru}${top.contract ? ` по договору ${top.contract}` : ""}.`
|
||||
: "";
|
||||
return `Due-date aging на ${aging.as_of_date} проверен по срокам оплаты договоров и датам расчетных документов: подтверждено просроченных строк ${aging.overdue_rows}, сумма ${aging.overdue_amount_human_ru}.${topText}`;
|
||||
}
|
||||
if (aging.evidence_status === "no_payment_terms_configured") {
|
||||
return `Due-date aging на ${aging.as_of_date} проверен по открытым расчетам: брутто ${aging.gross_open_amount_human_ru}, строк с суммой ${aging.rows_with_amount}, но в проверенных договорах срок оплаты не установлен. Подтвержденной просрочки по договорным срокам оплаты нет.`;
|
||||
}
|
||||
if (aging.evidence_status === "insufficient_due_date_basis") {
|
||||
return `Due-date aging на ${aging.as_of_date} запускался, но по строкам с установленным сроком оплаты не хватило даты расчетного документа для честного расчета due date. Просрочка не подтверждена.`;
|
||||
}
|
||||
return `Due-date aging на ${aging.as_of_date} проверен: строк с установленным сроком оплаты ${aging.rows_with_payment_terms}, подтвержденной просрочки не найдено; не просрочено по расчету ${aging.not_yet_due_amount_human_ru}.`;
|
||||
}
|
||||
function financialFlowHintTextRuFromBucket(bucket) {
|
||||
const rows = bucket?.financial_flow_hint_rows ?? 0;
|
||||
const rowsText = rows > 0 ? ` (${rows} строк)` : "";
|
||||
if (bucket?.financial_flow_hint === "loan_or_credit") {
|
||||
return ` По полям банковского документа доминирует кредитный/заемный признак${rowsText}; это не обычный поставщик и не клиентская выручка без отдельной проверки назначения.`;
|
||||
}
|
||||
if (bucket?.financial_flow_hint === "bank_fee_or_service") {
|
||||
return ` По полям банковского документа доминирует признак банковской комиссии/услуг банка${rowsText}; это не обычный поставщик товаров/услуг без отдельной проверки договора.`;
|
||||
}
|
||||
if (bucket?.financial_flow_hint === "tax_or_budget") {
|
||||
return ` По полям банковского документа доминирует налоговый/бюджетный признак${rowsText}.`;
|
||||
}
|
||||
if (bucket?.financial_flow_hint === "payroll_or_social") {
|
||||
return ` По полям банковского документа доминирует зарплатный/социальный признак${rowsText}.`;
|
||||
}
|
||||
if (bucket?.financial_flow_hint === "supplier_payment") {
|
||||
return ` По полям банковского документа доминирует признак оплаты поставщику${rowsText}; если получатель по названию является банком, это все равно требует осторожной трактовки.`;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
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 `Проверка концентрации закупок/исходящих платежей за ${period}: крупнейший получатель исходящих денег ${topName}${topShare}${topAmount}, всего исходящих платежей ${total}. По названию это банк/финансовая организация, поэтому зависимость от обычного поставщика этим не подтверждается.${financialFlowHintTextRuFromBucket(top)}${nonFinancialText}${contractText} Надежность поставщиков, качество поставок, назначение каждого платежа и полная структура расходов этим срезом не доказаны.`;
|
||||
}
|
||||
if (quality.evidence_status === "reviewed_procurement_concentration") {
|
||||
return `Проверка концентрации закупок/исходящих платежей за ${period}: крупнейший поставщик/получатель исходящих платежей ${topName}${topShare}${topAmount}, всего исходящих платежей ${total}.${contractText} Это проверенный сигнал концентрации закупок/исходящих платежей, но не аудит надежности поставщика, качества поставок и полной структуры расходов.`;
|
||||
}
|
||||
return `Проверка концентрации закупок/исходящих платежей за ${period} нашла исходящие платежи на ${total}, но надежной небанковской концентрации поставщика по найденным строкам не хватает.${contractText} Полный аудит поставщицкого риска не подтвержден.`;
|
||||
}
|
||||
function businessOverviewInventoryQualityEventsText(overview) {
|
||||
const quality = overview.inventory_quality_events;
|
||||
if (!quality) {
|
||||
return null;
|
||||
}
|
||||
const period = quality.period_scope ?? "проверенное окно";
|
||||
const organization = overview.organization_scope ? ` по организации ${overview.organization_scope}` : "";
|
||||
const eventWindow = quality.first_event_date && quality.latest_event_date
|
||||
? ` Окно найденных событий: ${quality.first_event_date} - ${quality.latest_event_date}.`
|
||||
: "";
|
||||
if (quality.evidence_status === "reviewed_no_quality_events_found") {
|
||||
return `Коротко: проверил складские документы списания, оприходования, инвентаризации и переоценки${organization} за ${period}; подтвержденных событий списания/корректировки/инвентаризации/переоценки не найдено. Это сильный отрицательный сигнал по доступным документам 1С, но не рыночная ликвидационная стоимость и не управленческий резерв под неликвиды.`;
|
||||
}
|
||||
if (quality.evidence_status === "reviewed_inventory_control_events_only") {
|
||||
return `Коротко: проверил складские quality-события${organization} за ${period}; списаний и оприходований/корректировок с суммой не найдено, но есть инвентаризации ${quality.inventory_count_rows} и переоценки ${quality.revaluation_rows}.${eventWindow} Это контрольные складские документы, а не подтвержденный резерв или рыночная ликвидационная оценка.`;
|
||||
}
|
||||
return `Коротко: проверил складские quality-события${organization} за ${period}; списаний ${quality.writeoff_rows} на ${quality.writeoff_amount_human_ru}, оприходований/корректировок ${quality.receipt_adjustment_rows} на ${quality.receipt_adjustment_amount_human_ru}, инвентаризаций ${quality.inventory_count_rows}, переоценок ${quality.revaluation_rows}.${eventWindow} Это подтвержденные документы 1С по складским событиям, но не самостоятельная рыночная ликвидационная стоимость и не расчет управленческого резерва.`;
|
||||
}
|
||||
function headlineFor(mode, pilot) {
|
||||
const askedMonthlyBreakdown = pilot.derived_bidirectional_value_flow?.aggregation_axis === "month" ||
|
||||
pilot.derived_value_flow?.aggregation_axis === "month";
|
||||
|
|
@ -679,43 +469,6 @@ function headlineFor(mode, pilot) {
|
|||
}
|
||||
if (isBusinessOverviewPilot(pilot) && pilot.derived_business_overview && mode === "confirmed_with_bounded_inference") {
|
||||
const overview = pilot.derived_business_overview;
|
||||
if (isProfitMarginBoundaryTurn(pilot)) {
|
||||
const accountingFinancialResultText = businessOverviewAccountingFinancialResultText(overview);
|
||||
if (accountingFinancialResultText) {
|
||||
return accountingFinancialResultText;
|
||||
}
|
||||
return "Нельзя точно подтвердить чистую прибыль и маржу по текущему срезу 1С; есть только ограниченный операционный денежный/товарный сигнал, а не полный отчет о прибыли и не бухгалтерский финрезультат.";
|
||||
}
|
||||
if (isDebtDueDateBoundaryTurn(pilot)) {
|
||||
const dueDateText = businessOverviewDebtDueDateAgingText(overview);
|
||||
if (dueDateText) {
|
||||
return dueDateText;
|
||||
}
|
||||
return "Нельзя точно определить, какая дебиторка просрочена, по текущему срезу 1С; есть только debt-quality proxy, но нет проверенного due-date маршрута по договорам, срокам оплаты и погашению расчетов.";
|
||||
}
|
||||
if (isInventoryReserveBoundaryTurn(pilot)) {
|
||||
const inventoryQualityEventsText = businessOverviewInventoryQualityEventsText(overview);
|
||||
if (inventoryQualityEventsText) {
|
||||
return inventoryQualityEventsText;
|
||||
}
|
||||
const inventoryBasis = overview.inventory_staleness_risk_proxy
|
||||
? "есть только складской staleness-risk proxy по найденным строкам"
|
||||
: overview.inventory_position || overview.inventory_turnover_proxy
|
||||
? "есть только ограниченные складские proxy-сигналы по найденным строкам"
|
||||
: "нет отдельного складского среза на дату и проверки учетной политики резервов";
|
||||
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"
|
||||
: "procurement concentration proxy";
|
||||
return `Коротко: точный риск зависимости от одного поставщика по текущим данным не подтвержден; есть только ${proxyLabel}: ${businessOverviewSupplierBoundaryBasis(overview)}. Это сигнал концентрации исходящих платежей, а не полный аудит надежности поставщиков, условий, качества и структуры всех расходов.`;
|
||||
}
|
||||
const families = [];
|
||||
if (overview.incoming_customer_revenue.rows_with_amount > 0 ||
|
||||
overview.outgoing_supplier_payout.rows_with_amount > 0) {
|
||||
|
|
@ -739,9 +492,6 @@ function headlineFor(mode, pilot) {
|
|||
if (overview.tax_position) {
|
||||
families.push("НДС-позиция");
|
||||
}
|
||||
if (overview.accounting_financial_result) {
|
||||
families.push("учетный финрезультат 90/91/99");
|
||||
}
|
||||
if (overview.trading_margin_proxy) {
|
||||
families.push("торговый margin proxy");
|
||||
}
|
||||
|
|
@ -757,9 +507,6 @@ function headlineFor(mode, pilot) {
|
|||
if (overview.debt_staleness_risk_proxy) {
|
||||
families.push("staleness risk proxy открытых расчетов");
|
||||
}
|
||||
if (overview.debt_due_date_aging) {
|
||||
families.push("due-date aging открытых расчетов");
|
||||
}
|
||||
if (overview.inventory_position) {
|
||||
families.push("складской срез на дату");
|
||||
}
|
||||
|
|
@ -769,25 +516,18 @@ function headlineFor(mode, pilot) {
|
|||
if (overview.inventory_staleness_risk_proxy) {
|
||||
families.push("staleness risk proxy склада");
|
||||
}
|
||||
if (overview.inventory_quality_events) {
|
||||
families.push("складские quality-события");
|
||||
}
|
||||
const unknownFamilies = overview.accounting_financial_result
|
||||
? ["аудированная/юридически подтвержденная прибыль"]
|
||||
: [overview.trading_margin_proxy ? "чистая прибыль/точная маржа" : "прибыль/маржа"];
|
||||
const unknownFamilies = [overview.trading_margin_proxy ? "чистая прибыль/точная маржа" : "прибыль/маржа"];
|
||||
if (!overview.tax_position) {
|
||||
unknownFamilies.push("НДС");
|
||||
}
|
||||
if (!overview.debt_position) {
|
||||
unknownFamilies.push("долговой срез");
|
||||
}
|
||||
if (!overview.debt_due_date_aging) {
|
||||
unknownFamilies.push(overview.debt_staleness_risk_proxy
|
||||
? "договорные сроки оплаты/due-date просрочка"
|
||||
: overview.debt_open_settlement_quality
|
||||
? "due-date просрочка"
|
||||
: "качество открытых расчетов");
|
||||
}
|
||||
unknownFamilies.push(overview.debt_staleness_risk_proxy
|
||||
? "договорные сроки оплаты/due-date просрочка"
|
||||
: overview.debt_open_settlement_quality
|
||||
? "due-date просрочка"
|
||||
: "качество открытых расчетов");
|
||||
unknownFamilies.push(businessOverviewInventoryUnknownLabel(overview));
|
||||
const metricLead = businessOverviewHeadlineMetricsLine(overview);
|
||||
if (metricLead) {
|
||||
|
|
@ -978,29 +718,16 @@ 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 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.");
|
||||
claims.push("Do not present a debt-position snapshot as debt aging, overdue debt, or credit-quality analysis.");
|
||||
claims.push("Do not present open-settlement concentration as contractual due-date aging or confirmed overdue debt.");
|
||||
claims.push("Do not present business overview debt staleness risk proxy as confirmed overdue debt, contractual delinquency, credit risk, or due-date aging.");
|
||||
claims.push("Do not claim contractual overdue debt unless the due-date aging route found configured payment terms and enough settlement-date evidence.");
|
||||
claims.push("Do not present an inventory snapshot or purchase-date aging signal as turnover, obsolescence, liquidation value, or full inventory health.");
|
||||
claims.push("Do not present business overview inventory turnover proxy as full inventory liquidity, FIFO turnover, obsolescence analysis, or liquidation value.");
|
||||
claims.push("Do not present business overview inventory staleness risk proxy as confirmed obsolete stock, reserve, write-off, or liquidation value.");
|
||||
if (pilot.derived_business_overview?.inventory_quality_events) {
|
||||
claims.push("Do not present reviewed inventory quality events as confirmed obsolete stock, reserve policy, market liquidation value, management reserve, or full inventory health.");
|
||||
}
|
||||
if (pilot.derived_business_overview?.top_customers?.some(isFinancialInstitutionBucket) ||
|
||||
pilot.derived_business_overview?.top_suppliers?.some(isFinancialInstitutionBucket)) {
|
||||
claims.push("Do not present bank-like counterparties as ordinary customers, suppliers, revenue, procurement dependency, or business quality evidence without payment-purpose/contract proof.");
|
||||
}
|
||||
if (pilot.derived_business_overview?.missing_proof_families?.length) {
|
||||
claims.push("Do not present business overview missing proof families as checked, executed, or confirmed routes.");
|
||||
}
|
||||
|
|
@ -1009,9 +736,6 @@ function buildMustNotClaim(pilot) {
|
|||
if (pilot.derived_ranked_value_flow) {
|
||||
claims.push("Do not present a bounded ranking as a complete all-time ranking outside the checked period and organization.");
|
||||
claims.push("Do not imply the top-ranked counterparty is globally final when probe-limit or scope boundaries still exist.");
|
||||
if (pilot.derived_ranked_value_flow.ranked_values.some(isFinancialInstitutionBucket)) {
|
||||
claims.push("Do not present bank-like counterparties as ordinary customers, suppliers, revenue, procurement dependency, or business quality evidence without payment-purpose/contract proof.");
|
||||
}
|
||||
}
|
||||
if (isDocumentPilot(pilot)) {
|
||||
claims.push("Do not claim full document history outside the checked period.");
|
||||
|
|
@ -1161,38 +885,24 @@ function derivedRankedValueFlowConfirmedLine(pilot) {
|
|||
return null;
|
||||
}
|
||||
const leader = ranking.ranked_values[0];
|
||||
const leaderLooksFinancial = isFinancialInstitutionBucket(leader);
|
||||
const organization = ranking.organization_scope ? ` по организации ${ranking.organization_scope}` : "";
|
||||
const period = ranking.period_scope ? ` за период ${ranking.period_scope}` : " в проверенном окне";
|
||||
const roleCaveat = leaderLooksFinancial
|
||||
? ranking.value_flow_direction === "outgoing_supplier_payout"
|
||||
? " По названию это банк/финансовая организация, поэтому без назначения платежа/договора не называю это обычным поставщиком."
|
||||
: " По названию это банк/финансовая организация, поэтому без назначения платежа не называю это клиентской выручкой или бизнес-заказчиком."
|
||||
: "";
|
||||
if (ranking.ranked_values.length === 1) {
|
||||
const singleLead = leaderLooksFinancial
|
||||
? ranking.value_flow_direction === "outgoing_supplier_payout"
|
||||
? "В проверенных исходящих платежах найден один банковский/финансовый получатель"
|
||||
: "В проверенных входящих поступлениях найден один банковский/финансовый источник"
|
||||
: ranking.value_flow_direction === "outgoing_supplier_payout"
|
||||
? "В проверенных исходящих платежах найден один контрагент"
|
||||
: "В проверенных входящих поступлениях найден один контрагент";
|
||||
const singleLead = ranking.value_flow_direction === "outgoing_supplier_payout"
|
||||
? "В проверенных исходящих платежах найден один контрагент"
|
||||
: "В проверенных входящих поступлениях найден один контрагент";
|
||||
const limitCaveat = ranking.coverage_limited_by_probe_limit
|
||||
? " Лимит строк проверки достигнут; сравнение с другими контрагентами может быть неполным."
|
||||
: " Других контрагентов в этом проверенном срезе не найдено, поэтому это не полноценный сравнительный рейтинг.";
|
||||
return `${singleLead} ${leader.axis_value}${organization}${period}: ${leader.total_amount_human_ru} по ${leader.rows_with_amount} строкам с суммой.${roleCaveat}${limitCaveat}`;
|
||||
return `${singleLead} ${leader.axis_value}${organization}${period}: ${leader.total_amount_human_ru} по ${leader.rows_with_amount} строкам с суммой.${limitCaveat}`;
|
||||
}
|
||||
const directionLead = leaderLooksFinancial
|
||||
const directionLead = ranking.ranking_need === "bottom_asc"
|
||||
? ranking.value_flow_direction === "outgoing_supplier_payout"
|
||||
? "Крупнейший получатель исходящих денег"
|
||||
: "Крупнейший входящий денежный источник"
|
||||
: ranking.ranking_need === "bottom_asc"
|
||||
? ranking.value_flow_direction === "outgoing_supplier_payout"
|
||||
? "Меньше всего заплатили контрагенту"
|
||||
: "Меньше всего денег принёс контрагент"
|
||||
: ranking.value_flow_direction === "outgoing_supplier_payout"
|
||||
? "Больше всего заплатили контрагенту"
|
||||
: "Больше всего денег принёс контрагент";
|
||||
? "Меньше всего заплатили контрагенту"
|
||||
: "Меньше всего денег принёс контрагент"
|
||||
: ranking.value_flow_direction === "outgoing_supplier_payout"
|
||||
? "Больше всего заплатили контрагенту"
|
||||
: "Больше всего денег принёс контрагент";
|
||||
const tail = ranking.ranked_values
|
||||
.slice(1, 3)
|
||||
.map((bucket) => `${bucket.axis_value} — ${bucket.total_amount_human_ru}`)
|
||||
|
|
@ -1201,7 +911,7 @@ function derivedRankedValueFlowConfirmedLine(pilot) {
|
|||
const limitCaveat = ranking.coverage_limited_by_probe_limit
|
||||
? " Лимит строк проверки достигнут; рейтинг может быть неполным."
|
||||
: "";
|
||||
return `${directionLead} ${leader.axis_value}${organization}${period}: ${leader.total_amount_human_ru} по ${leader.rows_with_amount} строкам с суммой.${roleCaveat}${trail}${limitCaveat}`;
|
||||
return `${directionLead} ${leader.axis_value}${organization}${period}: ${leader.total_amount_human_ru} по ${leader.rows_with_amount} строкам с суммой.${trail}${limitCaveat}`;
|
||||
}
|
||||
function derivedValueFlowConfirmedLine(pilot) {
|
||||
const flow = pilot.derived_value_flow;
|
||||
|
|
@ -1360,25 +1070,17 @@ function derivedBusinessOverviewConfirmedLines(pilot) {
|
|||
if (strongestIncomingYear) {
|
||||
lines.push(`Самый сильный год по подтвержденным входящим поступлениям: ${strongestIncomingYear.year_bucket} — ${strongestIncomingYear.incoming_total_amount_human_ru} по ${strongestIncomingYear.incoming_rows_with_amount} строкам с суммой. Это не бухгалтерская прибыль.`);
|
||||
}
|
||||
const incomingLeaderLine = businessOverviewIncomingLeaderLine(overview);
|
||||
if (incomingLeaderLine) {
|
||||
lines.push(incomingLeaderLine);
|
||||
const leader = overview.top_customers[0];
|
||||
if (leader) {
|
||||
lines.push(`Самый крупный подтвержденный клиент в проверенном срезе: ${leader.axis_value} — ${leader.total_amount_human_ru}.`);
|
||||
}
|
||||
const outgoingLeaderLine = businessOverviewOutgoingLeaderLine(overview);
|
||||
if (outgoingLeaderLine) {
|
||||
lines.push(outgoingLeaderLine);
|
||||
}
|
||||
const vendorQualityText = businessOverviewVendorProcurementQualityText(overview);
|
||||
if (vendorQualityText) {
|
||||
lines.push(vendorQualityText);
|
||||
const supplierLeader = overview.top_suppliers?.[0];
|
||||
if (supplierLeader) {
|
||||
lines.push(`Самый крупный подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${supplierLeader.axis_value} — ${supplierLeader.total_amount_human_ru}.`);
|
||||
}
|
||||
if (overview.yearly_breakdown?.length) {
|
||||
lines.push(`Годовая раскладка операционного денежного потока построена по подтвержденным строкам 1С за ${yearCountHumanRu(overview.yearly_breakdown.length)}.`);
|
||||
}
|
||||
if (overview.incoming_customer_revenue.coverage_recovered_by_period_chunking ||
|
||||
overview.outgoing_supplier_payout.coverage_recovered_by_period_chunking) {
|
||||
lines.push("Денежное покрытие бизнес-обзора за год восстановлено через помесячные 1С-проверки, а не только через широкий общий запрос.");
|
||||
}
|
||||
if (overview.activity_period) {
|
||||
lines.push(`Окно подтвержденной активности в 1С: ${overview.activity_period.first_activity_date} — ${overview.activity_period.latest_activity_date}; ориентировочно ${overview.activity_period.duration_human_ru}.`);
|
||||
}
|
||||
|
|
@ -1422,12 +1124,6 @@ function derivedBusinessOverviewConfirmedLines(pilot) {
|
|||
: "сбалансирован";
|
||||
lines.push(`НДС-позиция за ${overview.tax_position.period_scope}: книга продаж ${overview.tax_position.sales_vat_amount_human_ru}, книга покупок/вычеты ${overview.tax_position.purchase_vat_amount_human_ru}, нетто ${taxDirection} ${overview.tax_position.net_vat_amount_human_ru}.`);
|
||||
}
|
||||
if (overview.accounting_financial_result) {
|
||||
const accountingFinancialResultText = businessOverviewAccountingFinancialResultText(overview);
|
||||
if (accountingFinancialResultText) {
|
||||
lines.push(accountingFinancialResultText);
|
||||
}
|
||||
}
|
||||
if (overview.trading_margin_proxy) {
|
||||
const proxy = overview.trading_margin_proxy;
|
||||
const marginText = proxy.margin_to_revenue_pct === null ? "не рассчитана" : `${proxy.margin_to_revenue_pct}%`;
|
||||
|
|
@ -1460,10 +1156,6 @@ function derivedBusinessOverviewConfirmedLines(pilot) {
|
|||
const counterpartyText = proxy.top_contract_counterparty ? ` / ${proxy.top_contract_counterparty}` : "";
|
||||
lines.push(`Staleness risk proxy открытых расчетов на ${proxy.as_of_date}: самый старый договорный сигнал ${proxy.oldest_contract_start_date}, возраст ${proxy.max_contract_age_days} дн.; старейший крупный договор ${proxy.top_contract}${counterpartyText} держит ${proxy.top_contract_amount_human_ru} (${proxy.top_contract_share_pct}% брутто открытых остатков); оценка ${debtStalenessRiskBandRu(proxy.risk_band)}. Это не подтвержденная просрочка, не кредитный риск и не due-date aging.`);
|
||||
}
|
||||
const dueDateText = businessOverviewDebtDueDateAgingText(overview);
|
||||
if (dueDateText) {
|
||||
lines.push(dueDateText);
|
||||
}
|
||||
if (overview.inventory_position) {
|
||||
const leader = overview.inventory_position.top_items[0];
|
||||
const leaderText = leader
|
||||
|
|
@ -1491,10 +1183,6 @@ function derivedBusinessOverviewConfirmedLines(pilot) {
|
|||
const proxy = overview.inventory_staleness_risk_proxy;
|
||||
lines.push(`Staleness risk proxy склада на ${proxy.as_of_date}: самая ранняя дата закупочного сигнала ${proxy.oldest_purchase_date}, возраст ${proxy.max_purchase_age_days} дн., sales-to-stock ${proxy.sales_to_stock_amount_ratio}x, оценка ${inventoryStalenessRiskBandRu(proxy.risk_band)}. Это не подтвержденная неликвидность, не резерв и не ликвидационная стоимость.`);
|
||||
}
|
||||
const inventoryQualityEventsText = businessOverviewInventoryQualityEventsText(overview);
|
||||
if (inventoryQualityEventsText) {
|
||||
lines.push(inventoryQualityEventsText.replace(/^Коротко:\s*/u, ""));
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
function businessOverviewCashSynthesisLine(overview) {
|
||||
|
|
@ -1515,13 +1203,6 @@ function businessOverviewCustomerConcentrationLine(overview) {
|
|||
return null;
|
||||
}
|
||||
const share = percentText(leader.total_amount, overview.incoming_customer_revenue.total_amount);
|
||||
if (isFinancialInstitutionBucket(leader)) {
|
||||
const base = share
|
||||
? `Крупнейший входящий денежный источник ${leader.axis_value} дает около ${share} проверенных входящих поступлений (${leader.total_amount_human_ru})`
|
||||
: `Крупнейший входящий денежный источник в проверенном срезе: ${rankedBucketAmountLabel(leader)}`;
|
||||
const nonFinancial = firstNonFinancialInstitutionBucket(overview.top_customers.slice(1));
|
||||
return `${base}. По названию это банк/финансовая организация, поэтому это не доказывает клиентскую выручку или зависимость от клиента.${nonFinancial ? ` Крупнейший небанковский входящий контрагент: ${rankedBucketAmountLabel(nonFinancial)}.` : ""}`;
|
||||
}
|
||||
return share
|
||||
? `Концентрация входящего потока: крупнейший подтвержденный клиент ${leader.axis_value} дает около ${share} проверенных входящих поступлений (${leader.total_amount_human_ru}). Это сигнал зависимости от клиента, а не полный customer-risk аудит.`
|
||||
: `Крупнейший подтвержденный клиент в проверенном срезе: ${leader.axis_value} — ${leader.total_amount_human_ru}.`;
|
||||
|
|
@ -1532,15 +1213,8 @@ function businessOverviewSupplierConcentrationLine(overview) {
|
|||
return null;
|
||||
}
|
||||
const share = percentText(leader.total_amount, overview.outgoing_supplier_payout.total_amount);
|
||||
if (isFinancialInstitutionBucket(leader)) {
|
||||
const base = share
|
||||
? `Концентрация исходящего потока: крупнейший получатель исходящих денег ${leader.axis_value} держит около ${share} проверенных исходящих платежей (${leader.total_amount_human_ru})`
|
||||
: `Крупнейший получатель исходящих денег в проверенном срезе: ${rankedBucketAmountLabel(leader)}`;
|
||||
const nonFinancial = firstNonFinancialInstitutionBucket(overview.top_suppliers.slice(1));
|
||||
return `${base}. По названию это банк/финансовая организация, поэтому это не доказательство зависимости от обычного поставщика без проверки назначения платежа/договора.${nonFinancial ? ` Крупнейший небанковский получатель исходящих денег: ${rankedBucketAmountLabel(nonFinancial)}.` : ""}`;
|
||||
}
|
||||
return share
|
||||
? `Концентрация исходящего потока: крупнейший подтвержденный поставщик/получатель исходящих платежей ${leader.axis_value} держит около ${share} проверенных исходящих платежей (${leader.total_amount_human_ru}). Это сигнал концентрации закупок/исходящих платежей по найденным строкам, а не полный аудит поставщицкого риска или структура всех расходов.`
|
||||
? `Концентрация исходящего потока: крупнейший подтвержденный поставщик/получатель исходящих платежей ${leader.axis_value} держит около ${share} проверенных исходящих платежей (${leader.total_amount_human_ru}). Это сигнал procurement concentration по найденным строкам, а не полный vendor-risk аудит или структура всех расходов.`
|
||||
: `Крупнейший подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${leader.axis_value} — ${leader.total_amount_human_ru}.`;
|
||||
}
|
||||
function businessOverviewYearlyOperatingLine(overview) {
|
||||
|
|
@ -1565,7 +1239,7 @@ function businessOverviewYearlyOperatingLine(overview) {
|
|||
: `нетто в плюс ${strongestNetYear.net_amount_human_ru}`;
|
||||
parts.push(`лучший год по расчетному операционному нетто ${strongestNetYear.year_bucket}: ${netText}`);
|
||||
}
|
||||
return `Годовая динамика по проверенным строкам: ${parts.join("; ")}. Это операционный денежный сигнал, не бухгалтерская прибыль и не финрезультат.`;
|
||||
return `Годовая динамика по проверенным строкам: ${parts.join("; ")}. Это operating-flow proxy, не бухгалтерская прибыль и не финрезультат.`;
|
||||
}
|
||||
function businessOverviewRiskSynthesisLine(overview) {
|
||||
const signals = [];
|
||||
|
|
@ -1583,18 +1257,6 @@ function businessOverviewRiskSynthesisLine(overview) {
|
|||
: `маржинальность proxy ${overview.trading_margin_proxy.margin_to_revenue_pct}%`;
|
||||
signals.push(`торговый спред proxy ${overview.trading_margin_proxy.gross_spread_proxy_human_ru}, ${marginText}`);
|
||||
}
|
||||
if (overview.accounting_financial_result) {
|
||||
const result = overview.accounting_financial_result;
|
||||
const direction = result.final_result_direction === "profit"
|
||||
? "учетная прибыль"
|
||||
: result.final_result_direction === "loss"
|
||||
? "учетный убыток"
|
||||
: "нулевой учетный финрезультат";
|
||||
const marginText = result.net_margin_to_revenue_pct === null
|
||||
? "маржа к подтвержденной выручке не рассчитана"
|
||||
: `маржа к подтвержденной выручке ${result.net_margin_to_revenue_pct}%`;
|
||||
signals.push(`${direction} по закрытию счетов 90/91/99 ${result.final_result_amount_human_ru}, ${marginText}`);
|
||||
}
|
||||
if (overview.debt_position) {
|
||||
const debtDirection = overview.debt_position.net_debt_position_direction === "net_receivable"
|
||||
? `дебиторка больше кредиторки на ${overview.debt_position.net_debt_position_amount_human_ru}`
|
||||
|
|
@ -1613,16 +1275,6 @@ function businessOverviewRiskSynthesisLine(overview) {
|
|||
if (overview.debt_staleness_risk_proxy) {
|
||||
signals.push(`staleness risk proxy открытых расчетов: ${debtStalenessRiskBandRu(overview.debt_staleness_risk_proxy.risk_band)}, возраст ${overview.debt_staleness_risk_proxy.max_contract_age_days} дн., концентрация старейшего крупного договора ${overview.debt_staleness_risk_proxy.top_contract_share_pct}%`);
|
||||
}
|
||||
if (overview.debt_due_date_aging) {
|
||||
const aging = overview.debt_due_date_aging;
|
||||
signals.push(aging.evidence_status === "confirmed_overdue"
|
||||
? `due-date aging: подтвержденная просрочка ${aging.overdue_amount_human_ru}, строк ${aging.overdue_rows}`
|
||||
: aging.evidence_status === "no_payment_terms_configured"
|
||||
? "due-date aging: проверено, но сроки оплаты в договорах не установлены; подтвержденной просрочки нет"
|
||||
: aging.evidence_status === "insufficient_due_date_basis"
|
||||
? "due-date aging: не хватило даты расчетного документа для честного расчета просрочки"
|
||||
: `due-date aging: проверено, подтвержденной просрочки не найдено`);
|
||||
}
|
||||
if (overview.document_activity_profile) {
|
||||
const topDocument = overview.document_activity_profile.top_document_types[0];
|
||||
const topSection = overview.document_activity_profile.top_account_sections[0];
|
||||
|
|
@ -1662,15 +1314,6 @@ function businessOverviewRiskSynthesisLine(overview) {
|
|||
if (overview.inventory_staleness_risk_proxy) {
|
||||
signals.push(`staleness risk proxy склада: ${inventoryStalenessRiskBandRu(overview.inventory_staleness_risk_proxy.risk_band)}, возраст ${overview.inventory_staleness_risk_proxy.max_purchase_age_days} дн.`);
|
||||
}
|
||||
if (overview.inventory_quality_events) {
|
||||
const quality = overview.inventory_quality_events;
|
||||
if (quality.evidence_status === "reviewed_no_quality_events_found") {
|
||||
signals.push("складские quality-события: документы списания, оприходования, инвентаризации и переоценки проверены, подтвержденных событий не найдено");
|
||||
}
|
||||
else {
|
||||
signals.push(`складские quality-события: списаний ${quality.writeoff_rows} на ${quality.writeoff_amount_human_ru}, оприходований/корректировок ${quality.receipt_adjustment_rows} на ${quality.receipt_adjustment_amount_human_ru}, инвентаризаций ${quality.inventory_count_rows}, переоценок ${quality.revaluation_rows}`);
|
||||
}
|
||||
}
|
||||
return signals.length > 0
|
||||
? `Риски и контуры внимания по подтвержденным данным: ${signals.join("; ")}.`
|
||||
: null;
|
||||
|
|
@ -1684,8 +1327,7 @@ function businessOverviewExecutiveVerdictLine(overview) {
|
|||
overview.debt_staleness_risk_proxy ||
|
||||
overview.inventory_position ||
|
||||
overview.inventory_turnover_proxy ||
|
||||
overview.inventory_staleness_risk_proxy ||
|
||||
overview.inventory_quality_events);
|
||||
overview.inventory_staleness_risk_proxy);
|
||||
const hasOperationalProfileSignal = Boolean(overview.document_activity_profile || overview.counterparty_profile || overview.contract_usage_profile);
|
||||
const hasExtraSignals = hasTaxDebtInventorySignals || hasOperationalProfileSignal;
|
||||
if (!hasCash && !hasExtraSignals) {
|
||||
|
|
@ -1769,19 +1411,12 @@ function buildAssistantMcpDiscoveryAnswerDraft(pilot) {
|
|||
if (pilot.derived_business_overview?.tax_position) {
|
||||
pushReason(reasonCodes, "answer_contains_business_overview_tax_position");
|
||||
}
|
||||
if (pilot.derived_business_overview?.accounting_financial_result) {
|
||||
pushReason(reasonCodes, "answer_contains_business_overview_accounting_financial_result");
|
||||
}
|
||||
if (pilot.derived_business_overview?.trading_margin_proxy) {
|
||||
pushReason(reasonCodes, "answer_contains_business_overview_trading_margin_proxy");
|
||||
}
|
||||
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");
|
||||
}
|
||||
|
|
@ -1806,10 +1441,6 @@ function buildAssistantMcpDiscoveryAnswerDraft(pilot) {
|
|||
if (pilot.derived_business_overview?.debt_staleness_risk_proxy) {
|
||||
pushReason(reasonCodes, "answer_contains_business_overview_debt_staleness_risk_proxy");
|
||||
}
|
||||
if (pilot.derived_business_overview?.debt_due_date_aging) {
|
||||
pushReason(reasonCodes, "answer_contains_business_overview_debt_due_date_aging");
|
||||
pushReason(reasonCodes, `answer_contains_business_overview_debt_due_date_aging_${pilot.derived_business_overview.debt_due_date_aging.evidence_status}`);
|
||||
}
|
||||
if (pilot.derived_business_overview?.inventory_position) {
|
||||
pushReason(reasonCodes, "answer_contains_business_overview_inventory_position");
|
||||
}
|
||||
|
|
@ -1819,10 +1450,6 @@ function buildAssistantMcpDiscoveryAnswerDraft(pilot) {
|
|||
if (pilot.derived_business_overview?.inventory_staleness_risk_proxy) {
|
||||
pushReason(reasonCodes, "answer_contains_business_overview_inventory_staleness_risk_proxy");
|
||||
}
|
||||
if (pilot.derived_business_overview?.inventory_quality_events) {
|
||||
pushReason(reasonCodes, "answer_contains_business_overview_inventory_quality_events");
|
||||
pushReason(reasonCodes, `answer_contains_business_overview_inventory_quality_events_${pilot.derived_business_overview.inventory_quality_events.evidence_status}`);
|
||||
}
|
||||
if (pilot.derived_business_overview?.missing_proof_families?.length) {
|
||||
pushReason(reasonCodes, "answer_contains_business_overview_missing_proof_ledger");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,11 +33,6 @@ function isMcpDiscoveryEntryPointContract(value) {
|
|||
return (record?.schema_version === "assistant_mcp_discovery_runtime_entry_point_v1" &&
|
||||
record?.policy_owner === "assistantMcpDiscoveryRuntimeEntryPoint");
|
||||
}
|
||||
function isRouteCandidateContract(value) {
|
||||
const record = toRecordObject(value);
|
||||
return (record?.schema_version === "assistant_mcp_route_candidate_v1" &&
|
||||
record?.policy_owner === "assistantMcpDiscoveryRuntimeBridge");
|
||||
}
|
||||
function resolveEntryPoint(input) {
|
||||
if (isMcpDiscoveryEntryPointContract(input.entryPoint)) {
|
||||
return input.entryPoint;
|
||||
|
|
@ -52,7 +47,6 @@ function buildAssistantMcpDiscoveryDebugAttachmentFields(input) {
|
|||
const bridge = toRecordObject(entryPoint?.bridge);
|
||||
const planner = toRecordObject(bridge?.planner);
|
||||
const chainAlignment = toRecordObject(planner?.catalog_chain_template_alignment);
|
||||
const routeCandidate = isRouteCandidateContract(bridge?.route_candidate) ? bridge.route_candidate : null;
|
||||
const answerDraft = toRecordObject(bridge?.answer_draft);
|
||||
return {
|
||||
assistant_mcp_discovery_entry_point_v1: entryPoint,
|
||||
|
|
@ -65,16 +59,6 @@ function buildAssistantMcpDiscoveryDebugAttachmentFields(input) {
|
|||
mcp_discovery_catalog_chain_alignment_status: toNonEmptyString(chainAlignment?.alignment_status),
|
||||
mcp_discovery_catalog_chain_top_match: toNonEmptyString(chainAlignment?.top_chain_template_match),
|
||||
mcp_discovery_catalog_chain_selected_matches_top: chainAlignment?.selected_chain_matches_top === true,
|
||||
mcp_discovery_route_candidate_v1: routeCandidate,
|
||||
mcp_discovery_route_candidate_status: toNonEmptyString(routeCandidate?.candidate_status),
|
||||
mcp_discovery_route_candidate_fact_family: toNonEmptyString(routeCandidate?.business_fact_family),
|
||||
mcp_discovery_route_candidate_action_family: toNonEmptyString(routeCandidate?.action_family),
|
||||
mcp_discovery_route_candidate_proof_expectation: toNonEmptyString(routeCandidate?.proof_expectation),
|
||||
mcp_discovery_route_candidate_missing_axes: toStringArray(routeCandidate?.missing_axes),
|
||||
mcp_discovery_route_candidate_provided_axes: toStringArray(routeCandidate?.provided_axes),
|
||||
mcp_discovery_route_candidate_executable_now: routeCandidate?.executable_now === true,
|
||||
mcp_discovery_route_candidate_enablement_reason: toNonEmptyString(routeCandidate?.enablement_reason),
|
||||
mcp_discovery_route_candidate_next_action: toNonEmptyString(routeCandidate?.recommended_next_action),
|
||||
mcp_discovery_answer_mode: toNonEmptyString(answerDraft?.answer_mode),
|
||||
mcp_discovery_business_fact_answer_allowed: bridge?.business_fact_answer_allowed === true,
|
||||
mcp_discovery_user_facing_response_allowed: bridge?.user_facing_response_allowed === true,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -5,7 +5,6 @@ exports.planAssistantMcpDiscovery = planAssistantMcpDiscovery;
|
|||
const assistantMcpDiscoveryPolicy_1 = require("./assistantMcpDiscoveryPolicy");
|
||||
const assistantMcpCatalogIndex_1 = require("./assistantMcpCatalogIndex");
|
||||
exports.ASSISTANT_MCP_DISCOVERY_PLANNER_SCHEMA_VERSION = "assistant_mcp_discovery_planner_v1";
|
||||
const CHUNKED_COVERAGE_PROBE_BUDGET = 30;
|
||||
function toNonEmptyString(value) {
|
||||
if (value === null || value === undefined) {
|
||||
return null;
|
||||
|
|
@ -386,14 +385,12 @@ function budgetOverrideFor(input, recipe) {
|
|||
(recipe.semanticDataNeed === "counterparty value-flow evidence" ||
|
||||
recipe.semanticDataNeed === "bidirectional value-flow comparison evidence" ||
|
||||
recipe.semanticDataNeed === "ranked value-flow evidence");
|
||||
const isBusinessOverviewRecipe = recipe.primitives.includes("query_movements") &&
|
||||
recipe.chainId === "business_overview";
|
||||
if (!isValueFlowRecipe && !isBusinessOverviewRecipe) {
|
||||
if (!isValueFlowRecipe) {
|
||||
return {};
|
||||
}
|
||||
if (requestedAggregationAxis === "month" || isYearDateScope(meaning)) {
|
||||
return {
|
||||
maxProbeCount: CHUNKED_COVERAGE_PROBE_BUDGET
|
||||
maxProbeCount: 30
|
||||
};
|
||||
}
|
||||
return {};
|
||||
|
|
@ -604,7 +601,7 @@ function recipeFor(input) {
|
|||
extraReasons: primitiveSelection.reasonCodes
|
||||
});
|
||||
}
|
||||
if (dataNeedGraph?.ranking_need) {
|
||||
if (dataNeedGraph?.ranking_need && !hasSubjectCandidates(dataNeedGraph)) {
|
||||
pushUnique(axes, "aggregate_axis");
|
||||
pushUnique(axes, "amount");
|
||||
pushUnique(axes, "coverage_target");
|
||||
|
|
@ -624,10 +621,7 @@ 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,
|
||||
...(hasSubjectCandidates(dataNeedGraph) ? ["planner_kept_ranking_over_subject_scope_candidates"] : [])
|
||||
]
|
||||
extraReasons: primitiveSelection.reasonCodes
|
||||
});
|
||||
}
|
||||
if (openScopeTotalWithoutSubject) {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ exports.ASSISTANT_MCP_DISCOVERY_PRIMITIVES = [
|
|||
];
|
||||
const DEFAULT_DISCOVERY_BUDGET = {
|
||||
max_probe_count: 3,
|
||||
max_rows_per_probe: 200
|
||||
max_rows_per_probe: 100
|
||||
};
|
||||
const MAX_PROBE_COUNT = 36;
|
||||
const MAX_ROWS_PER_PROBE = 500;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.ASSISTANT_MCP_DISCOVERY_RESPONSE_CANDIDATE_SCHEMA_VERSION = void 0;
|
||||
exports.buildAssistantMcpDiscoveryResponseCandidate = buildAssistantMcpDiscoveryResponseCandidate;
|
||||
const counterpartyRoleHeuristics_1 = require("./counterpartyRoleHeuristics");
|
||||
exports.ASSISTANT_MCP_DISCOVERY_RESPONSE_CANDIDATE_SCHEMA_VERSION = "assistant_mcp_discovery_response_candidate_v1";
|
||||
function toRecordObject(value) {
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
|
|
@ -17,23 +16,6 @@ function toNonEmptyString(value) {
|
|||
const text = String(value).trim();
|
||||
return text.length > 0 ? text : null;
|
||||
}
|
||||
function normalizeQuestionText(value) {
|
||||
return String(value ?? "")
|
||||
.toLowerCase()
|
||||
.replace(/ё/g, "е")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
function requestsFinancialCounterpartyBoundary(turnMeaning, graph) {
|
||||
const text = normalizeQuestionText([
|
||||
turnMeaning?.raw_message,
|
||||
turnMeaning?.effective_message,
|
||||
graph?.source_message,
|
||||
graph?.question
|
||||
].join(" "));
|
||||
return (/(?:банк|сбербанк|финанс|кредит|депозит)/iu.test(text) &&
|
||||
/(?:клиент|поставщик|выручк|топ|обычн|роль|поток)/iu.test(text));
|
||||
}
|
||||
function toStringList(value) {
|
||||
if (!Array.isArray(value)) {
|
||||
return [];
|
||||
|
|
@ -85,48 +67,7 @@ function hasInternalMechanics(value) {
|
|||
function userFacingLines(values) {
|
||||
return uniqueStrings(values).filter((line) => !hasInternalMechanics(line));
|
||||
}
|
||||
function sanitizeUserFacingMechanics(value) {
|
||||
let text = String(value ?? "").replace(/MCP-срез(?:ом|у|е|а)?/giu, (match) => {
|
||||
const normalized = match.toLowerCase();
|
||||
if (normalized.endsWith("ом")) {
|
||||
return "срезом 1С";
|
||||
}
|
||||
if (normalized.endsWith("у")) {
|
||||
return "срезу 1С";
|
||||
}
|
||||
if (normalized.endsWith("е")) {
|
||||
return "срезе 1С";
|
||||
}
|
||||
if (normalized.endsWith("а")) {
|
||||
return "среза 1С";
|
||||
}
|
||||
return "срез 1С";
|
||||
});
|
||||
const replacements = [
|
||||
[/\bprocurement-concentration route\b/giu, "проверка концентрации закупок/исходящих платежей"],
|
||||
[/\breviewed vendor-risk route\b/giu, "отдельная проверка поставщицкого риска"],
|
||||
[/\bvendor-risk route\b/giu, "проверка поставщицкого риска"],
|
||||
[/\bdue-date route\b/giu, "проверка просрочки по срокам оплаты"],
|
||||
[/\bdebt-quality proxy\b/giu, "ограниченный долговой сигнал"],
|
||||
[/\bstaleness-risk proxy\b/giu, "косвенный признак залежалости"],
|
||||
[/\bstaleness risk proxy\b/giu, "косвенный признак залежалости"],
|
||||
[/\boperating-flow proxy\b/giu, "денежный операционный показатель"],
|
||||
[/\btrading-margin proxy\b/giu, "товарная маржинальность по проверенным документам"],
|
||||
[/\bprocurement concentration proxy\b/giu, "сигнал концентрации закупок/исходящих платежей"],
|
||||
[/\boutgoing cash concentration proxy\b/giu, "сигнал концентрации исходящих денег"],
|
||||
[/\bproxy-сигналы\b/giu, "косвенные признаки"],
|
||||
[/\bproxy\b/giu, "косвенный показатель"],
|
||||
[/\bsales-to-stock\b/giu, "отношение продаж к остатку"],
|
||||
[/\boverdue\/due-date aging\b/giu, "просрочку по договорным срокам"],
|
||||
[/\bP&L\b/gu, "полный отчет о прибылях и убытках"]
|
||||
];
|
||||
for (const [pattern, replacement] of replacements) {
|
||||
text = text.replace(pattern, replacement);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
function localizeLine(value) {
|
||||
const sanitizedValue = sanitizeUserFacingMechanics(value);
|
||||
if (/^1C activity rows were found for the requested counterparty scope$/i.test(value)) {
|
||||
return "В 1С найдены строки активности в запрошенном срезе.";
|
||||
}
|
||||
|
|
@ -147,7 +88,7 @@ function localizeLine(value) {
|
|||
return `В 1С проверены входящие и исходящие денежные строки в запрошенном срезе: ${incoming}, ${outgoing}.`;
|
||||
}
|
||||
if (/^Requested period hit the MCP row limit, but the approved monthly recovery probe budget is smaller than the required subperiod count$/i.test(value)) {
|
||||
return "Запрошенный период достиг лимита строк; доступного бюджета помесячных дозапросов не хватило, чтобы покрыть все подпериоды.";
|
||||
return "Запрошенный период уперся в лимит строк MCP; доступного бюджета помесячных дозапросов не хватило, чтобы покрыть все подпериоды.";
|
||||
}
|
||||
const counterpartyMatch = value.match(/^1C activity rows were found for counterparty\s+(.+)$/i);
|
||||
if (counterpartyMatch) {
|
||||
|
|
@ -172,10 +113,10 @@ function localizeLine(value) {
|
|||
}
|
||||
const movementRowsMatch = value.match(/^1C movement rows were found for counterparty\s+(.+)$/i);
|
||||
if (movementRowsMatch) {
|
||||
return `В 1С найдены строки движений по контрагенту ${movementRowsMatch[1]}.`;
|
||||
return `В 1С найдены строки движений по контрагенту ${movementRowsMatch[1]}.`;
|
||||
}
|
||||
if (/^1C movement rows were found for the requested scope$/i.test(value)) {
|
||||
return "В 1С найдены строки движений по запрошенному контуру.";
|
||||
return "В 1С найдены строки движений по запрошенному контуру.";
|
||||
}
|
||||
const supplierPayoutMatch = value.match(/^1C supplier-payout rows were found for counterparty\s+(.+)$/i);
|
||||
if (supplierPayoutMatch) {
|
||||
|
|
@ -203,7 +144,7 @@ function localizeLine(value) {
|
|||
return "Срез документов ограничен только подтвержденными строками документов в проверенном окне.";
|
||||
}
|
||||
if (/^Counterparty movement evidence is limited to confirmed 1C movement rows in the checked scope$/i.test(value)) {
|
||||
return "Срез движений ограничен только подтвержденными строками движений в проверенном окне.";
|
||||
return "Срез движений ограничен только подтвержденными строками движений в проверенном окне.";
|
||||
}
|
||||
if (/^Counterparty value-flow total was calculated from confirmed 1C movement rows$/i.test(value)) {
|
||||
return "Сумма входящих поступлений рассчитана только по подтвержденным строкам поступлений в 1С.";
|
||||
|
|
@ -286,10 +227,10 @@ function localizeLine(value) {
|
|||
return "Полный срез документов без явно проверенного периода не подтвержден.";
|
||||
}
|
||||
if (/^Full movement history outside the checked period is not proven by this MCP discovery pilot$/i.test(value)) {
|
||||
return "Полный исторический срез движений вне проверенного периода этим поиском не подтвержден.";
|
||||
return "Полный исторический срез движений вне проверенного периода этим поиском не подтвержден.";
|
||||
}
|
||||
if (/^Full movement history is not proven without an explicit checked period$/i.test(value)) {
|
||||
return "Полный срез движений без явно проверенного периода не подтвержден.";
|
||||
return "Полный срез движений без явно проверенного периода не подтвержден.";
|
||||
}
|
||||
if (/^Full supplier-payout amount outside the checked period is not proven by this MCP discovery pilot$/i.test(value)) {
|
||||
return "Полный объем исходящих платежей вне проверенного периода этим поиском не подтвержден.";
|
||||
|
|
@ -304,10 +245,10 @@ function localizeLine(value) {
|
|||
return "Полный двусторонний денежный поток за все время без явно проверенного периода не подтвержден.";
|
||||
}
|
||||
if (/^Requested period coverage was recovered through monthly 1C value-flow probes after the broad probe hit the row limit$/i.test(value)) {
|
||||
return "Покрытие запрошенного периода восстановлено помесячными проверками 1С после того, как общая выборка достигла лимита строк.";
|
||||
return "Покрытие запрошенного периода восстановлено помесячными проверками 1С после того, как общая выборка уперлась в лимит строк.";
|
||||
}
|
||||
if (/^Requested period coverage for bidirectional value-flow was recovered through monthly 1C side probes after a broad probe hit the row limit$/i.test(value)) {
|
||||
return "Покрытие запрошенного периода по двустороннему денежному потоку восстановлено помесячными проверками 1С после того, как общая выборка достигла лимита строк хотя бы по одной стороне.";
|
||||
return "Покрытие запрошенного периода по двустороннему денежному потоку восстановлено помесячными проверками 1С после того, как общая выборка уперлась в лимит строк хотя бы по одной стороне.";
|
||||
}
|
||||
if (/^Requested period coverage was recovered through monthly 1C value-flow probes$/i.test(value)) {
|
||||
return "Покрытие запрошенного периода восстановлено помесячными проверками 1С.";
|
||||
|
|
@ -327,7 +268,7 @@ function localizeLine(value) {
|
|||
if (/^Complete requested-period coverage for bidirectional value-flow is not proven by the available checked rows$/i.test(value)) {
|
||||
return "Полное покрытие запрошенного периода по двустороннему денежному потоку не подтверждено доступными проверенными строками.";
|
||||
}
|
||||
return sanitizedValue;
|
||||
return value;
|
||||
}
|
||||
function section(title, lines) {
|
||||
const clean = userFacingLines(lines.map(localizeLine));
|
||||
|
|
@ -403,15 +344,10 @@ function businessOverviewCoverageLimitLine(overview) {
|
|||
if (outgoing?.coverage_limited_by_probe_limit === true) {
|
||||
limited.push("исходящие");
|
||||
}
|
||||
const continuation = "Если нужен полный сквозной ответ, безопасный следующий шаг — выбрать конкретный год или квартал для дозапроса: тогда широкий срез можно собрать частями без выдачи непроверенного итога.";
|
||||
return limited.length > 0
|
||||
? `Важно: по направлению ${limited.join(" и ")} проверка достигла лимита строк; это расширенный проверенный срез найденных строк, но не гарантия полного бухгалтерского оборота без отдельной полной выгрузки. ${continuation}`
|
||||
? `Важно: ${limited.join(" и ")} уперлись в лимит выборки MCP, поэтому это проверенный срез найденных строк, а не гарантированно полный бухгалтерский оборот.`
|
||||
: null;
|
||||
}
|
||||
function joinBusinessReplyLines(lines) {
|
||||
const reply = userFacingLines(lines.map(localizeLine)).join("\n").trim();
|
||||
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
|
||||
}
|
||||
function businessOverviewYearRowsLine(overview) {
|
||||
const years = Array.isArray(overview.yearly_breakdown) ? overview.yearly_breakdown : [];
|
||||
const values = years
|
||||
|
|
@ -435,52 +371,6 @@ function firstOverviewAxisLabel(rows, amountKey = "total_amount_human_ru") {
|
|||
const amount = moneyText(first?.[amountKey]);
|
||||
return label && amount ? `${label} — ${sentenceAmount(amount) ?? amount}` : null;
|
||||
}
|
||||
function firstNonFinancialOverviewAxisLabel(rows, amountKey = "total_amount_human_ru") {
|
||||
if (!Array.isArray(rows)) {
|
||||
return null;
|
||||
}
|
||||
for (const row of rows) {
|
||||
const item = toRecordObject(row);
|
||||
const label = toNonEmptyString(item?.axis_value);
|
||||
if (!label || (0, counterpartyRoleHeuristics_1.isLikelyFinancialInstitutionCounterparty)(label)) {
|
||||
continue;
|
||||
}
|
||||
const amount = moneyText(item?.[amountKey]);
|
||||
if (amount) {
|
||||
return `${label} — ${sentenceAmount(amount) ?? amount}`;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function overviewAxisLooksFinancial(row) {
|
||||
if (!row) {
|
||||
return false;
|
||||
}
|
||||
return (row.counterparty_role_hint === "bank_or_financial_institution" ||
|
||||
(0, counterpartyRoleHeuristics_1.isLikelyFinancialInstitutionCounterparty)(row.axis_value));
|
||||
}
|
||||
function financialFlowHintTextRuFromRecord(row) {
|
||||
const hint = toNonEmptyString(row?.financial_flow_hint);
|
||||
const rows = typeof row?.financial_flow_hint_rows === "number" && Number.isFinite(row.financial_flow_hint_rows)
|
||||
? ` (${row.financial_flow_hint_rows} строк)`
|
||||
: "";
|
||||
if (hint === "loan_or_credit") {
|
||||
return `По полям банковского документа доминирует кредитный/заемный признак${rows}; это не обычная поставка и не клиентская выручка без отдельной проверки назначения.`;
|
||||
}
|
||||
if (hint === "bank_fee_or_service") {
|
||||
return `По полям банковского документа доминирует признак банковской комиссии/услуг банка${rows}; это не обычный поставщик товаров/услуг без отдельной проверки договора.`;
|
||||
}
|
||||
if (hint === "tax_or_budget") {
|
||||
return `По полям банковского документа доминирует налоговый/бюджетный признак${rows}; это не поставщик и не клиентская выручка.`;
|
||||
}
|
||||
if (hint === "payroll_or_social") {
|
||||
return `По полям банковского документа доминирует зарплатный/социальный признак${rows}; это не поставщик и не клиентская выручка.`;
|
||||
}
|
||||
if (hint === "supplier_payment") {
|
||||
return `По полям банковского документа доминирует признак оплаты поставщику${rows}; если получатель по названию является банком, это все равно требует осторожной трактовки.`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function businessOverviewTaxLine(overview) {
|
||||
const tax = toRecordObject(overview.tax_position);
|
||||
if (!tax) {
|
||||
|
|
@ -561,8 +451,6 @@ function bidirectionalNetLabel(direction) {
|
|||
return "нетто в нашу сторону";
|
||||
}
|
||||
function buildCompactBidirectionalValueFlowReply(entryPoint, draft) {
|
||||
const turnInput = toRecordObject(entryPoint.turn_input);
|
||||
const turnMeaning = toRecordObject(turnInput?.turn_meaning_ref);
|
||||
const bridge = toRecordObject(entryPoint.bridge);
|
||||
const pilot = toRecordObject(bridge?.pilot);
|
||||
const flow = toRecordObject(pilot?.derived_bidirectional_value_flow);
|
||||
|
|
@ -577,13 +465,7 @@ function buildCompactBidirectionalValueFlowReply(entryPoint, draft) {
|
|||
if (!incomingAmount && !outgoingAmount && !netAmount) {
|
||||
return null;
|
||||
}
|
||||
const counterparty = toNonEmptyString(flow.counterparty);
|
||||
const organizationScope = toNonEmptyString(turnMeaning?.explicit_organization_scope);
|
||||
const subjectLead = counterparty
|
||||
? `по контрагенту ${counterparty}`
|
||||
: organizationScope
|
||||
? `по компании ${organizationScope}`
|
||||
: "по выбранному контуру";
|
||||
const counterparty = toNonEmptyString(flow.counterparty) ?? "запрошенному контрагенту";
|
||||
const period = toNonEmptyString(flow.period_scope);
|
||||
const periodText = period ? ` за период ${period}` : " в проверенном окне";
|
||||
const incomingRows = sideRowsText(incoming);
|
||||
|
|
@ -592,7 +474,7 @@ function buildCompactBidirectionalValueFlowReply(entryPoint, draft) {
|
|||
const outgoingDates = sideDateText(outgoing);
|
||||
const netLabel = bidirectionalNetLabel(flow.net_direction);
|
||||
const lines = [
|
||||
`Коротко: ${subjectLead}${periodText} по найденным строкам 1С получили ${incomingAmount ?? "0 руб."}, заплатили ${outgoingAmount ?? "0 руб."}; расчетное ${netLabel}: ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}.`
|
||||
`Коротко: по контрагенту ${counterparty}${periodText} по найденным строкам 1С получили ${incomingAmount ?? "0 руб."}, заплатили ${outgoingAmount ?? "0 руб."}; расчетное ${netLabel}: ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}.`
|
||||
];
|
||||
const basis = [];
|
||||
if (incomingRows) {
|
||||
|
|
@ -605,14 +487,15 @@ function buildCompactBidirectionalValueFlowReply(entryPoint, draft) {
|
|||
lines.push(`Основа: ${basis.join("; ")}.`);
|
||||
}
|
||||
if (flow.coverage_limited_by_probe_limit === true) {
|
||||
lines.push("Важно: часть проверки достигла лимита строк, поэтому это проверенный срез найденных движений, а не гарантия полного периода.");
|
||||
lines.push("Важно: часть проверки уперлась в лимит строк, поэтому это проверенный срез найденных движений, а не гарантия полного периода.");
|
||||
}
|
||||
lines.push("Метод: нетто рассчитано как подтвержденные входящие строки 1С минус подтвержденные исходящие строки; это не полное бухгалтерское сальдо вне проверенного окна.");
|
||||
const fallbackNextStep = toNonEmptyString(draft.next_step_line);
|
||||
if (fallbackNextStep) {
|
||||
lines.push(`Следующий шаг: ${localizeLine(fallbackNextStep)}`);
|
||||
}
|
||||
return joinBusinessReplyLines(lines);
|
||||
const reply = lines.join("\n").trim();
|
||||
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
|
||||
}
|
||||
function compactComparable(value) {
|
||||
return String(value ?? "")
|
||||
|
|
@ -727,219 +610,26 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
|||
const topCustomer = toRecordObject(Array.isArray(overview.top_customers) ? overview.top_customers[0] : null);
|
||||
const customerName = toNonEmptyString(topCustomer?.axis_value);
|
||||
const customerAmount = moneyText(topCustomer?.total_amount_human_ru);
|
||||
const topCustomerLooksFinancial = overviewAxisLooksFinancial(topCustomer);
|
||||
const nonFinancialCustomer = firstNonFinancialOverviewAxisLabel(topCustomerLooksFinancial ? overview.top_customers : []);
|
||||
const topCustomerLead = customerName && customerAmount
|
||||
? topCustomerLooksFinancial
|
||||
? `; крупнейший входящий денежный источник: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount} (похоже на банк/финорганизацию, не называю это клиентской выручкой без назначения платежа)${nonFinancialCustomer ? `; крупнейший небанковский входящий контрагент: ${nonFinancialCustomer}` : ""}`
|
||||
: `; крупнейший источник входящих денег: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}`
|
||||
? `; крупнейший источник входящих денег: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}`
|
||||
: "";
|
||||
const topSupplierRecord = toRecordObject(Array.isArray(overview.top_suppliers) ? overview.top_suppliers[0] : null);
|
||||
const topSupplier = firstOverviewAxisLabel(overview.top_suppliers);
|
||||
const topSupplierLooksFinancial = overviewAxisLooksFinancial(topSupplierRecord);
|
||||
const nonFinancialSupplier = firstNonFinancialOverviewAxisLabel(topSupplierLooksFinancial ? overview.top_suppliers : []);
|
||||
const topSupplierLead = topSupplier
|
||||
? topSupplierLooksFinancial
|
||||
? `; крупнейший получатель исходящих денег: ${topSupplier} (похоже на банк/финорганизацию, не называю это обычным поставщиком без назначения платежа/договора)${nonFinancialSupplier ? `; крупнейший небанковский получатель исходящих денег: ${nonFinancialSupplier}` : ""}`
|
||||
: `; крупнейший получатель исходящих денег: ${topSupplier}`
|
||||
: "";
|
||||
const topSupplierLead = topSupplier ? `; крупнейший получатель исходящих денег: ${topSupplier}` : "";
|
||||
const roleBoundaryLead = topCustomer || topSupplier ? "; клиент/поставщик как бизнес-роли этим денежным срезом не подтверждены" : "";
|
||||
const financialBoundaryRequested = requestsFinancialCounterpartyBoundary(turnMeaning, graph);
|
||||
const requestedFinancialBoundaryLine = financialBoundaryRequested
|
||||
? topCustomerLooksFinancial || topSupplierLooksFinancial
|
||||
? "Отдельно по банкам: если денежный топ ведет банк/финансовая организация, это нельзя автоматически читать как обычного клиента или поставщика; нужны назначение платежа, вид операции и договор. Поэтому такой поток не является доказанной клиентской выручкой, обычной поставкой или чистой прибылью без отдельной проверки."
|
||||
: "Отдельно по банкам: банк/финансовую организацию в денежных топах нельзя автоматически читать как обычного клиента или поставщика; нужны назначение платежа, вид операции и договор. Поэтому такой поток не является доказанной клиентской выручкой, обычной поставкой или чистой прибылью без отдельной проверки."
|
||||
: null;
|
||||
const graphReasonCodes = toStringList(graph?.reason_codes);
|
||||
const directMoneyAnswer = graphReasonCodes.includes("data_need_graph_business_overview_direct_money_answer");
|
||||
const crossScopeExecutiveSummary = Boolean(separateSubject && previousCounterpartySummary);
|
||||
const lines = [];
|
||||
const actionFamily = toNonEmptyString(turnMeaning?.asked_action_family);
|
||||
const unsupportedFamily = toNonEmptyString(turnMeaning?.unsupported_but_understood_family);
|
||||
const profitMarginBoundary = actionFamily === "profit_margin_boundary" || unsupportedFamily === "profit_margin_boundary";
|
||||
const debtDueDateBoundary = actionFamily === "debt_due_date_boundary" || unsupportedFamily === "debt_due_date_boundary";
|
||||
const vendorRiskBoundary = actionFamily === "vendor_risk_procurement_boundary" || unsupportedFamily === "vendor_risk_procurement_boundary";
|
||||
const inventoryReserveBoundary = actionFamily === "inventory_reserve_boundary" || unsupportedFamily === "inventory_reserve_liquidation_boundary";
|
||||
if (profitMarginBoundary) {
|
||||
const accountingFinancialResult = toRecordObject(overview.accounting_financial_result);
|
||||
if (accountingFinancialResult) {
|
||||
const direction = toNonEmptyString(accountingFinancialResult.final_result_direction);
|
||||
const amount = moneyText(accountingFinancialResult.final_result_amount_human_ru);
|
||||
const periodScope = toNonEmptyString(accountingFinancialResult.period_scope) ?? period;
|
||||
const marginPct = typeof accountingFinancialResult.net_margin_to_revenue_pct === "number" &&
|
||||
Number.isFinite(accountingFinancialResult.net_margin_to_revenue_pct)
|
||||
? `${accountingFinancialResult.net_margin_to_revenue_pct}%`
|
||||
: null;
|
||||
const directionText = direction === "profit"
|
||||
? "учетная прибыль"
|
||||
: direction === "loss"
|
||||
? "учетный убыток"
|
||||
: "нулевой учетный финрезультат";
|
||||
const amountText = amount
|
||||
? direction === "loss"
|
||||
? `минус ${amount}`
|
||||
: amount
|
||||
: "сумма не распознана";
|
||||
lines.push(`Коротко: нет, денежное операционное нетто не стоит считать чистой прибылью. Отдельно по закрытию счетов 90/91/99 в 1С за ${periodScope} подтвержден ${directionText}: ${amountText}${marginPct ? `; маржа к подтвержденной выручке ${marginPct}` : "; маржа к подтвержденной выручке не рассчитана"}.`);
|
||||
lines.push("Это учетный финрезультат по найденным строкам закрытия периода в 1С, а не внешний аудит и не юридически подтвержденная отчетность.");
|
||||
return joinBusinessReplyLines(lines);
|
||||
}
|
||||
const headline = toNonEmptyString(draft.headline);
|
||||
const cleanHeadline = headline?.replace(/^Коротко:\s*/iu, "").trim();
|
||||
lines.push(cleanHeadline
|
||||
? `Коротко: ${localizeLine(cleanHeadline)}`
|
||||
: "Коротко: нельзя точно подтвердить чистую прибыль и маржу по текущему срезу 1С; есть только ограниченный операционный денежный/товарный сигнал, а не полный отчет о прибыли и не бухгалтерский финансовый результат.");
|
||||
const boundaryLines = userFacingLines([
|
||||
...toStringList(draft.confirmed_lines),
|
||||
...toStringList(draft.inference_lines),
|
||||
...toStringList(draft.unknown_lines)
|
||||
])
|
||||
.filter((line) => /(?:прибыл|марж|финанс|p\s*&\s*l|p&l|расход|себестоим|закрыт|profit|margin|financial)/iu.test(line))
|
||||
.slice(0, 2);
|
||||
if (boundaryLines.length > 0) {
|
||||
lines.push(...boundaryLines.map(localizeLine));
|
||||
}
|
||||
lines.push("Для точного отчета о прибыли нужны отдельная проверка себестоимости, расходов, закрытия периода и финрезультата; текущий ограниченный сигнал нельзя выдавать за подтвержденную чистую прибыль или маржу.");
|
||||
if (limitLine) {
|
||||
lines.push(limitLine);
|
||||
}
|
||||
return joinBusinessReplyLines(lines);
|
||||
}
|
||||
if (debtDueDateBoundary) {
|
||||
const dueDateAging = toRecordObject(overview.debt_due_date_aging);
|
||||
if (dueDateAging) {
|
||||
const status = toNonEmptyString(dueDateAging.evidence_status);
|
||||
const asOfDate = toNonEmptyString(dueDateAging.as_of_date) ?? "проверенную дату";
|
||||
const overdueAmount = moneyText(dueDateAging.overdue_amount_human_ru);
|
||||
const grossAmount = moneyText(dueDateAging.gross_open_amount_human_ru);
|
||||
const rowsWithPaymentTerms = typeof dueDateAging.rows_with_payment_terms === "number" && Number.isFinite(dueDateAging.rows_with_payment_terms)
|
||||
? dueDateAging.rows_with_payment_terms
|
||||
: null;
|
||||
const rowsWithAmount = typeof dueDateAging.rows_with_amount === "number" && Number.isFinite(dueDateAging.rows_with_amount)
|
||||
? dueDateAging.rows_with_amount
|
||||
: null;
|
||||
const dueDateScopePrefix = organizationScope ? `по компании ${organizationScope} ` : "";
|
||||
if (status === "confirmed_overdue") {
|
||||
lines.push(`Коротко: ${dueDateScopePrefix}на ${asOfDate} подтвержденная просрочка есть: ${overdueAmount ?? "сумма не распознана"} по ${dueDateAging.overdue_rows ?? "найденным"} строкам.`);
|
||||
lines.push("Основа ответа: открытые расчеты 60/62/76, договорный срок оплаты и дата расчетного документа; это проверка просрочки по срокам оплаты, а не просто возраст договора.");
|
||||
}
|
||||
else if (status === "no_payment_terms_configured") {
|
||||
lines.push(`Коротко: ${dueDateScopePrefix}на ${asOfDate} подтвержденной просрочки нет: открытые расчеты проверены${grossAmount ? ` на ${grossAmount}` : ""}, но в найденных договорах срок оплаты не установлен.`);
|
||||
lines.push(rowsWithAmount !== null
|
||||
? `Проверено строк с суммой: ${rowsWithAmount}. Без установленного срока оплаты нельзя честно назвать эти остатки просрочкой.`
|
||||
: "Без установленного срока оплаты нельзя честно назвать эти остатки просрочкой.");
|
||||
}
|
||||
else if (status === "insufficient_due_date_basis") {
|
||||
lines.push(`Коротко: ${dueDateScopePrefix}на ${asOfDate} просрочка не подтверждена: по строкам с установленным сроком оплаты не хватило даты расчетного документа.`);
|
||||
if (rowsWithPaymentTerms !== null) {
|
||||
lines.push(`Строк с установленным сроком оплаты: ${rowsWithPaymentTerms}; нужен документ-основание с датой, чтобы посчитать договорный срок оплаты.`);
|
||||
}
|
||||
}
|
||||
else {
|
||||
lines.push(`Коротко: ${dueDateScopePrefix}на ${asOfDate} проверка просрочки по срокам оплаты выполнена, подтвержденной просрочки не найдено${rowsWithPaymentTerms !== null ? `; строк с установленным сроком оплаты ${rowsWithPaymentTerms}` : ""}.`);
|
||||
}
|
||||
return joinBusinessReplyLines(lines);
|
||||
}
|
||||
const headline = toNonEmptyString(draft.headline);
|
||||
const cleanHeadline = headline?.replace(/^Коротко:\s*/iu, "").trim();
|
||||
lines.push(cleanHeadline
|
||||
? `Коротко: ${localizeLine(cleanHeadline)}`
|
||||
: "Коротко: нельзя точно определить, какая дебиторка просрочена, по текущему срезу 1С; есть только ограниченный долговой сигнал, но нет проверки договорных сроков оплаты.");
|
||||
lines.push("Проверить нужно отдельно: договоры, сроки оплаты, погашение и закрытие задолженности; без этого нельзя доказать просрочку по договорным срокам.");
|
||||
return joinBusinessReplyLines(lines);
|
||||
}
|
||||
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(`Коротко: проверка концентрации закупок/исходящих платежей за ${periodScope} не подтверждает зависимость от обычного поставщика: крупнейший получатель исходящих денег ${topOutgoingName ?? "не распознан"}${topOutgoingShare ? ` держит около ${topOutgoingShare}` : ""}${topOutgoingAmount ? ` (${topOutgoingAmount})` : ""}, но по названию это банк/финансовая организация${totalText}.`);
|
||||
const financialHintText = financialFlowHintTextRuFromRecord(topOutgoingRecord);
|
||||
if (financialHintText) {
|
||||
lines.push(financialHintText);
|
||||
}
|
||||
if (nonFinancialName) {
|
||||
lines.push(`Крупнейший небанковский получатель исходящих денег: ${nonFinancialName}${nonFinancialShare ? `, около ${nonFinancialShare}` : ""}${nonFinancialAmount ? ` (${nonFinancialAmount})` : ""}. Это уже сигнал закупочной/исходящей концентрации, но не аудит надежности поставщика.`);
|
||||
}
|
||||
}
|
||||
else if (status === "reviewed_procurement_concentration") {
|
||||
lines.push(`Коротко: точный риск зависимости от одного поставщика не подтвержден полностью; проверка концентрации закупок/исходящих платежей за ${periodScope} нашла крупнейшего получателя исходящего потока: ${topOutgoingName ?? nonFinancialName ?? "получатель не распознан"}${topOutgoingShare ? ` держит около ${topOutgoingShare}` : nonFinancialShare ? ` держит около ${nonFinancialShare}` : ""}${topOutgoingAmount ? ` (${topOutgoingAmount})` : nonFinancialAmount ? ` (${nonFinancialAmount})` : ""}${totalText}.`);
|
||||
}
|
||||
else {
|
||||
lines.push(`Коротко: проверка концентрации закупок/исходящих платежей за ${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}`);
|
||||
return joinBusinessReplyLines(lines);
|
||||
}
|
||||
const supplierBasis = topSupplier
|
||||
? topSupplierLooksFinancial
|
||||
? `крупнейший получатель исходящих денег: ${topSupplier}; по названию это банк/финансовая организация, поэтому это не доказанная зависимость от обычного поставщика${nonFinancialSupplier ? `; крупнейший небанковский получатель исходящих денег: ${nonFinancialSupplier}` : ""}`
|
||||
: `крупнейший подтвержденный поставщик/получатель исходящих платежей: ${topSupplier}`
|
||||
: outgoingAmount
|
||||
? `исходящие платежи/закупочный поток в проверенном срезе: ${outgoingAmount}`
|
||||
: "есть только ограниченный срез исходящих платежей без полного профиля поставщицкого риска";
|
||||
const proxyLabel = topSupplierLooksFinancial
|
||||
? "сигнал концентрации исходящих денег"
|
||||
: "сигнал концентрации закупок/исходящих платежей";
|
||||
lines.push(`Коротко: точный риск зависимости от одного поставщика по текущим данным не подтвержден; есть только ${proxyLabel}: ${supplierBasis}.`);
|
||||
lines.push("Это сигнал концентрации закупок/исходящих платежей, а не полный аудит надежности поставщиков, условий, качества и структуры всех расходов.");
|
||||
lines.push("Для точного вывода нужна отдельная проверка поставщицкого риска: поставщики, договорные условия, качество поставок, сроки, доля в закупках и полная структура расходов.");
|
||||
return joinBusinessReplyLines(lines);
|
||||
}
|
||||
if (inventoryReserveBoundary) {
|
||||
const headline = toNonEmptyString(draft.headline);
|
||||
const inventoryQualityEvents = toRecordObject(overview.inventory_quality_events);
|
||||
const cleanHeadline = headline?.replace(/^Коротко:\s*/iu, "").trim();
|
||||
const reserveBasis = cleanHeadline ? localizeLine(cleanHeadline).replace(/^проверил/iu, "Проверены") : null;
|
||||
lines.push(reserveBasis
|
||||
? `Коротко: точно подтвердить резерв под неликвиды нельзя. ${reserveBasis}`
|
||||
: "Коротко: точно подтвердить резерв под неликвиды по текущим данным нельзя.");
|
||||
if (inventoryQualityEvents) {
|
||||
return joinBusinessReplyLines(lines);
|
||||
}
|
||||
const boundaryLines = userFacingLines([
|
||||
...toStringList(draft.unknown_lines),
|
||||
...toStringList(draft.limitation_lines)
|
||||
])
|
||||
.filter((line) => /(?:резерв|неликвид|склад|товар|reserve|obsolete|inventory|stock)/iu.test(line))
|
||||
.slice(0, 2);
|
||||
if (boundaryLines.length > 0) {
|
||||
lines.push(...boundaryLines.map(localizeLine));
|
||||
}
|
||||
lines.push("Проверить нужно отдельно: складской срез на дату, учетную политику резервов, списания и ликвидационную стоимость; косвенные признаки нельзя выдавать за доказанный факт резерва.");
|
||||
return joinBusinessReplyLines(lines);
|
||||
}
|
||||
if (crossScopeExecutiveSummary && separateSubject && previousCounterpartySummary && (incomingAmount || outgoingAmount || netAmount)) {
|
||||
lines.push(`Коротко: по компании ${organizationScope ?? "в выбранном контуре"} ${period} подтвержден денежный срез: получили ${incomingAmount ?? "0 руб."}, исходящие платежи/списания ${outgoingAmount ?? "0 руб."}, ${netDirection} ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}${previousCounterpartySummary.lead}; можно утверждать только эти подтвержденные срезы, нельзя называть это чистой прибылью, полным оборотом или доказанной ролью главного клиента/поставщика.`);
|
||||
lines.push(previousCounterpartySummary.line);
|
||||
lines.push(`Можно утверждать: по компании подтвержден операционный денежный сигнал по найденным строкам 1С; по ${separateSubject} отдельно подтверждены входящие/исходящие строки, расчетное нетто и документы из предыдущего контрагентского среза.`);
|
||||
lines.push(`Можно утверждать: по компании подтвержден operating-flow proxy по найденным строкам 1С; по ${separateSubject} отдельно подтверждены входящие/исходящие строки, расчетное нетто и документы из предыдущего контрагентского среза.`);
|
||||
lines.push(`Нельзя утверждать: это не чистая прибыль, не полный бухгалтерский оборот вне проверенного окна и не доказательство, что ${separateSubject} является главным клиентом или поставщиком как бизнес-роль.`);
|
||||
if (limitLine) {
|
||||
lines.push(limitLine);
|
||||
}
|
||||
return joinBusinessReplyLines(lines);
|
||||
const reply = lines.join("\n").trim();
|
||||
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
|
||||
}
|
||||
if (rankingNeed) {
|
||||
const incomingLeader = strongestIncomingYear(overview);
|
||||
|
|
@ -950,7 +640,7 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
|||
if (!leaderYear || !leaderAmount) {
|
||||
return null;
|
||||
}
|
||||
lines.push(`Коротко: ${organizationPrefix}в доступном проверенном срезе 1С по входящим денежным строкам лидирует ${leaderYear}: ${leaderAmount}${Number.isFinite(leaderRows) && leaderRows > 0 ? ` по ${leaderRows} строкам с суммой` : ""}; это не полный бухгалтерский рейтинг доходности.`);
|
||||
lines.push(`Коротко: в доступном проверенном MCP-срезе по входящим денежным строкам лидирует ${leaderYear}: ${leaderAmount}${Number.isFinite(leaderRows) && leaderRows > 0 ? ` по ${leaderRows} строкам с суммой` : ""}; это не полный бухгалтерский рейтинг доходности.`);
|
||||
const netYear = toNonEmptyString(netLeader?.year_bucket);
|
||||
const netYearAmount = moneyText(netLeader?.net_amount_human_ru);
|
||||
if (netYear && netYearAmount) {
|
||||
|
|
@ -961,9 +651,6 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
|||
if (incomingAmount && outgoingAmount && netAmount) {
|
||||
lines.push(`Сверка по окну: входящие ${incomingAmount}, исходящие ${outgoingAmount}, ${netDirection} ${sentenceAmount(netAmount) ?? netAmount}.`);
|
||||
}
|
||||
if (requestedFinancialBoundaryLine) {
|
||||
lines.push(requestedFinancialBoundaryLine);
|
||||
}
|
||||
const yearRows = businessOverviewYearRowsLine(overview);
|
||||
if (yearRows) {
|
||||
lines.push(yearRows);
|
||||
|
|
@ -971,14 +658,9 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
|||
}
|
||||
else if (incomingAmount || outgoingAmount || netAmount) {
|
||||
lines.push(`Коротко: ${organizationPrefix}${period} по подтвержденным строкам 1С получили ${incomingAmount ?? "0 руб."}; исходящие платежи/списания ${outgoingAmount ?? "0 руб."}; ${netDirection} ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб"}${topCustomerLead}${topSupplierLead}${roleBoundaryLead}${separateSubjectLead}.`);
|
||||
lines.push('Метод: "заработали" здесь считаю как операционный денежный показатель по 1С; это не чистая прибыль и не финрезультат.');
|
||||
lines.push('Метод: "заработали" здесь считаю как денежный operating-flow proxy по 1С; это не чистая прибыль и не финрезультат.');
|
||||
if (!directMoneyAnswer && customerName && customerAmount) {
|
||||
lines.push(topCustomerLooksFinancial
|
||||
? `Крупнейший входящий денежный источник в этом срезе: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}. По названию это банк/финансовая организация, поэтому без назначения платежа не называю это клиентской выручкой.${nonFinancialCustomer ? ` Крупнейший небанковский входящий контрагент: ${nonFinancialCustomer}.` : ""}`
|
||||
: `Крупнейший подтвержденный источник входящих денег в этом срезе: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}.`);
|
||||
}
|
||||
if (requestedFinancialBoundaryLine) {
|
||||
lines.push(requestedFinancialBoundaryLine);
|
||||
lines.push(`Крупнейший подтвержденный источник входящих денег в этом срезе: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}.`);
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
|
@ -989,14 +671,10 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
|||
`Отдельно по контрагенту ${separateSubject}: этот итог не переносит суммы компании на контрагента. Можно утверждать только разделение контура; нельзя делать вывод о выручке, долге или прибыльности ${separateSubject} без отдельного контрагентского среза документов и движений.`);
|
||||
}
|
||||
if (!directMoneyAnswer && topSupplier) {
|
||||
lines.push(topSupplierLooksFinancial
|
||||
? `Крупнейший получатель исходящих денег: ${topSupplier}. По названию это банк/финансовая организация, поэтому без назначения платежа/договора не считаю это обычным поставщиком.${nonFinancialSupplier ? ` Крупнейший небанковский получатель исходящих денег: ${nonFinancialSupplier}.` : ""}`
|
||||
: `Крупнейший подтвержденный получатель исходящих денег: ${topSupplier}.`);
|
||||
lines.push(`Крупнейший подтвержденный получатель исходящих денег: ${topSupplier}.`);
|
||||
}
|
||||
if (!directMoneyAnswer && (topCustomer || topSupplier)) {
|
||||
lines.push(topCustomerLooksFinancial || topSupplierLooksFinancial
|
||||
? "Важно по ролям: текущий денежный срез подтверждает источники и получателей денег, но банковские контрагенты требуют проверки назначения платежа/счетов и не доказывают роль клиента или поставщика."
|
||||
: "Важно по ролям: текущий денежный срез подтверждает денежные источники и получателей, но не доказывает, что это главный клиент или главный поставщик как бизнес-роль.");
|
||||
lines.push("Важно по ролям: текущий денежный срез подтверждает денежные источники и получателей, но не доказывает, что это главный клиент или главный поставщик как бизнес-роль.");
|
||||
}
|
||||
if (!directMoneyAnswer) {
|
||||
lines.push(`Что подтверждено: денежный срез по компании${organizationScope ? ` ${organizationScope}` : ""}${period ? ` ${period}` : ""}${topCustomer ? ", крупнейший источник входящих денег" : ""}${topSupplier ? ", крупнейший получатель исходящих денег" : ""}.`);
|
||||
|
|
@ -1031,7 +709,8 @@ function buildCompactBusinessOverviewReply(entryPoint, draft) {
|
|||
lines.push(limitLine);
|
||||
}
|
||||
lines.push("Для ответа именно про чистую прибыль нужно отдельно считать себестоимость, расходы и закрытие периода.");
|
||||
return joinBusinessReplyLines(lines);
|
||||
const reply = lines.join("\n").trim();
|
||||
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
|
||||
}
|
||||
function statusFrom(entryPoint) {
|
||||
if (!entryPoint || entryPoint.entry_status === "skipped_not_applicable") {
|
||||
|
|
|
|||
|
|
@ -167,21 +167,6 @@ 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 ?? ""));
|
||||
}
|
||||
|
|
@ -195,9 +180,6 @@ function hasMetadataDiscoveryPriority(input, entryPoint) {
|
|||
if (!isMetadataDiscoveryTurn(entryPoint)) {
|
||||
return false;
|
||||
}
|
||||
if (!isExplicitMetadataDiscoveryTurn(entryPoint)) {
|
||||
return false;
|
||||
}
|
||||
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
|
||||
return !isInventoryExactAddressIntent(detectedIntent);
|
||||
}
|
||||
|
|
@ -268,83 +250,6 @@ 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 hasExactBankOperationsAddressReply(input, entryPoint) {
|
||||
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
|
||||
return false;
|
||||
}
|
||||
if (!hasEffectivelyFactualAddressReply(input)) {
|
||||
return false;
|
||||
}
|
||||
const source = String(input.currentReplySource ?? input.livingChatSource ?? "").trim().toLowerCase();
|
||||
if (source !== "address_query_runtime_v1" && source !== "address_exact" && source !== "address_lane") {
|
||||
return false;
|
||||
}
|
||||
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
|
||||
const selectedRecipe = toNonEmptyString(input.addressRuntimeMeta?.selected_recipe);
|
||||
const isBankIntent = detectedIntent === "bank_operations_by_counterparty" || detectedIntent === "bank_operations_by_contract";
|
||||
const isBankRecipe = selectedRecipe === "address_bank_operations_by_counterparty_v1" ||
|
||||
selectedRecipe === "address_bank_operations_by_contract_v1";
|
||||
if (!isBankIntent || !isBankRecipe) {
|
||||
return false;
|
||||
}
|
||||
const grounding = toRecordObject(input.addressRuntimeMeta?.answer_grounding_check);
|
||||
const groundingStatus = toNonEmptyString(grounding?.status);
|
||||
const mcpCallStatus = toNonEmptyString(input.addressRuntimeMeta?.mcp_call_status);
|
||||
const routeMode = toNonEmptyString(input.addressRuntimeMeta?.capability_route_mode);
|
||||
return Boolean(mcpCallStatus === "matched_non_empty" ||
|
||||
groundingStatus === "grounded" ||
|
||||
routeMode === "exact" ||
|
||||
hasFullConfirmedTruth(input));
|
||||
}
|
||||
function hasValueFlowActionConflictWithDiscoveryTurnMeaning(input, entryPoint) {
|
||||
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
|
||||
return false;
|
||||
|
|
@ -358,9 +263,6 @@ function hasValueFlowActionConflictWithDiscoveryTurnMeaning(input, entryPoint) {
|
|||
if (askedDomain !== "counterparty_value") {
|
||||
return false;
|
||||
}
|
||||
if (hasExactBankOperationsAddressReply(input, entryPoint)) {
|
||||
return false;
|
||||
}
|
||||
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
|
||||
if (askedAction === "payout") {
|
||||
return detectedIntent !== "supplier_payouts_profile";
|
||||
|
|
@ -415,7 +317,16 @@ function hasExactMatchedFactualAddressReply(input, entryPoint) {
|
|||
return false;
|
||||
}
|
||||
}
|
||||
return 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 hasOpenScopeValueFlowDiscoveryPriority(input, entryPoint) {
|
||||
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
|
||||
|
|
@ -501,9 +412,6 @@ function hasSemanticConflictWithDiscoveryTurnMeaning(input, entryPoint) {
|
|||
if (hasRuntimeMatchedExactReply(input, entryPoint)) {
|
||||
return false;
|
||||
}
|
||||
if (hasExactBankOperationsAddressReply(input, entryPoint)) {
|
||||
return false;
|
||||
}
|
||||
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
|
||||
const turnMeaning = readDiscoveryTurnMeaning(entryPoint);
|
||||
const askedDomain = toNonEmptyString(turnMeaning?.asked_domain_family);
|
||||
|
|
@ -598,9 +506,6 @@ 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 exactBankOperationsAddressReply = hasExactBankOperationsAddressReply(input, entryPoint);
|
||||
const openScopeValueFlowDiscoveryPriority = hasOpenScopeValueFlowDiscoveryPriority(input, entryPoint);
|
||||
const metadataDiscoveryPriority = hasMetadataDiscoveryPriority(input, entryPoint);
|
||||
const valueFlowActionConflictWithDiscoveryTurnMeaning = hasValueFlowActionConflictWithDiscoveryTurnMeaning(input, entryPoint);
|
||||
|
|
@ -656,15 +561,6 @@ 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 (exactBankOperationsAddressReply) {
|
||||
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_exact_bank_operations_address_reply");
|
||||
}
|
||||
if (deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") {
|
||||
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_broad_business_summary_over_clarification_candidate");
|
||||
}
|
||||
|
|
@ -689,9 +585,6 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
|
|||
!exactMatchedFactualAddressReply &&
|
||||
!runtimeAdjustedExactReply &&
|
||||
!runtimeMatchedExactReply &&
|
||||
!staleMetadataDiscoveryFallbackAgainstExactAddressReply &&
|
||||
!exactValueFlowReplyForBusinessOverviewDirectMoneyNeed &&
|
||||
!exactBankOperationsAddressReply &&
|
||||
!(deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") &&
|
||||
ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) &&
|
||||
candidate.eligible_for_future_hot_runtime &&
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.ASSISTANT_MCP_ROUTE_CANDIDATE_SCHEMA_VERSION = exports.ASSISTANT_MCP_DISCOVERY_LOOP_STATE_SCHEMA_VERSION = exports.ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION = void 0;
|
||||
exports.ASSISTANT_MCP_DISCOVERY_LOOP_STATE_SCHEMA_VERSION = exports.ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION = void 0;
|
||||
exports.runAssistantMcpDiscoveryRuntimeBridge = runAssistantMcpDiscoveryRuntimeBridge;
|
||||
const assistantMcpDiscoveryAnswerAdapter_1 = require("./assistantMcpDiscoveryAnswerAdapter");
|
||||
const assistantMcpDiscoveryPilotExecutor_1 = require("./assistantMcpDiscoveryPilotExecutor");
|
||||
const assistantMcpDiscoveryPlanner_1 = require("./assistantMcpDiscoveryPlanner");
|
||||
exports.ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION = "assistant_mcp_discovery_runtime_bridge_v1";
|
||||
exports.ASSISTANT_MCP_DISCOVERY_LOOP_STATE_SCHEMA_VERSION = "assistant_mcp_discovery_loop_state_v1";
|
||||
exports.ASSISTANT_MCP_ROUTE_CANDIDATE_SCHEMA_VERSION = "assistant_mcp_route_candidate_v1";
|
||||
function normalizeReasonCode(value) {
|
||||
const normalized = value
|
||||
.trim()
|
||||
|
|
@ -59,21 +58,6 @@ function loopStatusFor(bridgeStatus) {
|
|||
}
|
||||
return "ready_for_next_hop";
|
||||
}
|
||||
function routeCandidateStatusFor(bridgeStatus, pilot, missingProofFamily) {
|
||||
if (bridgeStatus === "blocked" || pilot.pilot_status === "blocked") {
|
||||
return "blocked";
|
||||
}
|
||||
if (bridgeStatus === "needs_clarification" || pilot.pilot_status === "skipped_needs_clarification") {
|
||||
return "needs_user_scope";
|
||||
}
|
||||
if (bridgeStatus === "unsupported" || pilot.pilot_status === "unsupported") {
|
||||
return "needs_route_enablement";
|
||||
}
|
||||
if (missingProofFamily) {
|
||||
return "needs_route_enablement";
|
||||
}
|
||||
return "ready_for_reviewed_execution";
|
||||
}
|
||||
function flattenAxes(pilot, source) {
|
||||
const result = [];
|
||||
for (const step of pilot.dry_run.execution_steps) {
|
||||
|
|
@ -99,119 +83,6 @@ function entityCandidatesFromPlanner(planner) {
|
|||
const values = planner.discovery_plan.turn_meaning_ref?.explicit_entity_candidates ?? [];
|
||||
return uniqueStrings(values);
|
||||
}
|
||||
function firstNonEmpty(values) {
|
||||
for (const value of values) {
|
||||
const text = String(value ?? "").trim();
|
||||
if (text) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function routeCandidateProofFamiliesFor(actionFamily, proofExpectation) {
|
||||
const combined = `${actionFamily ?? ""} ${proofExpectation ?? ""}`.trim().toLowerCase();
|
||||
const result = [];
|
||||
const add = (family) => {
|
||||
if (!result.includes(family)) {
|
||||
result.push(family);
|
||||
}
|
||||
};
|
||||
if (!combined || combined === "broad_evaluation bounded_inference") {
|
||||
return result;
|
||||
}
|
||||
if (/(?:inventory|stock|warehouse|reserve|liquidation|write[-_ ]?off|obsolete|obsolescence)/iu.test(combined)) {
|
||||
add("inventory_reserve_liquidation_quality");
|
||||
}
|
||||
if (/(?:debt|due[-_ ]?date|overdue|aging|credit[-_ ]?risk)/iu.test(combined)) {
|
||||
add("debt_due_date_aging_quality");
|
||||
}
|
||||
if (/(?:vendor|supplier|procurement|sourcing)/iu.test(combined)) {
|
||||
add("vendor_risk_procurement_quality");
|
||||
}
|
||||
if (/(?:profit|margin|pnl|p&l|financial[-_ ]?result)/iu.test(combined)) {
|
||||
add("accounting_profit_margin");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function routeCandidateMissingProofFamily(planner, pilot) {
|
||||
if (planner.data_need_graph?.business_fact_family !== "business_overview") {
|
||||
return null;
|
||||
}
|
||||
const wantedFamilies = routeCandidateProofFamiliesFor(planner.data_need_graph?.action_family ?? null, planner.data_need_graph?.proof_expectation ?? null);
|
||||
if (wantedFamilies.length <= 0) {
|
||||
return null;
|
||||
}
|
||||
const missingProofFamilies = pilot.derived_business_overview?.missing_proof_families ?? [];
|
||||
return missingProofFamilies.find((item) => wantedFamilies.includes(item.family)) ?? null;
|
||||
}
|
||||
function routeCandidateEnablementReason(status, pilot, missingAxes, missingProofFamily) {
|
||||
if (status === "ready_for_reviewed_execution") {
|
||||
return null;
|
||||
}
|
||||
if (status === "needs_user_scope") {
|
||||
return missingAxes.length > 0
|
||||
? `Missing scope axes: ${missingAxes.join(", ")}`
|
||||
: "Selected chain needs user clarification before MCP execution";
|
||||
}
|
||||
if (missingProofFamily) {
|
||||
return [
|
||||
`Missing reviewed proof family: ${missingProofFamily.family}`,
|
||||
`next_required_evidence=${missingProofFamily.next_required_evidence}`,
|
||||
missingProofFamily.current_supported_evidence
|
||||
? `current_supported_evidence=${missingProofFamily.current_supported_evidence}`
|
||||
: null,
|
||||
`must_not_claim=${missingProofFamily.must_not_claim}`
|
||||
]
|
||||
.filter((item) => Boolean(item))
|
||||
.join("; ");
|
||||
}
|
||||
return firstNonEmpty([
|
||||
...pilot.query_limitations,
|
||||
...pilot.evidence.unknown_facts,
|
||||
"Selected chain is not safely executable by the reviewed MCP runtime yet"
|
||||
]);
|
||||
}
|
||||
function routeCandidateNextAction(status) {
|
||||
if (status === "ready_for_reviewed_execution") {
|
||||
return "Execute through the reviewed runtime bridge and truth gate.";
|
||||
}
|
||||
if (status === "needs_user_scope") {
|
||||
return "Ask the user for the missing scope axes before MCP execution.";
|
||||
}
|
||||
if (status === "needs_route_enablement") {
|
||||
return "Create or wire a reviewed exact route for the selected chain before treating the fact as answerable.";
|
||||
}
|
||||
return "Do not execute until the blocking reason is resolved.";
|
||||
}
|
||||
function buildRouteCandidate(planner, pilot, bridgeStatus) {
|
||||
const plannerClarificationGaps = planner.discovery_plan.clarification_gaps ?? [];
|
||||
const providedAxes = flattenAxes(pilot, "provided_axes");
|
||||
const missingAxes = plannerClarificationGaps.length > 0 ? plannerClarificationGaps : flattenAxes(pilot, "missing_axis_options");
|
||||
const missingProofFamily = routeCandidateMissingProofFamily(planner, pilot);
|
||||
const candidateStatus = routeCandidateStatusFor(bridgeStatus, pilot, missingProofFamily);
|
||||
return {
|
||||
schema_version: exports.ASSISTANT_MCP_ROUTE_CANDIDATE_SCHEMA_VERSION,
|
||||
policy_owner: "assistantMcpDiscoveryRuntimeBridge",
|
||||
candidate_status: candidateStatus,
|
||||
selected_chain_id: planner.selected_chain_id,
|
||||
selected_chain_summary: planner.selected_chain_summary,
|
||||
nearest_catalog_chain_template: planner.catalog_chain_template_alignment.top_chain_template_match,
|
||||
catalog_alignment_status: planner.catalog_chain_template_alignment.alignment_status,
|
||||
business_fact_family: planner.data_need_graph?.business_fact_family ?? null,
|
||||
action_family: planner.data_need_graph?.action_family ?? null,
|
||||
proof_expectation: planner.data_need_graph?.proof_expectation ?? null,
|
||||
required_axes: [...planner.required_axes],
|
||||
provided_axes: providedAxes,
|
||||
missing_axes: missingAxes,
|
||||
executable_now: candidateStatus === "ready_for_reviewed_execution",
|
||||
enablement_reason: routeCandidateEnablementReason(candidateStatus, pilot, missingAxes, missingProofFamily),
|
||||
recommended_next_action: routeCandidateNextAction(candidateStatus),
|
||||
forbidden_overclaim_flags: uniqueStrings([
|
||||
...(planner.data_need_graph?.forbidden_overclaim_flags ?? []),
|
||||
...(missingProofFamily ? [missingProofFamily.must_not_claim] : [])
|
||||
])
|
||||
};
|
||||
}
|
||||
function buildLoopState(planner, pilot, bridgeStatus) {
|
||||
const plannerClarificationGaps = planner.discovery_plan.clarification_gaps ?? [];
|
||||
return {
|
||||
|
|
@ -249,13 +120,10 @@ async function runAssistantMcpDiscoveryRuntimeBridge(input) {
|
|||
const answerDraft = (0, assistantMcpDiscoveryAnswerAdapter_1.buildAssistantMcpDiscoveryAnswerDraft)(pilot);
|
||||
const bridgeStatus = bridgeStatusFor(pilot, answerDraft);
|
||||
const loopState = buildLoopState(planner, pilot, bridgeStatus);
|
||||
const routeCandidate = buildRouteCandidate(planner, pilot, bridgeStatus);
|
||||
const reasonCodes = uniqueStrings([...planner.reason_codes, ...pilot.reason_codes, ...answerDraft.reason_codes]);
|
||||
pushReason(reasonCodes, `runtime_bridge_status_${bridgeStatus}`);
|
||||
pushReason(reasonCodes, "runtime_bridge_not_wired_to_hot_assistant_answer");
|
||||
pushReason(reasonCodes, `runtime_bridge_loop_state_${loopState.loop_status}`);
|
||||
pushReason(reasonCodes, "runtime_bridge_route_candidate_built");
|
||||
pushReason(reasonCodes, `runtime_bridge_route_candidate_${routeCandidate.candidate_status}`);
|
||||
return {
|
||||
schema_version: exports.ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION,
|
||||
policy_owner: "assistantMcpDiscoveryRuntimeBridge",
|
||||
|
|
@ -265,7 +133,6 @@ async function runAssistantMcpDiscoveryRuntimeBridge(input) {
|
|||
pilot,
|
||||
answer_draft: answerDraft,
|
||||
loop_state: loopState,
|
||||
route_candidate: routeCandidate,
|
||||
user_facing_response_allowed: bridgeStatus !== "blocked",
|
||||
business_fact_answer_allowed: businessFactAnswerAllowed(answerDraft),
|
||||
requires_user_clarification: bridgeStatus === "needs_clarification",
|
||||
|
|
|
|||
|
|
@ -130,11 +130,6 @@ function isGarbageSemanticAnchorCandidate(value) {
|
|||
"всему",
|
||||
"всей",
|
||||
"всем",
|
||||
"год",
|
||||
"года",
|
||||
"году",
|
||||
"годом",
|
||||
"годы",
|
||||
"выводу",
|
||||
"выводам",
|
||||
"аудиту",
|
||||
|
|
@ -168,7 +163,6 @@ function isGarbageSemanticAnchorCandidate(value) {
|
|||
}
|
||||
if (/^(?:по\s+)?(?:этим|этими)\s+данн\p{L}*$/iu.test(text) ||
|
||||
/^(?:и\s+)?кто\s+(?:главн\p{L}*|основн\p{L}*|крупн\p{L}*)\s+(?:клиент|покупател|поставщик|контрагент)(?:\s+в)?$/iu.test(text) ||
|
||||
/^(?:или\s+)?(?:обычн\p{L}*\s+)?(?:клиент|поставщик|покупател\p{L}*|заказчик|контрагент)(?:\s+или\s+(?:клиент|поставщик|покупател\p{L}*|заказчик|контрагент))?$/iu.test(text) ||
|
||||
/^(?:что|чего)\s+(?:подтвержден\p{L}*|не\s+хватает)/iu.test(text) ||
|
||||
/^(?:можно\s+ли|если\s+нет|дай\s+proxy|дай\s+прокси)/iu.test(text)) {
|
||||
return true;
|
||||
|
|
@ -632,30 +626,6 @@ function hasOrganizationLevelEarningsOverviewSignal(text) {
|
|||
/(?:\u043a\u0430\u043a\w*|\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u0441\u043a\u043e\u043a\w*|\u043f\u043e\u043a\u0430\u0436|\u0434\u0430\u0439|\u0443\s+\u043d\u0430\u0441|\u043d\u0430\u0448\w*|\u043c\u044b\b|\u043a\u043e\u043c\u043f\u0430\u043d|\u0431\u0438\u0437\u043d\u0435\u0441|\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446|\u0432\s+\u0446\u0435\u043b\u043e\u043c|\u0432\u043e\u043e\u0431\u0449\u0435|(?:19|20)\d{2}|all\s+time|what|which|how\s+much|show|give|company|business|organization|our|we|us)/iu.test(text);
|
||||
return hasYearRankingCue || hasCompanyEarningsCue || hasCompanyProfitMarginCue;
|
||||
}
|
||||
function hasOrganizationLevelProfitMarginBoundaryOverviewSignal(text) {
|
||||
if (!text) {
|
||||
return false;
|
||||
}
|
||||
const hasProfitMarginCue = /(?:\u043f\u0440\u0438\u0431\u044b\u043b\w*|\u043c\u0430\u0440\u0436\w*|\u0440\u0435\u043d\u0442\u0430\u0431\w*|\u0444\u0438\u043d(?:\u0430\u043d\u0441\w*)?\s*[- ]?\s*\u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442|p\s*&\s*l|profit(?:ability)?|margin|financial\s+result)/iu.test(text);
|
||||
const hasCompanyScopeCue = /(?:\u0443\s+\u043d\u0430\u0441|\u043d\u0430\u0448\w*|\u043c\u044b\b|\u043f\u043e\s+\u043a\u043e\u043c\u043f\u0430\u043d|\u043a\u043e\u043c\u043f\u0430\u043d|\u0431\u0438\u0437\u043d\u0435\u0441|\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446|\u0432\s+\u0446\u0435\u043b\u043e\u043c|\b(?:\u043e\u043e\u043e|\u0438\u043f|\u0430\u043e|\u043f\u0430\u043e|\u0437\u0430\u043e|\u043e\u0430\u043e)\b|(?:19|20)\d{2}|company|business|organization|our|we|us)/iu.test(text);
|
||||
return hasProfitMarginCue && hasCompanyScopeCue;
|
||||
}
|
||||
function hasProfitMarginBoundaryFollowupSignal(text) {
|
||||
if (!text) {
|
||||
return false;
|
||||
}
|
||||
const hasProfitOrResultCue = /(?:\u043f\u0440\u0438\u0431\u044b\u043b\w*|\u0443\u0431\u044b\u0442\w*|\u043c\u0430\u0440\u0436\w*|\u0440\u0435\u043d\u0442\u0430\u0431\w*|\u0444\u0438\u043d(?:\u0430\u043d\u0441\w*)?\s*[- ]?\s*\u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442|p\s*&\s*l|profit|loss|margin|financial\s+result)/iu.test(text);
|
||||
const hasFollowupShape = /(?:\u044d\u0442\u043e|\u0438\u0442\u043e\u0433|\u0438\u0442\u043e\u0433\u043e|\u043f\u043e\u043b\u0443\u0447\w*|\u043a\u043e\u0440\u043e\u0442\w*|\u0432\s+\u0438\u0442\u043e\u0433\u0435|\u043c\u043e\u0436\u043d\u043e\s+(?:\u043b\u0438\s+)?\u0441\u043a\u0430\u0437\u0430\u0442\u044c|is\s+it|result|short|brief)/iu.test(text);
|
||||
return hasProfitOrResultCue && hasFollowupShape;
|
||||
}
|
||||
function hasDebtDueDateBoundaryFollowupSignal(text) {
|
||||
if (!text) {
|
||||
return false;
|
||||
}
|
||||
const hasDueDateCue = /(?:\u043f\u0440\u043e\u0441\u0440\u043e\u0447\w*|\u0441\u0440\u043e\u043a\w*\s+\u043e\u043f\u043b\u0430\u0442|\u0434\u043e\u043a\u0430\u0437\w*|\u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\w*|due[-\s]?date|overdue|debt\s+aging|aging)/iu.test(text);
|
||||
const hasFollowupShape = /(?:\u0442\u043e\s+\u0435\u0441\u0442\u044c|\u043f\u043e\u0447\u0435\u043c\u0443|\u043a\u043e\u0440\u043e\u0442\w*|\u043d\u0435\u043b\u044c\u0437\u044f|\u043c\u043e\u0436\u043d\u043e\s+\u043b\u0438|\u0437\u043d\u0430\u0447\u0438\u0442|\u0432\u044b\u0445\u043e\u0434\u0438\u0442|why|short|brief|so)/iu.test(text);
|
||||
return hasDueDateCue && hasFollowupShape;
|
||||
}
|
||||
function hasOrganizationLevelDebtDueDateOverviewSignal(text) {
|
||||
if (!text) {
|
||||
return false;
|
||||
|
|
@ -706,18 +676,12 @@ function hasCrossScopeExecutiveSummarySignal(text) {
|
|||
/(?:\u043e\u0442\u0434\u0435\u043b\u044c\u043d\p{L}*\s+\u043f\u043e|\u043f\u043e\s+\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\p{L}*|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\p{L}*|\u0433\u0440\u0443\u043f\u043f\p{L}*\s+\u0441\u0432\u043a|\u0441\u0432\u043a|counterpart(?:y|ies)?)/iu.test(text) &&
|
||||
/(?:\u0447\u0442\u043e\s+\u043c\u043e\u0436\u043d\p{L}*|\u0447\u0442\u043e\s+\u043d\u0435\u043b\u044c\u0437\p{L}*|\u0432\u044b\u0432\u043e\u0434\p{L}*|allowed|forbidden|cannot|can\s+say)/iu.test(text));
|
||||
}
|
||||
function hasPlainBusinessOverviewSignal(text) {
|
||||
const hasPlainOverviewCue = /(?:\u0432\u0437\u0440\u043e\u0441\u043b\p{L}*[\s\S]{0,40}\u043e\u0431\u0437\u043e\u0440|\u043a\u0440\u0430\u0442\u043a\p{L}*\s+\u043e\u0431\u0437\u043e\u0440|\u043e\u0431\u0437\u043e\u0440[\s\S]{0,100}(?:\u0432\u0445\u043e\u0434\u044f\u0449|\u0438\u0441\u0445\u043e\u0434\u044f\u0449|\u043d\u0435\u0442\u0442\u043e|incoming|outgoing|net))/iu.test(text);
|
||||
const hasCompanyOrOperatingScopeCue = /(?:\u043e\u043e\u043e|\u0438\u043f|\u0430\u043e|\u043f\u0430\u043e|\u043a\u043e\u043c\u043f\u0430\u043d|\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446|\u0431\u0438\u0437\u043d\u0435\u0441|\u0443\s+\u043d\u0430\u0441|\u043d\u0430\u0448\p{L}*|(?:19|20)\d{2}|company|organization|business)/iu.test(text);
|
||||
return hasPlainOverviewCue && hasCompanyOrOperatingScopeCue;
|
||||
}
|
||||
function hasBusinessOverviewSignal(text) {
|
||||
if (hasCrossScopeExecutiveSummarySignal(text) ||
|
||||
hasOrganizationLevelEarningsOverviewSignal(text) ||
|
||||
hasOrganizationLevelDebtPositionOverviewSignal(text) ||
|
||||
hasOrganizationLevelDebtDueDateOverviewSignal(text) ||
|
||||
hasOrganizationLevelInventoryReserveLiquidationOverviewSignal(text) ||
|
||||
hasPlainBusinessOverviewSignal(text) ||
|
||||
hasOrganizationLevelSupplierQualityOverviewSignal(text)) {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -747,12 +711,6 @@ function hasExplicitVatQuestionSignal(text) {
|
|||
return (/(?:\u043d\u0434\u0441|vat)/iu.test(text) &&
|
||||
/(?:\u0437\u0430|\u043d\u0430|\u043f\u0435\u0440\u0438\u043e\u0434|\u043f\u043e\u0437\u0438\u0446|\u043a\s+\u0443\u043f\u043b\u0430\u0442|\u043a\s+\u0432\u043e\u0437\u043c\u0435\u0449|\u043e\u0441\u043d\u043e\u0432\u0430\u043d|\u043d\u0430\u043b\u043e\u0433\u043e\u0432\p{L}*\s+\u0432\u044b\u0432\u043e\u0434|tax\s+period|tax\s+position)/iu.test(text));
|
||||
}
|
||||
function hasExplicitVatMovementEvidenceSignal(text) {
|
||||
if (!/(?:\u043d\u0434\u0441|vat)/iu.test(text)) {
|
||||
return false;
|
||||
}
|
||||
return hasMovementEvidenceFollowupSignal(text);
|
||||
}
|
||||
function hasBusinessOverviewSeparateCounterpartySignal(text) {
|
||||
if (!text) {
|
||||
return false;
|
||||
|
|
@ -900,7 +858,6 @@ function rawEntityResolutionCandidate(text) {
|
|||
function rawScopedEntityCandidateFromText(text) {
|
||||
const source = (0, addressTextRepair_1.repairAddressMojibakeText)(String(text ?? ""));
|
||||
const patterns = [
|
||||
/(?:^|[\s,.;:!?])(?:по|у|для|for|by)\s+(.+?)(?=$|[,.;:!?]|\s+(?:за|на|в|во|к|по|сколько|скок|как|какое|какой|какая|какие|получ\p{L}*|заплат\p{L}*|нетто|документ\p{L}*|движени\p{L}*|операц\p{L}*|плат[её]ж\p{L}*)(?=$|[\s,.;:!?]))/iu,
|
||||
/(?:^|[\s,.;:!?])(?:по|у|для|for|by)\s+([\p{L}\d._-]{2,})(?=$|[\s,.;:!?])/iu,
|
||||
/(?:документ(?:ам|ы)?|движени(?:ям|я)?|операци(?:ям|и)?|плат[её]ж(?:ам|и)?)\s+([\p{L}\d._-]{2,})(?=$|[\s,.;:!?])/iu
|
||||
];
|
||||
|
|
@ -1145,7 +1102,6 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
const rawReferentialDocumentExclusionSignal = hasReferentialDocumentExclusionFollowupSignal(repairedUserText ?? rawUserText ?? "");
|
||||
const rawPrimaryBusinessOverviewSignal = hasBusinessOverviewSignal(rawText);
|
||||
const explicitVatQuestionSignal = hasExplicitVatQuestionSignal(rawText);
|
||||
const explicitVatMovementEvidenceSignal = hasExplicitVatMovementEvidenceSignal(rawText);
|
||||
const explicitVatSuppressesBusinessOverviewContinuation = Boolean(explicitVatQuestionSignal && !rawPrimaryBusinessOverviewSignal);
|
||||
const businessOverviewContinuationSignal = hasBusinessOverviewFollowupSeed(followupSeed) &&
|
||||
hasBusinessOverviewContinuationSignal(rawText) &&
|
||||
|
|
@ -1158,7 +1114,6 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
(hasValueFlowSignal(rawText) || hasValueRankingSignal(rawText) || rawBidirectionalValueFlowSignal);
|
||||
const rawMetadataSignal = !rawLifecycleSignal &&
|
||||
!rawValueFlowSignal &&
|
||||
!explicitVatMovementEvidenceSignal &&
|
||||
!rawReferentialDocumentExclusionSignal &&
|
||||
hasMetadataSignal(rawText);
|
||||
const rawEntityResolutionSignal = !rawLifecycleSignal && !rawValueFlowSignal && !rawMetadataSignal && hasEntityResolutionSignal(rawText);
|
||||
|
|
@ -1173,7 +1128,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
const explicitDateScopeLiteralDetected = hasExplicitDateScopeLiteral(dateScopeSignalText);
|
||||
const relativeCurrentDateHintDetected = hasRelativeCurrentDateHint(rawText);
|
||||
const rawDateScope = collectDateScopeFromRawText(dateScopeSignalText);
|
||||
const rawMetadataScopeHint = rawMetadataSignal || explicitVatMovementEvidenceSignal ? metadataScopeHintFromRawText(rawText) : null;
|
||||
const rawMetadataScopeHint = rawMetadataSignal ? metadataScopeHintFromRawText(rawText) : null;
|
||||
const rawTopicSwitchSignal = hasExplicitTopicSwitchSignal(rawText);
|
||||
const rawEntityCandidate = rawEntityResolutionSignal ? rawEntityResolutionCandidate(rawEntitySourceText) : null;
|
||||
const rawScopedEntityCandidate = !predecomposeEntities.counterparty &&
|
||||
|
|
@ -1192,41 +1147,11 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
const rawAggregationAxis = toNonEmptyString(assistantTurnMeaning?.asked_aggregation_axis);
|
||||
const unsupported = toNonEmptyString(assistantTurnMeaning?.unsupported_but_understood_family);
|
||||
const broadBusinessEvaluationUnsupported = unsupported === "broad_business_evaluation";
|
||||
const seededBusinessOverviewSignal = broadBusinessEvaluationUnsupported ||
|
||||
const businessOverviewSignal = rawBusinessOverviewSignal ||
|
||||
broadBusinessEvaluationUnsupported ||
|
||||
rawDomain === "business_summary" ||
|
||||
rawDomain === "business_overview" ||
|
||||
rawAction === "broad_evaluation";
|
||||
const inventoryReserveBusinessOverviewSignal = (rawBusinessOverviewSignal || seededBusinessOverviewSignal) &&
|
||||
hasOrganizationLevelInventoryReserveLiquidationOverviewSignal(rawText);
|
||||
const debtDueDateFollowupBusinessOverviewSignal = businessOverviewContinuationSignal && hasDebtDueDateBoundaryFollowupSignal(rawText);
|
||||
const debtDueDateBusinessOverviewSignal = ((rawBusinessOverviewSignal || seededBusinessOverviewSignal) &&
|
||||
hasOrganizationLevelDebtDueDateOverviewSignal(rawText)) ||
|
||||
debtDueDateFollowupBusinessOverviewSignal;
|
||||
const supplierQualityBusinessOverviewSignal = (rawBusinessOverviewSignal || seededBusinessOverviewSignal) && hasOrganizationLevelSupplierQualityOverviewSignal(rawText);
|
||||
const profitMarginFollowupBusinessOverviewSignal = businessOverviewContinuationSignal && hasProfitMarginBoundaryFollowupSignal(rawText);
|
||||
const profitMarginBusinessOverviewSignal = ((rawBusinessOverviewSignal || seededBusinessOverviewSignal) &&
|
||||
hasOrganizationLevelProfitMarginBoundaryOverviewSignal(rawText)) ||
|
||||
profitMarginFollowupBusinessOverviewSignal;
|
||||
const businessOverviewActionFamily = inventoryReserveBusinessOverviewSignal
|
||||
? "inventory_reserve_boundary"
|
||||
: debtDueDateBusinessOverviewSignal
|
||||
? "debt_due_date_boundary"
|
||||
: supplierQualityBusinessOverviewSignal
|
||||
? "vendor_risk_procurement_boundary"
|
||||
: profitMarginBusinessOverviewSignal
|
||||
? "profit_margin_boundary"
|
||||
: "broad_evaluation";
|
||||
const businessOverviewUnsupportedFamily = inventoryReserveBusinessOverviewSignal
|
||||
? "inventory_reserve_liquidation_boundary"
|
||||
: debtDueDateBusinessOverviewSignal
|
||||
? "debt_due_date_boundary"
|
||||
: supplierQualityBusinessOverviewSignal
|
||||
? "vendor_risk_procurement_boundary"
|
||||
: profitMarginBusinessOverviewSignal
|
||||
? "profit_margin_boundary"
|
||||
: "broad_business_evaluation";
|
||||
const businessOverviewSignal = rawBusinessOverviewSignal ||
|
||||
seededBusinessOverviewSignal;
|
||||
const businessOverviewSeparateCounterpartySignal = Boolean(businessOverviewSignal && hasBusinessOverviewSeparateCounterpartySignal(rawText));
|
||||
const businessOverviewSeparateCounterpartyCandidate = businessOverviewSeparateCounterpartySignal
|
||||
? businessOverviewSeparateCounterpartyCandidateFromText(rawText)
|
||||
|
|
@ -1251,7 +1176,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
: rawAssistantTurnMeaningOrganizationScope;
|
||||
const rawOrganizationMentionSignal = hasOrganizationScopeSignalUtf8(rawText);
|
||||
const rawOrganizationScope = extractOrganizationScopeFromRawText(rawUserText ?? rawEffectiveText ?? rawSignalSourceText);
|
||||
const currentTurnFreshOrganizationScope = predecomposeEntities.organization ?? rawOrganizationScope;
|
||||
const currentTurnFreshOrganizationScope = rawOrganizationScope ?? predecomposeEntities.organization;
|
||||
const currentTurnOrganizationScope = currentTurnFreshOrganizationScope ?? assistantTurnMeaningOrganizationScope;
|
||||
const followupCounterpartyIsMetadataOrganizationScope = Boolean(followupSeed.subjectResolutionOptional &&
|
||||
followupSeed.counterparty &&
|
||||
|
|
@ -1575,21 +1500,9 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
const semanticDataNeed = metadataAmbiguityLaneClarificationApplicable
|
||||
? "metadata lane clarification"
|
||||
: semanticNeedFor({
|
||||
domain: explicitVatMovementEvidenceSignal
|
||||
? "movements"
|
||||
: businessOverviewSignal
|
||||
? "business_overview"
|
||||
: rawDomain ?? seededDomain,
|
||||
action: explicitVatMovementEvidenceSignal
|
||||
? "list_movements"
|
||||
: businessOverviewSignal
|
||||
? businessOverviewActionFamily
|
||||
: rawAction ?? seededAction,
|
||||
unsupported: explicitVatMovementEvidenceSignal
|
||||
? "movement_evidence"
|
||||
: businessOverviewSignal
|
||||
? businessOverviewUnsupportedFamily
|
||||
: unsupported ?? seededUnsupported,
|
||||
domain: businessOverviewSignal ? "business_overview" : rawDomain ?? seededDomain,
|
||||
action: businessOverviewSignal ? "broad_evaluation" : rawAction ?? seededAction,
|
||||
unsupported: businessOverviewSignal ? "broad_business_evaluation" : unsupported ?? seededUnsupported,
|
||||
lifecycleSignal,
|
||||
valueFlowSignal,
|
||||
metadataSignal: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable,
|
||||
|
|
@ -1617,9 +1530,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
followupSeed.discoveryEntity ??
|
||||
followupSeed.metadataSelectedEntitySet ??
|
||||
null;
|
||||
const metadataScopedLaneWithoutSubject = Boolean((metadataGroundedMovementLaneApplicable ||
|
||||
metadataGroundedDocumentLaneApplicable ||
|
||||
explicitVatMovementEvidenceSignal) &&
|
||||
const metadataScopedLaneWithoutSubject = Boolean((metadataGroundedMovementLaneApplicable || metadataGroundedDocumentLaneApplicable) &&
|
||||
!effectiveFollowupCounterparty &&
|
||||
metadataLaneCarryoverAvailable);
|
||||
const groundedFollowupEntity = metadataScopedLaneWithoutSubject
|
||||
|
|
@ -1794,27 +1705,23 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
normalizedFollowupDateScope);
|
||||
const clarificationLoopSeedApplied = Boolean(followupSeed.loopStatus === "awaiting_clarification" && followupSeed.loopSelectedChainId);
|
||||
const turnMeaning = {
|
||||
raw_message: repairedUserText ?? rawUserText ?? null,
|
||||
effective_message: repairedEffectiveText ?? rawEffectiveText ?? null,
|
||||
asked_domain_family: businessOverviewSignal
|
||||
? "business_overview"
|
||||
: lifecycleSignal
|
||||
? "counterparty_lifecycle"
|
||||
: valueFlowSignal
|
||||
? "counterparty_value"
|
||||
: explicitVatMovementEvidenceSignal
|
||||
: metadataGroundedMovementLaneApplicable
|
||||
? "movements"
|
||||
: metadataGroundedMovementLaneApplicable
|
||||
? "movements"
|
||||
: metadataGroundedDocumentLaneApplicable
|
||||
? "documents"
|
||||
: entityResolutionSignal
|
||||
? "entity_resolution"
|
||||
: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
|
||||
? "metadata"
|
||||
: rawDomain ?? seededDomain,
|
||||
: metadataGroundedDocumentLaneApplicable
|
||||
? "documents"
|
||||
: entityResolutionSignal
|
||||
? "entity_resolution"
|
||||
: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
|
||||
? "metadata"
|
||||
: rawDomain ?? seededDomain,
|
||||
asked_action_family: businessOverviewSignal
|
||||
? businessOverviewActionFamily
|
||||
? "broad_evaluation"
|
||||
: lifecycleSignal
|
||||
? "activity_duration"
|
||||
: valueFlowSignal
|
||||
|
|
@ -1823,17 +1730,15 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
: payoutSignal
|
||||
? "payout"
|
||||
: rawAction ?? seededAction ?? "turnover"
|
||||
: explicitVatMovementEvidenceSignal
|
||||
: metadataGroundedMovementLaneApplicable
|
||||
? "list_movements"
|
||||
: metadataGroundedMovementLaneApplicable
|
||||
? "list_movements"
|
||||
: metadataGroundedDocumentLaneApplicable
|
||||
? "list_documents"
|
||||
: entityResolutionSignal
|
||||
? "search_business_entity"
|
||||
: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
|
||||
? metadataActionFromRawText(rawText) ?? seededAction
|
||||
: rawAction ?? seededAction,
|
||||
: metadataGroundedDocumentLaneApplicable
|
||||
? "list_documents"
|
||||
: entityResolutionSignal
|
||||
? "search_business_entity"
|
||||
: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
|
||||
? metadataActionFromRawText(rawText) ?? seededAction
|
||||
: rawAction ?? seededAction,
|
||||
asked_aggregation_axis: monthlyAggregationSignal ? "month" : rawAggregationAxis,
|
||||
seeded_ranking_need: valueFlowSignal && followupSeed.rankingNeed && !rawEntitySearchOverridesStaleScope
|
||||
? followupSeed.rankingNeed
|
||||
|
|
@ -1852,7 +1757,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
explicit_date_scope: explicitDateScope,
|
||||
subject_resolution_optional: metadataScopedLaneWithoutSubject || undefined,
|
||||
unsupported_but_understood_family: businessOverviewSignal
|
||||
? businessOverviewUnsupportedFamily
|
||||
? "broad_business_evaluation"
|
||||
: unsupported ??
|
||||
(lifecycleSignal
|
||||
? "counterparty_lifecycle"
|
||||
|
|
@ -1866,23 +1771,20 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
? "movement_evidence"
|
||||
: metadataGroundedDocumentLaneApplicable
|
||||
? "document_evidence"
|
||||
: explicitVatMovementEvidenceSignal
|
||||
? "movement_evidence"
|
||||
: metadataAmbiguityLaneClarificationApplicable
|
||||
? "metadata_lane_choice_clarification"
|
||||
: entityResolutionSignal
|
||||
? "entity_resolution"
|
||||
: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
|
||||
? "1c_metadata_surface"
|
||||
: followupDiscoverySeedApplicable
|
||||
? seededUnsupported
|
||||
: null),
|
||||
: metadataAmbiguityLaneClarificationApplicable
|
||||
? "metadata_lane_choice_clarification"
|
||||
: entityResolutionSignal
|
||||
? "entity_resolution"
|
||||
: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
|
||||
? "1c_metadata_surface"
|
||||
: followupDiscoverySeedApplicable
|
||||
? seededUnsupported
|
||||
: null),
|
||||
stale_replay_forbidden: Boolean(assistantTurnMeaning?.stale_replay_forbidden ||
|
||||
businessOverviewSignal ||
|
||||
unsupported ||
|
||||
lifecycleSignal ||
|
||||
valueFlowSignal ||
|
||||
explicitVatMovementEvidenceSignal ||
|
||||
metadataGroundedMovementLaneApplicable ||
|
||||
metadataGroundedDocumentLaneApplicable ||
|
||||
metadataAmbiguityLaneClarificationApplicable ||
|
||||
|
|
@ -1892,12 +1794,6 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
followupDiscoverySeedApplicable)
|
||||
};
|
||||
const cleanTurnMeaning = {};
|
||||
if (toNonEmptyString(turnMeaning.raw_message)) {
|
||||
cleanTurnMeaning.raw_message = turnMeaning.raw_message;
|
||||
}
|
||||
if (toNonEmptyString(turnMeaning.effective_message)) {
|
||||
cleanTurnMeaning.effective_message = turnMeaning.effective_message;
|
||||
}
|
||||
if (toNonEmptyString(turnMeaning.asked_domain_family)) {
|
||||
cleanTurnMeaning.asked_domain_family = turnMeaning.asked_domain_family;
|
||||
}
|
||||
|
|
@ -1945,15 +1841,11 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
}
|
||||
const currentTurnValueFlowExactOverrideApplicable = Boolean(valueFlowSignal &&
|
||||
explicitIntentCandidate &&
|
||||
(rawValueFlowAggregateQuestionSignal || hasValueRankingSignal(rawText)) &&
|
||||
rawValueFlowAggregateQuestionSignal &&
|
||||
semanticDataNeed &&
|
||||
(entityCandidates.length > 0 || explicitOrganizationScope || openScopeValueFlowWithoutResolvedCounterparty));
|
||||
const runDiscovery = shouldRunDiscovery({
|
||||
unsupported: explicitVatMovementEvidenceSignal
|
||||
? "movement_evidence"
|
||||
: businessOverviewSignal
|
||||
? "broad_business_evaluation"
|
||||
: unsupported ?? seededUnsupported,
|
||||
unsupported: businessOverviewSignal ? "broad_business_evaluation" : unsupported ?? seededUnsupported,
|
||||
lifecycleSignal,
|
||||
valueFlowSignal,
|
||||
metadataSignal: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable,
|
||||
|
|
@ -1968,7 +1860,6 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
metadataGroundedDocumentLaneApplicable ||
|
||||
groundedValueFlowFollowupApplicable,
|
||||
forceDiscoveryOverExplicitIntent: businessOverviewSignal ||
|
||||
explicitVatMovementEvidenceSignal ||
|
||||
Boolean(entityResolutionClarificationCandidate) ||
|
||||
organizationClarificationFollowupApplicable ||
|
||||
periodClarificationFollowupApplicable ||
|
||||
|
|
@ -1992,19 +1883,17 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
? "followup_context"
|
||||
: metadataGroundedDocumentLaneApplicable
|
||||
? "followup_context"
|
||||
: explicitVatMovementEvidenceSignal
|
||||
? "raw_text"
|
||||
: predecomposeContract
|
||||
? "predecompose_contract"
|
||||
: lifecycleSignal
|
||||
: predecomposeContract
|
||||
? "predecompose_contract"
|
||||
: lifecycleSignal
|
||||
? "raw_text"
|
||||
: valueFlowSignal
|
||||
? "raw_text"
|
||||
: valueFlowSignal
|
||||
: entityResolutionSignal
|
||||
? "raw_text"
|
||||
: entityResolutionSignal
|
||||
: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
|
||||
? "raw_text"
|
||||
: rawMetadataSignal || effectiveMetadataFollowupSeedApplicable
|
||||
? "raw_text"
|
||||
: "none";
|
||||
: "none";
|
||||
if (lifecycleSignal) {
|
||||
pushReason(reasonCodes, "mcp_discovery_lifecycle_signal_detected");
|
||||
}
|
||||
|
|
@ -2014,9 +1903,6 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
if (rawMetadataSignal) {
|
||||
pushReason(reasonCodes, "mcp_discovery_metadata_signal_detected");
|
||||
}
|
||||
if (explicitVatMovementEvidenceSignal) {
|
||||
pushReason(reasonCodes, "mcp_discovery_vat_movement_evidence_signal_detected");
|
||||
}
|
||||
if (entityResolutionSignal) {
|
||||
pushReason(reasonCodes, "mcp_discovery_entity_resolution_signal_detected");
|
||||
}
|
||||
|
|
@ -2140,12 +2026,6 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
|
|||
if (businessOverviewContinuationSignal) {
|
||||
pushReason(reasonCodes, "mcp_discovery_business_overview_continuation_from_followup_context");
|
||||
}
|
||||
if (profitMarginFollowupBusinessOverviewSignal) {
|
||||
pushReason(reasonCodes, "mcp_discovery_business_overview_profit_margin_followup_boundary");
|
||||
}
|
||||
if (debtDueDateFollowupBusinessOverviewSignal) {
|
||||
pushReason(reasonCodes, "mcp_discovery_business_overview_debt_due_date_followup_boundary");
|
||||
}
|
||||
if (explicitVatSuppressesBusinessOverviewContinuation) {
|
||||
pushReason(reasonCodes, "mcp_discovery_business_overview_continuation_suppressed_by_explicit_vat_question");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -182,25 +182,18 @@ function scoreOrganizationMentionInMessage(message, organization) {
|
|||
variantScore = Math.max(variantScore, variant.length * 5);
|
||||
continue;
|
||||
}
|
||||
let fuzzyTokenScore = 0;
|
||||
const fuzzyMatched = messageTokens.some((messageToken) => {
|
||||
if (messageToken === variant) {
|
||||
fuzzyTokenScore = Math.max(fuzzyTokenScore, variant.length * 5);
|
||||
return true;
|
||||
}
|
||||
if (messageToken.length >= 5 && variant.length >= 5) {
|
||||
const prefixMatched = messageToken.startsWith(variant) || variant.startsWith(messageToken);
|
||||
if (prefixMatched) {
|
||||
const prefixLength = Math.min(messageToken.length, variant.length);
|
||||
fuzzyTokenScore = Math.max(fuzzyTokenScore, variant.length * (prefixLength >= 7 ? 5 : 3));
|
||||
}
|
||||
return prefixMatched;
|
||||
return messageToken.startsWith(variant) || variant.startsWith(messageToken);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (fuzzyMatched) {
|
||||
matched = true;
|
||||
variantScore = Math.max(variantScore, Math.max(20, fuzzyTokenScore || variant.length * 3));
|
||||
variantScore = Math.max(variantScore, Math.max(20, variant.length * 3));
|
||||
}
|
||||
}
|
||||
if (matched) {
|
||||
|
|
|
|||
|
|
@ -313,13 +313,6 @@ function createAssistantRoutePolicy(deps) {
|
|||
const hasTemporalCue = /(?:на\s+эту\s+же\s+дат[ауеы]|на\s+тот\s+же\s+период|за\s+этот\s+же\s+период|за\s+этот\s+период|март|апрел|ма[йя]|июн|июл|август|сентябр|октябр|ноябр|декабр|\b(?:19|20)\d{2}\b)/iu.test(normalized);
|
||||
return hasRequestCue && hasTemporalCue;
|
||||
}
|
||||
function hasOrganizationClarificationTextCue(text) {
|
||||
const normalized = compactWhitespace(repairAddressMojibake(String(text ?? "")).toLowerCase());
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
return /(?<!\p{L})(?:\u043e\u043e\u043e|\u0438\u043f|\u0430\u043e|\u043f\u0430\u043e|\u0437\u0430\u043e)(?!\p{L})|(?:\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446|\u043a\u043e\u043c\u043f\u0430\u043d|llc|company|organization)/iu.test(normalized);
|
||||
}
|
||||
function resolveAssistantOrchestrationDecision(input) {
|
||||
const rawUserMessage = String(input?.rawUserMessage ?? input?.userMessage ?? "");
|
||||
const effectiveAddressUserMessage = String(input?.effectiveAddressUserMessage ?? rawUserMessage);
|
||||
|
|
@ -482,27 +475,6 @@ function createAssistantRoutePolicy(deps) {
|
|||
const followupPreviousFilters = followupContext?.previous_filters && typeof followupContext.previous_filters === "object"
|
||||
? followupContext.previous_filters
|
||||
: null;
|
||||
const followupLoopStatus = toNonEmptyString(followupContext?.previous_discovery_loop_status);
|
||||
const followupLoopSelectedChainId = toNonEmptyString(followupContext?.previous_discovery_loop_selected_chain_id);
|
||||
const followupLoopPendingAxes = Array.isArray(followupContext?.previous_discovery_loop_pending_axes)
|
||||
? followupContext.previous_discovery_loop_pending_axes.map((item) => toNonEmptyString(item)).filter(Boolean)
|
||||
: [];
|
||||
const currentTurnPredecomposeOrganization = toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.entities?.organization) ??
|
||||
(toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.semantics?.anchor_kind) === "organization"
|
||||
? toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.semantics?.anchor_value)
|
||||
: null);
|
||||
const routeCandidateOrganizationClarificationDetected = Boolean(followupContext &&
|
||||
followupLoopStatus === "awaiting_clarification" &&
|
||||
followupLoopSelectedChainId &&
|
||||
followupLoopPendingAxes.includes("organization") &&
|
||||
(currentTurnPredecomposeOrganization ||
|
||||
explicitOrganizationClarificationSelection ||
|
||||
[
|
||||
rawUserMessage,
|
||||
repairedRawUserMessage,
|
||||
effectiveAddressUserMessage,
|
||||
repairedEffectiveAddressUserMessage
|
||||
].some((message) => hasOrganizationClarificationTextCue(message))));
|
||||
const protectedInventoryShortFollowup = Boolean(followupContext &&
|
||||
(isInventorySelectedObjectIntent(followupPreviousIntent) ||
|
||||
(followupPreviousIntent === "inventory_on_hand_as_of_date" &&
|
||||
|
|
@ -552,7 +524,6 @@ function createAssistantRoutePolicy(deps) {
|
|||
"net_value_flow"
|
||||
].includes(String(toNonEmptyString(assistantTurnMeaning?.asked_action_family) ?? "")) ||
|
||||
/(?:нетто|сальдо|сколько\s+мы\s+(?:получили|заплатили)|incoming|outgoing)/iu.test(analyticsSample)));
|
||||
const effectiveGroundedValueFlowFollowupContextDetected = groundedValueFlowFollowupContextDetected || routeCandidateOrganizationClarificationDetected;
|
||||
const baseToolGatePreservesAddressLane = Boolean(baseToolGate?.runAddressLane &&
|
||||
[
|
||||
"address_intent_resolver_detected",
|
||||
|
|
@ -562,15 +533,14 @@ function createAssistantRoutePolicy(deps) {
|
|||
].includes(String(baseToolGate?.reason ?? ""))) ||
|
||||
Boolean(baseToolGate?.runAddressLane &&
|
||||
String(baseToolGate?.reason ?? "") === "followup_context_detected" &&
|
||||
effectiveGroundedValueFlowFollowupContextDetected);
|
||||
groundedValueFlowFollowupContextDetected);
|
||||
const nonDomainQueryIndexed = Boolean(!llmFirstAddressCandidate &&
|
||||
deterministicNonDomainGuard &&
|
||||
(llmFirstUnsupportedCandidate || llmContractMode === null) &&
|
||||
!baseToolGatePreservesAddressLane &&
|
||||
!effectiveGroundedValueFlowFollowupContextDetected &&
|
||||
!groundedValueFlowFollowupContextDetected &&
|
||||
!protectedInventoryShortFollowup &&
|
||||
!organizationClarificationContinuationDetected &&
|
||||
!routeCandidateOrganizationClarificationDetected);
|
||||
!organizationClarificationContinuationDetected);
|
||||
const lastAddressAssistantDebug = sessionItems
|
||||
? findLastAddressAssistantItem(sessionItems)?.debug ?? null
|
||||
: null;
|
||||
|
|
@ -613,7 +583,7 @@ function createAssistantRoutePolicy(deps) {
|
|||
!turnMeaningIntentCandidate &&
|
||||
!dataScopeMetaQuery &&
|
||||
!dangerOrCoercionSignal &&
|
||||
!effectiveGroundedValueFlowFollowupContextDetected &&
|
||||
!groundedValueFlowFollowupContextDetected &&
|
||||
!organizationClarificationContinuationDetected);
|
||||
const hardMetaMode = resolveHardMetaMode({
|
||||
dataScopeMetaQuery,
|
||||
|
|
@ -778,7 +748,7 @@ function createAssistantRoutePolicy(deps) {
|
|||
!dataScopeMetaQuery &&
|
||||
!capabilityMetaQuery &&
|
||||
!dangerOrCoercionSignal &&
|
||||
!effectiveGroundedValueFlowFollowupContextDetected &&
|
||||
!groundedValueFlowFollowupContextDetected &&
|
||||
!organizationClarificationContinuationDetected);
|
||||
if (unsupportedCurrentTurnMeaningBoundary) {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1944,9 +1944,7 @@ function textMojibakeScoreForAddress(value) {
|
|||
const hardMarkers = (source.match(/[Ѓѓ‚„…†‡€‰‹ЉЊЌЋЏ\uFFFD?’“”•–—™љ›њќћџ]/g) ?? []).length;
|
||||
const pairMarkers = (source.match(/(?:Р.|С.|Ð.|Ñ.)/g) ?? []).length;
|
||||
const doubleEncodedMarkers = (source.match(/(?:Г[Ђ-џ]|В[Ђ-џ]|Ã.|Â.)/gu) ?? []).length;
|
||||
const replacement = (source.match(/\uFFFD/g) ?? []).length;
|
||||
const c1Controls = (source.match(/[\u0080-\u009f]/g) ?? []).length;
|
||||
return cyrillic + latin - replacement * 8 - c1Controls * 5 - hardMarkers * 3 - pairMarkers * 2 - doubleEncodedMarkers * 2;
|
||||
return cyrillic + latin - hardMarkers * 3 - pairMarkers * 2 - doubleEncodedMarkers * 2;
|
||||
}
|
||||
function looksLikeMojibakeForAddress(value) {
|
||||
const source = String(value ?? "");
|
||||
|
|
@ -1964,36 +1962,16 @@ function looksLikeMojibakeForAddress(value) {
|
|||
}
|
||||
return false;
|
||||
}
|
||||
function encodeWin1251MojibakeBytesForAddress(value) {
|
||||
const chunks = [];
|
||||
for (const char of String(value ?? "")) {
|
||||
const code = char.codePointAt(0) ?? 0;
|
||||
if (code >= 0x80 && code <= 0x9f) {
|
||||
chunks.push(Buffer.from([code]));
|
||||
continue;
|
||||
}
|
||||
chunks.push(iconv_lite_1.default.encode(char, "win1251"));
|
||||
}
|
||||
return Buffer.concat(chunks);
|
||||
}
|
||||
function decodeUtf8FromWin1251MojibakeForAddress(value) {
|
||||
return encodeWin1251MojibakeBytesForAddress(value).toString("utf8");
|
||||
}
|
||||
function repairKnownReplacementDamagedAddressText(value) {
|
||||
return String(value ?? "")
|
||||
.replace(/\u0410\u041b\u042c\u0422\u0415\u0420\u041d\u0410\u0422[\uFFFD?]+\u0412\u0410/giu, "\u0410\u041b\u042c\u0422\u0415\u0420\u041d\u0410\u0422\u0418\u0412\u0410")
|
||||
.replace(/\u041e\u0411\u0429[\uFFFD?]+\u0419/giu, "\u041e\u0411\u0429\u0418\u0419");
|
||||
}
|
||||
function repairAddressMojibake(value) {
|
||||
const source = repairKnownReplacementDamagedAddressText(String(value ?? ""));
|
||||
if (!looksLikeMojibakeForAddress(source) && !/[\u0080-\u009f\uFFFD]/.test(source)) {
|
||||
const source = String(value ?? "");
|
||||
if (!looksLikeMojibakeForAddress(source)) {
|
||||
return source;
|
||||
}
|
||||
let candidate = source;
|
||||
for (let pass = 0; pass < 3; pass += 1) {
|
||||
let improved = false;
|
||||
try {
|
||||
const fromWin1251 = decodeUtf8FromWin1251MojibakeForAddress(candidate);
|
||||
const fromWin1251 = iconv_lite_1.default.encode(candidate, "win1251").toString("utf8");
|
||||
if (textMojibakeScoreForAddress(fromWin1251) > textMojibakeScoreForAddress(candidate)) {
|
||||
candidate = fromWin1251;
|
||||
improved = true;
|
||||
|
|
@ -2008,16 +1986,11 @@ function repairAddressMojibake(value) {
|
|||
}
|
||||
}
|
||||
catch (_error) { }
|
||||
const repairedKnownText = repairKnownReplacementDamagedAddressText(candidate);
|
||||
if (repairedKnownText !== candidate) {
|
||||
candidate = repairedKnownText;
|
||||
improved = true;
|
||||
}
|
||||
if (!improved) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return repairKnownReplacementDamagedAddressText(candidate);
|
||||
return candidate;
|
||||
}
|
||||
function sanitizeOutgoingAssistantText(value, fallback = "Не смог сформировать читаемый ответ. Уточните запрос.") {
|
||||
const repaired = repairAddressMojibake(String(value ?? ""));
|
||||
|
|
@ -2123,9 +2096,6 @@ function isAddressLaneDebugPayload(debug) {
|
|||
if (typeof debug.mcp_call_status === "string" && debug.mcp_call_status.trim().length > 0) {
|
||||
return true;
|
||||
}
|
||||
if (debug.mcp_discovery_response_applied === true && debug.assistant_mcp_discovery_entry_point_v1) {
|
||||
return true;
|
||||
}
|
||||
if (typeof debug.anchor_type === "string" && debug.anchor_type.trim().length > 0) {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -4341,25 +4311,18 @@ function scoreOrganizationMentionInMessage(message, organization) {
|
|||
variantScore = Math.max(variantScore, variant.length * 5);
|
||||
continue;
|
||||
}
|
||||
let fuzzyTokenScore = 0;
|
||||
const fuzzyMatched = messageTokens.some((messageToken) => {
|
||||
if (messageToken === variant) {
|
||||
fuzzyTokenScore = Math.max(fuzzyTokenScore, variant.length * 5);
|
||||
return true;
|
||||
}
|
||||
if (messageToken.length >= 5 && variant.length >= 5) {
|
||||
const prefixMatched = messageToken.startsWith(variant) || variant.startsWith(messageToken);
|
||||
if (prefixMatched) {
|
||||
const prefixLength = Math.min(messageToken.length, variant.length);
|
||||
fuzzyTokenScore = Math.max(fuzzyTokenScore, variant.length * (prefixLength >= 7 ? 5 : 3));
|
||||
}
|
||||
return prefixMatched;
|
||||
return messageToken.startsWith(variant) || variant.startsWith(messageToken);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (fuzzyMatched) {
|
||||
matched = true;
|
||||
variantScore = Math.max(variantScore, Math.max(20, fuzzyTokenScore || variant.length * 3));
|
||||
variantScore = Math.max(variantScore, Math.max(20, variant.length * 3));
|
||||
}
|
||||
}
|
||||
if (matched) {
|
||||
|
|
@ -4955,7 +4918,6 @@ class AssistantService {
|
|||
hasLivingChatSignal,
|
||||
shouldEmitOrganizationSelectionReply,
|
||||
hasAssistantCapabilityQuestionSignal,
|
||||
resolveOrganizationSelectionFromMessage,
|
||||
resolveDataScopeProbe: () => resolveAssistantDataScopeProbe(),
|
||||
applyScriptGuard: applyLivingChatScriptGuardFromPolicy,
|
||||
applyGroundingGuard: applyLivingChatGroundingGuardFromPolicy,
|
||||
|
|
|
|||
|
|
@ -120,22 +120,6 @@ function createAssistantTransitionPolicy(deps) {
|
|||
const samples = [userMessage, alternateMessage].map((item) => normalizeFollowupText(item)).filter(Boolean);
|
||||
return samples.some((sample) => /(?:итог|summary|резюм|вывод|что\s+(?:мы\s+)?подтверд|что\s+понят|что\s+можно|что\s+нельзя|собери\s+коротк)/iu.test(sample) && /(?:контрагент|группа\s+свк|свк|отдельн)/iu.test(sample));
|
||||
}
|
||||
function hasBusinessOverviewBoundaryFollowupCue(text) {
|
||||
const normalized = normalizeFollowupText(text);
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
const hasBoundaryCue = /(?:\u043f\u0440\u043e\u0441\u0440\u043e\u0447|\u0441\u0440\u043e\u043a\w*\s+\u043e\u043f\u043b\u0430\u0442|\u0434\u043e\u043a\u0430\u0437|\u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0434|\u043f\u0440\u0438\u0431\u044b\u043b|\u0443\u0431\u044b\u0442|\u043c\u0430\u0440\u0436|\u0440\u0435\u0437\u0435\u0440\u0432|\u043d\u0435\u043b\u0438\u043a\u0432\u0438\u0434|\u043f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a|\u0432\u0435\u043d\u0434\u043e\u0440|due[-\s]?date|overdue|aging|profit|loss|margin|vendor|risk)/iu.test(normalized);
|
||||
const hasFollowupShape = /(?:\u0442\u043e\s+\u0435\u0441\u0442\u044c|\u043f\u043e\u0447\u0435\u043c\u0443|\u043a\u043e\u0440\u043e\u0442\u043a|\u043d\u0435\u043b\u044c\u0437\u044f|\u043c\u043e\u0436\u043d\u043e\s+\u043b\u0438|\u0437\u043d\u0430\u0447\u0438\u0442|\u0432\u044b\u0445\u043e\u0434\u0438\u0442|\u0438\u0442\u043e\u0433|why|short|brief|so)/iu.test(normalized);
|
||||
return hasBoundaryCue && hasFollowupShape;
|
||||
}
|
||||
function hasOrganizationClarificationTextCue(text) {
|
||||
const normalized = deps.compactWhitespace(deps.repairAddressMojibake(String(text ?? "")).toLowerCase());
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
return /(?<!\p{L})(?:\u043e\u043e\u043e|\u0438\u043f|\u0430\u043e|\u043f\u0430\u043e|\u0437\u0430\u043e)(?!\p{L})|(?:\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446|\u043a\u043e\u043c\u043f\u0430\u043d|llc|company|organization)/iu.test(normalized);
|
||||
}
|
||||
function parseDmyDateToIso(value) {
|
||||
const match = String(value ?? "").trim().match(/^(\d{2})\.(\d{2})\.(\d{4})$/);
|
||||
if (!match) {
|
||||
|
|
@ -449,11 +433,7 @@ function createAssistantTransitionPolicy(deps) {
|
|||
(deps.toNonEmptyString(alternateMessage)
|
||||
? deps.hasDataRetrievalRequestSignal(String(alternateMessage ?? ""))
|
||||
: false);
|
||||
const rawBusinessOverviewBoundaryFollowupCue = hasBusinessOverviewBoundaryFollowupCue(userMessage) ||
|
||||
(deps.toNonEmptyString(alternateMessage)
|
||||
? hasBusinessOverviewBoundaryFollowupCue(String(alternateMessage ?? ""))
|
||||
: false);
|
||||
if (rawCapabilityMetaQuery && !rawDataRetrievalSignal && !rawBusinessOverviewBoundaryFollowupCue) {
|
||||
if (rawCapabilityMetaQuery && !rawDataRetrievalSignal) {
|
||||
return null;
|
||||
}
|
||||
const assistantTurnMeaning = typeof deps.resolveAssistantTurnMeaning === "function"
|
||||
|
|
@ -512,25 +492,10 @@ function createAssistantTransitionPolicy(deps) {
|
|||
: false));
|
||||
const sourceIntentHint = (0, assistantContinuityPolicy_1.readAddressDebugIntent)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const sourceDiscoveryPilotScopeHint = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryPilotScope)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const sourceDiscoveryLoopStatusHint = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopStatus)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const sourceDiscoveryLoopSelectedChainIdHint = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopSelectedChainId)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const sourceDiscoveryLoopPendingAxesHint = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopPendingAxes)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const sourceDiscoveryLoopProvidedAxesHint = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopProvidedAxes)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const currentTurnPredecomposeOrganization = deps.toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.entities?.organization) ??
|
||||
(deps.toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.semantics?.anchor_kind) === "organization"
|
||||
? deps.toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.semantics?.anchor_value)
|
||||
: null);
|
||||
const mcpDiscoveryOrganizationClarificationContinuation = Boolean(sourceDiscoveryLoopStatusHint === "awaiting_clarification" &&
|
||||
sourceDiscoveryLoopSelectedChainIdHint &&
|
||||
sourceDiscoveryLoopPendingAxesHint.includes("organization") &&
|
||||
(currentTurnPredecomposeOrganization ||
|
||||
explicitOrganizationClarificationSelection ||
|
||||
[userMessage, alternateMessage].some((message) => hasOrganizationClarificationTextCue(message))));
|
||||
const hasValueFlowCarryoverSourceHint = sourceIntentHint === "customer_revenue_and_payments" ||
|
||||
sourceDiscoveryPilotScopeHint === "counterparty_value_flow_query_movements_v1" ||
|
||||
sourceDiscoveryPilotScopeHint === "counterparty_supplier_payout_query_movements_v1" ||
|
||||
sourceDiscoveryPilotScopeHint === "counterparty_bidirectional_value_flow_query_movements_v1";
|
||||
const hasBusinessOverviewCarryoverSourceHint = sourceDiscoveryPilotScopeHint === "business_overview_route_template_v1";
|
||||
const navigationSessionState = (0, assistantContinuityPolicy_1.resolveNavigationSessionContextState)(addressNavigationState, deps.toNonEmptyString, deps.normalizeOrganizationScopeValue);
|
||||
const navigationFocusObjectHint = navigationSessionState.focusObject;
|
||||
const hasNavigationInventoryItemFocusHint = Boolean(deps.toNonEmptyString(navigationFocusObjectHint?.label) &&
|
||||
|
|
@ -556,28 +521,20 @@ function createAssistantTransitionPolicy(deps) {
|
|||
const shortValueFlowRetargetAlternate = hasValueFlowCarryoverSourceHint && deps.toNonEmptyString(alternateMessage)
|
||||
? hasShortValueFlowRetargetCue(String(alternateMessage ?? ""))
|
||||
: false;
|
||||
const businessOverviewBoundaryFollowupPrimary = hasBusinessOverviewCarryoverSourceHint && hasBusinessOverviewBoundaryFollowupCue(userMessage);
|
||||
const businessOverviewBoundaryFollowupAlternate = hasBusinessOverviewCarryoverSourceHint && deps.toNonEmptyString(alternateMessage)
|
||||
? hasBusinessOverviewBoundaryFollowupCue(String(alternateMessage ?? ""))
|
||||
: false;
|
||||
const explicitSummaryBundleReuseSignal = hasExplicitSummaryBundleReuseSignal(userMessage, alternateMessage);
|
||||
let hasPrimaryFollowupSignal = deps.hasAddressFollowupContextSignal(userMessage) ||
|
||||
Boolean(debtRoleSwapPrimary) ||
|
||||
shortValueFlowRetargetPrimary ||
|
||||
businessOverviewBoundaryFollowupPrimary ||
|
||||
inventoryShortFollowupPrimary ||
|
||||
inventoryPurchaseDateVatBridge ||
|
||||
explicitSummaryBundleReuseSignal ||
|
||||
mcpDiscoveryOrganizationClarificationContinuation;
|
||||
explicitSummaryBundleReuseSignal;
|
||||
let hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage)
|
||||
? deps.hasAddressFollowupContextSignal(alternateMessage) ||
|
||||
Boolean(debtRoleSwapAlternate) ||
|
||||
shortValueFlowRetargetAlternate ||
|
||||
businessOverviewBoundaryFollowupAlternate ||
|
||||
inventoryShortFollowupAlternate ||
|
||||
inventoryPurchaseDateVatBridge ||
|
||||
explicitSummaryBundleReuseSignal ||
|
||||
mcpDiscoveryOrganizationClarificationContinuation
|
||||
explicitSummaryBundleReuseSignal
|
||||
: false;
|
||||
const hasPrimaryIndexReferenceSignal = deps.extractDisplayedEntityIndexMention(userMessage) !== null;
|
||||
const hasAlternateIndexReferenceSignal = deps.toNonEmptyString(alternateMessage)
|
||||
|
|
@ -600,7 +557,6 @@ function createAssistantTransitionPolicy(deps) {
|
|||
let hasStrongFollowupReference = hasPrimaryIndexReferenceSignal ||
|
||||
hasAlternateIndexReferenceSignal ||
|
||||
hasOrganizationClarificationContinuation ||
|
||||
mcpDiscoveryOrganizationClarificationContinuation ||
|
||||
hasImplicitContinuationSignal ||
|
||||
hasSuggestedIntentPivotSignal ||
|
||||
inventoryShortFollowupPrimary ||
|
||||
|
|
@ -614,8 +570,6 @@ function createAssistantTransitionPolicy(deps) {
|
|||
Boolean(debtRoleSwapIntent) ||
|
||||
shortValueFlowRetargetPrimary ||
|
||||
shortValueFlowRetargetAlternate ||
|
||||
businessOverviewBoundaryFollowupPrimary ||
|
||||
businessOverviewBoundaryFollowupAlternate ||
|
||||
deps.hasFollowupMarker(userMessage) ||
|
||||
deps.hasReferentialPointer(userMessage) ||
|
||||
(deps.toNonEmptyString(alternateMessage)
|
||||
|
|
@ -625,7 +579,6 @@ function createAssistantTransitionPolicy(deps) {
|
|||
const hasConcreteFollowupReference = hasPrimaryIndexReferenceSignal ||
|
||||
hasAlternateIndexReferenceSignal ||
|
||||
hasOrganizationClarificationContinuation ||
|
||||
mcpDiscoveryOrganizationClarificationContinuation ||
|
||||
inventoryShortFollowupPrimary ||
|
||||
inventoryShortFollowupAlternate ||
|
||||
hasInventoryRootTemporalFollowupPrimary ||
|
||||
|
|
@ -637,8 +590,6 @@ function createAssistantTransitionPolicy(deps) {
|
|||
Boolean(debtRoleSwapIntent) ||
|
||||
shortValueFlowRetargetPrimary ||
|
||||
shortValueFlowRetargetAlternate ||
|
||||
businessOverviewBoundaryFollowupPrimary ||
|
||||
businessOverviewBoundaryFollowupAlternate ||
|
||||
deps.hasFollowupMarker(userMessage) ||
|
||||
deps.hasReferentialPointer(userMessage) ||
|
||||
(deps.toNonEmptyString(alternateMessage)
|
||||
|
|
@ -666,7 +617,6 @@ function createAssistantTransitionPolicy(deps) {
|
|||
!hasImplicitContinuationSignal &&
|
||||
!hasSuggestedIntentPivotSignal &&
|
||||
!hasOrganizationClarificationContinuation &&
|
||||
!mcpDiscoveryOrganizationClarificationContinuation &&
|
||||
!hasIndexReferenceSignal &&
|
||||
!explicitSummaryBundleReuseSignal) {
|
||||
return null;
|
||||
|
|
@ -682,7 +632,6 @@ function createAssistantTransitionPolicy(deps) {
|
|||
!hasImplicitContinuationSignal &&
|
||||
!hasSuggestedIntentPivotSignal &&
|
||||
!hasOrganizationClarificationContinuation &&
|
||||
!mcpDiscoveryOrganizationClarificationContinuation &&
|
||||
!hasIndexReferenceSignal &&
|
||||
!explicitSummaryBundleReuseSignal) {
|
||||
return null;
|
||||
|
|
@ -701,10 +650,10 @@ function createAssistantTransitionPolicy(deps) {
|
|||
const sourceDiscoveryMetadataAmbiguityEntitySets = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryMetadataAmbiguityEntitySets)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const sourceDiscoveryEntityResolutionStatus = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryEntityResolutionStatus)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const sourceDiscoveryEntityCandidates = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryEntityCandidates)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const sourceDiscoveryLoopStatus = sourceDiscoveryLoopStatusHint;
|
||||
const sourceDiscoveryLoopSelectedChainId = sourceDiscoveryLoopSelectedChainIdHint;
|
||||
const sourceDiscoveryLoopPendingAxes = sourceDiscoveryLoopPendingAxesHint;
|
||||
const sourceDiscoveryLoopProvidedAxes = sourceDiscoveryLoopProvidedAxesHint;
|
||||
const sourceDiscoveryLoopStatus = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopStatus)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const sourceDiscoveryLoopSelectedChainId = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopSelectedChainId)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const sourceDiscoveryLoopPendingAxes = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopPendingAxes)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const sourceDiscoveryLoopProvidedAxes = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopProvidedAxes)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const sourceDiscoveryLoopAskedDomainFamily = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopAskedDomainFamily)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const sourceDiscoveryLoopAskedActionFamily = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopAskedActionFamily)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
const sourceDiscoveryLoopUnsupportedFamily = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryLoopUnsupportedFamily)(carryoverSourceDebug, deps.toNonEmptyString);
|
||||
|
|
@ -740,7 +689,6 @@ function createAssistantTransitionPolicy(deps) {
|
|||
explicitIntentFamily &&
|
||||
sourceIntentFamily !== explicitIntentFamily &&
|
||||
!hasOrganizationClarificationContinuation &&
|
||||
!mcpDiscoveryOrganizationClarificationContinuation &&
|
||||
!hasImplicitContinuationSignal &&
|
||||
!hasIndexReferenceSignal &&
|
||||
!hasInventoryRootTemporalFollowupPrimary &&
|
||||
|
|
@ -749,8 +697,6 @@ function createAssistantTransitionPolicy(deps) {
|
|||
!hasInventoryRootRestatementAlternate &&
|
||||
!inventoryShortFollowupPrimary &&
|
||||
!inventoryShortFollowupAlternate &&
|
||||
!businessOverviewBoundaryFollowupPrimary &&
|
||||
!businessOverviewBoundaryFollowupAlternate &&
|
||||
!foreignAccountingPivotOverInventory &&
|
||||
!deps.hasFollowupMarker(userMessage) &&
|
||||
!deps.hasReferentialPointer(userMessage) &&
|
||||
|
|
@ -807,29 +753,24 @@ function createAssistantTransitionPolicy(deps) {
|
|||
hasSuggestedIntentPivotSignal ||
|
||||
Boolean(debtRoleSwapPrimary) ||
|
||||
shortValueFlowRetargetPrimary ||
|
||||
businessOverviewBoundaryFollowupPrimary ||
|
||||
inventoryShortFollowupPrimary ||
|
||||
inventoryPurchaseDateVatBridge ||
|
||||
explicitSummaryBundleReuseSignal ||
|
||||
hasInventoryRootTemporalFollowupPrimary ||
|
||||
mcpDiscoveryOrganizationClarificationContinuation;
|
||||
hasInventoryRootTemporalFollowupPrimary;
|
||||
hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage)
|
||||
? deps.hasAddressFollowupContextSignal(alternateMessage) ||
|
||||
hasSuggestedIntentPivotSignal ||
|
||||
Boolean(debtRoleSwapAlternate) ||
|
||||
shortValueFlowRetargetAlternate ||
|
||||
businessOverviewBoundaryFollowupAlternate ||
|
||||
inventoryShortFollowupAlternate ||
|
||||
inventoryPurchaseDateVatBridge ||
|
||||
explicitSummaryBundleReuseSignal ||
|
||||
hasInventoryRootTemporalFollowupAlternate ||
|
||||
mcpDiscoveryOrganizationClarificationContinuation
|
||||
hasInventoryRootTemporalFollowupAlternate
|
||||
: false;
|
||||
hasStrongFollowupReference =
|
||||
hasPrimaryIndexReferenceSignal ||
|
||||
hasAlternateIndexReferenceSignal ||
|
||||
hasOrganizationClarificationContinuation ||
|
||||
mcpDiscoveryOrganizationClarificationContinuation ||
|
||||
hasSuggestedIntentPivotSignal ||
|
||||
hasImplicitContinuationSignal ||
|
||||
inventoryShortFollowupPrimary ||
|
||||
|
|
@ -841,8 +782,6 @@ function createAssistantTransitionPolicy(deps) {
|
|||
Boolean(debtRoleSwapIntent) ||
|
||||
shortValueFlowRetargetPrimary ||
|
||||
shortValueFlowRetargetAlternate ||
|
||||
businessOverviewBoundaryFollowupPrimary ||
|
||||
businessOverviewBoundaryFollowupAlternate ||
|
||||
deps.hasFollowupMarker(userMessage) ||
|
||||
deps.hasReferentialPointer(userMessage) ||
|
||||
(deps.toNonEmptyString(alternateMessage)
|
||||
|
|
@ -955,8 +894,7 @@ function createAssistantTransitionPolicy(deps) {
|
|||
hasInventoryRootRestatementAlternate ||
|
||||
hasSelectedObjectInventorySignalPrimary ||
|
||||
hasSelectedObjectInventorySignalAlternate));
|
||||
const explicitIntentForCarryover = debtRoleSwapIntent ? debtRoleSwapIntent : explicitIntent;
|
||||
const carryoverTargetIntent = (0, assistantContinuityPolicy_1.resolveFollowupTargetIntent)(inventoryPurchaseDateVatBridge, selectedObjectRetargetIntent, explicitIntentForCarryover, sourceIntent, followupSelectionMode, deps.toNonEmptyString(inventoryRootFrame?.intent), displayedEntityTargetIntent, previousIntent, explicitInventorySameDatePivot);
|
||||
const carryoverTargetIntent = (0, assistantContinuityPolicy_1.resolveFollowupTargetIntent)(inventoryPurchaseDateVatBridge, selectedObjectRetargetIntent, explicitIntent, sourceIntent, followupSelectionMode, deps.toNonEmptyString(inventoryRootFrame?.intent), displayedEntityTargetIntent, previousIntent, explicitInventorySameDatePivot);
|
||||
return {
|
||||
followupContext: {
|
||||
previous_intent: previousIntent ?? undefined,
|
||||
|
|
|
|||
|
|
@ -54,7 +54,6 @@ function buildAssistantAddressAttemptRuntimeInput(runtimeInput, deps) {
|
|||
hasLivingChatSignal: deps.hasLivingChatSignal,
|
||||
shouldEmitOrganizationSelectionReply: deps.shouldEmitOrganizationSelectionReply,
|
||||
hasAssistantCapabilityQuestionSignal: deps.hasAssistantCapabilityQuestionSignal,
|
||||
resolveOrganizationSelectionFromMessage: deps.resolveOrganizationSelectionFromMessage,
|
||||
resolveDataScopeProbe: deps.resolveDataScopeProbe,
|
||||
applyScriptGuard: deps.applyScriptGuard,
|
||||
applyGroundingGuard: deps.applyGroundingGuard,
|
||||
|
|
|
|||
|
|
@ -1,46 +0,0 @@
|
|||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.isLikelyFinancialInstitutionCounterparty = isLikelyFinancialInstitutionCounterparty;
|
||||
exports.counterpartyRoleHintForName = counterpartyRoleHintForName;
|
||||
const FINANCIAL_INSTITUTION_PATTERNS = [
|
||||
/(?:^|[\s"«(,-])банк(?:$|[\s"»),.-])/u,
|
||||
/сбербанк/u,
|
||||
/(?:^|[\s"«(,-])сбер(?:$|[\s"»),.-])/u,
|
||||
/(?:^|[\s"«(,-])втб(?:$|[\s"»),.-])/u,
|
||||
/альфа[\s-]*банк/u,
|
||||
/тинькофф/u,
|
||||
/(?:^|[\s"«(,-])т[\s-]*банк(?:$|[\s"»),.-])/u,
|
||||
/газпромбанк/u,
|
||||
/росбанк/u,
|
||||
/райффайзен/u,
|
||||
/совкомбанк/u,
|
||||
/промсвязьбанк/u,
|
||||
/(?:^|[\s"«(,-])псб(?:$|[\s"»),.-])/u,
|
||||
/(?:^|[\s"«(,-])мкб(?:$|[\s"»),.-])/u,
|
||||
/ак[\s-]*барс/u,
|
||||
/уралсиб/u,
|
||||
/юникредит/u,
|
||||
/почта[\s-]*банк/u,
|
||||
/(?:^|[\s"«(,-])открытие(?:$|[\s"»),.-])/u,
|
||||
/кредитн(?:ая|ый|ое|ые)\s+организац/u
|
||||
];
|
||||
function normalizeCounterpartyRoleText(value) {
|
||||
return String(value ?? "")
|
||||
.toLowerCase()
|
||||
.replace(/ё/g, "е")
|
||||
.replace(/[._]+/g, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
function isLikelyFinancialInstitutionCounterparty(value) {
|
||||
const normalized = normalizeCounterpartyRoleText(value);
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
return FINANCIAL_INSTITUTION_PATTERNS.some((pattern) => pattern.test(normalized));
|
||||
}
|
||||
function counterpartyRoleHintForName(value) {
|
||||
return isLikelyFinancialInstitutionCounterparty(value)
|
||||
? "bank_or_financial_institution"
|
||||
: "ordinary_counterparty";
|
||||
}
|
||||
|
|
@ -174,7 +174,7 @@ export const ASSISTANT_MCP_PROXY_URL = (process.env.ASSISTANT_MCP_PROXY_URL ?? "
|
|||
);
|
||||
export const ASSISTANT_MCP_CHANNEL = process.env.ASSISTANT_MCP_CHANNEL ?? "default";
|
||||
export const ASSISTANT_MCP_TIMEOUT_MS = toNumberFlag(process.env.ASSISTANT_MCP_TIMEOUT_MS, 6000);
|
||||
export const ASSISTANT_MCP_LIVE_LIMIT = Math.max(1, Math.trunc(toNumberFlag(process.env.ASSISTANT_MCP_LIVE_LIMIT, 128)));
|
||||
export const ASSISTANT_MCP_LIVE_LIMIT = Math.max(1, Math.trunc(toNumberFlag(process.env.ASSISTANT_MCP_LIVE_LIMIT, 24)));
|
||||
export const VAT_PAYABLE_68_PREFIXES = toStringListFlag(process.env.VAT_PAYABLE_68_PREFIXES, ["68.02"]);
|
||||
export const VAT_PAYABLE_19_PREFIXES = toStringListFlag(process.env.VAT_PAYABLE_19_PREFIXES, ["19"]);
|
||||
|
||||
|
|
|
|||
|
|
@ -321,7 +321,7 @@ function parseAutoGenTitle(value: unknown): string | null {
|
|||
if (!title) {
|
||||
return null;
|
||||
}
|
||||
return repairAutogenMojibake(title).slice(0, 160);
|
||||
return title.slice(0, 160);
|
||||
}
|
||||
|
||||
function parseManualCaseDecision(value: unknown, fallback: ManualCaseDecision = "needs_dialog_policy_fix"): ManualCaseDecision {
|
||||
|
|
@ -1504,9 +1504,7 @@ function textMojibakeScore(value: string): number {
|
|||
const hardMarkers = (source.match(/[Ѓѓ‚„…†‡€‰‹ЉЊЌЋЏ‘’“”•–—™љ›њќћџ]/g) ?? []).length;
|
||||
const pairMarkers = (source.match(/(?:Р.|С.|Ð.|Ñ.)/g) ?? []).length;
|
||||
const doubleEncodedMarkers = (source.match(/(?:Г[Ђ-џ]|В[Ђ-џ]|Ã.|Â.)/gu) ?? []).length;
|
||||
const replacement = (source.match(/\uFFFD/g) ?? []).length;
|
||||
const c1Controls = (source.match(/[\u0080-\u009f]/g) ?? []).length;
|
||||
return cyrillic + latin - replacement * 8 - c1Controls * 5 - hardMarkers * 3 - pairMarkers * 2 - doubleEncodedMarkers * 2;
|
||||
return cyrillic + latin - hardMarkers * 3 - pairMarkers * 2 - doubleEncodedMarkers * 2;
|
||||
}
|
||||
|
||||
function looksLikeMojibake(value: string): boolean {
|
||||
|
|
@ -1523,39 +1521,16 @@ function looksLikeMojibake(value: string): boolean {
|
|||
return (source.match(/(?:Г[Ђ-џ]|В[Ђ-џ]|Ã.|Â.)/gu) ?? []).length >= 2;
|
||||
}
|
||||
|
||||
function encodeWin1251MojibakeBytes(value: string): Buffer {
|
||||
const chunks: Buffer[] = [];
|
||||
for (const char of String(value ?? "")) {
|
||||
const code = char.codePointAt(0) ?? 0;
|
||||
if (code >= 0x80 && code <= 0x9f) {
|
||||
chunks.push(Buffer.from([code]));
|
||||
continue;
|
||||
}
|
||||
chunks.push(iconv.encode(char, "win1251"));
|
||||
}
|
||||
return Buffer.concat(chunks);
|
||||
}
|
||||
|
||||
function decodeUtf8FromWin1251Mojibake(value: string): string {
|
||||
return encodeWin1251MojibakeBytes(value).toString("utf8");
|
||||
}
|
||||
|
||||
function repairKnownReplacementDamagedAutogenText(value: string): string {
|
||||
return String(value ?? "")
|
||||
.replace(/\u0410\u041b\u042c\u0422\u0415\u0420\u041d\u0410\u0422[\uFFFD?]+\u0412\u0410/giu, "\u0410\u041b\u042c\u0422\u0415\u0420\u041d\u0410\u0422\u0418\u0412\u0410")
|
||||
.replace(/\u041e\u0411\u0429[\uFFFD?]+\u0419/giu, "\u041e\u0411\u0429\u0418\u0419");
|
||||
}
|
||||
|
||||
function repairAutogenMojibake(value: string): string {
|
||||
const source = repairKnownReplacementDamagedAutogenText(String(value ?? ""));
|
||||
if (!looksLikeMojibake(source) && !/[\u0080-\u009f\uFFFD]/.test(source)) {
|
||||
const source = String(value ?? "");
|
||||
if (!looksLikeMojibake(source)) {
|
||||
return source;
|
||||
}
|
||||
let candidate = source;
|
||||
for (let pass = 0; pass < 3; pass += 1) {
|
||||
let improved = false;
|
||||
try {
|
||||
const fromWin1251 = decodeUtf8FromWin1251Mojibake(candidate);
|
||||
const fromWin1251 = iconv.encode(candidate, "win1251").toString("utf8");
|
||||
if (textMojibakeScore(fromWin1251) > textMojibakeScore(candidate)) {
|
||||
candidate = fromWin1251;
|
||||
improved = true;
|
||||
|
|
@ -1572,16 +1547,11 @@ function repairAutogenMojibake(value: string): string {
|
|||
} catch {
|
||||
// ignore
|
||||
}
|
||||
const repairedKnownText = repairKnownReplacementDamagedAutogenText(candidate);
|
||||
if (repairedKnownText !== candidate) {
|
||||
candidate = repairedKnownText;
|
||||
improved = true;
|
||||
}
|
||||
if (!improved) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return repairKnownReplacementDamagedAutogenText(candidate);
|
||||
return candidate;
|
||||
}
|
||||
|
||||
function sanitizeGeneratedQuestion(value: string): string {
|
||||
|
|
@ -1805,8 +1775,7 @@ function extractQuestionsFromAutogenOutput(rawText: string): string[] {
|
|||
|
||||
export const __autoRunsQuestionTestUtils = {
|
||||
splitQuestionCandidates,
|
||||
extractQuestionsFromAutogenOutput,
|
||||
repairAutogenMojibake
|
||||
extractQuestionsFromAutogenOutput
|
||||
};
|
||||
|
||||
async function generateQwenSeedQuestionsLive(input: {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import type { AppServices } from "../serverContext";
|
|||
import { ApiError, ok } from "../utils/http";
|
||||
import type { EvalRunMode, NormalizeRequestPayload } from "../types/normalizer";
|
||||
import type { EvalTarget } from "../types/assistantEval";
|
||||
import { repairAddressMojibakeText } from "../services/addressTextRepair";
|
||||
|
||||
type EvalAsyncStatus = "queued" | "running" | "completed" | "failed" | "canceled";
|
||||
|
||||
|
|
@ -68,7 +67,7 @@ function toArray(value: unknown): unknown[] {
|
|||
}
|
||||
|
||||
function normalizeQuestionChunk(value: string): string {
|
||||
return repairAddressMojibakeText(String(value ?? ""))
|
||||
return String(value ?? "")
|
||||
.replace(/\r/g, " ")
|
||||
.replace(/\t/g, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
|
|
@ -137,7 +136,7 @@ function normalizeRuntimeQuestionList(items: string[]): string[] {
|
|||
}
|
||||
|
||||
function splitQuestionCandidate(raw: string): string[] {
|
||||
const normalized = repairAddressMojibakeText(String(raw ?? "")).replace(/\r/g, "\n").trim();
|
||||
const normalized = String(raw ?? "").replace(/\r/g, "\n").trim();
|
||||
if (!normalized) {
|
||||
return [];
|
||||
}
|
||||
|
|
@ -534,8 +533,7 @@ export function buildEvalRouter(services: AppServices): Router {
|
|||
}
|
||||
const questions = normalizeRuntimeQuestions(body.questions);
|
||||
const scenarioQuestions = normalizeRuntimeQuestions(body.scenarioQuestions, { dedupe: false, splitCandidates: false });
|
||||
const scenarioTitleRaw = toStringSafe(body.scenarioTitle);
|
||||
const scenarioTitle = scenarioTitleRaw ? repairAddressMojibakeText(scenarioTitleRaw) : null;
|
||||
const scenarioTitle = toStringSafe(body.scenarioTitle);
|
||||
|
||||
const jobId = `job-${nanoid(10)}`;
|
||||
const runId = `assistant-stage1-${nanoid(10)}`;
|
||||
|
|
|
|||
|
|
@ -8,25 +8,6 @@ const LIMIT_PATTERN = /(?:\btop\b|\blimit\b|первые|топ)[\s\-–—_:№
|
|||
const VALUE_ANALYTICS_SAMPLE_LIMIT = 1000;
|
||||
const COUNTERPARTY_PATTERN =
|
||||
/(?:по\s+контрагенту|контрагент(?:у|а)?|по\s+контре|контра|по\s+компан(?:ии|ию|ия)|компан(?:ия|ии|ию)|по\s+организац(?:ии|ию|ия)|организац(?:ия|ии|ию)|по\s+поставщик(?:у|а)?|поставщик(?:у|а)?|по\s+клиент(?:у|а)?|клиент(?:у|а)?|по\s+покупател(?:ю|я)|покупател(?:ю|я)|по\s+партнер(?:у|а)?|партнер(?:у|а)?|by\s+counterparty|counterparty|by\s+company|company|by\s+supplier|supplier|by\s+vendor|vendor|by\s+customer|customer|by\s+client|client|by\s+partner|partner)\s+([^\r\n,.;:]+)/iu;
|
||||
const KNOWN_FINANCIAL_COUNTERPARTY_ANCHORS: Array<{ pattern: RegExp; value: string }> = [
|
||||
{
|
||||
pattern:
|
||||
/(?<![\p{L}\p{N}])(?:\u043f\u0430\u043e\s+|\u0430\u043e\s+)?\u0441\u0431\u0435\u0440\u0431\u0430\u043d\u043a(?:\s*,?\s*(?:\u043f\u0430\u043e|\u0430\u043e))?(?![\p{L}\p{N}])/iu,
|
||||
value: "\u0421\u0411\u0415\u0420\u0411\u0410\u041d\u041a"
|
||||
},
|
||||
{
|
||||
pattern: /(?<![\p{L}\p{N}])(?:\u0431\u0430\u043d\u043a\s+)?\u0432\u0442\u0431(?![\p{L}\p{N}])/iu,
|
||||
value: "\u0412\u0422\u0411"
|
||||
},
|
||||
{
|
||||
pattern: /(?<![\p{L}\p{N}])\u0430\u043b\u044c\u0444\u0430[-\s]?\u0431\u0430\u043d\u043a(?![\p{L}\p{N}])/iu,
|
||||
value: "\u0410\u041b\u042c\u0424\u0410-\u0411\u0410\u041d\u041a"
|
||||
},
|
||||
{
|
||||
pattern: /(?<![\p{L}\p{N}])(?:\u0442\u0438\u043d\u044c\u043a\u043e\u0444\u0444|\u0442[-\s]?\u0431\u0430\u043d\u043a)(?![\p{L}\p{N}])/iu,
|
||||
value: "\u0422-\u0411\u0410\u041d\u041a"
|
||||
}
|
||||
];
|
||||
const CONTRACT_PATTERN =
|
||||
/(?:по\s+(?:договору|контракту)|(?:договор|контракт)(?:у|а)?\s*(?:№|#|n)?|by\s+contract|contract(?:\s*(?:no|number|#|n))?)\s+([^\r\n,.;:]+)/i;
|
||||
const DATE_DMY_PATTERN = /\b(\d{1,2})[.\/-](\d{1,2})[.\/-](\d{2,4})\b/;
|
||||
|
|
@ -713,19 +694,6 @@ function isLikelyCounterpartyToken(rawToken: string): boolean {
|
|||
return !isCounterpartyNoiseToken(lowered);
|
||||
}
|
||||
|
||||
function extractKnownFinancialCounterpartyAnchor(text: string): string | undefined {
|
||||
const source = String(text ?? "");
|
||||
if (!source.trim()) {
|
||||
return undefined;
|
||||
}
|
||||
for (const anchor of KNOWN_FINANCIAL_COUNTERPARTY_ANCHORS) {
|
||||
if (anchor.pattern.test(source)) {
|
||||
return anchor.value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function isLowQualityCounterpartyAnchorValue(rawValue: string): boolean {
|
||||
const value = String(rawValue ?? "")
|
||||
.trim()
|
||||
|
|
@ -737,13 +705,6 @@ function isLowQualityCounterpartyAnchorValue(rawValue: string): boolean {
|
|||
if (/(?:за\s+вс[её]\s+время|за\s+всю\s+истори(?:ю|и)|all\s+time|entire\s+period|full\s+history)/iu.test(value)) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
/^(?:или|это|там|может|можно|обычн\p{L}*|клиентск\p{L}*|банковск\p{L}*(?:\/|\s+и\s+)?финансов\p{L}*)\b/iu.test(
|
||||
value
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
const tokens = value
|
||||
.split(/[^a-zа-я0-9]+/iu)
|
||||
.map((token) => token.trim())
|
||||
|
|
@ -818,33 +779,6 @@ function isLowQualityCounterpartyAnchorValue(rawValue: string): boolean {
|
|||
lowQualityTimeTokens.has(token) ||
|
||||
/^(?:январ|феврал|март|апрел|ма(?:й|я|е)|июн|июл|август|сентябр|октябр|ноябр|декабр)/iu.test(token);
|
||||
const lowQualityGenericTokens = new Set([
|
||||
"или",
|
||||
"обычный",
|
||||
"обычная",
|
||||
"обычное",
|
||||
"обычные",
|
||||
"обычного",
|
||||
"обычному",
|
||||
"обычным",
|
||||
"контрагент",
|
||||
"контрагента",
|
||||
"контрагенту",
|
||||
"клиент",
|
||||
"клиента",
|
||||
"клиенту",
|
||||
"клиентом",
|
||||
"клиенты",
|
||||
"поставщик",
|
||||
"поставщика",
|
||||
"поставщику",
|
||||
"поставщиком",
|
||||
"поставщики",
|
||||
"покупатель",
|
||||
"покупателя",
|
||||
"покупателю",
|
||||
"заказчик",
|
||||
"заказчика",
|
||||
"заказчику",
|
||||
"деньги",
|
||||
"денег",
|
||||
"деньгам",
|
||||
|
|
@ -1454,10 +1388,6 @@ function isLowQualityWarehouseAnchorValue(rawValue: string): boolean {
|
|||
"лежали",
|
||||
"на",
|
||||
"по",
|
||||
"остатка",
|
||||
"остаткам",
|
||||
"остатками",
|
||||
"остатков",
|
||||
"компания",
|
||||
"компании",
|
||||
"компанию",
|
||||
|
|
@ -1555,7 +1485,7 @@ function extractInventoryWarehouseAnchor(text: string): string | undefined {
|
|||
isLowQualityWarehouseAnchorValue(candidate) ||
|
||||
normalizedCandidate.startsWith("по состоянию") ||
|
||||
isTemporalWarehousePhrase(candidate) ||
|
||||
/^(?:сейчас|на|дату|дате|остат(?:ок|ки|ка|кам|ками|ков)|по\s+остат(?:кам|ки|ку|ка|ков))$/iu.test(candidate)
|
||||
/^(?:сейчас|на|дату|дате|остаток|остатки)$/iu.test(candidate)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
|
@ -1811,16 +1741,6 @@ function shouldExpandSampleForValueAnalytics(intent: AddressIntent): boolean {
|
|||
);
|
||||
}
|
||||
|
||||
function shouldPreferKnownFinancialCounterpartyAnchor(intent: AddressIntent): boolean {
|
||||
return (
|
||||
intent === "bank_operations_by_counterparty" ||
|
||||
intent === "customer_revenue_and_payments" ||
|
||||
intent === "supplier_payouts_profile" ||
|
||||
intent === "list_documents_by_counterparty" ||
|
||||
intent === "list_contracts_by_counterparty"
|
||||
);
|
||||
}
|
||||
|
||||
export function extractAddressFilters(userMessage: string, intent: AddressIntent): AddressFilterExtraction {
|
||||
const rawText = String(userMessage ?? "").trim();
|
||||
const text = normalizeMojibakeString(rawText);
|
||||
|
|
@ -1908,14 +1828,6 @@ export function extractAddressFilters(userMessage: string, intent: AddressIntent
|
|||
}
|
||||
|
||||
const allowGenericCounterpartyAnchor = !isInventoryTraceIntent(intent);
|
||||
const knownFinancialCounterparty =
|
||||
allowGenericCounterpartyAnchor && shouldPreferKnownFinancialCounterpartyAnchor(intent)
|
||||
? extractKnownFinancialCounterpartyAnchor(text)
|
||||
: undefined;
|
||||
if (knownFinancialCounterparty && !filters.counterparty) {
|
||||
filters.counterparty = knownFinancialCounterparty;
|
||||
warnings.push("counterparty_anchor_derived_from_known_financial_name");
|
||||
}
|
||||
const counterpartyMatch = allowGenericCounterpartyAnchor ? text.match(COUNTERPARTY_PATTERN) : null;
|
||||
if (counterpartyMatch && !filters.counterparty) {
|
||||
filters.counterparty = cleanupAnchorValue(String(counterpartyMatch[1]));
|
||||
|
|
@ -2102,6 +2014,7 @@ export function extractAddressFilters(userMessage: string, intent: AddressIntent
|
|||
warnings.includes("period_derived_from_year_range_phrase") ||
|
||||
warnings.includes("period_derived_from_year_phrase");
|
||||
const preserveDerivedPeriodWindow =
|
||||
usesAsOfPrimaryWindow(intent) ||
|
||||
intent === "inventory_on_hand_as_of_date" ||
|
||||
intent === "inventory_supplier_stock_overlap_as_of_date";
|
||||
if (periodWasDerivedHeuristically && !warnings.includes("exact_historical_period_window_requested")) {
|
||||
|
|
|
|||
|
|
@ -2136,7 +2136,7 @@ function hasBidirectionalValueFlowComparisonSignal(text: string): boolean {
|
|||
normalized
|
||||
);
|
||||
const hasOutgoingCue =
|
||||
/(?:\u0438\u0441\u0445\u043e\u0434\u044f\u0449|\u0441\u043f\u0438\u0441\u0430\u043d|\u0437\u0430\u043f\u043b\u0430\u0442|\u043f\u043b\u0430\u0442\u0438\u043b|\u0432\u044b\u043f\u043b\u0430\u0442|\u0432\u044b\u043f\u043b\u0430\u0447|\u0443\u043f\u043b\u0430\u0442|\u043e\u043f\u043b\u0430\u0442|outflow|outgoing|payout|paid)/iu.test(
|
||||
/(?:\u0438\u0441\u0445\u043e\u0434\u044f\u0449|\u0441\u043f\u0438\u0441\u0430\u043d|\u0437\u0430\u043f\u043b\u0430\u0442|\u043f\u043b\u0430\u0442\u0438\u043b|\u043e\u043f\u043b\u0430\u0442|outflow|outgoing|payout)/iu.test(
|
||||
normalized
|
||||
);
|
||||
const hasComparisonCue =
|
||||
|
|
@ -2144,7 +2144,7 @@ function hasBidirectionalValueFlowComparisonSignal(text: string): boolean {
|
|||
normalized
|
||||
);
|
||||
const hasValueFlowCue =
|
||||
/(?:\u0434\u0435\u043d\u044c\u0433|\u0434\u0435\u043d\u0435\u0433|\u0434\u0435\u043d\u0435\u0436|\u0441\u0440\u0435\u0434\u0441\u0442\u0432|\u043f\u043e\u0442\u043e\u043a|\u043e\u0431\u043e\u0440\u043e\u0442|money|cash|funds|flow)/iu.test(
|
||||
/(?:\u0434\u0435\u043d\u044c\u0433|\u0434\u0435\u043d\u0435\u0433|\u0434\u0435\u043d\u0435\u0436|\u043f\u043e\u0442\u043e\u043a|\u043e\u0431\u043e\u0440\u043e\u0442|money|cash|flow)/iu.test(
|
||||
normalized
|
||||
);
|
||||
const hasNetAmountCue = /(?:сколько|сумм|итог|нетто|сальдо|минус|net|total|sum)/iu.test(normalized);
|
||||
|
|
@ -2166,11 +2166,7 @@ function hasVatPeriodInspectionBridgeSignal(text: string): boolean {
|
|||
normalized
|
||||
);
|
||||
const forecastOnlyCue = /(?:прогноз|план|примерн|ориентировочн)/iu.test(normalized) && !hasInspectionCue;
|
||||
const hasVatMovementInspectionCue =
|
||||
/(?:покаж|движен|операц|по\s+сч(?:е|ё)т|покаж|движен|операц|РїРѕ\s+СЃС‡(?:Рµ|С‘)С‚|show|movement|movements|operation|operations|account)/iu.test(
|
||||
normalized
|
||||
);
|
||||
return hasPeriodCue && (hasInspectionCue || hasVatMovementInspectionCue) && !forecastOnlyCue;
|
||||
return hasPeriodCue && hasInspectionCue && !forecastOnlyCue;
|
||||
}
|
||||
|
||||
function resolveUnicodeAddressIntentBridge(text: string): AddressIntentResolution | null {
|
||||
|
|
@ -2315,18 +2311,6 @@ 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");
|
||||
}
|
||||
|
|
@ -2586,11 +2570,7 @@ function resolveUnicodeAddressIntentBridge(text: string): AddressIntentResolutio
|
|||
if (
|
||||
/(?:поставщик|vendor|supplier|кому\s+(?:ушло|платили|заплатили)|выплат|исходящ|списан|сгрузил)/iu.test(normalized) &&
|
||||
!/(?:аванс.*(?:не\s+)?закрыт|закрыт.*аванс)/iu.test(normalized) &&
|
||||
(hasMoneyCue ||
|
||||
hasRankingCue ||
|
||||
/заплат|платил|платили|уплат|плат[её]ж|оплат|выплат|outflow|payout|хвост|задержк|проблем/iu.test(
|
||||
normalized
|
||||
))
|
||||
(hasMoneyCue || hasRankingCue || /плат[её]ж|оплат|выплат|outflow|payout|хвост|задержк|проблем/iu.test(normalized))
|
||||
) {
|
||||
return unicodeBridgeResolution(
|
||||
/(?:хвост|задержк|проблем)/iu.test(normalized) ? "list_payables_counterparties" : "supplier_payouts_profile",
|
||||
|
|
@ -2959,7 +2939,7 @@ function resolveUnicodeAddressIntentBridge(text: string): AddressIntentResolutio
|
|||
|
||||
if (
|
||||
/(?:поставщик|vendor|supplier|кому\s+(?:ушло|платили|заплатили)|выплат|исходящ|списан|сгрузил)/iu.test(normalized) &&
|
||||
(hasMoneyCue || hasRankingCue || /заплат|платил|платили|уплат|плат[её]ж|оплат|выплат|outflow|payout/iu.test(normalized))
|
||||
(hasMoneyCue || hasRankingCue || /плат[её]ж|оплат|выплат|outflow|payout/iu.test(normalized))
|
||||
) {
|
||||
return unicodeBridgeResolution(
|
||||
"supplier_payouts_profile",
|
||||
|
|
@ -3023,67 +3003,16 @@ 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_])намс(?=$|[^\p{L}0-9_])/giu, "$1нам")
|
||||
.replace(/(^|[^\p{L}0-9_])какиек(?=$|[^\p{L}0-9_])/giu, "$1какие");
|
||||
.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");
|
||||
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];
|
||||
|
|
@ -3105,22 +3034,6 @@ export function resolveAddressIntent(userMessage: string): AddressIntentResoluti
|
|||
};
|
||||
}
|
||||
|
||||
const hasExplicitVatLiabilityPeriodBridge =
|
||||
/(?:\u043d\u0434\u0441|vat)/iu.test(text) &&
|
||||
/(?:\b(?:19|20)\d{2}\b|\u0437\u0430\s+(?:\d{4}|\u0433\u043e\u0434|\u043f\u0435\u0440\u0438\u043e\u0434|\u043a\u0432\u0430\u0440\u0442\u0430\u043b|\u043c\u0435\u0441\u044f\u0446))/iu.test(
|
||||
text
|
||||
) &&
|
||||
/(?:\u043a\u0430\u043a\u043e\u0439|\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u043d\u0430\u0447\u0438\u0441\u043b|\u0443\u043f\u043b\u0430\u0447|\u0443\u043f\u043b\u0430\u0442|\u043f\u0440\u043e\u0434\u0430\u0436|\u043f\u043e\u043a\u0443\u043f|\u0432\u044b\u0447\u0435\u0442|\u043a\s+\u0443\u043f\u043b\u0430\u0442|\u043a\s+\u0432\u043e\u0437\u043c\u0435\u0449|\u043f\u043e\u0437\u0438\u0446|liability|payable|charged|paid|sales|purchase|deduction|position)/iu.test(
|
||||
text
|
||||
);
|
||||
if (hasExplicitVatLiabilityPeriodBridge) {
|
||||
return {
|
||||
intent: "vat_liability_confirmed_for_tax_period",
|
||||
confidence: "high",
|
||||
reasons: ["vat_liability_explicit_period_bridge_signal_detected"]
|
||||
};
|
||||
}
|
||||
|
||||
const hasLooseVatPayableBridge =
|
||||
/(?:\u043d\u0434\u0441|vat)/iu.test(text) &&
|
||||
/(?:\u043a\u0430\u043a\u043e\u0439\s+\u043d\u0434\u0441\s+(?:(?:\u043d\u0430\u043c|(?:\u043c\u044b\s+)?\u0434\u043e\u043b\u0436\u043d\u044b)\s+)?(?:\u043d\u0430\u0434\u043e|\u043d\u0443\u0436\u043d\u043e|\u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e)|(?:\u043d\u0430\u043c|\u043c\u044b\s+)?\u043d\u0430\u0434\u043e\s+(?:\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u0442\u044c|\u0441\u0433\u0440\u0443\u0437\u0438\u0442\u044c)|(?:\u043d\u0430\u043c|\u043c\u044b\s+)?\u043d\u0443\u0436\u043d\u043e\s+(?:\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u0442\u044c|\u0441\u0433\u0440\u0443\u0437\u0438\u0442\u044c)|\u043c\u044b\s+\u0434\u043e\u043b\u0436\u043d\u044b\s+(?:\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u0442\u044c|\u0441\u0433\u0440\u0443\u0437\u0438\u0442\u044c)|\u043d\u0434\u0441\s+\u043a\s+\u0443\u043f\u043b\u0430\u0442\u0435)/iu.test(
|
||||
|
|
|
|||
|
|
@ -88,9 +88,6 @@ interface NormalizedAddressRow {
|
|||
organization?: string | null;
|
||||
counterparty?: string | null;
|
||||
contract?: string | null;
|
||||
operation_kind?: string | null;
|
||||
payment_purpose?: string | null;
|
||||
comment?: string | null;
|
||||
}
|
||||
|
||||
interface AddressTryHandleOptions {
|
||||
|
|
@ -1468,14 +1465,6 @@ function toNormalizedRows(rows: Array<Record<string, unknown>>): NormalizedAddre
|
|||
);
|
||||
const counterparty = firstNonEmptyString(row.Контрагент, row.Counterparty, row.counterparty);
|
||||
const contract = firstNonEmptyString(row.Договор, row.Contract, row.contract);
|
||||
const operationKind = firstNonEmptyString(row.ВидОперации, row.OperationKind, row.operation_kind, row.operationType);
|
||||
const paymentPurpose = firstNonEmptyString(
|
||||
row.НазначениеПлатежа,
|
||||
row.PaymentPurpose,
|
||||
row.payment_purpose,
|
||||
row.paymentPurpose
|
||||
);
|
||||
const comment = firstNonEmptyString(row.Комментарий, row.Comment, row.comment);
|
||||
const analytics = collectAnalyticsStrings(row);
|
||||
|
||||
return {
|
||||
|
|
@ -1490,10 +1479,7 @@ function toNormalizedRows(rows: Array<Record<string, unknown>>): NormalizedAddre
|
|||
warehouse,
|
||||
organization,
|
||||
counterparty,
|
||||
contract,
|
||||
operation_kind: operationKind,
|
||||
payment_purpose: paymentPurpose,
|
||||
comment
|
||||
contract
|
||||
};
|
||||
})
|
||||
.filter((item) => Boolean(item.period || item.registrator));
|
||||
|
|
|
|||
|
|
@ -207,52 +207,6 @@ const OPEN_CONTRACTS_CONFIRMED_AS_OF_QUERY_TEMPLATE = `
|
|||
Сумма __ORDER_DIRECTION__
|
||||
`;
|
||||
|
||||
const DEBT_DUE_DATE_AGING_QUERY_TEMPLATE = `
|
||||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
||||
__AS_OF_EXPR__ КАК Период,
|
||||
"DUE_DATE_OPEN_BALANCE" КАК Регистратор,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Счет) КАК СчетДт,
|
||||
"" КАК СчетКт,
|
||||
Остатки.СуммаРазвернутыйОстатокДт КАК Сумма,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто1) КАК СубконтоДт1,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто2) КАК СубконтоДт2,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто3) КАК СубконтоДт3,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто1) КАК Контрагент,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто2) КАК Договор,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто3) КАК ДокументРасчетов,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Организация) КАК Организация,
|
||||
ВЫРАЗИТЬ(Остатки.Субконто2 КАК Справочник.ДоговорыКонтрагентов).Дата КАК ДатаДоговора,
|
||||
ВЫРАЗИТЬ(Остатки.Субконто2 КАК Справочник.ДоговорыКонтрагентов).УстановленСрокОплаты КАК УстановленСрокОплаты,
|
||||
ВЫРАЗИТЬ(Остатки.Субконто2 КАК Справочник.ДоговорыКонтрагентов).СрокОплаты КАК СрокОплаты,
|
||||
"debit_open_balance" КАК НаправлениеОстатка
|
||||
ИЗ
|
||||
РегистрБухгалтерии.Хозрасчетный.Остатки(__AS_OF_EXPR__, , , ) КАК Остатки
|
||||
__WHERE_DT__
|
||||
ОБЪЕДИНИТЬ ВСЕ
|
||||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
||||
__AS_OF_EXPR__ КАК Период,
|
||||
"DUE_DATE_OPEN_BALANCE" КАК Регистратор,
|
||||
"" КАК СчетДт,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Счет) КАК СчетКт,
|
||||
Остатки.СуммаРазвернутыйОстатокКт КАК Сумма,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто1) КАК СубконтоДт1,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто2) КАК СубконтоДт2,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто3) КАК СубконтоДт3,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто1) КАК Контрагент,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто2) КАК Договор,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Субконто3) КАК ДокументРасчетов,
|
||||
ПРЕДСТАВЛЕНИЕ(Остатки.Организация) КАК Организация,
|
||||
ВЫРАЗИТЬ(Остатки.Субконто2 КАК Справочник.ДоговорыКонтрагентов).Дата КАК ДатаДоговора,
|
||||
ВЫРАЗИТЬ(Остатки.Субконто2 КАК Справочник.ДоговорыКонтрагентов).УстановленСрокОплаты КАК УстановленСрокОплаты,
|
||||
ВЫРАЗИТЬ(Остатки.Субконто2 КАК Справочник.ДоговорыКонтрагентов).СрокОплаты КАК СрокОплаты,
|
||||
"credit_open_balance" КАК НаправлениеОстатка
|
||||
ИЗ
|
||||
РегистрБухгалтерии.Хозрасчетный.Остатки(__AS_OF_EXPR__, , , ) КАК Остатки
|
||||
__WHERE_KT__
|
||||
УПОРЯДОЧИТЬ ПО
|
||||
Сумма __ORDER_DIRECTION__
|
||||
`;
|
||||
|
||||
const VAT_PAYABLE_CONFIRMED_AS_OF_QUERY_TEMPLATE = `
|
||||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
||||
__AS_OF_EXPR__ КАК Период,
|
||||
|
|
@ -296,62 +250,6 @@ const INVENTORY_ON_HAND_AS_OF_QUERY_TEMPLATE = `
|
|||
Количество __ORDER_DIRECTION__
|
||||
`;
|
||||
|
||||
const INVENTORY_QUALITY_EVENTS_QUERY_TEMPLATE = `
|
||||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
||||
Списание.Дата КАК Период,
|
||||
ПРЕДСТАВЛЕНИЕ(Списание.Ссылка) КАК Регистратор,
|
||||
"Списание товаров" КАК ТипСобытия,
|
||||
ПРЕДСТАВЛЕНИЕ(Списание.Организация) КАК Организация,
|
||||
ПРЕДСТАВЛЕНИЕ(Списание.Склад) КАК Склад,
|
||||
Списание.СуммаДокумента КАК Сумма,
|
||||
Списание.Основание КАК Основание,
|
||||
Списание.Комментарий КАК Комментарий
|
||||
ИЗ
|
||||
Документ.СписаниеТоваров КАК Списание
|
||||
__WHERE_WRITE_OFF__
|
||||
ОБЪЕДИНИТЬ ВСЕ
|
||||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
||||
Оприходование.Дата КАК Период,
|
||||
ПРЕДСТАВЛЕНИЕ(Оприходование.Ссылка) КАК Регистратор,
|
||||
"Оприходование товаров" КАК ТипСобытия,
|
||||
ПРЕДСТАВЛЕНИЕ(Оприходование.Организация) КАК Организация,
|
||||
ПРЕДСТАВЛЕНИЕ(Оприходование.Склад) КАК Склад,
|
||||
Оприходование.СуммаДокумента КАК Сумма,
|
||||
Оприходование.Основание КАК Основание,
|
||||
Оприходование.Комментарий КАК Комментарий
|
||||
ИЗ
|
||||
Документ.ОприходованиеТоваров КАК Оприходование
|
||||
__WHERE_RECEIPT__
|
||||
ОБЪЕДИНИТЬ ВСЕ
|
||||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
||||
Инвентаризация.Дата КАК Период,
|
||||
ПРЕДСТАВЛЕНИЕ(Инвентаризация.Ссылка) КАК Регистратор,
|
||||
"Инвентаризация товаров на складе" КАК ТипСобытия,
|
||||
ПРЕДСТАВЛЕНИЕ(Инвентаризация.Организация) КАК Организация,
|
||||
ПРЕДСТАВЛЕНИЕ(Инвентаризация.Склад) КАК Склад,
|
||||
0 КАК Сумма,
|
||||
Инвентаризация.ПричинаПроведенияИнвентаризации КАК Основание,
|
||||
Инвентаризация.Комментарий КАК Комментарий
|
||||
ИЗ
|
||||
Документ.ИнвентаризацияТоваровНаСкладе КАК Инвентаризация
|
||||
__WHERE_INVENTORY_COUNT__
|
||||
ОБЪЕДИНИТЬ ВСЕ
|
||||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
||||
Переоценка.Дата КАК Период,
|
||||
ПРЕДСТАВЛЕНИЕ(Переоценка.Ссылка) КАК Регистратор,
|
||||
"Переоценка товаров в рознице" КАК ТипСобытия,
|
||||
ПРЕДСТАВЛЕНИЕ(Переоценка.Организация) КАК Организация,
|
||||
ПРЕДСТАВЛЕНИЕ(Переоценка.Склад) КАК Склад,
|
||||
0 КАК Сумма,
|
||||
"" КАК Основание,
|
||||
Переоценка.Комментарий КАК Комментарий
|
||||
ИЗ
|
||||
Документ.ПереоценкаТоваровВРознице КАК Переоценка
|
||||
__WHERE_REVALUATION__
|
||||
УПОРЯДОЧИТЬ ПО
|
||||
Период __ORDER_DIRECTION__
|
||||
`;
|
||||
|
||||
const BANK_DOCS_QUERY_TEMPLATE = `
|
||||
ВЫБРАТЬ ПЕРВЫЕ __LIMIT__
|
||||
БанкСписание.Дата КАК Период,
|
||||
|
|
@ -360,10 +258,7 @@ const BANK_DOCS_QUERY_TEMPLATE = `
|
|||
"" КАК СчетКт,
|
||||
БанкСписание.СуммаДокумента КАК Сумма,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкСписание.Контрагент) КАК Контрагент,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкСписание.ДоговорКонтрагента) КАК Договор,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкСписание.ВидОперации) КАК ВидОперации,
|
||||
БанкСписание.НазначениеПлатежа КАК НазначениеПлатежа,
|
||||
БанкСписание.Комментарий КАК Комментарий
|
||||
ПРЕДСТАВЛЕНИЕ(БанкСписание.ДоговорКонтрагента) КАК Договор
|
||||
ИЗ
|
||||
Документ.СписаниеСРасчетногоСчета КАК БанкСписание
|
||||
__WHERE_OUT__
|
||||
|
|
@ -375,10 +270,7 @@ __WHERE_OUT__
|
|||
"" КАК СчетКт,
|
||||
БанкПоступление.СуммаДокумента КАК Сумма,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкПоступление.Контрагент) КАК Контрагент,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкПоступление.ДоговорКонтрагента) КАК Договор,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкПоступление.ВидОперации) КАК ВидОперации,
|
||||
"" КАК НазначениеПлатежа,
|
||||
БанкПоступление.Комментарий КАК Комментарий
|
||||
ПРЕДСТАВЛЕНИЕ(БанкПоступление.ДоговорКонтрагента) КАК Договор
|
||||
ИЗ
|
||||
Документ.ПоступлениеНаРасчетныйСчет КАК БанкПоступление
|
||||
__WHERE_IN__
|
||||
|
|
@ -650,9 +542,7 @@ const CUSTOMER_REVENUE_PROFILE_QUERY_TEMPLATE = `
|
|||
"" КАК СчетКт,
|
||||
БанкПоступление.СуммаДокумента КАК Сумма,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкПоступление.Контрагент) КАК Контрагент,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкПоступление.ДоговорКонтрагента) КАК Договор,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкПоступление.ВидОперации) КАК ВидОперации,
|
||||
БанкПоступление.Комментарий КАК Комментарий
|
||||
ПРЕДСТАВЛЕНИЕ(БанкПоступление.ДоговорКонтрагента) КАК Договор
|
||||
ИЗ
|
||||
Документ.ПоступлениеНаРасчетныйСчет КАК БанкПоступление
|
||||
__WHERE_IN__
|
||||
|
|
@ -668,10 +558,7 @@ const SUPPLIER_PAYOUT_PROFILE_QUERY_TEMPLATE = `
|
|||
"" КАК СчетКт,
|
||||
БанкСписание.СуммаДокумента КАК Сумма,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкСписание.Контрагент) КАК Контрагент,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкСписание.ДоговорКонтрагента) КАК Договор,
|
||||
ПРЕДСТАВЛЕНИЕ(БанкСписание.ВидОперации) КАК ВидОперации,
|
||||
БанкСписание.НазначениеПлатежа КАК НазначениеПлатежа,
|
||||
БанкСписание.Комментарий КАК Комментарий
|
||||
ПРЕДСТАВЛЕНИЕ(БанкСписание.ДоговорКонтрагента) КАК Договор
|
||||
ИЗ
|
||||
Документ.СписаниеСРасчетногоСчета КАК БанкСписание
|
||||
__WHERE_OUT__
|
||||
|
|
@ -860,7 +747,7 @@ const BASE_RECIPES: AddressRecipeDefinition[] = [
|
|||
purpose: "Build customer value ranking and incoming deal profile from bank inflow docs",
|
||||
required_filters: [],
|
||||
optional_filters: ["period_from", "period_to", "organization", "counterparty", "contract", "limit", "sort"],
|
||||
default_limit: 200,
|
||||
default_limit: 20,
|
||||
account_scope_mode: "preferred",
|
||||
query_template: "customer_revenue_profile"
|
||||
},
|
||||
|
|
@ -870,7 +757,7 @@ const BASE_RECIPES: AddressRecipeDefinition[] = [
|
|||
purpose: "Build supplier payout ranking and outgoing deal profile from bank outflow docs",
|
||||
required_filters: [],
|
||||
optional_filters: ["period_from", "period_to", "organization", "counterparty", "contract", "limit", "sort"],
|
||||
default_limit: 200,
|
||||
default_limit: 20,
|
||||
account_scope_mode: "preferred",
|
||||
query_template: "supplier_payout_profile"
|
||||
},
|
||||
|
|
@ -915,17 +802,6 @@ const BASE_RECIPES: AddressRecipeDefinition[] = [
|
|||
account_scope_mode: "preferred",
|
||||
query_template: "vat_liability_confirmed_tax_period_profile"
|
||||
},
|
||||
{
|
||||
recipe_id: "address_accounting_financial_result_for_organization_v1",
|
||||
intent: "accounting_financial_result_for_organization",
|
||||
purpose: "Build reviewed accounting financial-result aggregate from 90/91/99 period-close movements",
|
||||
required_filters: ["period_from", "period_to"],
|
||||
optional_filters: ["organization", "limit", "sort"],
|
||||
default_limit: 32,
|
||||
account_scope: ["90", "91", "99"],
|
||||
account_scope_mode: "strict",
|
||||
query_template: "accounting_financial_result_profile"
|
||||
},
|
||||
{
|
||||
recipe_id: "address_inventory_on_hand_as_of_date_v1",
|
||||
intent: "inventory_on_hand_as_of_date",
|
||||
|
|
@ -1025,16 +901,6 @@ const BASE_RECIPES: AddressRecipeDefinition[] = [
|
|||
account_scope_mode: "strict",
|
||||
query_template: "inventory_aging_by_purchase_date_profile"
|
||||
},
|
||||
{
|
||||
recipe_id: "address_inventory_quality_events_for_organization_v1",
|
||||
intent: "inventory_quality_events_for_organization",
|
||||
purpose: "Check posted inventory quality event documents: write-offs, stocktaking, receipt adjustments, and retail revaluation",
|
||||
required_filters: [],
|
||||
optional_filters: ["as_of_date", "period_from", "period_to", "organization", "warehouse", "limit", "sort"],
|
||||
default_limit: 400,
|
||||
account_scope_mode: "preferred",
|
||||
query_template: "inventory_quality_events_profile"
|
||||
},
|
||||
{
|
||||
recipe_id: "address_open_contracts_confirmed_as_of_date_v1",
|
||||
intent: "open_contracts_confirmed_as_of_date",
|
||||
|
|
@ -1046,17 +912,6 @@ const BASE_RECIPES: AddressRecipeDefinition[] = [
|
|||
account_scope_mode: "strict",
|
||||
query_template: "open_contracts_confirmed_as_of_balance_profile"
|
||||
},
|
||||
{
|
||||
recipe_id: "address_debt_due_date_aging_for_organization_v1",
|
||||
intent: "debt_due_date_aging_for_organization",
|
||||
purpose: "Check open 60/62/76 settlements against contract payment-term fields and settlement document dates before claiming overdue debt",
|
||||
required_filters: ["as_of_date"],
|
||||
optional_filters: ["period_from", "period_to", "organization", "counterparty", "contract", "limit", "sort"],
|
||||
default_limit: 400,
|
||||
account_scope: ["60", "62", "76"],
|
||||
account_scope_mode: "strict",
|
||||
query_template: "debt_due_date_aging_profile"
|
||||
},
|
||||
{
|
||||
recipe_id: "address_contracts_by_counterparty_v1",
|
||||
intent: "list_contracts_by_counterparty",
|
||||
|
|
@ -1291,37 +1146,6 @@ function buildContractValueWhereClause(filters: AddressFilterSet, fieldPath: str
|
|||
]);
|
||||
}
|
||||
|
||||
function buildContractReferenceCondition(filters: AddressFilterSet, fieldPaths: string[]): string | null {
|
||||
const contract = typeof filters.contract === "string" ? filters.contract.trim() : "";
|
||||
if (!contract) {
|
||||
return null;
|
||||
}
|
||||
const contractTokens = Array.from(
|
||||
new Set(
|
||||
contract
|
||||
.split(/[^A-Za-zА-Яа-яЁё0-9]+/u)
|
||||
.map((token) => token.trim())
|
||||
.filter((token) => token.length >= 3)
|
||||
.filter((token) => !["договор", "дог"].includes(token.toLowerCase()))
|
||||
)
|
||||
);
|
||||
const tokens = contractTokens.length > 0 ? contractTokens : [contract];
|
||||
const clauses = fieldPaths
|
||||
.map((fieldPath) => String(fieldPath ?? "").trim())
|
||||
.filter((fieldPath) => fieldPath.length > 0)
|
||||
.map((fieldPath) => {
|
||||
const tokenConditions = tokens.map((token) => {
|
||||
const escapedToken = toQueryStringLiteral(token);
|
||||
return `${fieldPath}.Наименование ПОДОБНО "%${escapedToken}%"`;
|
||||
});
|
||||
return tokenConditions.length === 1 ? tokenConditions[0] : `(${tokenConditions.join(" И ")})`;
|
||||
});
|
||||
if (clauses.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return clauses.length === 1 ? clauses[0] : `(${clauses.join(" ИЛИ ")})`;
|
||||
}
|
||||
|
||||
function normalizeAccountTokenForQuery(value: string): string {
|
||||
const source = String(value ?? "").trim().replace(",", ".");
|
||||
const match = source.match(/^(\d{2})(?:\.(\d{1,2}))?/);
|
||||
|
|
@ -1427,48 +1251,6 @@ function buildAccountPrefixPredicate(fieldPath: string, prefixes: string[]): str
|
|||
return clauses.length === 1 ? clauses[0] : `(${clauses.join(" ИЛИ ")})`;
|
||||
}
|
||||
|
||||
function buildDebtDueDateAgingWhereClause(
|
||||
filters: AddressFilterSet,
|
||||
amountFieldPath: string,
|
||||
accountPredicate: string
|
||||
): string {
|
||||
const conditions = [
|
||||
`${amountFieldPath} > 0`,
|
||||
`(${accountPredicate})`,
|
||||
buildOrganizationReferenceCondition(filters, ["Остатки.Организация"]),
|
||||
buildCounterpartyReferenceCondition(filters, ["Остатки.Субконто1"]),
|
||||
buildContractReferenceCondition(filters, ["Остатки.Субконто2"])
|
||||
].filter((item): item is string => Boolean(item));
|
||||
return `ГДЕ\n ${conditions.join("\n И ")}`;
|
||||
}
|
||||
|
||||
function buildDebtDueDateAgingQuery(filters: AddressFilterSet, resolvedLimit: number): string {
|
||||
const asOfExpr =
|
||||
(typeof filters.as_of_date === "string" && filters.as_of_date.trim().length > 0
|
||||
? toDateTimeExpr(filters.as_of_date, true)
|
||||
: null) ??
|
||||
(typeof filters.period_to === "string" && filters.period_to.trim().length > 0
|
||||
? toDateTimeExpr(filters.period_to, true)
|
||||
: null) ??
|
||||
(typeof filters.period_from === "string" && filters.period_from.trim().length > 0
|
||||
? toDateTimeExpr(filters.period_from, true)
|
||||
: null) ??
|
||||
"ТЕКУЩАЯДАТА()";
|
||||
const accountPredicate = buildAccountPrefixPredicate("Остатки.Счет", ["60", "62", "76"]);
|
||||
return DEBT_DUE_DATE_AGING_QUERY_TEMPLATE
|
||||
.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||
.replaceAll("__AS_OF_EXPR__", asOfExpr)
|
||||
.replaceAll(
|
||||
"__WHERE_DT__",
|
||||
buildDebtDueDateAgingWhereClause(filters, "Остатки.СуммаРазвернутыйОстатокДт", accountPredicate)
|
||||
)
|
||||
.replaceAll(
|
||||
"__WHERE_KT__",
|
||||
buildDebtDueDateAgingWhereClause(filters, "Остатки.СуммаРазвернутыйОстатокКт", accountPredicate)
|
||||
)
|
||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||
}
|
||||
|
||||
function buildInventoryMovementQuery(
|
||||
filters: AddressFilterSet,
|
||||
resolvedLimit: number,
|
||||
|
|
@ -1509,77 +1291,6 @@ function buildInventoryMovementQuery(
|
|||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||
}
|
||||
|
||||
function buildWarehouseReferenceCondition(filters: AddressFilterSet, fieldPaths: string[]): string | null {
|
||||
const warehouse = typeof filters.warehouse === "string" ? filters.warehouse.trim() : "";
|
||||
if (!warehouse) {
|
||||
return null;
|
||||
}
|
||||
const tokens = Array.from(
|
||||
new Set(
|
||||
warehouse
|
||||
.split(/[^A-Za-zА-Яа-яЁё0-9]+/u)
|
||||
.map((token) => token.trim())
|
||||
.filter((token) => token.length >= 3)
|
||||
.filter((token) => !["склад", "warehouse"].includes(token.toLowerCase()))
|
||||
)
|
||||
);
|
||||
const effectiveTokens = tokens.length > 0 ? tokens : [warehouse];
|
||||
const clauses = fieldPaths
|
||||
.map((fieldPath) => String(fieldPath ?? "").trim())
|
||||
.filter((fieldPath) => fieldPath.length > 0)
|
||||
.map((fieldPath) => {
|
||||
const tokenConditions = effectiveTokens.map((token) => {
|
||||
const escapedToken = toQueryStringLiteral(token);
|
||||
return `${fieldPath}.Наименование ПОДОБНО "%${escapedToken}%"`;
|
||||
});
|
||||
return tokenConditions.length === 1 ? tokenConditions[0] : `(${tokenConditions.join(" И ")})`;
|
||||
});
|
||||
if (clauses.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return clauses.length === 1 ? clauses[0] : `(${clauses.join(" ИЛИ ")})`;
|
||||
}
|
||||
|
||||
function buildInventoryQualityDocumentWhereClause(
|
||||
filters: AddressFilterSet,
|
||||
dateFieldPath: string,
|
||||
organizationFieldPath: string,
|
||||
warehouseFieldPath: string
|
||||
): string {
|
||||
return buildWhereClause(filters, dateFieldPath, [
|
||||
`${dateFieldPath.replace(/\.Дата$/u, ".Проведен")} = ИСТИНА`,
|
||||
buildOrganizationReferenceCondition(filters, [organizationFieldPath]),
|
||||
buildWarehouseReferenceCondition(filters, [warehouseFieldPath])
|
||||
].filter((item): item is string => Boolean(item)));
|
||||
}
|
||||
|
||||
function buildInventoryQualityEventsQuery(filters: AddressFilterSet, resolvedLimit: number): string {
|
||||
return INVENTORY_QUALITY_EVENTS_QUERY_TEMPLATE
|
||||
.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||
.replace(
|
||||
"__WHERE_WRITE_OFF__",
|
||||
buildInventoryQualityDocumentWhereClause(filters, "Списание.Дата", "Списание.Организация", "Списание.Склад")
|
||||
)
|
||||
.replace(
|
||||
"__WHERE_RECEIPT__",
|
||||
buildInventoryQualityDocumentWhereClause(filters, "Оприходование.Дата", "Оприходование.Организация", "Оприходование.Склад")
|
||||
)
|
||||
.replace(
|
||||
"__WHERE_INVENTORY_COUNT__",
|
||||
buildInventoryQualityDocumentWhereClause(
|
||||
filters,
|
||||
"Инвентаризация.Дата",
|
||||
"Инвентаризация.Организация",
|
||||
"Инвентаризация.Склад"
|
||||
)
|
||||
)
|
||||
.replace(
|
||||
"__WHERE_REVALUATION__",
|
||||
buildInventoryQualityDocumentWhereClause(filters, "Переоценка.Дата", "Переоценка.Организация", "Переоценка.Склад")
|
||||
)
|
||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||
}
|
||||
|
||||
function buildInventoryItemReferenceCondition(filters: AddressFilterSet, fieldPaths: string[]): string | null {
|
||||
const item = typeof filters.item === "string" ? filters.item.trim() : "";
|
||||
if (!item) {
|
||||
|
|
@ -1629,179 +1340,6 @@ function buildCounterpartyReferenceCondition(filters: AddressFilterSet, fieldPat
|
|||
return clauses.length === 1 ? clauses[0] : `(${clauses.join(" ИЛИ ")})`;
|
||||
}
|
||||
|
||||
const ORGANIZATION_REFERENCE_STOP_WORDS = new Set([
|
||||
"ооо",
|
||||
"зао",
|
||||
"оао",
|
||||
"ао",
|
||||
"пао",
|
||||
"ип",
|
||||
"на",
|
||||
"за",
|
||||
"по",
|
||||
"конец",
|
||||
"начало",
|
||||
"год",
|
||||
"года",
|
||||
"период",
|
||||
"можно",
|
||||
"точно",
|
||||
"понять",
|
||||
"какая",
|
||||
"какой",
|
||||
"какие",
|
||||
"какую",
|
||||
"компания",
|
||||
"компании",
|
||||
"организация",
|
||||
"организации",
|
||||
"дебиторка",
|
||||
"дебиторки",
|
||||
"кредиторка",
|
||||
"кредиторки",
|
||||
"просрочена",
|
||||
"просроченные",
|
||||
"просрочка",
|
||||
"срок",
|
||||
"оплаты",
|
||||
"прибыль",
|
||||
"маржа",
|
||||
"ндс"
|
||||
]);
|
||||
|
||||
const ORGANIZATION_REFERENCE_BOUNDARY_WORDS = new Set([
|
||||
"на",
|
||||
"за",
|
||||
"конец",
|
||||
"начало",
|
||||
"можно",
|
||||
"точно",
|
||||
"понять",
|
||||
"какая",
|
||||
"какой",
|
||||
"какие",
|
||||
"какую",
|
||||
"дебиторка",
|
||||
"дебиторки",
|
||||
"кредиторка",
|
||||
"кредиторки",
|
||||
"просрочена",
|
||||
"просроченные",
|
||||
"просрочка",
|
||||
"прибыль",
|
||||
"маржа",
|
||||
"ндс"
|
||||
]);
|
||||
|
||||
function organizationReferenceTokens(organization: string): string[] {
|
||||
const rawTokens = organization
|
||||
.split(/[^A-Za-zА-Яа-яЁё0-9]+/u)
|
||||
.map((token) => token.trim())
|
||||
.filter((token) => token.length > 0);
|
||||
const boundaryIndex = rawTokens.findIndex((token) => {
|
||||
const lower = token.toLowerCase();
|
||||
return /^\d+$/.test(token) || ORGANIZATION_REFERENCE_BOUNDARY_WORDS.has(lower);
|
||||
});
|
||||
const scopedTokens = boundaryIndex > 0 ? rawTokens.slice(0, boundaryIndex) : rawTokens;
|
||||
return Array.from(
|
||||
new Set(
|
||||
scopedTokens
|
||||
.filter((token) => token.length >= 3)
|
||||
.filter((token) => !/^\d+$/.test(token))
|
||||
.filter((token) => !ORGANIZATION_REFERENCE_STOP_WORDS.has(token.toLowerCase()))
|
||||
)
|
||||
).slice(0, 4);
|
||||
}
|
||||
|
||||
function buildOrganizationReferenceCondition(filters: AddressFilterSet, fieldPaths: string[]): string | null {
|
||||
const organization = typeof filters.organization === "string" ? filters.organization.trim() : "";
|
||||
if (!organization) {
|
||||
return null;
|
||||
}
|
||||
const organizationTokens = organizationReferenceTokens(organization);
|
||||
const tokens = organizationTokens.length > 0 ? organizationTokens : [organization];
|
||||
const clauses = fieldPaths
|
||||
.map((fieldPath) => String(fieldPath ?? "").trim())
|
||||
.filter((fieldPath) => fieldPath.length > 0)
|
||||
.map((fieldPath) => {
|
||||
const tokenConditions = tokens.map((token) => {
|
||||
const escapedToken = toQueryStringLiteral(token);
|
||||
return `(Организации.Наименование ПОДОБНО "%${escapedToken}%" ИЛИ Организации.НаименованиеПолное ПОДОБНО "%${escapedToken}%")`;
|
||||
});
|
||||
const referenceSubquery =
|
||||
`(ВЫБРАТЬ Организации.Ссылка ИЗ Справочник.Организации КАК Организации ` +
|
||||
`ГДЕ ${tokenConditions.length === 1 ? tokenConditions[0] : tokenConditions.join(" И ")})`;
|
||||
return `${fieldPath} В ${referenceSubquery}`;
|
||||
});
|
||||
if (clauses.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return clauses.length === 1 ? clauses[0] : `(${clauses.join(" ИЛИ ")})`;
|
||||
}
|
||||
|
||||
function buildAccountingFinancialResultAggregateSelect(
|
||||
filters: AddressFilterSet,
|
||||
marker: string,
|
||||
debitLabel: string,
|
||||
creditLabel: string,
|
||||
debitPrefixes: string[],
|
||||
creditPrefixes: string[]
|
||||
): string {
|
||||
const whereClause = buildWhereClause(
|
||||
filters,
|
||||
"Движения.Период",
|
||||
[
|
||||
debitPrefixes.length > 0 ? buildAccountPrefixPredicate("Движения.СчетДт", debitPrefixes) : null,
|
||||
creditPrefixes.length > 0 ? buildAccountPrefixPredicate("Движения.СчетКт", creditPrefixes) : null,
|
||||
buildOrganizationReferenceCondition(filters, ["Движения.Организация"])
|
||||
].filter((item): item is string => Boolean(item))
|
||||
);
|
||||
return `
|
||||
ВЫБРАТЬ
|
||||
ДАТАВРЕМЯ(2000, 1, 1, 0, 0, 0) КАК Период,
|
||||
"${marker}" КАК Регистратор,
|
||||
"${debitLabel}" КАК СчетДт,
|
||||
"${creditLabel}" КАК СчетКт,
|
||||
ЕСТЬNULL(СУММА(Движения.Сумма), 0) КАК Сумма,
|
||||
"" КАК СубконтоДт1,
|
||||
"" КАК СубконтоДт2,
|
||||
"" КАК СубконтоДт3,
|
||||
"" КАК СубконтоКт1,
|
||||
"" КАК СубконтоКт2,
|
||||
"" КАК СубконтоКт3,
|
||||
"" КАК Организация
|
||||
ИЗ
|
||||
РегистрБухгалтерии.Хозрасчетный.ДвиженияССубконто КАК Движения
|
||||
${whereClause}`;
|
||||
}
|
||||
|
||||
function buildAccountingFinancialResultQuery(filters: AddressFilterSet): string {
|
||||
const rows = [
|
||||
["ACC90_REVENUE_KT", "ANY", "90.01", [], ["90.01"]],
|
||||
["ACC90_COST_DT", "90.02", "ANY", ["90.02"], []],
|
||||
["ACC90_SELLING_DT", "90.07", "ANY", ["90.07"], []],
|
||||
["ACC90_ADMIN_DT", "90.08", "ANY", ["90.08"], []],
|
||||
["ACC90_RESULT_TO_99_PROFIT", "90.09", "99", ["90.09"], ["99"]],
|
||||
["ACC90_RESULT_FROM_99_LOSS", "99", "90.09", ["99"], ["90.09"]],
|
||||
["ACC91_RESULT_TO_99_PROFIT", "91.09", "99", ["91.09"], ["99"]],
|
||||
["ACC91_RESULT_FROM_99_LOSS", "99", "91.09", ["99"], ["91.09"]],
|
||||
["ACC99_TO84_PROFIT_TRANSFER", "99", "84", ["99"], ["84"]],
|
||||
["ACC84_TO99_LOSS_TRANSFER", "84", "99", ["84"], ["99"]]
|
||||
] as const;
|
||||
return rows
|
||||
.map(([marker, debitLabel, creditLabel, debitPrefixes, creditPrefixes]) =>
|
||||
buildAccountingFinancialResultAggregateSelect(
|
||||
filters,
|
||||
marker,
|
||||
debitLabel,
|
||||
creditLabel,
|
||||
[...debitPrefixes],
|
||||
[...creditPrefixes]
|
||||
).trim()
|
||||
)
|
||||
.join("\nОБЪЕДИНИТЬ ВСЕ\n");
|
||||
}
|
||||
|
||||
function buildInventorySaleDocumentQuery(filters: AddressFilterSet, resolvedLimit: number): string {
|
||||
const itemCondition = buildInventoryItemReferenceCondition(filters, ["Товары.Номенклатура"]);
|
||||
return INVENTORY_SALE_DOCUMENTS_QUERY_TEMPLATE
|
||||
|
|
@ -1920,8 +1458,6 @@ function maxLimitForIntent(intent: AddressIntent): number {
|
|||
intent === "contract_usage_and_value" ||
|
||||
intent === "vat_payable_forecast" ||
|
||||
intent === "vat_liability_confirmed_for_tax_period" ||
|
||||
intent === "accounting_financial_result_for_organization" ||
|
||||
intent === "debt_due_date_aging_for_organization" ||
|
||||
intent === "inventory_on_hand_as_of_date" ||
|
||||
intent === "inventory_purchase_provenance_for_item" ||
|
||||
intent === "inventory_purchase_documents_for_item" ||
|
||||
|
|
@ -1931,7 +1467,6 @@ function maxLimitForIntent(intent: AddressIntent): number {
|
|||
intent === "inventory_profitability_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date" ||
|
||||
intent === "inventory_quality_events_for_organization" ||
|
||||
intent === "open_contracts_confirmed_as_of_date" ||
|
||||
intent === "list_contracts_by_counterparty" ||
|
||||
intent === "list_documents_by_counterparty" ||
|
||||
|
|
@ -1983,8 +1518,7 @@ export function buildAddressRecipePlan(
|
|||
recipe.query_template === "counterparty_roles_profile" ||
|
||||
recipe.query_template === "contract_usage_profile" ||
|
||||
recipe.query_template === "vat_payable_forecast_profile" ||
|
||||
recipe.query_template === "vat_liability_confirmed_tax_period_profile" ||
|
||||
recipe.query_template === "accounting_financial_result_profile";
|
||||
recipe.query_template === "vat_liability_confirmed_tax_period_profile";
|
||||
const baseLimit =
|
||||
typeof filters.limit === "number" && Number.isFinite(filters.limit)
|
||||
? Math.max(1, Math.min(maxLimit, Math.trunc(filters.limit)))
|
||||
|
|
@ -2125,10 +1659,6 @@ export function buildAddressRecipePlan(
|
|||
.replaceAll("__WHERE_CLAUSE__", buildManagementWhereClause(filters, "Движения.Период"))
|
||||
.replaceAll("__PERIOD_TO_EXPR__", periodToExpr);
|
||||
})()
|
||||
: recipe.query_template === "accounting_financial_result_profile"
|
||||
? buildAccountingFinancialResultQuery(filters)
|
||||
: recipe.query_template === "debt_due_date_aging_profile"
|
||||
? buildDebtDueDateAgingQuery(filters, resolvedLimit)
|
||||
: recipe.query_template === "vat_payable_confirmed_as_of_balance_profile"
|
||||
? (() => {
|
||||
const asOfExpr =
|
||||
|
|
@ -2186,8 +1716,6 @@ export function buildAddressRecipePlan(
|
|||
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
||||
: recipe.query_template === "inventory_aging_by_purchase_date_profile"
|
||||
? buildInventoryMovementQuery(filters, resolvedLimit, "dt")
|
||||
: recipe.query_template === "inventory_quality_events_profile"
|
||||
? buildInventoryQualityEventsQuery(filters, resolvedLimit)
|
||||
: recipe.query_template === "contracts_by_counterparty_profile"
|
||||
? CONTRACTS_BY_COUNTERPARTY_QUERY_TEMPLATE.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||
: recipe.query_template === "open_contracts_confirmed_as_of_balance_profile"
|
||||
|
|
@ -2222,10 +1750,10 @@ export function buildAddressRecipePlan(
|
|||
? toDateTimeExpr(filters.period_from, true)
|
||||
: null) ??
|
||||
"ТЕКУЩАЯДАТА()";
|
||||
return OPEN_CONTRACTS_CONFIRMED_AS_OF_QUERY_TEMPLATE
|
||||
return PAYABLES_CONFIRMED_AS_OF_QUERY_TEMPLATE
|
||||
.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||
.replaceAll("__AS_OF_EXPR__", asOfExpr)
|
||||
.replaceAll("__OPEN_CONTRACT_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["60", "76"]))
|
||||
.replaceAll("__PAYABLE_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["60", "76"]))
|
||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||
})()
|
||||
: recipe.query_template === "receivables_confirmed_as_of_balance_profile"
|
||||
|
|
@ -2241,10 +1769,10 @@ export function buildAddressRecipePlan(
|
|||
? toDateTimeExpr(filters.period_from, true)
|
||||
: null) ??
|
||||
"ТЕКУЩАЯДАТА()";
|
||||
return OPEN_CONTRACTS_CONFIRMED_AS_OF_QUERY_TEMPLATE
|
||||
return RECEIVABLES_CONFIRMED_AS_OF_QUERY_TEMPLATE
|
||||
.replaceAll("__LIMIT__", String(resolvedLimit))
|
||||
.replaceAll("__AS_OF_EXPR__", asOfExpr)
|
||||
.replaceAll("__OPEN_CONTRACT_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["62", "76"]))
|
||||
.replaceAll("__RECEIVABLE_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["62", "76"]))
|
||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||
})()
|
||||
: MOVEMENTS_QUERY_TEMPLATE
|
||||
|
|
|
|||
|
|
@ -1,26 +1,17 @@
|
|||
import iconv from "iconv-lite";
|
||||
|
||||
const MOJIBAKE_CONTINUATION_CLASS =
|
||||
"[\\u0080-\\u00bf\\u0401-\\u040f\\u0451-\\u045f\\u2018-\\u201e\\u2020-\\u2022\\u2013-\\u2014\\u2122\\u20ac]";
|
||||
const MOJIBAKE_PAIR_PATTERN = new RegExp(`(?:[\\u0420\\u0421]${MOJIBAKE_CONTINUATION_CLASS})`, "gu");
|
||||
|
||||
function compactWhitespace(value: string): string {
|
||||
return value.replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
function countMatches(value: string, pattern: RegExp): number {
|
||||
return (String(value ?? "").match(pattern) ?? []).length;
|
||||
}
|
||||
|
||||
function textMojibakeScore(value: string): number {
|
||||
const source = String(value ?? "");
|
||||
const cyrillic = countMatches(source, /[\u0400-\u04ff]/g);
|
||||
const latin = countMatches(source, /[A-Za-z]/g);
|
||||
const replacement = countMatches(source, /\uFFFD/g);
|
||||
const c1Controls = countMatches(source, /[\u0080-\u009f]/g);
|
||||
const pairMarkers = countMatches(source, MOJIBAKE_PAIR_PATTERN);
|
||||
const doubleEncodedMarkers = countMatches(source, /(?:\u0420[\u00a0-\u00bf]\u0421|\u0413[\u0080-\u00bf]|\u00c3.|\u00c2.)/gu);
|
||||
return cyrillic + latin - replacement * 8 - c1Controls * 5 - pairMarkers * 3 - doubleEncodedMarkers * 2;
|
||||
const cyrillic = (source.match(/[\u0400-\u04ff]/g) ?? []).length;
|
||||
const latin = (source.match(/[A-Za-z]/g) ?? []).length;
|
||||
const replacement = (source.match(/[<5B>]/g) ?? []).length;
|
||||
const pairMarkers = (source.match(/(?:Р.|С.|Ð.|Ñ.)/g) ?? []).length;
|
||||
const doubleEncodedMarkers = (source.match(/(?:Р“[Р-џ]|Р’[Р-џ]|Ã.|Â.)/gu) ?? []).length;
|
||||
return cyrillic + latin - replacement * 3 - pairMarkers * 2 - doubleEncodedMarkers * 2;
|
||||
}
|
||||
|
||||
function looksLikeAddressMojibake(value: string): boolean {
|
||||
|
|
@ -28,51 +19,30 @@ function looksLikeAddressMojibake(value: string): boolean {
|
|||
if (!source.trim()) {
|
||||
return false;
|
||||
}
|
||||
if (/[\u0080-\u009f\uFFFD]/.test(source)) {
|
||||
if (/[<EFBFBD>]/.test(source)) {
|
||||
return true;
|
||||
}
|
||||
if (countMatches(source, MOJIBAKE_PAIR_PATTERN) >= 2) {
|
||||
if ((source.match(/(?:Р.|С.|Ð.|Ñ.)/g) ?? []).length >= 2) {
|
||||
return true;
|
||||
}
|
||||
return countMatches(source, /(?:\u0420[\u00a0-\u00bf]\u0421|\u0413[\u0080-\u00bf]|\u00c3.|\u00c2.)/gu) >= 2;
|
||||
}
|
||||
|
||||
function encodeWin1251MojibakeBytes(value: string): Buffer {
|
||||
const chunks: Buffer[] = [];
|
||||
for (const char of String(value ?? "")) {
|
||||
const code = char.codePointAt(0) ?? 0;
|
||||
if (code >= 0x80 && code <= 0x9f) {
|
||||
chunks.push(Buffer.from([code]));
|
||||
continue;
|
||||
}
|
||||
chunks.push(iconv.encode(char, "win1251"));
|
||||
if ((source.match(/(?:Р“[Р-џ]|Р’[Р-џ]|Ã.|Â.)/gu) ?? []).length >= 2) {
|
||||
return true;
|
||||
}
|
||||
return Buffer.concat(chunks);
|
||||
}
|
||||
|
||||
function decodeUtf8FromWin1251Mojibake(value: string): string {
|
||||
return encodeWin1251MojibakeBytes(value).toString("utf8");
|
||||
}
|
||||
|
||||
function repairKnownReplacementDamagedRussianText(value: string): string {
|
||||
return String(value ?? "")
|
||||
.replace(/\u0410\u041b\u042c\u0422\u0415\u0420\u041d\u0410\u0422[\uFFFD?]+\u0412\u0410/giu, "\u0410\u041b\u042c\u0422\u0415\u0420\u041d\u0410\u0422\u0418\u0412\u0410")
|
||||
.replace(/\u041e\u0411\u0429[\uFFFD?]+\u0419/giu, "\u041e\u0411\u0429\u0418\u0419");
|
||||
return false;
|
||||
}
|
||||
|
||||
export function repairAddressMojibakeText(value: string): string {
|
||||
const source = String(value ?? "");
|
||||
const sourceWithKnownRepairs = repairKnownReplacementDamagedRussianText(source);
|
||||
if (!looksLikeAddressMojibake(sourceWithKnownRepairs)) {
|
||||
return sourceWithKnownRepairs;
|
||||
if (!looksLikeAddressMojibake(source)) {
|
||||
return source;
|
||||
}
|
||||
|
||||
let candidate = sourceWithKnownRepairs;
|
||||
let candidate = source;
|
||||
for (let pass = 0; pass < 3; pass += 1) {
|
||||
let improved = false;
|
||||
|
||||
try {
|
||||
const fromWin1251 = decodeUtf8FromWin1251Mojibake(candidate);
|
||||
const fromWin1251 = iconv.encode(candidate, "win1251").toString("utf8");
|
||||
if (textMojibakeScore(fromWin1251) > textMojibakeScore(candidate)) {
|
||||
candidate = fromWin1251;
|
||||
improved = true;
|
||||
|
|
@ -91,20 +61,14 @@ export function repairAddressMojibakeText(value: string): string {
|
|||
// Ignore decode failures and keep the current candidate.
|
||||
}
|
||||
|
||||
const repairedKnownText = repairKnownReplacementDamagedRussianText(candidate);
|
||||
if (repairedKnownText !== candidate) {
|
||||
candidate = repairedKnownText;
|
||||
improved = true;
|
||||
}
|
||||
|
||||
if (!improved) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return repairKnownReplacementDamagedRussianText(candidate);
|
||||
return candidate;
|
||||
}
|
||||
|
||||
export function normalizeRussianComparableText(value: unknown): string {
|
||||
return compactWhitespace(repairAddressMojibakeText(String(value ?? "")).toLowerCase()).replace(/\u0451/g, "\u0435");
|
||||
return compactWhitespace(repairAddressMojibakeText(String(value ?? "")).toLowerCase()).replace(/ё/g, "е");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,9 +31,6 @@ export interface ComposeStageRow {
|
|||
organization?: string | null;
|
||||
counterparty?: string | null;
|
||||
contract?: string | null;
|
||||
operation_kind?: string | null;
|
||||
payment_purpose?: string | null;
|
||||
comment?: string | null;
|
||||
}
|
||||
|
||||
export interface VatDirectSourceProbeItem {
|
||||
|
|
@ -404,148 +401,6 @@ 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 hasBankIncomingRoleBoundaryQuestion(userMessage: string | null | undefined): boolean {
|
||||
const text = normalizeQuestionText(userMessage);
|
||||
return (
|
||||
/(?:входящ|поступлен|клиентск|выручк|кредит|депозит|возврат)/iu.test(text) &&
|
||||
/(?:банк|сбербанк|финанс)/iu.test(text)
|
||||
);
|
||||
}
|
||||
|
||||
function hasBankOutgoingRoleBoundaryQuestion(userMessage: string | null | undefined): boolean {
|
||||
const text = normalizeQuestionText(userMessage);
|
||||
return (
|
||||
/(?:исходящ|списан|платеж|поставщик|закуп|выплат)/iu.test(text) &&
|
||||
/(?:банк|сбербанк|финанс)/iu.test(text)
|
||||
);
|
||||
}
|
||||
|
||||
function bankOperationDirection(row: ComposeStageRow): "incoming" | "outgoing" | "unknown" {
|
||||
const text = normalizeQuestionText(`${row.registrator} ${row.operation_kind ?? ""}`);
|
||||
if (/(?:поступлени[ея]\s+на\s+расчетн|bank\s+receipt|incoming)/iu.test(text)) {
|
||||
return "incoming";
|
||||
}
|
||||
if (/(?:списани[ея]\s+с\s+расчетн|bank\s+payment|outgoing|write[-\s]?off)/iu.test(text)) {
|
||||
return "outgoing";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
function bankOperationDirectionLabel(direction: "incoming" | "outgoing" | "unknown"): string {
|
||||
if (direction === "incoming") {
|
||||
return "входящее поступление";
|
||||
}
|
||||
if (direction === "outgoing") {
|
||||
return "исходящее списание";
|
||||
}
|
||||
return "банковская операция без надежно распознанного направления";
|
||||
}
|
||||
|
||||
function summarizeBankOperationDirections(rows: ComposeStageRow[]): string {
|
||||
const summary = {
|
||||
incoming: { count: 0, amount: 0 },
|
||||
outgoing: { count: 0, amount: 0 },
|
||||
unknown: { count: 0, amount: 0 }
|
||||
};
|
||||
for (const row of rows) {
|
||||
const direction = bankOperationDirection(row);
|
||||
const amount = typeof row.amount === "number" && Number.isFinite(row.amount) ? Math.abs(row.amount) : 0;
|
||||
summary[direction].count += 1;
|
||||
summary[direction].amount += amount;
|
||||
}
|
||||
const parts: string[] = [];
|
||||
if (summary.incoming.count > 0) {
|
||||
parts.push(`входящие: ${formatMoneyRub(summary.incoming.amount)} (${summary.incoming.count} строк)`);
|
||||
}
|
||||
if (summary.outgoing.count > 0) {
|
||||
parts.push(`исходящие: ${formatMoneyRub(summary.outgoing.amount)} (${summary.outgoing.count} строк)`);
|
||||
}
|
||||
if (summary.unknown.count > 0) {
|
||||
parts.push(`без распознанного направления: ${formatMoneyRub(summary.unknown.amount)} (${summary.unknown.count} строк)`);
|
||||
}
|
||||
return parts.length > 0
|
||||
? `Сводка по направлению: ${parts.join("; ")}.`
|
||||
: "Сводка по направлению: подтвержденные строки не найдены.";
|
||||
}
|
||||
|
||||
function preferredBankEvidenceDirection(
|
||||
userMessage: string | null | undefined
|
||||
): "incoming" | "outgoing" | null {
|
||||
if (hasBankIncomingRoleBoundaryQuestion(userMessage)) {
|
||||
return "incoming";
|
||||
}
|
||||
if (hasBankOutgoingRoleBoundaryQuestion(userMessage)) {
|
||||
return "outgoing";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function bankOperationEvidenceLine(
|
||||
rows: ComposeStageRow[],
|
||||
preferredDirection: "incoming" | "outgoing" | null = null
|
||||
): string {
|
||||
const sample =
|
||||
(preferredDirection ? rows.find((row) => bankOperationDirection(row) === preferredDirection) : null) ?? rows[0];
|
||||
if (!sample) {
|
||||
return "Проверенная строка 1С не найдена.";
|
||||
}
|
||||
const direction = bankOperationDirection(sample);
|
||||
const parts = [`тип по документу: ${bankOperationDirectionLabel(direction)}`];
|
||||
const operationKind = String(sample.operation_kind ?? "").trim();
|
||||
const paymentPurpose = String(sample.payment_purpose ?? "").trim();
|
||||
const contract = String(sample.contract ?? "").trim();
|
||||
if (operationKind) {
|
||||
parts.push(`вид операции: ${operationKind}`);
|
||||
}
|
||||
if (paymentPurpose) {
|
||||
parts.push(`назначение платежа: ${paymentPurpose}`);
|
||||
}
|
||||
if (contract) {
|
||||
parts.push(`договор: ${contract}`);
|
||||
}
|
||||
if (!operationKind && !paymentPurpose && !contract) {
|
||||
parts.push("вид операции/назначение платежа/договор в материализованной строке не заполнены");
|
||||
}
|
||||
return `Основание 1С: ${parts.join("; ")}.`;
|
||||
}
|
||||
|
||||
function bankRoleBoundaryLine(userMessage: string | null | undefined, rows: ComposeStageRow[]): string | null {
|
||||
const incomingBoundary = hasBankIncomingRoleBoundaryQuestion(userMessage);
|
||||
const outgoingBoundary = hasBankOutgoingRoleBoundaryQuestion(userMessage);
|
||||
if (!incomingBoundary && !outgoingBoundary) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const directions = rows.map(bankOperationDirection);
|
||||
const hasIncomingRow = directions.includes("incoming");
|
||||
const hasOutgoingRow = directions.includes("outgoing");
|
||||
|
||||
if (incomingBoundary) {
|
||||
return hasIncomingRow
|
||||
? "Это не обычный клиент и не клиентская выручка автоматически: для банка/финорганизации нужен вид операции, назначение платежа и договор; кредитный, депозитный или возвратный смысл без этих полей не исключаю и не притягиваю."
|
||||
: hasOutgoingRow
|
||||
? "В найденных строках по банку подтверждено исходящее списание, а входящее поступление от банка в этом срезе не подтверждено; это не подтвержденная клиентская выручка, кредит или депозит."
|
||||
: "Входящее поступление от банка в найденных строках не подтверждено; это не подтвержденная клиентская выручка, кредитный или депозитный смысл.";
|
||||
}
|
||||
|
||||
return "Это не обычный поставщик автоматически: для банка/финорганизации нужен вид операции, назначение платежа и договор; текущий срез подтверждает банковский платежный контур, а не бизнес-роль поставщика.";
|
||||
}
|
||||
|
||||
function hasInventoryPurchaseDateActionFocus(userMessage: string | null | undefined): boolean {
|
||||
const text = normalizeQuestionText(userMessage);
|
||||
if (!text) {
|
||||
|
|
@ -850,19 +705,11 @@ function detectValueRankingFocus(userMessage: string | null | undefined): ValueR
|
|||
if (asksTotalMoneyEarned) {
|
||||
return "total_flow";
|
||||
}
|
||||
const hasCounterpartyRankingSubject =
|
||||
/(?:клиент|заказчик|покупател|контрагент|customer|client|counterpart|\u043a\u043b\u0438\u0435\u043d\u0442|\u0437\u0430\u043a\u0430\u0437\u0447\u0438\u043a|\u043f\u043e\u043a\u0443\u043f\u0430\u0442\u0435\u043b|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442)/iu.test(
|
||||
text
|
||||
);
|
||||
const asksExplicitYearBreakdown =
|
||||
/(?:по\s+годам|за\s+какие\s+годы|динамик\w*\s+по\s+год|yearly\s+breakdown|by\s+year|\u043f\u043e\s+\u0433\u043e\u0434\u0430\u043c|\u0437\u0430\s+\u043a\u0430\u043a\u0438\u0435\s+\u0433\u043e\u0434\u044b|\u0434\u0438\u043d\u0430\u043c\u0438\u043a\w*\s+\u043f\u043e\s+\u0433\u043e\u0434)/iu.test(
|
||||
text
|
||||
);
|
||||
const asksYearlyRevenueRanking =
|
||||
/(?:доходн|выручк|оборот|прибыл|деньг|денег|revenue|turnover|income)/iu.test(text) &&
|
||||
/(?:год|года|годы|year|years|по\s+годам)/iu.test(text) &&
|
||||
/(?:сам(?:ый|ая|ое|ые)|топ|луч|best|max|наибольш|больше)/iu.test(text);
|
||||
if (asksYearlyRevenueRanking && (!hasCounterpartyRankingSubject || asksExplicitYearBreakdown)) {
|
||||
if (asksYearlyRevenueRanking) {
|
||||
return "top_years_by_total";
|
||||
}
|
||||
if (/(?:сам(?:ый|ая|ое|ые)\s+высок[а-яё]*|highest|largest)\s+чек|(?:max\s+check|чек\s+макс)/iu.test(text)) {
|
||||
|
|
@ -925,46 +772,27 @@ 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 = isCounterpartyLikeToken(token, skipTokenPattern);
|
||||
if (normalized) {
|
||||
return normalized;
|
||||
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;
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
for (const token of row.analytics) {
|
||||
const normalized = String(token ?? "").trim();
|
||||
|
|
@ -1621,25 +1449,6 @@ 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 "поставщики/подрядчики";
|
||||
|
|
@ -1755,18 +1564,6 @@ 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) {
|
||||
|
|
@ -2101,259 +1898,6 @@ 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>();
|
||||
|
||||
|
|
@ -2557,51 +2101,6 @@ 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()
|
||||
|
|
@ -2610,13 +2109,6 @@ 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,
|
||||
|
|
@ -2672,58 +2164,6 @@ 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[]
|
||||
|
|
@ -3774,7 +3214,7 @@ function composeFactualReplyBody(
|
|||
const erroredSources = vatProbe.probedSources.filter((item) => item.status === "error").length;
|
||||
lines.push(
|
||||
"",
|
||||
"Покрытие VAT-источников в 1С:",
|
||||
"Покрытие VAT-источников через MCP:",
|
||||
`- Найдено VAT-объектов: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`,
|
||||
`- Прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`,
|
||||
`- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.`,
|
||||
|
|
@ -3801,7 +3241,7 @@ function composeFactualReplyBody(
|
|||
}
|
||||
lines.push("- Сумма прогноза выше рассчитана строго по оборотам 68.02*/19*; прямые VAT-источники показаны для проверки покрытия.");
|
||||
} else if (vatProbe && vatProbe.status === "error") {
|
||||
lines.push("", "Покрытие VAT-источников в 1С: дополнительная проверка завершилась ошибкой, поэтому использован только базовый контур 68.02*/19*.");
|
||||
lines.push("", "Покрытие VAT-источников через MCP: probe завершился ошибкой, поэтому использован только базовый контур 68.02*/19*.");
|
||||
}
|
||||
|
||||
if (!vatActivityDetected) {
|
||||
|
|
@ -3888,16 +3328,13 @@ function composeFactualReplyBody(
|
|||
options.periodFrom && options.periodTo ? `${formatDateRu(options.periodFrom)}..${formatDateRu(options.periodTo)}` : null;
|
||||
const formatConfirmedMoney = (value: number): string => (options.useRubCurrency ? formatMoneyRub(value) : formatMoney(value));
|
||||
const vatProbe = options.vatDirectSourceProbe ?? null;
|
||||
const organizationLabel = normalizeOrganizationScopeValue(options.organizationHint);
|
||||
const organizationScopeLabel = organizationLabel ? ` по организации ${organizationLabel}` : "";
|
||||
|
||||
const lines = [
|
||||
`Коротко: подтвержденный НДС к уплате за налоговый период${organizationScopeLabel} — ${formatConfirmedMoney(vatToPay)}.`,
|
||||
`Коротко: подтвержденный НДС к уплате за налоговый период — ${formatConfirmedMoney(vatToPay)}.`,
|
||||
`Если смотреть на возможный перенос или переплату, получается ${formatConfirmedMoney(carryoverOrOverpayment)}.`,
|
||||
"Это подтвержденный расчет по регистрам книг продаж и покупок, без surrogate-формулы 68/19.",
|
||||
"",
|
||||
"Что вошло в расчет:",
|
||||
...(organizationLabel ? [`- Организация: ${organizationLabel}.`] : []),
|
||||
`- Налоговый период расчета: ${periodWindowLabel ?? "не задан (нужен явный период)"}.`,
|
||||
`- НДС по книге продаж: ${formatConfirmedMoney(salesVat)}.`,
|
||||
`- НДС по книге покупок (вычеты): ${formatConfirmedMoney(purchaseVat)}.`,
|
||||
|
|
@ -3909,7 +3346,7 @@ function composeFactualReplyBody(
|
|||
const erroredSources = vatProbe.probedSources.filter((item) => item.status === "error").length;
|
||||
lines.push(
|
||||
"",
|
||||
"Покрытие VAT-источников в 1С:",
|
||||
"Покрытие VAT-источников через MCP:",
|
||||
`- Найдено VAT-объектов: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`,
|
||||
`- Прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`,
|
||||
`- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.`,
|
||||
|
|
@ -3918,11 +3355,11 @@ function composeFactualReplyBody(
|
|||
if (vatProbe.errors.length > 0) {
|
||||
lines.push(`- Ограничения probe: ${vatProbe.errors.slice(0, 2).join("; ")}.`);
|
||||
}
|
||||
lines.push("- Сумма расчета выше получена по книгам продаж/покупок; дополнительная проверка использована для контроля полноты VAT-источников.");
|
||||
lines.push("- Сумма расчета выше получена по книгам продаж/покупок; probe использован для контроля полноты VAT-источников.");
|
||||
} else if (vatProbe && vatProbe.status === "error") {
|
||||
lines.push(
|
||||
"",
|
||||
"Покрытие VAT-источников в 1С: дополнительная проверка недоступна, поэтому использован основной бухгалтерский срез.",
|
||||
"Покрытие VAT-источников через MCP: дополнительный probe недоступен (например, timeout metadata).",
|
||||
"Итоговая сумма НДС выше рассчитана по основному маршруту книг продаж/покупок; probe влияет только на диагностику покрытия."
|
||||
);
|
||||
if (vatProbe.errors.length > 0) {
|
||||
|
|
@ -4016,7 +3453,7 @@ function composeFactualReplyBody(
|
|||
const nonEmptySources = vatProbe.probedSources.filter((item) => item.status === "ok").length;
|
||||
lines.push(
|
||||
"",
|
||||
"Блок 2.1. Проверка VAT-источников в 1С",
|
||||
"Блок 2.1. MCP-проверка VAT-источников",
|
||||
`- VAT-объектов в метаданных 1С: ${formatNumberWithDots(vatProbe.objectsTotal)} (документы: ${formatNumberWithDots(vatProbe.documentsTotal)}, регистры: ${formatNumberWithDots(vatProbe.registersTotal)}).`,
|
||||
`- Пробных прямых источников проверено: ${formatNumberWithDots(vatProbe.probedSources.length)}.`,
|
||||
`- Источников с движениями до даты среза: ${formatNumberWithDots(nonEmptySources)}.`
|
||||
|
|
@ -4041,8 +3478,8 @@ function composeFactualReplyBody(
|
|||
} else if (vatProbe && vatProbe.status === "error") {
|
||||
lines.push(
|
||||
"",
|
||||
"Блок 2.1. Проверка VAT-источников в 1С",
|
||||
"- Дополнительная проверка VAT-источников завершилась ошибкой, поэтому срез подтвержден по доступному бухгалтерскому источнику (68*)."
|
||||
"Блок 2.1. MCP-проверка VAT-источников",
|
||||
"- Probe VAT-источников завершился ошибкой, поэтому срез подтвержден по доступному бухгалтерскому источнику (68*)."
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -4365,8 +3802,7 @@ function composeFactualReplyBody(
|
|||
|
||||
if (intent === "payables_confirmed_as_of_date") {
|
||||
const payablesAsOfDate = resolvePayablesAsOfDate(options);
|
||||
const balanceSnapshot = buildPayablesConfirmedBalanceSnapshot(rows, payablesAsOfDate);
|
||||
const confirmedBalances = balanceSnapshot.balances;
|
||||
const confirmedBalances = buildPayablesConfirmedBalanceAggregate(rows, payablesAsOfDate);
|
||||
const asOfDate = normalizeIsoDateOnly(options.asOfDate);
|
||||
const periodFrom = normalizeIsoDateOnly(options.periodFrom);
|
||||
const periodTo = normalizeIsoDateOnly(options.periodTo);
|
||||
|
|
@ -4387,39 +3823,6 @@ 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)}.`,
|
||||
"Это подтвержденный срез обязательств к оплате по точному остатку."
|
||||
|
|
@ -4440,7 +3843,6 @@ function composeFactualReplyBody(
|
|||
lines.push("Сводка");
|
||||
lines.push(`- Строк в выборке: ${formatNumberWithDots(rows.length)}.`);
|
||||
lines.push(`- Контрагентов с подтвержденным остатком к оплате: ${formatNumberWithDots(confirmedBalances.length)}.`);
|
||||
appendDebtMirrorDisclosure(lines, balanceSnapshot, "payables");
|
||||
|
||||
lines.push("");
|
||||
lines.push("Категории обязательств");
|
||||
|
|
@ -4478,8 +3880,7 @@ function composeFactualReplyBody(
|
|||
|
||||
if (intent === "receivables_confirmed_as_of_date") {
|
||||
const receivablesAsOfDate = resolveReceivablesAsOfDate(options);
|
||||
const balanceSnapshot = buildReceivablesConfirmedBalanceSnapshot(rows, receivablesAsOfDate);
|
||||
const confirmedBalances = balanceSnapshot.balances;
|
||||
const confirmedBalances = buildReceivablesConfirmedBalanceAggregate(rows, receivablesAsOfDate);
|
||||
const asOfDate = normalizeIsoDateOnly(options.asOfDate);
|
||||
const periodFrom = normalizeIsoDateOnly(options.periodFrom);
|
||||
const periodTo = normalizeIsoDateOnly(options.periodTo);
|
||||
|
|
@ -4500,39 +3901,6 @@ 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."
|
||||
|
|
@ -4553,7 +3921,6 @@ function composeFactualReplyBody(
|
|||
lines.push("Сводка");
|
||||
lines.push(`- Строк в выборке: ${formatNumberWithDots(rows.length)}.`);
|
||||
lines.push(`- Контрагентов с подтвержденным остатком к получению: ${formatNumberWithDots(confirmedBalances.length)}.`);
|
||||
appendDebtMirrorDisclosure(lines, balanceSnapshot, "receivables");
|
||||
|
||||
lines.push("");
|
||||
lines.push("Категории дебиторской задолженности");
|
||||
|
|
@ -4684,8 +4051,7 @@ function composeFactualReplyBody(
|
|||
};
|
||||
|
||||
if (options.requestedResultMode === "confirmed_balance") {
|
||||
const balanceSnapshot = buildPayablesConfirmedBalanceSnapshot(rows, payablesAsOfDate);
|
||||
const confirmedBalances = balanceSnapshot.balances;
|
||||
const confirmedBalances = buildPayablesConfirmedBalanceAggregate(rows, payablesAsOfDate);
|
||||
if (confirmedBalances.length > 0) {
|
||||
const categoryCounts = confirmedBalances.reduce<Record<PayablesLiabilityCategory, number>>(
|
||||
(acc, item) => {
|
||||
|
|
@ -4726,7 +4092,6 @@ 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),
|
||||
|
|
@ -5006,24 +4371,11 @@ function composeFactualReplyBody(
|
|||
}
|
||||
|
||||
if (intent === "bank_operations_by_counterparty") {
|
||||
const rowCounterparties = uniqueStrings(
|
||||
rows
|
||||
.map((row) => extractCounterpartyName(row))
|
||||
.filter((item): item is string => Boolean(item))
|
||||
);
|
||||
const counterparty = resolvePreferredCounterpartyDisplayLabel(options.counterpartyHint, rowCounterparties);
|
||||
const roleBoundary = bankRoleBoundaryLine(options.userMessage, rows);
|
||||
const visibleRows = rows.slice(0, Math.min(rows.length, 5));
|
||||
const lines = [
|
||||
`Коротко: найдено банковских операций${counterparty ? ` по ${counterparty}` : " по контрагенту"} — ${rows.length}.`,
|
||||
summarizeBankOperationDirections(rows),
|
||||
roleBoundary ?? "Показываю подтвержденные банковские операции из текущего среза.",
|
||||
bankOperationEvidenceLine(rows, preferredBankEvidenceDirection(options.userMessage)),
|
||||
...formatTopRows(visibleRows, visibleRows.length)
|
||||
`Коротко: найдено банковских операций по контрагенту — ${rows.length}.`,
|
||||
"Показываю подтвержденные банковские операции из текущего среза.",
|
||||
...formatTopRows(rows, rows.length)
|
||||
];
|
||||
if (rows.length > visibleRows.length) {
|
||||
lines.push(`Показаны первые ${visibleRows.length} из ${rows.length}; полный список остается в подтвержденном срезе.`);
|
||||
}
|
||||
return {
|
||||
responseType: "FACTUAL_LIST",
|
||||
text: lines.join("\n")
|
||||
|
|
@ -5031,17 +4383,11 @@ function composeFactualReplyBody(
|
|||
}
|
||||
|
||||
if (intent === "bank_operations_by_contract") {
|
||||
const visibleRows = rows.slice(0, Math.min(rows.length, 5));
|
||||
const lines = [
|
||||
`Коротко: найдено банковских операций по договору — ${rows.length}.`,
|
||||
summarizeBankOperationDirections(rows),
|
||||
"Показываю подтвержденные банковские операции из текущего среза.",
|
||||
bankOperationEvidenceLine(rows),
|
||||
...formatTopRows(visibleRows, visibleRows.length)
|
||||
...formatTopRows(rows, rows.length)
|
||||
];
|
||||
if (rows.length > visibleRows.length) {
|
||||
lines.push(`Показаны первые ${visibleRows.length} из ${rows.length}; полный список остается в подтвержденном срезе.`);
|
||||
}
|
||||
return {
|
||||
responseType: "FACTUAL_LIST",
|
||||
text: lines.join("\n")
|
||||
|
|
|
|||
|
|
@ -148,9 +148,9 @@ export function composeCounterpartyAnalyticsReply(
|
|||
const includeRoles = focus === "full_profile" || focus === "roles_only";
|
||||
const directLead =
|
||||
focus === "suppliers_only"
|
||||
? `Поставщиков с ролью поставщика: ${supplierOnly}.`
|
||||
? `Поставщиков (только supplier-роль): ${supplierOnly}.`
|
||||
: focus === "customers_only"
|
||||
? `Заказчиков с ролью покупателя: ${customerOnly}.`
|
||||
? `Заказчиков (только customer-роль): ${customerOnly}.`
|
||||
: focus === "mixed_only"
|
||||
? `Контрагентов со смешанной ролью: ${mixedActive}.`
|
||||
: includeTotal && totalCounterparties > 0
|
||||
|
|
@ -175,9 +175,9 @@ export function composeCounterpartyAnalyticsReply(
|
|||
|
||||
if (includeRoles) {
|
||||
if (resolvedActive > 0 || activeCounterparties > 0) {
|
||||
lines.push("Распределение ролей по активности:");
|
||||
lines.push(`Заказчики с ролью покупателя: ${customerOnly}.`);
|
||||
lines.push(`Поставщики с ролью поставщика: ${supplierOnly}.`);
|
||||
lines.push("Роли контрагентов по активности:");
|
||||
lines.push(`Заказчики (только customer-роль): ${customerOnly}.`);
|
||||
lines.push(`Поставщики (только supplier-роль): ${supplierOnly}.`);
|
||||
lines.push(`Смешанные (и покупатель, и поставщик): ${mixedActive}.`);
|
||||
lines.push(`4. Всего активных контрагентов: ${activeCounterparties}.`);
|
||||
if (otherCounterparties !== null) {
|
||||
|
|
@ -189,10 +189,10 @@ export function composeCounterpartyAnalyticsReply(
|
|||
}
|
||||
|
||||
if (focus === "suppliers_only") {
|
||||
lines.push(`Поставщиков с ролью поставщика: ${supplierOnly}.`);
|
||||
lines.push(`Поставщиков (только supplier-роль): ${supplierOnly}.`);
|
||||
}
|
||||
if (focus === "customers_only") {
|
||||
lines.push(`Заказчиков с ролью покупателя: ${customerOnly}.`);
|
||||
lines.push(`Заказчиков (только customer-роль): ${customerOnly}.`);
|
||||
}
|
||||
if (focus === "mixed_only") {
|
||||
lines.push(`Контрагентов со смешанной ролью: ${mixedActive}.`);
|
||||
|
|
@ -525,27 +525,6 @@ export function composeCounterpartyAnalyticsReply(
|
|||
const limit = deps.detectRankingLimit(options.userMessage, 20);
|
||||
const minOpsForAvgCheck = deps.detectMinOpsForAvgCheck(options.userMessage);
|
||||
const normalizedQuestion = deps.normalizeQuestionText(options.userMessage);
|
||||
const asksSingleBestCounterparty =
|
||||
focus === "top_by_total" &&
|
||||
/(?:какой|кто|which|who|какой|кто)/iu.test(normalizedQuestion) &&
|
||||
/(?:больше\s+всего|сам(?:ый|ая|ое|ые)|наибольш|прин[её]с|highest|most|больше\s+всего|сам(?:ый|ая|РѕРµ|ые)|наибол|РїСЂРёРЅ[её]СЃ)/iu.test(
|
||||
normalizedQuestion
|
||||
) &&
|
||||
!/(?:\btop\b|топ|рейтинг|список|первые|покажи\s+топ|дай\s+топ|покаж\w*\s+топ|дай\s+топ)/iu.test(
|
||||
normalizedQuestion
|
||||
);
|
||||
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>();
|
||||
|
|
@ -749,7 +728,7 @@ export function composeCounterpartyAnalyticsReply(
|
|||
lines.push(
|
||||
...visible.map(
|
||||
(item, index) =>
|
||||
`${index + 1}. ${item.name} | максимальная разовая сумма: ${deps.formatMoneyRub(item.maxSingle)} | сумма: ${deps.formatMoneyRub(item.total)} | операций: ${item.ops}`
|
||||
`${index + 1}. ${item.name} | max single: ${item.maxSingle} | максимальная разовая сумма: ${deps.formatMoneyRub(item.maxSingle)} | сумма: ${deps.formatMoneyRub(item.total)} | операций: ${item.ops}`
|
||||
)
|
||||
);
|
||||
return buildFactualListReply(lines);
|
||||
|
|
@ -807,12 +786,8 @@ export function composeCounterpartyAnalyticsReply(
|
|||
return buildFactualListReply(lines);
|
||||
}
|
||||
|
||||
const visible = rankedByTotal.slice(0, effectiveLimit);
|
||||
const visible = rankedByTotal.slice(0, limit);
|
||||
const singleCandidateOnly = rankedByTotal.length === 1;
|
||||
const rankingPeriodLabel =
|
||||
options.periodFrom && options.periodTo
|
||||
? `за период ${deps.formatDateRu(options.periodFrom)}..${deps.formatDateRu(options.periodTo)}`
|
||||
: "за доступное время";
|
||||
const heading = singleCandidateOnly
|
||||
? isSupplier
|
||||
? "Найденный поставщик по сумме выплат:"
|
||||
|
|
@ -822,17 +797,14 @@ export function composeCounterpartyAnalyticsReply(
|
|||
: `Топ-${visible.length} заказчиков по сумме поступлений:`;
|
||||
const leadingCounterparty = visible[0] ?? null;
|
||||
lines.unshift(heading);
|
||||
if (options.periodFrom && options.periodTo) {
|
||||
lines.push(`Период рейтинга: ${rankingPeriodLabel}.`);
|
||||
}
|
||||
if (leadingCounterparty) {
|
||||
const directAnswerLine = singleCandidateOnly
|
||||
? isSupplier
|
||||
? `В выбранном срезе найден один поставщик: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг.`
|
||||
: `В выбранном срезе найден один клиент: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это не полноценный сравнительный рейтинг; сумма является денежным потоком, а не чистой прибылью.`
|
||||
: isSupplier
|
||||
? `Крупнейший поставщик по подтвержденным выплатам ${rankingPeriodLabel}: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).`
|
||||
: `Самый доходный клиент ${rankingPeriodLabel} по подтвержденным поступлениям: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это денежный поток, а не чистая прибыль.`;
|
||||
? `Крупнейший поставщик по подтвержденным выплатам за доступное время: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям).`
|
||||
: `Самый доходный клиент за доступное время по подтвержденным поступлениям: ${leadingCounterparty.name} (${deps.formatMoneyRub(leadingCounterparty.total)} по ${leadingCounterparty.ops} операциям). Это денежный поток, а не чистая прибыль.`;
|
||||
lines.unshift(directAnswerLine);
|
||||
}
|
||||
lines.push(
|
||||
|
|
|
|||
|
|
@ -220,20 +220,11 @@ function hasExplicitLooseByAnchorToken(text: string): boolean {
|
|||
}
|
||||
|
||||
const FOLLOWUP_LOW_QUALITY_COUNTERPARTY_TOKENS = new Set([
|
||||
"мы",
|
||||
"нам",
|
||||
"нас",
|
||||
"наш",
|
||||
"наша",
|
||||
"наше",
|
||||
"наши",
|
||||
"унас",
|
||||
"есть",
|
||||
"же",
|
||||
"что",
|
||||
"все",
|
||||
"всё",
|
||||
"или",
|
||||
"кроме",
|
||||
"помимо",
|
||||
"этого",
|
||||
|
|
@ -252,30 +243,6 @@ const FOLLOWUP_LOW_QUALITY_COUNTERPARTY_TOKENS = new Set([
|
|||
"договора",
|
||||
"контрагент",
|
||||
"контрагента",
|
||||
"контрагенту",
|
||||
"клиент",
|
||||
"клиента",
|
||||
"клиенту",
|
||||
"клиентом",
|
||||
"клиенты",
|
||||
"поставщик",
|
||||
"поставщика",
|
||||
"поставщику",
|
||||
"поставщиком",
|
||||
"поставщики",
|
||||
"покупатель",
|
||||
"покупателя",
|
||||
"покупателю",
|
||||
"заказчик",
|
||||
"заказчика",
|
||||
"заказчику",
|
||||
"обычный",
|
||||
"обычная",
|
||||
"обычное",
|
||||
"обычные",
|
||||
"обычного",
|
||||
"обычному",
|
||||
"обычным",
|
||||
"еще",
|
||||
"ещё",
|
||||
"другие",
|
||||
|
|
@ -878,22 +845,6 @@ function hasBroadCounterpartyRankingCue(text: string): boolean {
|
|||
);
|
||||
}
|
||||
|
||||
function isBroadDebtPolarityQuestion(intent: AddressIntent, text: string): boolean {
|
||||
if (intent !== "payables_confirmed_as_of_date" && intent !== "receivables_confirmed_as_of_date") {
|
||||
return false;
|
||||
}
|
||||
const normalized = textWithRepairedVariant(String(text ?? "")).toLowerCase().replace(/ё/g, "е");
|
||||
if (!/(?:долж|задолж|дебитор|кредитор|обязательств)/iu.test(normalized)) {
|
||||
return false;
|
||||
}
|
||||
if (/(?:по\s+(?:нему|ней|ним|этому|этой|этому\s+контрагенту|этой\s+компании|поставщику|клиенту|покупателю|заказчику)|\bон\b|\bона\b)/iu.test(normalized)) {
|
||||
return false;
|
||||
}
|
||||
return /(?:^|[\s,.;:!?()\-])(?:кто|кому|какие|какой|список|топ|все|всех|всего)(?=$|[\s,.;:!?()\-])/iu.test(
|
||||
normalized
|
||||
);
|
||||
}
|
||||
|
||||
function mergeFollowupFilters(
|
||||
current: AddressFilterSet,
|
||||
intent: AddressIntent,
|
||||
|
|
@ -1103,16 +1054,11 @@ function mergeFollowupFilters(
|
|||
previousCounterparty ??
|
||||
(followupContext.previous_anchor_type === "counterparty" ? previousAnchorValue : null);
|
||||
const currentCounterparty = toNonEmptyString(merged.counterparty);
|
||||
const suppressCounterpartyForBroadDebtQuestion = isBroadDebtPolarityQuestion(intent, userMessage) && !currentCounterparty;
|
||||
const shouldInheritCounterparty =
|
||||
!suppressCounterpartyForBroadDebtQuestion &&
|
||||
(!currentCounterparty ||
|
||||
(Boolean(inheritedCounterparty) &&
|
||||
isLowQualityCounterpartyAnchor(currentCounterparty) &&
|
||||
!isLowQualityCounterpartyAnchor(inheritedCounterparty)));
|
||||
if (inheritedCounterparty && suppressCounterpartyForBroadDebtQuestion) {
|
||||
reasons.push("counterparty_carryover_suppressed_for_broad_debt_polarity_question");
|
||||
}
|
||||
!currentCounterparty ||
|
||||
(Boolean(inheritedCounterparty) &&
|
||||
isLowQualityCounterpartyAnchor(currentCounterparty) &&
|
||||
!isLowQualityCounterpartyAnchor(inheritedCounterparty));
|
||||
if (inheritedCounterparty && shouldInheritCounterparty) {
|
||||
merged.counterparty = inheritedCounterparty;
|
||||
reasons.push(currentCounterparty ? "counterparty_replaced_from_followup_context" : "counterparty_from_followup_context");
|
||||
|
|
@ -1499,12 +1445,6 @@ 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")) {
|
||||
|
|
|
|||
|
|
@ -65,7 +65,6 @@ export interface RunAssistantAddressAttemptRuntimeInput<ResponseType = unknown>
|
|||
hasLivingChatSignal: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["hasLivingChatSignal"];
|
||||
shouldEmitOrganizationSelectionReply: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["shouldEmitOrganizationSelectionReply"];
|
||||
hasAssistantCapabilityQuestionSignal: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["hasAssistantCapabilityQuestionSignal"];
|
||||
resolveOrganizationSelectionFromMessage: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["resolveOrganizationSelectionFromMessage"];
|
||||
resolveDataScopeProbe: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["resolveDataScopeProbe"];
|
||||
applyScriptGuard: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["applyScriptGuard"];
|
||||
applyGroundingGuard: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["applyGroundingGuard"];
|
||||
|
|
@ -186,7 +185,6 @@ export async function runAssistantAddressAttemptRuntime<ResponseType = unknown>(
|
|||
hasLivingChatSignal: input.hasLivingChatSignal,
|
||||
shouldEmitOrganizationSelectionReply: input.shouldEmitOrganizationSelectionReply,
|
||||
hasAssistantCapabilityQuestionSignal: input.hasAssistantCapabilityQuestionSignal,
|
||||
resolveOrganizationSelectionFromMessage: input.resolveOrganizationSelectionFromMessage,
|
||||
resolveDataScopeProbe: input.resolveDataScopeProbe,
|
||||
applyScriptGuard: input.applyScriptGuard,
|
||||
applyGroundingGuard: input.applyGroundingGuard,
|
||||
|
|
|
|||
|
|
@ -575,14 +575,6 @@ function isReferentialCounterpartyPlaceholder(
|
|||
return false;
|
||||
}
|
||||
return new Set([
|
||||
"мы",
|
||||
"нам",
|
||||
"нас",
|
||||
"наш",
|
||||
"наша",
|
||||
"наше",
|
||||
"наши",
|
||||
"унас",
|
||||
"он",
|
||||
"она",
|
||||
"оно",
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ export interface BuildAssistantLivingChatAttemptRuntimeInputInput<ResponseType =
|
|||
hasLivingChatSignal: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["hasLivingChatSignal"];
|
||||
shouldEmitOrganizationSelectionReply: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["shouldEmitOrganizationSelectionReply"];
|
||||
hasAssistantCapabilityQuestionSignal: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["hasAssistantCapabilityQuestionSignal"];
|
||||
resolveOrganizationSelectionFromMessage: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["resolveOrganizationSelectionFromMessage"];
|
||||
resolveDataScopeProbe: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["resolveDataScopeProbe"];
|
||||
applyScriptGuard: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["applyScriptGuard"];
|
||||
applyGroundingGuard: RunAssistantLivingChatAttemptRuntimeInput<ResponseType>["applyGroundingGuard"];
|
||||
|
|
@ -80,7 +79,6 @@ export function buildAssistantLivingChatAttemptRuntimeInput<ResponseType = unkno
|
|||
hasLivingChatSignal: input.hasLivingChatSignal,
|
||||
shouldEmitOrganizationSelectionReply: input.shouldEmitOrganizationSelectionReply,
|
||||
hasAssistantCapabilityQuestionSignal: input.hasAssistantCapabilityQuestionSignal,
|
||||
resolveOrganizationSelectionFromMessage: input.resolveOrganizationSelectionFromMessage,
|
||||
resolveDataScopeProbe: input.resolveDataScopeProbe,
|
||||
applyScriptGuard: input.applyScriptGuard,
|
||||
applyGroundingGuard: input.applyGroundingGuard,
|
||||
|
|
|
|||
|
|
@ -78,7 +78,6 @@ export async function runAssistantLivingChatAttemptRuntime<ResponseType = unknow
|
|||
hasLivingChatSignal: input.hasLivingChatSignal,
|
||||
shouldEmitOrganizationSelectionReply: input.shouldEmitOrganizationSelectionReply,
|
||||
hasAssistantCapabilityQuestionSignal: input.hasAssistantCapabilityQuestionSignal,
|
||||
resolveOrganizationSelectionFromMessage: input.resolveOrganizationSelectionFromMessage,
|
||||
resolveDataScopeProbe: input.resolveDataScopeProbe,
|
||||
executeLlmChat,
|
||||
applyScriptGuard: input.applyScriptGuard,
|
||||
|
|
|
|||
|
|
@ -59,7 +59,6 @@ export function buildAssistantLivingChatHandlerRuntimeInput<ResponseType = unkno
|
|||
hasLivingChatSignal: input.hasLivingChatSignal,
|
||||
shouldEmitOrganizationSelectionReply: input.shouldEmitOrganizationSelectionReply,
|
||||
hasAssistantCapabilityQuestionSignal: input.hasAssistantCapabilityQuestionSignal,
|
||||
resolveOrganizationSelectionFromMessage: input.resolveOrganizationSelectionFromMessage,
|
||||
resolveDataScopeProbe: input.resolveDataScopeProbe,
|
||||
executeLlmChat: input.executeLlmChat,
|
||||
applyScriptGuard: input.applyScriptGuard,
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ export interface TryHandleAssistantLivingChatRuntimeInput<ResponseType = unknown
|
|||
hasLivingChatSignal: AssistantLivingChatRuntimeInput["hasLivingChatSignal"];
|
||||
shouldEmitOrganizationSelectionReply: AssistantLivingChatRuntimeInput["shouldEmitOrganizationSelectionReply"];
|
||||
hasAssistantCapabilityQuestionSignal: AssistantLivingChatRuntimeInput["hasAssistantCapabilityQuestionSignal"];
|
||||
resolveOrganizationSelectionFromMessage: AssistantLivingChatRuntimeInput["resolveOrganizationSelectionFromMessage"];
|
||||
resolveDataScopeProbe: AssistantLivingChatRuntimeInput["resolveDataScopeProbe"];
|
||||
executeLlmChat: AssistantLivingChatRuntimeInput["executeLlmChat"];
|
||||
applyScriptGuard: AssistantLivingChatRuntimeInput["applyScriptGuard"];
|
||||
|
|
@ -82,7 +81,6 @@ export async function tryHandleAssistantLivingChatRuntime<ResponseType = unknown
|
|||
hasLivingChatSignal: input.hasLivingChatSignal,
|
||||
shouldEmitOrganizationSelectionReply: input.shouldEmitOrganizationSelectionReply,
|
||||
hasAssistantCapabilityQuestionSignal: input.hasAssistantCapabilityQuestionSignal,
|
||||
resolveOrganizationSelectionFromMessage: input.resolveOrganizationSelectionFromMessage,
|
||||
resolveDataScopeProbe: input.resolveDataScopeProbe,
|
||||
executeLlmChat: input.executeLlmChat,
|
||||
applyScriptGuard: input.applyScriptGuard,
|
||||
|
|
|
|||
|
|
@ -40,7 +40,6 @@ export interface AssistantLivingChatRuntimeInput {
|
|||
hasLivingChatSignal: (message: string) => boolean;
|
||||
shouldEmitOrganizationSelectionReply: (message: string, activeOrganization: string | null) => boolean;
|
||||
hasAssistantCapabilityQuestionSignal: (message: string) => boolean;
|
||||
resolveOrganizationSelectionFromMessage: (message: string, knownOrganizations: unknown[]) => string | null;
|
||||
resolveDataScopeProbe: () => Promise<Record<string, unknown> | null>;
|
||||
executeLlmChat: () => Promise<string>;
|
||||
applyScriptGuard: (chatText: string, userMessage: string) => {
|
||||
|
|
@ -79,54 +78,6 @@ function hasPriorAssistantTurn(items: unknown[]): boolean {
|
|||
return items.some((item) => item && typeof item === "object" && (item as { role?: string }).role === "assistant");
|
||||
}
|
||||
|
||||
function shouldProbeBareOrganizationScopeCandidate(input: {
|
||||
userMessage: string;
|
||||
selectedOrganization: string | null;
|
||||
activeOrganization: string | null;
|
||||
dataScopeMetaQuery: boolean;
|
||||
capabilityMetaQuery: boolean;
|
||||
destructiveSignal: boolean;
|
||||
dangerSignal: boolean;
|
||||
operationalSignal: boolean;
|
||||
}): boolean {
|
||||
if (
|
||||
input.selectedOrganization ||
|
||||
input.activeOrganization ||
|
||||
input.dataScopeMetaQuery ||
|
||||
input.capabilityMetaQuery ||
|
||||
input.destructiveSignal ||
|
||||
input.dangerSignal ||
|
||||
input.operationalSignal
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const raw = String(input.userMessage ?? "").trim();
|
||||
if (!raw || raw.length > 80 || /[?!]/u.test(raw) || /\d/u.test(raw) || !/\p{L}/u.test(raw)) {
|
||||
return false;
|
||||
}
|
||||
const tokenCount = raw.split(/\s+/u).filter(Boolean).length;
|
||||
if (tokenCount < 1 || tokenCount > 5) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const normalized = raw
|
||||
.toLowerCase()
|
||||
.replace(/\u0451/gu, "\u0435")
|
||||
.replace(/\s+/gu, " ")
|
||||
.trim();
|
||||
if (
|
||||
/^(?:\u043f\u0440\u0438\u0432\u0435\u0442|\u0437\u0434\u0440\u0430\u0432\u0441\u0442\u0432\u0443\u0439|\u0437\u0434\u0440\u0430\u0432\u0441\u0442\u0432\u0443\u0439\u0442\u0435|\u0434\u0430|\u043d\u0435\u0442|\u043e\u043a|\u043e\u043a\u0435\u0439|\u0441\u043f\u0430\u0441\u0438\u0431\u043e|\u043f\u043e\u043a\u0430|\u0433\u043e|\u0434\u0430\u043b\u044c\u0448\u0435|\u043f\u043e\u043d\u044f\u043b|\u043f\u043e\u043d\u044f\u043b\u0430)(?:\s|$)/iu.test(
|
||||
normalized
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return !/(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e|\u043f\u043e\u043a\u0430\u0436\u0438|\u0434\u0430\u0439|\u0440\u0430\u0441\u0441\u043a\u0430\u0436\u0438|\u0447\u0442\u043e|\u043a\u0430\u043a|\u0433\u0434\u0435|\u043a\u043e\u0433\u0434\u0430|\u043f\u043e\u0447\u0435\u043c\u0443|\u0437\u0430\u0447\u0435\u043c|\u043c\u043e\u0436\u0435\u0448\u044c|\u0443\u043c\u0435\u0435\u0448\u044c|\u043d\u0430\u0434\u043e|\u043d\u0443\u0436\u043d\u043e|\u0445\u043e\u0447\u0443|\u043e\u0441\u0442\u0430\u0442\u043a|\u043d\u0434\u0441|\u0434\u043e\u043b\u0433|\u0434\u0435\u0431\u0438\u0442\u043e\u0440|\u043a\u0440\u0435\u0434\u0438\u0442\u043e\u0440|\u0441\u043a\u043b\u0430\u0434|\u0442\u043e\u0432\u0430\u0440|\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442|\u043e\u0431\u043e\u0440\u043e\u0442|\u0432\u044b\u0440\u0443\u0447\u043a|\u043f\u0440\u0438\u0431\u044b\u043b)/iu.test(
|
||||
normalized
|
||||
);
|
||||
}
|
||||
|
||||
function buildDeterministicSmalltalkLeadReply(): string {
|
||||
return "\u041f\u0440\u0438\u0432\u0435\u0442! \u0412\u0441\u0451 \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u043e.";
|
||||
}
|
||||
|
|
@ -209,8 +160,6 @@ export async function runAssistantLivingChatRuntime(
|
|||
let livingChatGroundingGuardApplied = false;
|
||||
let livingChatGroundingGuardReason: string | null = null;
|
||||
let livingChatProactiveScopeOfferApplied = false;
|
||||
let livingChatBareScopeProbeAttempted = false;
|
||||
let livingChatBareScopeProbeMatchedOrganization: string | null = null;
|
||||
const continuityActiveOrganization = organizationAuthority.continuityActiveOrganization;
|
||||
let knownOrganizations = [...organizationAuthority.knownOrganizations];
|
||||
let selectedOrganization = organizationAuthority.selectedOrganization;
|
||||
|
|
@ -237,32 +186,6 @@ export async function runAssistantLivingChatRuntime(
|
|||
const lastMemoryAddressDebug = memoryRecapContext.lastMemoryAddressDebug;
|
||||
const lastAnswerInspectionAddressDebug = memoryRecapContext.lastAnswerInspectionAddressDebug;
|
||||
|
||||
if (
|
||||
shouldProbeBareOrganizationScopeCandidate({
|
||||
userMessage,
|
||||
selectedOrganization,
|
||||
activeOrganization,
|
||||
dataScopeMetaQuery,
|
||||
capabilityMetaQuery,
|
||||
destructiveSignal,
|
||||
dangerSignal,
|
||||
operationalSignal
|
||||
})
|
||||
) {
|
||||
dataScopeProbe = await input.resolveDataScopeProbe();
|
||||
livingChatBareScopeProbeAttempted = true;
|
||||
knownOrganizations = input.mergeKnownOrganizations([
|
||||
...knownOrganizations,
|
||||
...(Array.isArray(dataScopeProbe?.organizations) ? (dataScopeProbe.organizations as unknown[]) : [])
|
||||
]);
|
||||
const probedOrganization = input.resolveOrganizationSelectionFromMessage(userMessage, knownOrganizations);
|
||||
if (probedOrganization) {
|
||||
selectedOrganization = probedOrganization;
|
||||
activeOrganization = probedOrganization;
|
||||
livingChatBareScopeProbeMatchedOrganization = probedOrganization;
|
||||
}
|
||||
}
|
||||
|
||||
if (capabilityMetaQuery && (destructiveSignal || dangerSignal)) {
|
||||
chatText = input.buildAssistantSafetyRefusalReply();
|
||||
livingChatSource = "deterministic_safety_refusal";
|
||||
|
|
@ -465,8 +388,6 @@ export async function runAssistantLivingChatRuntime(
|
|||
living_chat_grounding_guard_applied: livingChatGroundingGuardApplied,
|
||||
living_chat_grounding_guard_reason: livingChatGroundingGuardReason,
|
||||
living_chat_proactive_scope_offer_applied: livingChatProactiveScopeOfferApplied,
|
||||
living_chat_bare_scope_probe_attempted: livingChatBareScopeProbeAttempted,
|
||||
living_chat_bare_scope_probe_matched_organization: livingChatBareScopeProbeMatchedOrganization,
|
||||
living_chat_data_scope_probe_status: dataScopeProbe?.status ?? null,
|
||||
living_chat_data_scope_probe_channel: dataScopeProbe?.channel ?? null,
|
||||
living_chat_data_scope_probe_org_count: Array.isArray(dataScopeProbe?.organizations)
|
||||
|
|
|
|||
|
|
@ -279,10 +279,7 @@ export function createAssistantLivingModePolicy(deps: AssistantLivingModePolicyD
|
|||
if (hasAffectiveReactionCue) {
|
||||
return false;
|
||||
}
|
||||
const rawQuestionProbe = String(userMessage ?? "")
|
||||
.replace(/\uFFFD\?/g, "\uFFFD")
|
||||
.replace(/пїЅ\?/giu, "пїЅ");
|
||||
return normalized.length <= 36 && !/[?]/.test(rawQuestionProbe);
|
||||
return normalized.length <= 36 && !/[?]/.test(String(userMessage ?? ""));
|
||||
}
|
||||
|
||||
function hasAssistantDataScopeMetaQuestionSignal(text) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import type { AssistantMcpDiscoveryPilotExecutionContract } from "./assistantMcpDiscoveryPilotExecutor";
|
||||
import { isLikelyFinancialInstitutionCounterparty } from "./counterpartyRoleHeuristics";
|
||||
|
||||
export const ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION =
|
||||
"assistant_mcp_discovery_answer_draft_v1" as const;
|
||||
|
|
@ -27,7 +26,6 @@ export interface AssistantMcpDiscoveryAnswerDraftContract {
|
|||
}
|
||||
|
||||
type BusinessOverview = NonNullable<AssistantMcpDiscoveryPilotExecutionContract["derived_business_overview"]>;
|
||||
type BusinessOverviewRankedBucket = BusinessOverview["top_customers"][number];
|
||||
|
||||
function normalizeReasonCode(value: string): string | null {
|
||||
const normalized = value
|
||||
|
|
@ -485,34 +483,7 @@ function metadataRouteFamilyLabelRu(
|
|||
return null;
|
||||
}
|
||||
|
||||
function isInventoryReserveBoundaryTurn(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
|
||||
const action = pilot.evidence.query_plan.turn_meaning_ref?.asked_action_family;
|
||||
const unsupported = pilot.evidence.query_plan.turn_meaning_ref?.unsupported_but_understood_family;
|
||||
return action === "inventory_reserve_boundary" || unsupported === "inventory_reserve_liquidation_boundary";
|
||||
}
|
||||
|
||||
function isProfitMarginBoundaryTurn(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
|
||||
const action = pilot.evidence.query_plan.turn_meaning_ref?.asked_action_family;
|
||||
const unsupported = pilot.evidence.query_plan.turn_meaning_ref?.unsupported_but_understood_family;
|
||||
return action === "profit_margin_boundary" || unsupported === "profit_margin_boundary";
|
||||
}
|
||||
|
||||
function isDebtDueDateBoundaryTurn(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
|
||||
const action = pilot.evidence.query_plan.turn_meaning_ref?.asked_action_family;
|
||||
const unsupported = pilot.evidence.query_plan.turn_meaning_ref?.unsupported_but_understood_family;
|
||||
return action === "debt_due_date_boundary" || unsupported === "debt_due_date_boundary";
|
||||
}
|
||||
|
||||
function isVendorRiskBoundaryTurn(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
|
||||
const action = pilot.evidence.query_plan.turn_meaning_ref?.asked_action_family;
|
||||
const unsupported = pilot.evidence.query_plan.turn_meaning_ref?.unsupported_but_understood_family;
|
||||
return action === "vendor_risk_procurement_boundary" || unsupported === "vendor_risk_procurement_boundary";
|
||||
}
|
||||
|
||||
function businessOverviewInventoryUnknownLabel(overview: BusinessOverview): string {
|
||||
if (overview.inventory_quality_events) {
|
||||
return "рыночная ликвидационная стоимость и управленческий резерв склада";
|
||||
}
|
||||
if (overview.inventory_staleness_risk_proxy) {
|
||||
return "резервы/списания/ликвидационная стоимость склада";
|
||||
}
|
||||
|
|
@ -572,81 +543,6 @@ function inlineBusinessOverviewAmount(value: string): string {
|
|||
.replace(/[\s.]+$/u, "");
|
||||
}
|
||||
|
||||
function isFinancialInstitutionBucket(bucket: BusinessOverviewRankedBucket | null | undefined): boolean {
|
||||
if (!bucket) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
bucket.counterparty_role_hint === "bank_or_financial_institution" ||
|
||||
isLikelyFinancialInstitutionCounterparty(bucket.axis_value)
|
||||
);
|
||||
}
|
||||
|
||||
function firstNonFinancialInstitutionBucket(
|
||||
buckets: BusinessOverviewRankedBucket[] | null | undefined
|
||||
): BusinessOverviewRankedBucket | null {
|
||||
return (buckets ?? []).find((bucket) => !isFinancialInstitutionBucket(bucket)) ?? null;
|
||||
}
|
||||
|
||||
function rankedBucketAmountLabel(bucket: BusinessOverviewRankedBucket): string {
|
||||
return `${bucket.axis_value} — ${bucket.total_amount_human_ru}`;
|
||||
}
|
||||
|
||||
function businessOverviewIncomingLeaderLine(overview: BusinessOverview): string | null {
|
||||
const leader = overview.top_customers[0];
|
||||
if (!leader) {
|
||||
return null;
|
||||
}
|
||||
if (!isFinancialInstitutionBucket(leader)) {
|
||||
return `Самый крупный подтвержденный клиент в проверенном срезе: ${rankedBucketAmountLabel(leader)}.`;
|
||||
}
|
||||
const nonFinancial = firstNonFinancialInstitutionBucket(overview.top_customers.slice(1));
|
||||
const nonFinancialText = nonFinancial
|
||||
? ` Крупнейший небанковский входящий контрагент в этом же срезе: ${rankedBucketAmountLabel(nonFinancial)}.`
|
||||
: "";
|
||||
return (
|
||||
`Крупнейший входящий денежный источник в проверенном срезе: ${rankedBucketAmountLabel(leader)}. ` +
|
||||
"По названию это банк/финансовая организация, поэтому без проверки назначения платежа не называю это клиентской выручкой или бизнес-заказчиком." +
|
||||
nonFinancialText
|
||||
);
|
||||
}
|
||||
|
||||
function businessOverviewOutgoingLeaderLine(overview: BusinessOverview): string | null {
|
||||
const leader = overview.top_suppliers?.[0];
|
||||
if (!leader) {
|
||||
return null;
|
||||
}
|
||||
if (!isFinancialInstitutionBucket(leader)) {
|
||||
return `Самый крупный подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${rankedBucketAmountLabel(leader)}.`;
|
||||
}
|
||||
const nonFinancial = firstNonFinancialInstitutionBucket(overview.top_suppliers.slice(1));
|
||||
const nonFinancialText = nonFinancial
|
||||
? ` Крупнейший небанковский получатель исходящих денег в этом же срезе: ${rankedBucketAmountLabel(nonFinancial)}.`
|
||||
: "";
|
||||
return (
|
||||
`Крупнейший получатель исходящих денег в проверенном срезе: ${rankedBucketAmountLabel(leader)}. ` +
|
||||
"По названию это банк/финансовая организация, поэтому без назначения платежа/договора не считаю это обычным поставщиком." +
|
||||
nonFinancialText
|
||||
);
|
||||
}
|
||||
|
||||
function businessOverviewSupplierBoundaryBasis(overview: BusinessOverview): string {
|
||||
const leader = overview.top_suppliers?.[0] ?? null;
|
||||
if (!leader) {
|
||||
return "есть только общий срез исходящих платежей без надежного профиля поставщицкого риска";
|
||||
}
|
||||
const share = percentText(leader.total_amount, overview.outgoing_supplier_payout.total_amount);
|
||||
if (isFinancialInstitutionBucket(leader)) {
|
||||
const base = share
|
||||
? `крупнейший получатель исходящих денег ${leader.axis_value} держит около ${share} проверенного исходящего потока (${leader.total_amount_human_ru})`
|
||||
: `крупнейший получатель исходящих денег: ${rankedBucketAmountLabel(leader)}`;
|
||||
return `${base}; по названию это банк/финансовая организация, поэтому этот факт нельзя считать доказанной зависимостью от одного обычного поставщика`;
|
||||
}
|
||||
return share
|
||||
? `крупнейший подтвержденный поставщик/получатель исходящих платежей ${leader.axis_value} держит около ${share} проверенного исходящего потока (${leader.total_amount_human_ru})`
|
||||
: `крупнейший подтвержденный поставщик/получатель исходящих платежей: ${rankedBucketAmountLabel(leader)}`;
|
||||
}
|
||||
|
||||
function businessOverviewHeadlineMetricsLine(overview: BusinessOverview): string | null {
|
||||
const parts: string[] = [];
|
||||
if (overview.incoming_customer_revenue.rows_with_amount > 0) {
|
||||
|
|
@ -658,24 +554,6 @@ function businessOverviewHeadlineMetricsLine(overview: BusinessOverview): string
|
|||
if (overview.incoming_customer_revenue.rows_with_amount > 0 || overview.outgoing_supplier_payout.rows_with_amount > 0) {
|
||||
parts.push(`расчетное операционное нетто ${inlineBusinessOverviewAmount(overview.net_amount_human_ru)}`);
|
||||
}
|
||||
if (overview.accounting_financial_result) {
|
||||
const result = overview.accounting_financial_result;
|
||||
const direction =
|
||||
result.final_result_direction === "profit"
|
||||
? "учетная прибыль"
|
||||
: result.final_result_direction === "loss"
|
||||
? "учетный убыток"
|
||||
: "нулевой учетный финрезультат";
|
||||
const amount =
|
||||
result.final_result_direction === "loss"
|
||||
? `минус ${inlineBusinessOverviewAmount(result.final_result_amount_human_ru)}`
|
||||
: inlineBusinessOverviewAmount(result.final_result_amount_human_ru);
|
||||
const margin =
|
||||
result.net_margin_to_revenue_pct === null
|
||||
? "маржа к подтвержденной выручке не рассчитана"
|
||||
: `маржа к подтвержденной выручке ${result.net_margin_to_revenue_pct}%`;
|
||||
parts.push(`${direction} по закрытию счетов 90/91/99 ${amount}; ${margin}`);
|
||||
}
|
||||
const strongestIncomingYear = businessOverviewStrongestIncomingYear(overview);
|
||||
if (strongestIncomingYear) {
|
||||
parts.push(
|
||||
|
|
@ -683,131 +561,10 @@ function businessOverviewHeadlineMetricsLine(overview: BusinessOverview): string
|
|||
);
|
||||
}
|
||||
return parts.length > 0
|
||||
? overview.accounting_financial_result
|
||||
? `${parts.join("; ")}. Финрезультат ограничен найденными строками 1С и не является внешним аудитом или юридически подтвержденной отчетностью`
|
||||
: `${parts.join("; ")}. Это операционный денежный сигнал по найденным строкам, не бухгалтерская прибыль и не финрезультат`
|
||||
? `${parts.join("; ")}. Это operating-flow proxy по найденным строкам, не бухгалтерская прибыль и не финрезультат`
|
||||
: null;
|
||||
}
|
||||
|
||||
function businessOverviewAccountingFinancialResultText(overview: BusinessOverview): string | null {
|
||||
const result = overview.accounting_financial_result;
|
||||
if (!result) {
|
||||
return null;
|
||||
}
|
||||
const direction =
|
||||
result.final_result_direction === "profit"
|
||||
? "учетная прибыль"
|
||||
: result.final_result_direction === "loss"
|
||||
? "учетный убыток"
|
||||
: "нулевой учетный финрезультат";
|
||||
const signedAmount =
|
||||
result.final_result_direction === "loss"
|
||||
? `минус ${result.final_result_amount_human_ru}`
|
||||
: result.final_result_amount_human_ru;
|
||||
const marginText =
|
||||
result.net_margin_to_revenue_pct === null
|
||||
? "маржа к подтвержденной выручке не рассчитана"
|
||||
: `маржа к подтвержденной выручке ${result.net_margin_to_revenue_pct}%`;
|
||||
const basis =
|
||||
result.final_transfer_basis === "account_99_to_84_period_close"
|
||||
? "по закрытию 99 на 84"
|
||||
: "по закрытию 90/91 на 99";
|
||||
return `Нет: денежное операционное нетто не стоит считать чистой прибылью. Отдельно по закрытию счетов 90/91/99 в 1С за ${result.period_scope} подтвержден ${direction}: ${signedAmount}; ${marginText}. Основа: ${basis}, ${result.period_close_rows_with_amount} строк(и) закрытия периода с суммой. Это учетный финрезультат по найденным строкам 1С, не внешний аудит и не юридически подтвержденная отчетность.`;
|
||||
}
|
||||
|
||||
function businessOverviewDebtDueDateAgingText(overview: BusinessOverview): string | null {
|
||||
const aging = overview.debt_due_date_aging;
|
||||
if (!aging) {
|
||||
return null;
|
||||
}
|
||||
if (aging.evidence_status === "confirmed_overdue") {
|
||||
const top = aging.top_overdue_items?.[0] ?? null;
|
||||
const topText = top
|
||||
? ` Самая старая строка: due date ${top.due_date}, просрочка ${top.overdue_days} дн., ${top.amount_human_ru}${top.contract ? ` по договору ${top.contract}` : ""}.`
|
||||
: "";
|
||||
return `Due-date aging на ${aging.as_of_date} проверен по срокам оплаты договоров и датам расчетных документов: подтверждено просроченных строк ${aging.overdue_rows}, сумма ${aging.overdue_amount_human_ru}.${topText}`;
|
||||
}
|
||||
if (aging.evidence_status === "no_payment_terms_configured") {
|
||||
return `Due-date aging на ${aging.as_of_date} проверен по открытым расчетам: брутто ${aging.gross_open_amount_human_ru}, строк с суммой ${aging.rows_with_amount}, но в проверенных договорах срок оплаты не установлен. Подтвержденной просрочки по договорным срокам оплаты нет.`;
|
||||
}
|
||||
if (aging.evidence_status === "insufficient_due_date_basis") {
|
||||
return `Due-date aging на ${aging.as_of_date} запускался, но по строкам с установленным сроком оплаты не хватило даты расчетного документа для честного расчета due date. Просрочка не подтверждена.`;
|
||||
}
|
||||
return `Due-date aging на ${aging.as_of_date} проверен: строк с установленным сроком оплаты ${aging.rows_with_payment_terms}, подтвержденной просрочки не найдено; не просрочено по расчету ${aging.not_yet_due_amount_human_ru}.`;
|
||||
}
|
||||
|
||||
function financialFlowHintTextRuFromBucket(bucket: BusinessOverviewRankedBucket | null | undefined): string {
|
||||
const rows = bucket?.financial_flow_hint_rows ?? 0;
|
||||
const rowsText = rows > 0 ? ` (${rows} строк)` : "";
|
||||
if (bucket?.financial_flow_hint === "loan_or_credit") {
|
||||
return ` По полям банковского документа доминирует кредитный/заемный признак${rowsText}; это не обычный поставщик и не клиентская выручка без отдельной проверки назначения.`;
|
||||
}
|
||||
if (bucket?.financial_flow_hint === "bank_fee_or_service") {
|
||||
return ` По полям банковского документа доминирует признак банковской комиссии/услуг банка${rowsText}; это не обычный поставщик товаров/услуг без отдельной проверки договора.`;
|
||||
}
|
||||
if (bucket?.financial_flow_hint === "tax_or_budget") {
|
||||
return ` По полям банковского документа доминирует налоговый/бюджетный признак${rowsText}.`;
|
||||
}
|
||||
if (bucket?.financial_flow_hint === "payroll_or_social") {
|
||||
return ` По полям банковского документа доминирует зарплатный/социальный признак${rowsText}.`;
|
||||
}
|
||||
if (bucket?.financial_flow_hint === "supplier_payment") {
|
||||
return ` По полям банковского документа доминирует признак оплаты поставщику${rowsText}; если получатель по названию является банком, это все равно требует осторожной трактовки.`;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
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 `Проверка концентрации закупок/исходящих платежей за ${period}: крупнейший получатель исходящих денег ${topName}${topShare}${topAmount}, всего исходящих платежей ${total}. По названию это банк/финансовая организация, поэтому зависимость от обычного поставщика этим не подтверждается.${financialFlowHintTextRuFromBucket(top)}${nonFinancialText}${contractText} Надежность поставщиков, качество поставок, назначение каждого платежа и полная структура расходов этим срезом не доказаны.`;
|
||||
}
|
||||
if (quality.evidence_status === "reviewed_procurement_concentration") {
|
||||
return `Проверка концентрации закупок/исходящих платежей за ${period}: крупнейший поставщик/получатель исходящих платежей ${topName}${topShare}${topAmount}, всего исходящих платежей ${total}.${contractText} Это проверенный сигнал концентрации закупок/исходящих платежей, но не аудит надежности поставщика, качества поставок и полной структуры расходов.`;
|
||||
}
|
||||
return `Проверка концентрации закупок/исходящих платежей за ${period} нашла исходящие платежи на ${total}, но надежной небанковской концентрации поставщика по найденным строкам не хватает.${contractText} Полный аудит поставщицкого риска не подтвержден.`;
|
||||
}
|
||||
|
||||
function businessOverviewInventoryQualityEventsText(overview: BusinessOverview): string | null {
|
||||
const quality = overview.inventory_quality_events;
|
||||
if (!quality) {
|
||||
return null;
|
||||
}
|
||||
const period = quality.period_scope ?? "проверенное окно";
|
||||
const organization = overview.organization_scope ? ` по организации ${overview.organization_scope}` : "";
|
||||
const eventWindow =
|
||||
quality.first_event_date && quality.latest_event_date
|
||||
? ` Окно найденных событий: ${quality.first_event_date} - ${quality.latest_event_date}.`
|
||||
: "";
|
||||
if (quality.evidence_status === "reviewed_no_quality_events_found") {
|
||||
return `Коротко: проверил складские документы списания, оприходования, инвентаризации и переоценки${organization} за ${period}; подтвержденных событий списания/корректировки/инвентаризации/переоценки не найдено. Это сильный отрицательный сигнал по доступным документам 1С, но не рыночная ликвидационная стоимость и не управленческий резерв под неликвиды.`;
|
||||
}
|
||||
if (quality.evidence_status === "reviewed_inventory_control_events_only") {
|
||||
return `Коротко: проверил складские quality-события${organization} за ${period}; списаний и оприходований/корректировок с суммой не найдено, но есть инвентаризации ${quality.inventory_count_rows} и переоценки ${quality.revaluation_rows}.${eventWindow} Это контрольные складские документы, а не подтвержденный резерв или рыночная ликвидационная оценка.`;
|
||||
}
|
||||
return `Коротко: проверил складские quality-события${organization} за ${period}; списаний ${quality.writeoff_rows} на ${quality.writeoff_amount_human_ru}, оприходований/корректировок ${quality.receipt_adjustment_rows} на ${quality.receipt_adjustment_amount_human_ru}, инвентаризаций ${quality.inventory_count_rows}, переоценок ${quality.revaluation_rows}.${eventWindow} Это подтвержденные документы 1С по складским событиям, но не самостоятельная рыночная ликвидационная стоимость и не расчет управленческого резерва.`;
|
||||
}
|
||||
|
||||
function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpDiscoveryPilotExecutionContract): string {
|
||||
const askedMonthlyBreakdown =
|
||||
pilot.derived_bidirectional_value_flow?.aggregation_axis === "month" ||
|
||||
|
|
@ -826,43 +583,6 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
|
|||
}
|
||||
if (isBusinessOverviewPilot(pilot) && pilot.derived_business_overview && mode === "confirmed_with_bounded_inference") {
|
||||
const overview = pilot.derived_business_overview;
|
||||
if (isProfitMarginBoundaryTurn(pilot)) {
|
||||
const accountingFinancialResultText = businessOverviewAccountingFinancialResultText(overview);
|
||||
if (accountingFinancialResultText) {
|
||||
return accountingFinancialResultText;
|
||||
}
|
||||
return "Нельзя точно подтвердить чистую прибыль и маржу по текущему срезу 1С; есть только ограниченный операционный денежный/товарный сигнал, а не полный отчет о прибыли и не бухгалтерский финрезультат.";
|
||||
}
|
||||
if (isDebtDueDateBoundaryTurn(pilot)) {
|
||||
const dueDateText = businessOverviewDebtDueDateAgingText(overview);
|
||||
if (dueDateText) {
|
||||
return dueDateText;
|
||||
}
|
||||
return "Нельзя точно определить, какая дебиторка просрочена, по текущему срезу 1С; есть только debt-quality proxy, но нет проверенного due-date маршрута по договорам, срокам оплаты и погашению расчетов.";
|
||||
}
|
||||
if (isInventoryReserveBoundaryTurn(pilot)) {
|
||||
const inventoryQualityEventsText = businessOverviewInventoryQualityEventsText(overview);
|
||||
if (inventoryQualityEventsText) {
|
||||
return inventoryQualityEventsText;
|
||||
}
|
||||
const inventoryBasis = overview.inventory_staleness_risk_proxy
|
||||
? "есть только складской staleness-risk proxy по найденным строкам"
|
||||
: overview.inventory_position || overview.inventory_turnover_proxy
|
||||
? "есть только ограниченные складские proxy-сигналы по найденным строкам"
|
||||
: "нет отдельного складского среза на дату и проверки учетной политики резервов";
|
||||
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"
|
||||
: "procurement concentration proxy";
|
||||
return `Коротко: точный риск зависимости от одного поставщика по текущим данным не подтвержден; есть только ${proxyLabel}: ${businessOverviewSupplierBoundaryBasis(overview)}. Это сигнал концентрации исходящих платежей, а не полный аудит надежности поставщиков, условий, качества и структуры всех расходов.`;
|
||||
}
|
||||
const families: string[] = [];
|
||||
if (
|
||||
overview.incoming_customer_revenue.rows_with_amount > 0 ||
|
||||
|
|
@ -888,9 +608,6 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
|
|||
if (overview.tax_position) {
|
||||
families.push("НДС-позиция");
|
||||
}
|
||||
if (overview.accounting_financial_result) {
|
||||
families.push("учетный финрезультат 90/91/99");
|
||||
}
|
||||
if (overview.trading_margin_proxy) {
|
||||
families.push("торговый margin proxy");
|
||||
}
|
||||
|
|
@ -906,9 +623,6 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
|
|||
if (overview.debt_staleness_risk_proxy) {
|
||||
families.push("staleness risk proxy открытых расчетов");
|
||||
}
|
||||
if (overview.debt_due_date_aging) {
|
||||
families.push("due-date aging открытых расчетов");
|
||||
}
|
||||
if (overview.inventory_position) {
|
||||
families.push("складской срез на дату");
|
||||
}
|
||||
|
|
@ -918,27 +632,20 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
|
|||
if (overview.inventory_staleness_risk_proxy) {
|
||||
families.push("staleness risk proxy склада");
|
||||
}
|
||||
if (overview.inventory_quality_events) {
|
||||
families.push("складские quality-события");
|
||||
}
|
||||
const unknownFamilies = overview.accounting_financial_result
|
||||
? ["аудированная/юридически подтвержденная прибыль"]
|
||||
: [overview.trading_margin_proxy ? "чистая прибыль/точная маржа" : "прибыль/маржа"];
|
||||
const unknownFamilies = [overview.trading_margin_proxy ? "чистая прибыль/точная маржа" : "прибыль/маржа"];
|
||||
if (!overview.tax_position) {
|
||||
unknownFamilies.push("НДС");
|
||||
}
|
||||
if (!overview.debt_position) {
|
||||
unknownFamilies.push("долговой срез");
|
||||
}
|
||||
if (!overview.debt_due_date_aging) {
|
||||
unknownFamilies.push(
|
||||
overview.debt_staleness_risk_proxy
|
||||
unknownFamilies.push(
|
||||
overview.debt_staleness_risk_proxy
|
||||
? "договорные сроки оплаты/due-date просрочка"
|
||||
: overview.debt_open_settlement_quality
|
||||
? "due-date просрочка"
|
||||
: "качество открытых расчетов"
|
||||
);
|
||||
}
|
||||
);
|
||||
unknownFamilies.push(businessOverviewInventoryUnknownLabel(overview));
|
||||
const metricLead = businessOverviewHeadlineMetricsLine(overview);
|
||||
if (metricLead) {
|
||||
|
|
@ -1137,30 +844,16 @@ 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 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.");
|
||||
claims.push("Do not present a debt-position snapshot as debt aging, overdue debt, or credit-quality analysis.");
|
||||
claims.push("Do not present open-settlement concentration as contractual due-date aging or confirmed overdue debt.");
|
||||
claims.push("Do not present business overview debt staleness risk proxy as confirmed overdue debt, contractual delinquency, credit risk, or due-date aging.");
|
||||
claims.push("Do not claim contractual overdue debt unless the due-date aging route found configured payment terms and enough settlement-date evidence.");
|
||||
claims.push("Do not present an inventory snapshot or purchase-date aging signal as turnover, obsolescence, liquidation value, or full inventory health.");
|
||||
claims.push("Do not present business overview inventory turnover proxy as full inventory liquidity, FIFO turnover, obsolescence analysis, or liquidation value.");
|
||||
claims.push("Do not present business overview inventory staleness risk proxy as confirmed obsolete stock, reserve, write-off, or liquidation value.");
|
||||
if (pilot.derived_business_overview?.inventory_quality_events) {
|
||||
claims.push("Do not present reviewed inventory quality events as confirmed obsolete stock, reserve policy, market liquidation value, management reserve, or full inventory health.");
|
||||
}
|
||||
if (
|
||||
pilot.derived_business_overview?.top_customers?.some(isFinancialInstitutionBucket) ||
|
||||
pilot.derived_business_overview?.top_suppliers?.some(isFinancialInstitutionBucket)
|
||||
) {
|
||||
claims.push("Do not present bank-like counterparties as ordinary customers, suppliers, revenue, procurement dependency, or business quality evidence without payment-purpose/contract proof.");
|
||||
}
|
||||
if (pilot.derived_business_overview?.missing_proof_families?.length) {
|
||||
claims.push("Do not present business overview missing proof families as checked, executed, or confirmed routes.");
|
||||
}
|
||||
|
|
@ -1169,9 +862,6 @@ function buildMustNotClaim(pilot: AssistantMcpDiscoveryPilotExecutionContract):
|
|||
if (pilot.derived_ranked_value_flow) {
|
||||
claims.push("Do not present a bounded ranking as a complete all-time ranking outside the checked period and organization.");
|
||||
claims.push("Do not imply the top-ranked counterparty is globally final when probe-limit or scope boundaries still exist.");
|
||||
if (pilot.derived_ranked_value_flow.ranked_values.some(isFinancialInstitutionBucket)) {
|
||||
claims.push("Do not present bank-like counterparties as ordinary customers, suppliers, revenue, procurement dependency, or business quality evidence without payment-purpose/contract proof.");
|
||||
}
|
||||
}
|
||||
if (isDocumentPilot(pilot)) {
|
||||
claims.push("Do not claim full document history outside the checked period.");
|
||||
|
|
@ -1338,40 +1028,26 @@ function derivedRankedValueFlowConfirmedLine(pilot: AssistantMcpDiscoveryPilotEx
|
|||
return null;
|
||||
}
|
||||
const leader = ranking.ranked_values[0];
|
||||
const leaderLooksFinancial = isFinancialInstitutionBucket(leader);
|
||||
const organization = ranking.organization_scope ? ` по организации ${ranking.organization_scope}` : "";
|
||||
const period = ranking.period_scope ? ` за период ${ranking.period_scope}` : " в проверенном окне";
|
||||
const roleCaveat = leaderLooksFinancial
|
||||
? ranking.value_flow_direction === "outgoing_supplier_payout"
|
||||
? " По названию это банк/финансовая организация, поэтому без назначения платежа/договора не называю это обычным поставщиком."
|
||||
: " По названию это банк/финансовая организация, поэтому без назначения платежа не называю это клиентской выручкой или бизнес-заказчиком."
|
||||
: "";
|
||||
if (ranking.ranked_values.length === 1) {
|
||||
const singleLead =
|
||||
leaderLooksFinancial
|
||||
? ranking.value_flow_direction === "outgoing_supplier_payout"
|
||||
? "В проверенных исходящих платежах найден один банковский/финансовый получатель"
|
||||
: "В проверенных входящих поступлениях найден один банковский/финансовый источник"
|
||||
: ranking.value_flow_direction === "outgoing_supplier_payout"
|
||||
? "В проверенных исходящих платежах найден один контрагент"
|
||||
: "В проверенных входящих поступлениях найден один контрагент";
|
||||
ranking.value_flow_direction === "outgoing_supplier_payout"
|
||||
? "В проверенных исходящих платежах найден один контрагент"
|
||||
: "В проверенных входящих поступлениях найден один контрагент";
|
||||
const limitCaveat = ranking.coverage_limited_by_probe_limit
|
||||
? " Лимит строк проверки достигнут; сравнение с другими контрагентами может быть неполным."
|
||||
: " Других контрагентов в этом проверенном срезе не найдено, поэтому это не полноценный сравнительный рейтинг.";
|
||||
return `${singleLead} ${leader.axis_value}${organization}${period}: ${leader.total_amount_human_ru} по ${leader.rows_with_amount} строкам с суммой.${roleCaveat}${limitCaveat}`;
|
||||
return `${singleLead} ${leader.axis_value}${organization}${period}: ${leader.total_amount_human_ru} по ${leader.rows_with_amount} строкам с суммой.${limitCaveat}`;
|
||||
}
|
||||
const directionLead =
|
||||
leaderLooksFinancial
|
||||
ranking.ranking_need === "bottom_asc"
|
||||
? ranking.value_flow_direction === "outgoing_supplier_payout"
|
||||
? "Крупнейший получатель исходящих денег"
|
||||
: "Крупнейший входящий денежный источник"
|
||||
: ranking.ranking_need === "bottom_asc"
|
||||
? ranking.value_flow_direction === "outgoing_supplier_payout"
|
||||
? "Меньше всего заплатили контрагенту"
|
||||
: "Меньше всего денег принёс контрагент"
|
||||
: ranking.value_flow_direction === "outgoing_supplier_payout"
|
||||
? "Больше всего заплатили контрагенту"
|
||||
: "Больше всего денег принёс контрагент";
|
||||
? "Меньше всего заплатили контрагенту"
|
||||
: "Меньше всего денег принёс контрагент"
|
||||
: ranking.value_flow_direction === "outgoing_supplier_payout"
|
||||
? "Больше всего заплатили контрагенту"
|
||||
: "Больше всего денег принёс контрагент";
|
||||
const tail = ranking.ranked_values
|
||||
.slice(1, 3)
|
||||
.map((bucket) => `${bucket.axis_value} — ${bucket.total_amount_human_ru}`)
|
||||
|
|
@ -1380,7 +1056,7 @@ function derivedRankedValueFlowConfirmedLine(pilot: AssistantMcpDiscoveryPilotEx
|
|||
const limitCaveat = ranking.coverage_limited_by_probe_limit
|
||||
? " Лимит строк проверки достигнут; рейтинг может быть неполным."
|
||||
: "";
|
||||
return `${directionLead} ${leader.axis_value}${organization}${period}: ${leader.total_amount_human_ru} по ${leader.rows_with_amount} строкам с суммой.${roleCaveat}${trail}${limitCaveat}`;
|
||||
return `${directionLead} ${leader.axis_value}${organization}${period}: ${leader.total_amount_human_ru} по ${leader.rows_with_amount} строкам с суммой.${trail}${limitCaveat}`;
|
||||
}
|
||||
|
||||
function derivedValueFlowConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
|
||||
|
|
@ -1571,31 +1247,21 @@ function derivedBusinessOverviewConfirmedLines(pilot: AssistantMcpDiscoveryPilot
|
|||
`Самый сильный год по подтвержденным входящим поступлениям: ${strongestIncomingYear.year_bucket} — ${strongestIncomingYear.incoming_total_amount_human_ru} по ${strongestIncomingYear.incoming_rows_with_amount} строкам с суммой. Это не бухгалтерская прибыль.`
|
||||
);
|
||||
}
|
||||
const incomingLeaderLine = businessOverviewIncomingLeaderLine(overview);
|
||||
if (incomingLeaderLine) {
|
||||
lines.push(incomingLeaderLine);
|
||||
const leader = overview.top_customers[0];
|
||||
if (leader) {
|
||||
lines.push(`Самый крупный подтвержденный клиент в проверенном срезе: ${leader.axis_value} — ${leader.total_amount_human_ru}.`);
|
||||
}
|
||||
const outgoingLeaderLine = businessOverviewOutgoingLeaderLine(overview);
|
||||
if (outgoingLeaderLine) {
|
||||
lines.push(outgoingLeaderLine);
|
||||
}
|
||||
const vendorQualityText = businessOverviewVendorProcurementQualityText(overview);
|
||||
if (vendorQualityText) {
|
||||
lines.push(vendorQualityText);
|
||||
const supplierLeader = overview.top_suppliers?.[0];
|
||||
if (supplierLeader) {
|
||||
lines.push(
|
||||
`Самый крупный подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${supplierLeader.axis_value} — ${supplierLeader.total_amount_human_ru}.`
|
||||
);
|
||||
}
|
||||
if (overview.yearly_breakdown?.length) {
|
||||
lines.push(
|
||||
`Годовая раскладка операционного денежного потока построена по подтвержденным строкам 1С за ${yearCountHumanRu(overview.yearly_breakdown.length)}.`
|
||||
);
|
||||
}
|
||||
if (
|
||||
overview.incoming_customer_revenue.coverage_recovered_by_period_chunking ||
|
||||
overview.outgoing_supplier_payout.coverage_recovered_by_period_chunking
|
||||
) {
|
||||
lines.push(
|
||||
"Денежное покрытие бизнес-обзора за год восстановлено через помесячные 1С-проверки, а не только через широкий общий запрос."
|
||||
);
|
||||
}
|
||||
if (overview.activity_period) {
|
||||
lines.push(
|
||||
`Окно подтвержденной активности в 1С: ${overview.activity_period.first_activity_date} — ${overview.activity_period.latest_activity_date}; ориентировочно ${overview.activity_period.duration_human_ru}.`
|
||||
|
|
@ -1650,12 +1316,6 @@ function derivedBusinessOverviewConfirmedLines(pilot: AssistantMcpDiscoveryPilot
|
|||
`НДС-позиция за ${overview.tax_position.period_scope}: книга продаж ${overview.tax_position.sales_vat_amount_human_ru}, книга покупок/вычеты ${overview.tax_position.purchase_vat_amount_human_ru}, нетто ${taxDirection} ${overview.tax_position.net_vat_amount_human_ru}.`
|
||||
);
|
||||
}
|
||||
if (overview.accounting_financial_result) {
|
||||
const accountingFinancialResultText = businessOverviewAccountingFinancialResultText(overview);
|
||||
if (accountingFinancialResultText) {
|
||||
lines.push(accountingFinancialResultText);
|
||||
}
|
||||
}
|
||||
if (overview.trading_margin_proxy) {
|
||||
const proxy = overview.trading_margin_proxy;
|
||||
const marginText = proxy.margin_to_revenue_pct === null ? "не рассчитана" : `${proxy.margin_to_revenue_pct}%`;
|
||||
|
|
@ -1699,10 +1359,6 @@ function derivedBusinessOverviewConfirmedLines(pilot: AssistantMcpDiscoveryPilot
|
|||
`Staleness risk proxy открытых расчетов на ${proxy.as_of_date}: самый старый договорный сигнал ${proxy.oldest_contract_start_date}, возраст ${proxy.max_contract_age_days} дн.; старейший крупный договор ${proxy.top_contract}${counterpartyText} держит ${proxy.top_contract_amount_human_ru} (${proxy.top_contract_share_pct}% брутто открытых остатков); оценка ${debtStalenessRiskBandRu(proxy.risk_band)}. Это не подтвержденная просрочка, не кредитный риск и не due-date aging.`
|
||||
);
|
||||
}
|
||||
const dueDateText = businessOverviewDebtDueDateAgingText(overview);
|
||||
if (dueDateText) {
|
||||
lines.push(dueDateText);
|
||||
}
|
||||
if (overview.inventory_position) {
|
||||
const leader = overview.inventory_position.top_items[0];
|
||||
const leaderText = leader
|
||||
|
|
@ -1738,10 +1394,6 @@ function derivedBusinessOverviewConfirmedLines(pilot: AssistantMcpDiscoveryPilot
|
|||
`Staleness risk proxy склада на ${proxy.as_of_date}: самая ранняя дата закупочного сигнала ${proxy.oldest_purchase_date}, возраст ${proxy.max_purchase_age_days} дн., sales-to-stock ${proxy.sales_to_stock_amount_ratio}x, оценка ${inventoryStalenessRiskBandRu(proxy.risk_band)}. Это не подтвержденная неликвидность, не резерв и не ликвидационная стоимость.`
|
||||
);
|
||||
}
|
||||
const inventoryQualityEventsText = businessOverviewInventoryQualityEventsText(overview);
|
||||
if (inventoryQualityEventsText) {
|
||||
lines.push(inventoryQualityEventsText.replace(/^Коротко:\s*/u, ""));
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
|
|
@ -1764,13 +1416,6 @@ function businessOverviewCustomerConcentrationLine(overview: BusinessOverview):
|
|||
return null;
|
||||
}
|
||||
const share = percentText(leader.total_amount, overview.incoming_customer_revenue.total_amount);
|
||||
if (isFinancialInstitutionBucket(leader)) {
|
||||
const base = share
|
||||
? `Крупнейший входящий денежный источник ${leader.axis_value} дает около ${share} проверенных входящих поступлений (${leader.total_amount_human_ru})`
|
||||
: `Крупнейший входящий денежный источник в проверенном срезе: ${rankedBucketAmountLabel(leader)}`;
|
||||
const nonFinancial = firstNonFinancialInstitutionBucket(overview.top_customers.slice(1));
|
||||
return `${base}. По названию это банк/финансовая организация, поэтому это не доказывает клиентскую выручку или зависимость от клиента.${nonFinancial ? ` Крупнейший небанковский входящий контрагент: ${rankedBucketAmountLabel(nonFinancial)}.` : ""}`;
|
||||
}
|
||||
return share
|
||||
? `Концентрация входящего потока: крупнейший подтвержденный клиент ${leader.axis_value} дает около ${share} проверенных входящих поступлений (${leader.total_amount_human_ru}). Это сигнал зависимости от клиента, а не полный customer-risk аудит.`
|
||||
: `Крупнейший подтвержденный клиент в проверенном срезе: ${leader.axis_value} — ${leader.total_amount_human_ru}.`;
|
||||
|
|
@ -1782,15 +1427,8 @@ function businessOverviewSupplierConcentrationLine(overview: BusinessOverview):
|
|||
return null;
|
||||
}
|
||||
const share = percentText(leader.total_amount, overview.outgoing_supplier_payout.total_amount);
|
||||
if (isFinancialInstitutionBucket(leader)) {
|
||||
const base = share
|
||||
? `Концентрация исходящего потока: крупнейший получатель исходящих денег ${leader.axis_value} держит около ${share} проверенных исходящих платежей (${leader.total_amount_human_ru})`
|
||||
: `Крупнейший получатель исходящих денег в проверенном срезе: ${rankedBucketAmountLabel(leader)}`;
|
||||
const nonFinancial = firstNonFinancialInstitutionBucket(overview.top_suppliers.slice(1));
|
||||
return `${base}. По названию это банк/финансовая организация, поэтому это не доказательство зависимости от обычного поставщика без проверки назначения платежа/договора.${nonFinancial ? ` Крупнейший небанковский получатель исходящих денег: ${rankedBucketAmountLabel(nonFinancial)}.` : ""}`;
|
||||
}
|
||||
return share
|
||||
? `Концентрация исходящего потока: крупнейший подтвержденный поставщик/получатель исходящих платежей ${leader.axis_value} держит около ${share} проверенных исходящих платежей (${leader.total_amount_human_ru}). Это сигнал концентрации закупок/исходящих платежей по найденным строкам, а не полный аудит поставщицкого риска или структура всех расходов.`
|
||||
? `Концентрация исходящего потока: крупнейший подтвержденный поставщик/получатель исходящих платежей ${leader.axis_value} держит около ${share} проверенных исходящих платежей (${leader.total_amount_human_ru}). Это сигнал procurement concentration по найденным строкам, а не полный vendor-risk аудит или структура всех расходов.`
|
||||
: `Крупнейший подтвержденный поставщик/получатель исходящих платежей в проверенном срезе: ${leader.axis_value} — ${leader.total_amount_human_ru}.`;
|
||||
}
|
||||
|
||||
|
|
@ -1816,7 +1454,7 @@ function businessOverviewYearlyOperatingLine(overview: BusinessOverview): string
|
|||
: `нетто в плюс ${strongestNetYear.net_amount_human_ru}`;
|
||||
parts.push(`лучший год по расчетному операционному нетто ${strongestNetYear.year_bucket}: ${netText}`);
|
||||
}
|
||||
return `Годовая динамика по проверенным строкам: ${parts.join("; ")}. Это операционный денежный сигнал, не бухгалтерская прибыль и не финрезультат.`;
|
||||
return `Годовая динамика по проверенным строкам: ${parts.join("; ")}. Это operating-flow proxy, не бухгалтерская прибыль и не финрезультат.`;
|
||||
}
|
||||
|
||||
function businessOverviewRiskSynthesisLine(overview: BusinessOverview): string | null {
|
||||
|
|
@ -1836,20 +1474,6 @@ function businessOverviewRiskSynthesisLine(overview: BusinessOverview): string |
|
|||
: `маржинальность proxy ${overview.trading_margin_proxy.margin_to_revenue_pct}%`;
|
||||
signals.push(`торговый спред proxy ${overview.trading_margin_proxy.gross_spread_proxy_human_ru}, ${marginText}`);
|
||||
}
|
||||
if (overview.accounting_financial_result) {
|
||||
const result = overview.accounting_financial_result;
|
||||
const direction =
|
||||
result.final_result_direction === "profit"
|
||||
? "учетная прибыль"
|
||||
: result.final_result_direction === "loss"
|
||||
? "учетный убыток"
|
||||
: "нулевой учетный финрезультат";
|
||||
const marginText =
|
||||
result.net_margin_to_revenue_pct === null
|
||||
? "маржа к подтвержденной выручке не рассчитана"
|
||||
: `маржа к подтвержденной выручке ${result.net_margin_to_revenue_pct}%`;
|
||||
signals.push(`${direction} по закрытию счетов 90/91/99 ${result.final_result_amount_human_ru}, ${marginText}`);
|
||||
}
|
||||
if (overview.debt_position) {
|
||||
const debtDirection =
|
||||
overview.debt_position.net_debt_position_direction === "net_receivable"
|
||||
|
|
@ -1871,18 +1495,6 @@ function businessOverviewRiskSynthesisLine(overview: BusinessOverview): string |
|
|||
`staleness risk proxy открытых расчетов: ${debtStalenessRiskBandRu(overview.debt_staleness_risk_proxy.risk_band)}, возраст ${overview.debt_staleness_risk_proxy.max_contract_age_days} дн., концентрация старейшего крупного договора ${overview.debt_staleness_risk_proxy.top_contract_share_pct}%`
|
||||
);
|
||||
}
|
||||
if (overview.debt_due_date_aging) {
|
||||
const aging = overview.debt_due_date_aging;
|
||||
signals.push(
|
||||
aging.evidence_status === "confirmed_overdue"
|
||||
? `due-date aging: подтвержденная просрочка ${aging.overdue_amount_human_ru}, строк ${aging.overdue_rows}`
|
||||
: aging.evidence_status === "no_payment_terms_configured"
|
||||
? "due-date aging: проверено, но сроки оплаты в договорах не установлены; подтвержденной просрочки нет"
|
||||
: aging.evidence_status === "insufficient_due_date_basis"
|
||||
? "due-date aging: не хватило даты расчетного документа для честного расчета просрочки"
|
||||
: `due-date aging: проверено, подтвержденной просрочки не найдено`
|
||||
);
|
||||
}
|
||||
if (overview.document_activity_profile) {
|
||||
const topDocument = overview.document_activity_profile.top_document_types[0];
|
||||
const topSection = overview.document_activity_profile.top_account_sections[0];
|
||||
|
|
@ -1924,16 +1536,6 @@ function businessOverviewRiskSynthesisLine(overview: BusinessOverview): string |
|
|||
`staleness risk proxy склада: ${inventoryStalenessRiskBandRu(overview.inventory_staleness_risk_proxy.risk_band)}, возраст ${overview.inventory_staleness_risk_proxy.max_purchase_age_days} дн.`
|
||||
);
|
||||
}
|
||||
if (overview.inventory_quality_events) {
|
||||
const quality = overview.inventory_quality_events;
|
||||
if (quality.evidence_status === "reviewed_no_quality_events_found") {
|
||||
signals.push("складские quality-события: документы списания, оприходования, инвентаризации и переоценки проверены, подтвержденных событий не найдено");
|
||||
} else {
|
||||
signals.push(
|
||||
`складские quality-события: списаний ${quality.writeoff_rows} на ${quality.writeoff_amount_human_ru}, оприходований/корректировок ${quality.receipt_adjustment_rows} на ${quality.receipt_adjustment_amount_human_ru}, инвентаризаций ${quality.inventory_count_rows}, переоценок ${quality.revaluation_rows}`
|
||||
);
|
||||
}
|
||||
}
|
||||
return signals.length > 0
|
||||
? `Риски и контуры внимания по подтвержденным данным: ${signals.join("; ")}.`
|
||||
: null;
|
||||
|
|
@ -1949,8 +1551,7 @@ function businessOverviewExecutiveVerdictLine(overview: BusinessOverview): strin
|
|||
overview.debt_staleness_risk_proxy ||
|
||||
overview.inventory_position ||
|
||||
overview.inventory_turnover_proxy ||
|
||||
overview.inventory_staleness_risk_proxy ||
|
||||
overview.inventory_quality_events
|
||||
overview.inventory_staleness_risk_proxy
|
||||
);
|
||||
const hasOperationalProfileSignal = Boolean(
|
||||
overview.document_activity_profile || overview.counterparty_profile || overview.contract_usage_profile
|
||||
|
|
@ -2047,19 +1648,12 @@ export function buildAssistantMcpDiscoveryAnswerDraft(
|
|||
if (pilot.derived_business_overview?.tax_position) {
|
||||
pushReason(reasonCodes, "answer_contains_business_overview_tax_position");
|
||||
}
|
||||
if (pilot.derived_business_overview?.accounting_financial_result) {
|
||||
pushReason(reasonCodes, "answer_contains_business_overview_accounting_financial_result");
|
||||
}
|
||||
if (pilot.derived_business_overview?.trading_margin_proxy) {
|
||||
pushReason(reasonCodes, "answer_contains_business_overview_trading_margin_proxy");
|
||||
}
|
||||
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");
|
||||
}
|
||||
|
|
@ -2084,10 +1678,6 @@ export function buildAssistantMcpDiscoveryAnswerDraft(
|
|||
if (pilot.derived_business_overview?.debt_staleness_risk_proxy) {
|
||||
pushReason(reasonCodes, "answer_contains_business_overview_debt_staleness_risk_proxy");
|
||||
}
|
||||
if (pilot.derived_business_overview?.debt_due_date_aging) {
|
||||
pushReason(reasonCodes, "answer_contains_business_overview_debt_due_date_aging");
|
||||
pushReason(reasonCodes, `answer_contains_business_overview_debt_due_date_aging_${pilot.derived_business_overview.debt_due_date_aging.evidence_status}`);
|
||||
}
|
||||
if (pilot.derived_business_overview?.inventory_position) {
|
||||
pushReason(reasonCodes, "answer_contains_business_overview_inventory_position");
|
||||
}
|
||||
|
|
@ -2097,10 +1687,6 @@ export function buildAssistantMcpDiscoveryAnswerDraft(
|
|||
if (pilot.derived_business_overview?.inventory_staleness_risk_proxy) {
|
||||
pushReason(reasonCodes, "answer_contains_business_overview_inventory_staleness_risk_proxy");
|
||||
}
|
||||
if (pilot.derived_business_overview?.inventory_quality_events) {
|
||||
pushReason(reasonCodes, "answer_contains_business_overview_inventory_quality_events");
|
||||
pushReason(reasonCodes, `answer_contains_business_overview_inventory_quality_events_${pilot.derived_business_overview.inventory_quality_events.evidence_status}`);
|
||||
}
|
||||
if (pilot.derived_business_overview?.missing_proof_families?.length) {
|
||||
pushReason(reasonCodes, "answer_contains_business_overview_missing_proof_ledger");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import type { AssistantMcpDiscoveryRuntimeEntryPointContract } from "./assistantMcpDiscoveryRuntimeEntryPoint";
|
||||
import type { AssistantMcpRouteCandidateContract } from "./assistantMcpDiscoveryRuntimeBridge";
|
||||
|
||||
export interface AssistantMcpDiscoveryDebugAttachmentFields {
|
||||
assistant_mcp_discovery_entry_point_v1: AssistantMcpDiscoveryRuntimeEntryPointContract | null;
|
||||
|
|
@ -12,16 +11,6 @@ export interface AssistantMcpDiscoveryDebugAttachmentFields {
|
|||
mcp_discovery_catalog_chain_alignment_status: string | null;
|
||||
mcp_discovery_catalog_chain_top_match: string | null;
|
||||
mcp_discovery_catalog_chain_selected_matches_top: boolean;
|
||||
mcp_discovery_route_candidate_v1: AssistantMcpRouteCandidateContract | null;
|
||||
mcp_discovery_route_candidate_status: string | null;
|
||||
mcp_discovery_route_candidate_fact_family: string | null;
|
||||
mcp_discovery_route_candidate_action_family: string | null;
|
||||
mcp_discovery_route_candidate_proof_expectation: string | null;
|
||||
mcp_discovery_route_candidate_missing_axes: string[];
|
||||
mcp_discovery_route_candidate_provided_axes: string[];
|
||||
mcp_discovery_route_candidate_executable_now: boolean;
|
||||
mcp_discovery_route_candidate_enablement_reason: string | null;
|
||||
mcp_discovery_route_candidate_next_action: string | null;
|
||||
mcp_discovery_answer_mode: string | null;
|
||||
mcp_discovery_business_fact_answer_allowed: boolean;
|
||||
mcp_discovery_user_facing_response_allowed: boolean;
|
||||
|
|
@ -70,14 +59,6 @@ function isMcpDiscoveryEntryPointContract(value: unknown): value is AssistantMcp
|
|||
);
|
||||
}
|
||||
|
||||
function isRouteCandidateContract(value: unknown): value is AssistantMcpRouteCandidateContract {
|
||||
const record = toRecordObject(value);
|
||||
return (
|
||||
record?.schema_version === "assistant_mcp_route_candidate_v1" &&
|
||||
record?.policy_owner === "assistantMcpDiscoveryRuntimeBridge"
|
||||
);
|
||||
}
|
||||
|
||||
function resolveEntryPoint(input: AttachAssistantMcpDiscoveryDebugInput): AssistantMcpDiscoveryRuntimeEntryPointContract | null {
|
||||
if (isMcpDiscoveryEntryPointContract(input.entryPoint)) {
|
||||
return input.entryPoint;
|
||||
|
|
@ -96,7 +77,6 @@ export function buildAssistantMcpDiscoveryDebugAttachmentFields(
|
|||
const bridge = toRecordObject(entryPoint?.bridge);
|
||||
const planner = toRecordObject(bridge?.planner);
|
||||
const chainAlignment = toRecordObject(planner?.catalog_chain_template_alignment);
|
||||
const routeCandidate = isRouteCandidateContract(bridge?.route_candidate) ? bridge.route_candidate : null;
|
||||
const answerDraft = toRecordObject(bridge?.answer_draft);
|
||||
|
||||
return {
|
||||
|
|
@ -110,16 +90,6 @@ export function buildAssistantMcpDiscoveryDebugAttachmentFields(
|
|||
mcp_discovery_catalog_chain_alignment_status: toNonEmptyString(chainAlignment?.alignment_status),
|
||||
mcp_discovery_catalog_chain_top_match: toNonEmptyString(chainAlignment?.top_chain_template_match),
|
||||
mcp_discovery_catalog_chain_selected_matches_top: chainAlignment?.selected_chain_matches_top === true,
|
||||
mcp_discovery_route_candidate_v1: routeCandidate,
|
||||
mcp_discovery_route_candidate_status: toNonEmptyString(routeCandidate?.candidate_status),
|
||||
mcp_discovery_route_candidate_fact_family: toNonEmptyString(routeCandidate?.business_fact_family),
|
||||
mcp_discovery_route_candidate_action_family: toNonEmptyString(routeCandidate?.action_family),
|
||||
mcp_discovery_route_candidate_proof_expectation: toNonEmptyString(routeCandidate?.proof_expectation),
|
||||
mcp_discovery_route_candidate_missing_axes: toStringArray(routeCandidate?.missing_axes),
|
||||
mcp_discovery_route_candidate_provided_axes: toStringArray(routeCandidate?.provided_axes),
|
||||
mcp_discovery_route_candidate_executable_now: routeCandidate?.executable_now === true,
|
||||
mcp_discovery_route_candidate_enablement_reason: toNonEmptyString(routeCandidate?.enablement_reason),
|
||||
mcp_discovery_route_candidate_next_action: toNonEmptyString(routeCandidate?.recommended_next_action),
|
||||
mcp_discovery_answer_mode: toNonEmptyString(answerDraft?.answer_mode),
|
||||
mcp_discovery_business_fact_answer_allowed: bridge?.business_fact_answer_allowed === true,
|
||||
mcp_discovery_user_facing_response_allowed: bridge?.user_facing_response_allowed === true,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -110,8 +110,6 @@ interface PlannerBudgetOverride {
|
|||
maxProbeCount?: number;
|
||||
}
|
||||
|
||||
const CHUNKED_COVERAGE_PROBE_BUDGET = 30;
|
||||
|
||||
function toNonEmptyString(value: unknown): string | null {
|
||||
if (value === null || value === undefined) {
|
||||
return null;
|
||||
|
|
@ -609,15 +607,12 @@ function budgetOverrideFor(input: AssistantMcpDiscoveryPlannerInput, recipe: Pla
|
|||
(recipe.semanticDataNeed === "counterparty value-flow evidence" ||
|
||||
recipe.semanticDataNeed === "bidirectional value-flow comparison evidence" ||
|
||||
recipe.semanticDataNeed === "ranked value-flow evidence");
|
||||
const isBusinessOverviewRecipe =
|
||||
recipe.primitives.includes("query_movements") &&
|
||||
recipe.chainId === "business_overview";
|
||||
if (!isValueFlowRecipe && !isBusinessOverviewRecipe) {
|
||||
if (!isValueFlowRecipe) {
|
||||
return {};
|
||||
}
|
||||
if (requestedAggregationAxis === "month" || isYearDateScope(meaning)) {
|
||||
return {
|
||||
maxProbeCount: CHUNKED_COVERAGE_PROBE_BUDGET
|
||||
maxProbeCount: 30
|
||||
};
|
||||
}
|
||||
return {};
|
||||
|
|
@ -848,7 +843,7 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
|
|||
extraReasons: primitiveSelection.reasonCodes
|
||||
});
|
||||
}
|
||||
if (dataNeedGraph?.ranking_need) {
|
||||
if (dataNeedGraph?.ranking_need && !hasSubjectCandidates(dataNeedGraph)) {
|
||||
pushUnique(axes, "aggregate_axis");
|
||||
pushUnique(axes, "amount");
|
||||
pushUnique(axes, "coverage_target");
|
||||
|
|
@ -869,10 +864,7 @@ 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,
|
||||
...(hasSubjectCandidates(dataNeedGraph) ? ["planner_kept_ranking_over_subject_scope_candidates"] : [])
|
||||
]
|
||||
extraReasons: primitiveSelection.reasonCodes
|
||||
});
|
||||
}
|
||||
if (openScopeTotalWithoutSubject) {
|
||||
|
|
|
|||
|
|
@ -21,8 +21,6 @@ export type AssistantMcpDiscoveryEvidenceStatus = "confirmed" | "inferred_only"
|
|||
export type AssistantMcpDiscoveryAnswerPermission = "confirmed_answer" | "bounded_inference" | "checked_sources_only";
|
||||
|
||||
export interface AssistantMcpDiscoveryTurnMeaningRef {
|
||||
raw_message?: string | null;
|
||||
effective_message?: string | null;
|
||||
asked_domain_family?: string | null;
|
||||
asked_action_family?: string | null;
|
||||
asked_aggregation_axis?: string | null;
|
||||
|
|
@ -110,7 +108,7 @@ export interface AssistantMcpDiscoveryEvidenceContract {
|
|||
|
||||
const DEFAULT_DISCOVERY_BUDGET: AssistantMcpDiscoveryExecutionBudget = {
|
||||
max_probe_count: 3,
|
||||
max_rows_per_probe: 200
|
||||
max_rows_per_probe: 100
|
||||
};
|
||||
|
||||
const MAX_PROBE_COUNT = 36;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import type { AssistantMcpDiscoveryRuntimeEntryPointContract } from "./assistantMcpDiscoveryRuntimeEntryPoint";
|
||||
import { isLikelyFinancialInstitutionCounterparty } from "./counterpartyRoleHeuristics";
|
||||
|
||||
export const ASSISTANT_MCP_DISCOVERY_RESPONSE_CANDIDATE_SCHEMA_VERSION =
|
||||
"assistant_mcp_discovery_response_candidate_v1" as const;
|
||||
|
|
@ -39,27 +38,6 @@ function toNonEmptyString(value: unknown): string | null {
|
|||
return text.length > 0 ? text : null;
|
||||
}
|
||||
|
||||
function normalizeQuestionText(value: unknown): string {
|
||||
return String(value ?? "")
|
||||
.toLowerCase()
|
||||
.replace(/ё/g, "е")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function requestsFinancialCounterpartyBoundary(turnMeaning: Record<string, unknown> | null, graph: Record<string, unknown> | null): boolean {
|
||||
const text = normalizeQuestionText([
|
||||
turnMeaning?.raw_message,
|
||||
turnMeaning?.effective_message,
|
||||
graph?.source_message,
|
||||
graph?.question
|
||||
].join(" "));
|
||||
return (
|
||||
/(?:банк|сбербанк|финанс|кредит|депозит)/iu.test(text) &&
|
||||
/(?:клиент|поставщик|выручк|топ|обычн|роль|поток)/iu.test(text)
|
||||
);
|
||||
}
|
||||
|
||||
function toStringList(value: unknown): string[] {
|
||||
if (!Array.isArray(value)) {
|
||||
return [];
|
||||
|
|
@ -119,49 +97,7 @@ function userFacingLines(values: string[]): string[] {
|
|||
return uniqueStrings(values).filter((line) => !hasInternalMechanics(line));
|
||||
}
|
||||
|
||||
function sanitizeUserFacingMechanics(value: string): string {
|
||||
let text = String(value ?? "").replace(/MCP-срез(?:ом|у|е|а)?/giu, (match) => {
|
||||
const normalized = match.toLowerCase();
|
||||
if (normalized.endsWith("ом")) {
|
||||
return "срезом 1С";
|
||||
}
|
||||
if (normalized.endsWith("у")) {
|
||||
return "срезу 1С";
|
||||
}
|
||||
if (normalized.endsWith("е")) {
|
||||
return "срезе 1С";
|
||||
}
|
||||
if (normalized.endsWith("а")) {
|
||||
return "среза 1С";
|
||||
}
|
||||
return "срез 1С";
|
||||
});
|
||||
const replacements: Array<[RegExp, string]> = [
|
||||
[/\bprocurement-concentration route\b/giu, "проверка концентрации закупок/исходящих платежей"],
|
||||
[/\breviewed vendor-risk route\b/giu, "отдельная проверка поставщицкого риска"],
|
||||
[/\bvendor-risk route\b/giu, "проверка поставщицкого риска"],
|
||||
[/\bdue-date route\b/giu, "проверка просрочки по срокам оплаты"],
|
||||
[/\bdebt-quality proxy\b/giu, "ограниченный долговой сигнал"],
|
||||
[/\bstaleness-risk proxy\b/giu, "косвенный признак залежалости"],
|
||||
[/\bstaleness risk proxy\b/giu, "косвенный признак залежалости"],
|
||||
[/\boperating-flow proxy\b/giu, "денежный операционный показатель"],
|
||||
[/\btrading-margin proxy\b/giu, "товарная маржинальность по проверенным документам"],
|
||||
[/\bprocurement concentration proxy\b/giu, "сигнал концентрации закупок/исходящих платежей"],
|
||||
[/\boutgoing cash concentration proxy\b/giu, "сигнал концентрации исходящих денег"],
|
||||
[/\bproxy-сигналы\b/giu, "косвенные признаки"],
|
||||
[/\bproxy\b/giu, "косвенный показатель"],
|
||||
[/\bsales-to-stock\b/giu, "отношение продаж к остатку"],
|
||||
[/\boverdue\/due-date aging\b/giu, "просрочку по договорным срокам"],
|
||||
[/\bP&L\b/gu, "полный отчет о прибылях и убытках"]
|
||||
];
|
||||
for (const [pattern, replacement] of replacements) {
|
||||
text = text.replace(pattern, replacement);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
function localizeLine(value: string): string {
|
||||
const sanitizedValue = sanitizeUserFacingMechanics(value);
|
||||
if (/^1C activity rows were found for the requested counterparty scope$/i.test(value)) {
|
||||
return "В 1С найдены строки активности в запрошенном срезе.";
|
||||
}
|
||||
|
|
@ -190,7 +126,7 @@ function localizeLine(value: string): string {
|
|||
value
|
||||
)
|
||||
) {
|
||||
return "Запрошенный период достиг лимита строк; доступного бюджета помесячных дозапросов не хватило, чтобы покрыть все подпериоды.";
|
||||
return "Запрошенный период уперся в лимит строк MCP; доступного бюджета помесячных дозапросов не хватило, чтобы покрыть все подпериоды.";
|
||||
}
|
||||
const counterpartyMatch = value.match(/^1C activity rows were found for counterparty\s+(.+)$/i);
|
||||
if (counterpartyMatch) {
|
||||
|
|
@ -215,10 +151,10 @@ function localizeLine(value: string): string {
|
|||
}
|
||||
const movementRowsMatch = value.match(/^1C movement rows were found for counterparty\s+(.+)$/i);
|
||||
if (movementRowsMatch) {
|
||||
return `В 1С найдены строки движений по контрагенту ${movementRowsMatch[1]}.`;
|
||||
return `В 1С найдены строки движений по контрагенту ${movementRowsMatch[1]}.`;
|
||||
}
|
||||
if (/^1C movement rows were found for the requested scope$/i.test(value)) {
|
||||
return "В 1С найдены строки движений по запрошенному контуру.";
|
||||
return "В 1С найдены строки движений по запрошенному контуру.";
|
||||
}
|
||||
const supplierPayoutMatch = value.match(/^1C supplier-payout rows were found for counterparty\s+(.+)$/i);
|
||||
if (supplierPayoutMatch) {
|
||||
|
|
@ -250,7 +186,7 @@ function localizeLine(value: string): string {
|
|||
return "Срез документов ограничен только подтвержденными строками документов в проверенном окне.";
|
||||
}
|
||||
if (/^Counterparty movement evidence is limited to confirmed 1C movement rows in the checked scope$/i.test(value)) {
|
||||
return "Срез движений ограничен только подтвержденными строками движений в проверенном окне.";
|
||||
return "Срез движений ограничен только подтвержденными строками движений в проверенном окне.";
|
||||
}
|
||||
if (/^Counterparty value-flow total was calculated from confirmed 1C movement rows$/i.test(value)) {
|
||||
return "Сумма входящих поступлений рассчитана только по подтвержденным строкам поступлений в 1С.";
|
||||
|
|
@ -338,10 +274,10 @@ function localizeLine(value: string): string {
|
|||
return "Полный срез документов без явно проверенного периода не подтвержден.";
|
||||
}
|
||||
if (/^Full movement history outside the checked period is not proven by this MCP discovery pilot$/i.test(value)) {
|
||||
return "Полный исторический срез движений вне проверенного периода этим поиском не подтвержден.";
|
||||
return "Полный исторический срез движений вне проверенного периода этим поиском не подтвержден.";
|
||||
}
|
||||
if (/^Full movement history is not proven without an explicit checked period$/i.test(value)) {
|
||||
return "Полный срез движений без явно проверенного периода не подтвержден.";
|
||||
return "Полный срез движений без явно проверенного периода не подтвержден.";
|
||||
}
|
||||
if (/^Full supplier-payout amount outside the checked period is not proven by this MCP discovery pilot$/i.test(value)) {
|
||||
return "Полный объем исходящих платежей вне проверенного периода этим поиском не подтвержден.";
|
||||
|
|
@ -360,14 +296,14 @@ function localizeLine(value: string): string {
|
|||
value
|
||||
)
|
||||
) {
|
||||
return "Покрытие запрошенного периода восстановлено помесячными проверками 1С после того, как общая выборка достигла лимита строк.";
|
||||
return "Покрытие запрошенного периода восстановлено помесячными проверками 1С после того, как общая выборка уперлась в лимит строк.";
|
||||
}
|
||||
if (
|
||||
/^Requested period coverage for bidirectional value-flow was recovered through monthly 1C side probes after a broad probe hit the row limit$/i.test(
|
||||
value
|
||||
)
|
||||
) {
|
||||
return "Покрытие запрошенного периода по двустороннему денежному потоку восстановлено помесячными проверками 1С после того, как общая выборка достигла лимита строк хотя бы по одной стороне.";
|
||||
return "Покрытие запрошенного периода по двустороннему денежному потоку восстановлено помесячными проверками 1С после того, как общая выборка уперлась в лимит строк хотя бы по одной стороне.";
|
||||
}
|
||||
if (/^Requested period coverage was recovered through monthly 1C value-flow probes$/i.test(value)) {
|
||||
return "Покрытие запрошенного периода восстановлено помесячными проверками 1С.";
|
||||
|
|
@ -387,7 +323,7 @@ function localizeLine(value: string): string {
|
|||
if (/^Complete requested-period coverage for bidirectional value-flow is not proven by the available checked rows$/i.test(value)) {
|
||||
return "Полное покрытие запрошенного периода по двустороннему денежному потоку не подтверждено доступными проверенными строками.";
|
||||
}
|
||||
return sanitizedValue;
|
||||
return value;
|
||||
}
|
||||
|
||||
function section(title: string, lines: string[]): string | null {
|
||||
|
|
@ -471,18 +407,11 @@ function businessOverviewCoverageLimitLine(overview: Record<string, unknown>): s
|
|||
if (outgoing?.coverage_limited_by_probe_limit === true) {
|
||||
limited.push("исходящие");
|
||||
}
|
||||
const continuation =
|
||||
"Если нужен полный сквозной ответ, безопасный следующий шаг — выбрать конкретный год или квартал для дозапроса: тогда широкий срез можно собрать частями без выдачи непроверенного итога.";
|
||||
return limited.length > 0
|
||||
? `Важно: по направлению ${limited.join(" и ")} проверка достигла лимита строк; это расширенный проверенный срез найденных строк, но не гарантия полного бухгалтерского оборота без отдельной полной выгрузки. ${continuation}`
|
||||
? `Важно: ${limited.join(" и ")} уперлись в лимит выборки MCP, поэтому это проверенный срез найденных строк, а не гарантированно полный бухгалтерский оборот.`
|
||||
: null;
|
||||
}
|
||||
|
||||
function joinBusinessReplyLines(lines: string[]): string | null {
|
||||
const reply = userFacingLines(lines.map(localizeLine)).join("\n").trim();
|
||||
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
|
||||
}
|
||||
|
||||
function businessOverviewYearRowsLine(overview: Record<string, unknown>): string | null {
|
||||
const years = Array.isArray(overview.yearly_breakdown) ? overview.yearly_breakdown : [];
|
||||
const values = years
|
||||
|
|
@ -508,57 +437,6 @@ function firstOverviewAxisLabel(rows: unknown, amountKey = "total_amount_human_r
|
|||
return label && amount ? `${label} — ${sentenceAmount(amount) ?? amount}` : null;
|
||||
}
|
||||
|
||||
function firstNonFinancialOverviewAxisLabel(rows: unknown, amountKey = "total_amount_human_ru"): string | null {
|
||||
if (!Array.isArray(rows)) {
|
||||
return null;
|
||||
}
|
||||
for (const row of rows) {
|
||||
const item = toRecordObject(row);
|
||||
const label = toNonEmptyString(item?.axis_value);
|
||||
if (!label || isLikelyFinancialInstitutionCounterparty(label)) {
|
||||
continue;
|
||||
}
|
||||
const amount = moneyText(item?.[amountKey]);
|
||||
if (amount) {
|
||||
return `${label} — ${sentenceAmount(amount) ?? amount}`;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function overviewAxisLooksFinancial(row: Record<string, unknown> | null): boolean {
|
||||
if (!row) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
row.counterparty_role_hint === "bank_or_financial_institution" ||
|
||||
isLikelyFinancialInstitutionCounterparty(row.axis_value)
|
||||
);
|
||||
}
|
||||
|
||||
function financialFlowHintTextRuFromRecord(row: Record<string, unknown> | null): string | null {
|
||||
const hint = toNonEmptyString(row?.financial_flow_hint);
|
||||
const rows = typeof row?.financial_flow_hint_rows === "number" && Number.isFinite(row.financial_flow_hint_rows)
|
||||
? ` (${row.financial_flow_hint_rows} строк)`
|
||||
: "";
|
||||
if (hint === "loan_or_credit") {
|
||||
return `По полям банковского документа доминирует кредитный/заемный признак${rows}; это не обычная поставка и не клиентская выручка без отдельной проверки назначения.`;
|
||||
}
|
||||
if (hint === "bank_fee_or_service") {
|
||||
return `По полям банковского документа доминирует признак банковской комиссии/услуг банка${rows}; это не обычный поставщик товаров/услуг без отдельной проверки договора.`;
|
||||
}
|
||||
if (hint === "tax_or_budget") {
|
||||
return `По полям банковского документа доминирует налоговый/бюджетный признак${rows}; это не поставщик и не клиентская выручка.`;
|
||||
}
|
||||
if (hint === "payroll_or_social") {
|
||||
return `По полям банковского документа доминирует зарплатный/социальный признак${rows}; это не поставщик и не клиентская выручка.`;
|
||||
}
|
||||
if (hint === "supplier_payment") {
|
||||
return `По полям банковского документа доминирует признак оплаты поставщику${rows}; если получатель по названию является банком, это все равно требует осторожной трактовки.`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function businessOverviewTaxLine(overview: Record<string, unknown>): string | null {
|
||||
const tax = toRecordObject(overview.tax_position);
|
||||
if (!tax) {
|
||||
|
|
@ -651,8 +529,6 @@ function buildCompactBidirectionalValueFlowReply(
|
|||
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract,
|
||||
draft: Record<string, unknown>
|
||||
): string | null {
|
||||
const turnInput = toRecordObject(entryPoint.turn_input);
|
||||
const turnMeaning = toRecordObject(turnInput?.turn_meaning_ref);
|
||||
const bridge = toRecordObject(entryPoint.bridge);
|
||||
const pilot = toRecordObject(bridge?.pilot);
|
||||
const flow = toRecordObject(pilot?.derived_bidirectional_value_flow);
|
||||
|
|
@ -669,13 +545,7 @@ function buildCompactBidirectionalValueFlowReply(
|
|||
return null;
|
||||
}
|
||||
|
||||
const counterparty = toNonEmptyString(flow.counterparty);
|
||||
const organizationScope = toNonEmptyString(turnMeaning?.explicit_organization_scope);
|
||||
const subjectLead = counterparty
|
||||
? `по контрагенту ${counterparty}`
|
||||
: organizationScope
|
||||
? `по компании ${organizationScope}`
|
||||
: "по выбранному контуру";
|
||||
const counterparty = toNonEmptyString(flow.counterparty) ?? "запрошенному контрагенту";
|
||||
const period = toNonEmptyString(flow.period_scope);
|
||||
const periodText = period ? ` за период ${period}` : " в проверенном окне";
|
||||
const incomingRows = sideRowsText(incoming);
|
||||
|
|
@ -684,7 +554,7 @@ function buildCompactBidirectionalValueFlowReply(
|
|||
const outgoingDates = sideDateText(outgoing);
|
||||
const netLabel = bidirectionalNetLabel(flow.net_direction);
|
||||
const lines = [
|
||||
`Коротко: ${subjectLead}${periodText} по найденным строкам 1С получили ${incomingAmount ?? "0 руб."}, заплатили ${outgoingAmount ?? "0 руб."}; расчетное ${netLabel}: ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}.`
|
||||
`Коротко: по контрагенту ${counterparty}${periodText} по найденным строкам 1С получили ${incomingAmount ?? "0 руб."}, заплатили ${outgoingAmount ?? "0 руб."}; расчетное ${netLabel}: ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб."}.`
|
||||
];
|
||||
|
||||
const basis: string[] = [];
|
||||
|
|
@ -698,7 +568,7 @@ function buildCompactBidirectionalValueFlowReply(
|
|||
lines.push(`Основа: ${basis.join("; ")}.`);
|
||||
}
|
||||
if (flow.coverage_limited_by_probe_limit === true) {
|
||||
lines.push("Важно: часть проверки достигла лимита строк, поэтому это проверенный срез найденных движений, а не гарантия полного периода.");
|
||||
lines.push("Важно: часть проверки уперлась в лимит строк, поэтому это проверенный срез найденных движений, а не гарантия полного периода.");
|
||||
}
|
||||
lines.push("Метод: нетто рассчитано как подтвержденные входящие строки 1С минус подтвержденные исходящие строки; это не полное бухгалтерское сальдо вне проверенного окна.");
|
||||
|
||||
|
|
@ -707,7 +577,8 @@ function buildCompactBidirectionalValueFlowReply(
|
|||
lines.push(`Следующий шаг: ${localizeLine(fallbackNextStep)}`);
|
||||
}
|
||||
|
||||
return joinBusinessReplyLines(lines);
|
||||
const reply = lines.join("\n").trim();
|
||||
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
|
||||
}
|
||||
|
||||
function compactComparable(value: string | null): string {
|
||||
|
|
@ -850,265 +721,17 @@ function buildCompactBusinessOverviewReply(
|
|||
const topCustomer = toRecordObject(Array.isArray(overview.top_customers) ? overview.top_customers[0] : null);
|
||||
const customerName = toNonEmptyString(topCustomer?.axis_value);
|
||||
const customerAmount = moneyText(topCustomer?.total_amount_human_ru);
|
||||
const topCustomerLooksFinancial = overviewAxisLooksFinancial(topCustomer);
|
||||
const nonFinancialCustomer = firstNonFinancialOverviewAxisLabel(
|
||||
topCustomerLooksFinancial ? overview.top_customers : []
|
||||
);
|
||||
const topCustomerLead =
|
||||
customerName && customerAmount
|
||||
? topCustomerLooksFinancial
|
||||
? `; крупнейший входящий денежный источник: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount} (похоже на банк/финорганизацию, не называю это клиентской выручкой без назначения платежа)${nonFinancialCustomer ? `; крупнейший небанковский входящий контрагент: ${nonFinancialCustomer}` : ""}`
|
||||
: `; крупнейший источник входящих денег: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}`
|
||||
? `; крупнейший источник входящих денег: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}`
|
||||
: "";
|
||||
const topSupplierRecord = toRecordObject(Array.isArray(overview.top_suppliers) ? overview.top_suppliers[0] : null);
|
||||
const topSupplier = firstOverviewAxisLabel(overview.top_suppliers);
|
||||
const topSupplierLooksFinancial = overviewAxisLooksFinancial(topSupplierRecord);
|
||||
const nonFinancialSupplier = firstNonFinancialOverviewAxisLabel(
|
||||
topSupplierLooksFinancial ? overview.top_suppliers : []
|
||||
);
|
||||
const topSupplierLead = topSupplier
|
||||
? topSupplierLooksFinancial
|
||||
? `; крупнейший получатель исходящих денег: ${topSupplier} (похоже на банк/финорганизацию, не называю это обычным поставщиком без назначения платежа/договора)${nonFinancialSupplier ? `; крупнейший небанковский получатель исходящих денег: ${nonFinancialSupplier}` : ""}`
|
||||
: `; крупнейший получатель исходящих денег: ${topSupplier}`
|
||||
: "";
|
||||
const topSupplierLead = topSupplier ? `; крупнейший получатель исходящих денег: ${topSupplier}` : "";
|
||||
const roleBoundaryLead = topCustomer || topSupplier ? "; клиент/поставщик как бизнес-роли этим денежным срезом не подтверждены" : "";
|
||||
const financialBoundaryRequested = requestsFinancialCounterpartyBoundary(turnMeaning, graph);
|
||||
const requestedFinancialBoundaryLine = financialBoundaryRequested
|
||||
? topCustomerLooksFinancial || topSupplierLooksFinancial
|
||||
? "Отдельно по банкам: если денежный топ ведет банк/финансовая организация, это нельзя автоматически читать как обычного клиента или поставщика; нужны назначение платежа, вид операции и договор. Поэтому такой поток не является доказанной клиентской выручкой, обычной поставкой или чистой прибылью без отдельной проверки."
|
||||
: "Отдельно по банкам: банк/финансовую организацию в денежных топах нельзя автоматически читать как обычного клиента или поставщика; нужны назначение платежа, вид операции и договор. Поэтому такой поток не является доказанной клиентской выручкой, обычной поставкой или чистой прибылью без отдельной проверки."
|
||||
: null;
|
||||
const graphReasonCodes = toStringList(graph?.reason_codes);
|
||||
const directMoneyAnswer = graphReasonCodes.includes("data_need_graph_business_overview_direct_money_answer");
|
||||
const crossScopeExecutiveSummary = Boolean(separateSubject && previousCounterpartySummary);
|
||||
const lines: string[] = [];
|
||||
const actionFamily = toNonEmptyString(turnMeaning?.asked_action_family);
|
||||
const unsupportedFamily = toNonEmptyString(turnMeaning?.unsupported_but_understood_family);
|
||||
const profitMarginBoundary = actionFamily === "profit_margin_boundary" || unsupportedFamily === "profit_margin_boundary";
|
||||
const debtDueDateBoundary = actionFamily === "debt_due_date_boundary" || unsupportedFamily === "debt_due_date_boundary";
|
||||
const vendorRiskBoundary =
|
||||
actionFamily === "vendor_risk_procurement_boundary" || unsupportedFamily === "vendor_risk_procurement_boundary";
|
||||
const inventoryReserveBoundary =
|
||||
actionFamily === "inventory_reserve_boundary" || unsupportedFamily === "inventory_reserve_liquidation_boundary";
|
||||
|
||||
if (profitMarginBoundary) {
|
||||
const accountingFinancialResult = toRecordObject(overview.accounting_financial_result);
|
||||
if (accountingFinancialResult) {
|
||||
const direction = toNonEmptyString(accountingFinancialResult.final_result_direction);
|
||||
const amount = moneyText(accountingFinancialResult.final_result_amount_human_ru);
|
||||
const periodScope = toNonEmptyString(accountingFinancialResult.period_scope) ?? period;
|
||||
const marginPct =
|
||||
typeof accountingFinancialResult.net_margin_to_revenue_pct === "number" &&
|
||||
Number.isFinite(accountingFinancialResult.net_margin_to_revenue_pct)
|
||||
? `${accountingFinancialResult.net_margin_to_revenue_pct}%`
|
||||
: null;
|
||||
const directionText =
|
||||
direction === "profit"
|
||||
? "учетная прибыль"
|
||||
: direction === "loss"
|
||||
? "учетный убыток"
|
||||
: "нулевой учетный финрезультат";
|
||||
const amountText = amount
|
||||
? direction === "loss"
|
||||
? `минус ${amount}`
|
||||
: amount
|
||||
: "сумма не распознана";
|
||||
lines.push(
|
||||
`Коротко: нет, денежное операционное нетто не стоит считать чистой прибылью. Отдельно по закрытию счетов 90/91/99 в 1С за ${periodScope} подтвержден ${directionText}: ${amountText}${marginPct ? `; маржа к подтвержденной выручке ${marginPct}` : "; маржа к подтвержденной выручке не рассчитана"}.`
|
||||
);
|
||||
lines.push(
|
||||
"Это учетный финрезультат по найденным строкам закрытия периода в 1С, а не внешний аудит и не юридически подтвержденная отчетность."
|
||||
);
|
||||
return joinBusinessReplyLines(lines);
|
||||
}
|
||||
const headline = toNonEmptyString(draft.headline);
|
||||
const cleanHeadline = headline?.replace(/^Коротко:\s*/iu, "").trim();
|
||||
lines.push(
|
||||
cleanHeadline
|
||||
? `Коротко: ${localizeLine(cleanHeadline)}`
|
||||
: "Коротко: нельзя точно подтвердить чистую прибыль и маржу по текущему срезу 1С; есть только ограниченный операционный денежный/товарный сигнал, а не полный отчет о прибыли и не бухгалтерский финансовый результат."
|
||||
);
|
||||
const boundaryLines = userFacingLines([
|
||||
...toStringList(draft.confirmed_lines),
|
||||
...toStringList(draft.inference_lines),
|
||||
...toStringList(draft.unknown_lines)
|
||||
])
|
||||
.filter((line) => /(?:прибыл|марж|финанс|p\s*&\s*l|p&l|расход|себестоим|закрыт|profit|margin|financial)/iu.test(line))
|
||||
.slice(0, 2);
|
||||
if (boundaryLines.length > 0) {
|
||||
lines.push(...boundaryLines.map(localizeLine));
|
||||
}
|
||||
lines.push(
|
||||
"Для точного отчета о прибыли нужны отдельная проверка себестоимости, расходов, закрытия периода и финрезультата; текущий ограниченный сигнал нельзя выдавать за подтвержденную чистую прибыль или маржу."
|
||||
);
|
||||
if (limitLine) {
|
||||
lines.push(limitLine);
|
||||
}
|
||||
return joinBusinessReplyLines(lines);
|
||||
}
|
||||
|
||||
if (debtDueDateBoundary) {
|
||||
const dueDateAging = toRecordObject(overview.debt_due_date_aging);
|
||||
if (dueDateAging) {
|
||||
const status = toNonEmptyString(dueDateAging.evidence_status);
|
||||
const asOfDate = toNonEmptyString(dueDateAging.as_of_date) ?? "проверенную дату";
|
||||
const overdueAmount = moneyText(dueDateAging.overdue_amount_human_ru);
|
||||
const grossAmount = moneyText(dueDateAging.gross_open_amount_human_ru);
|
||||
const rowsWithPaymentTerms =
|
||||
typeof dueDateAging.rows_with_payment_terms === "number" && Number.isFinite(dueDateAging.rows_with_payment_terms)
|
||||
? dueDateAging.rows_with_payment_terms
|
||||
: null;
|
||||
const rowsWithAmount =
|
||||
typeof dueDateAging.rows_with_amount === "number" && Number.isFinite(dueDateAging.rows_with_amount)
|
||||
? dueDateAging.rows_with_amount
|
||||
: null;
|
||||
const dueDateScopePrefix = organizationScope ? `по компании ${organizationScope} ` : "";
|
||||
if (status === "confirmed_overdue") {
|
||||
lines.push(
|
||||
`Коротко: ${dueDateScopePrefix}на ${asOfDate} подтвержденная просрочка есть: ${overdueAmount ?? "сумма не распознана"} по ${dueDateAging.overdue_rows ?? "найденным"} строкам.`
|
||||
);
|
||||
lines.push("Основа ответа: открытые расчеты 60/62/76, договорный срок оплаты и дата расчетного документа; это проверка просрочки по срокам оплаты, а не просто возраст договора.");
|
||||
} else if (status === "no_payment_terms_configured") {
|
||||
lines.push(
|
||||
`Коротко: ${dueDateScopePrefix}на ${asOfDate} подтвержденной просрочки нет: открытые расчеты проверены${grossAmount ? ` на ${grossAmount}` : ""}, но в найденных договорах срок оплаты не установлен.`
|
||||
);
|
||||
lines.push(
|
||||
rowsWithAmount !== null
|
||||
? `Проверено строк с суммой: ${rowsWithAmount}. Без установленного срока оплаты нельзя честно назвать эти остатки просрочкой.`
|
||||
: "Без установленного срока оплаты нельзя честно назвать эти остатки просрочкой."
|
||||
);
|
||||
} else if (status === "insufficient_due_date_basis") {
|
||||
lines.push(
|
||||
`Коротко: ${dueDateScopePrefix}на ${asOfDate} просрочка не подтверждена: по строкам с установленным сроком оплаты не хватило даты расчетного документа.`
|
||||
);
|
||||
if (rowsWithPaymentTerms !== null) {
|
||||
lines.push(`Строк с установленным сроком оплаты: ${rowsWithPaymentTerms}; нужен документ-основание с датой, чтобы посчитать договорный срок оплаты.`);
|
||||
}
|
||||
} else {
|
||||
lines.push(
|
||||
`Коротко: ${dueDateScopePrefix}на ${asOfDate} проверка просрочки по срокам оплаты выполнена, подтвержденной просрочки не найдено${rowsWithPaymentTerms !== null ? `; строк с установленным сроком оплаты ${rowsWithPaymentTerms}` : ""}.`
|
||||
);
|
||||
}
|
||||
return joinBusinessReplyLines(lines);
|
||||
}
|
||||
const headline = toNonEmptyString(draft.headline);
|
||||
const cleanHeadline = headline?.replace(/^Коротко:\s*/iu, "").trim();
|
||||
lines.push(
|
||||
cleanHeadline
|
||||
? `Коротко: ${localizeLine(cleanHeadline)}`
|
||||
: "Коротко: нельзя точно определить, какая дебиторка просрочена, по текущему срезу 1С; есть только ограниченный долговой сигнал, но нет проверки договорных сроков оплаты."
|
||||
);
|
||||
lines.push(
|
||||
"Проверить нужно отдельно: договоры, сроки оплаты, погашение и закрытие задолженности; без этого нельзя доказать просрочку по договорным срокам."
|
||||
);
|
||||
return joinBusinessReplyLines(lines);
|
||||
}
|
||||
|
||||
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(
|
||||
`Коротко: проверка концентрации закупок/исходящих платежей за ${periodScope} не подтверждает зависимость от обычного поставщика: крупнейший получатель исходящих денег ${topOutgoingName ?? "не распознан"}${topOutgoingShare ? ` держит около ${topOutgoingShare}` : ""}${topOutgoingAmount ? ` (${topOutgoingAmount})` : ""}, но по названию это банк/финансовая организация${totalText}.`
|
||||
);
|
||||
const financialHintText = financialFlowHintTextRuFromRecord(topOutgoingRecord);
|
||||
if (financialHintText) {
|
||||
lines.push(financialHintText);
|
||||
}
|
||||
if (nonFinancialName) {
|
||||
lines.push(
|
||||
`Крупнейший небанковский получатель исходящих денег: ${nonFinancialName}${nonFinancialShare ? `, около ${nonFinancialShare}` : ""}${nonFinancialAmount ? ` (${nonFinancialAmount})` : ""}. Это уже сигнал закупочной/исходящей концентрации, но не аудит надежности поставщика.`
|
||||
);
|
||||
}
|
||||
} else if (status === "reviewed_procurement_concentration") {
|
||||
lines.push(
|
||||
`Коротко: точный риск зависимости от одного поставщика не подтвержден полностью; проверка концентрации закупок/исходящих платежей за ${periodScope} нашла крупнейшего получателя исходящего потока: ${topOutgoingName ?? nonFinancialName ?? "получатель не распознан"}${topOutgoingShare ? ` держит около ${topOutgoingShare}` : nonFinancialShare ? ` держит около ${nonFinancialShare}` : ""}${topOutgoingAmount ? ` (${topOutgoingAmount})` : nonFinancialAmount ? ` (${nonFinancialAmount})` : ""}${totalText}.`
|
||||
);
|
||||
} else {
|
||||
lines.push(
|
||||
`Коротко: проверка концентрации закупок/исходящих платежей за ${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}`
|
||||
);
|
||||
return joinBusinessReplyLines(lines);
|
||||
}
|
||||
const supplierBasis = topSupplier
|
||||
? topSupplierLooksFinancial
|
||||
? `крупнейший получатель исходящих денег: ${topSupplier}; по названию это банк/финансовая организация, поэтому это не доказанная зависимость от обычного поставщика${nonFinancialSupplier ? `; крупнейший небанковский получатель исходящих денег: ${nonFinancialSupplier}` : ""}`
|
||||
: `крупнейший подтвержденный поставщик/получатель исходящих платежей: ${topSupplier}`
|
||||
: outgoingAmount
|
||||
? `исходящие платежи/закупочный поток в проверенном срезе: ${outgoingAmount}`
|
||||
: "есть только ограниченный срез исходящих платежей без полного профиля поставщицкого риска";
|
||||
const proxyLabel = topSupplierLooksFinancial
|
||||
? "сигнал концентрации исходящих денег"
|
||||
: "сигнал концентрации закупок/исходящих платежей";
|
||||
lines.push(
|
||||
`Коротко: точный риск зависимости от одного поставщика по текущим данным не подтвержден; есть только ${proxyLabel}: ${supplierBasis}.`
|
||||
);
|
||||
lines.push(
|
||||
"Это сигнал концентрации закупок/исходящих платежей, а не полный аудит надежности поставщиков, условий, качества и структуры всех расходов."
|
||||
);
|
||||
lines.push(
|
||||
"Для точного вывода нужна отдельная проверка поставщицкого риска: поставщики, договорные условия, качество поставок, сроки, доля в закупках и полная структура расходов."
|
||||
);
|
||||
return joinBusinessReplyLines(lines);
|
||||
}
|
||||
|
||||
if (inventoryReserveBoundary) {
|
||||
const headline = toNonEmptyString(draft.headline);
|
||||
const inventoryQualityEvents = toRecordObject(overview.inventory_quality_events);
|
||||
const cleanHeadline = headline?.replace(/^Коротко:\s*/iu, "").trim();
|
||||
const reserveBasis = cleanHeadline ? localizeLine(cleanHeadline).replace(/^проверил/iu, "Проверены") : null;
|
||||
lines.push(
|
||||
reserveBasis
|
||||
? `Коротко: точно подтвердить резерв под неликвиды нельзя. ${reserveBasis}`
|
||||
: "Коротко: точно подтвердить резерв под неликвиды по текущим данным нельзя."
|
||||
);
|
||||
if (inventoryQualityEvents) {
|
||||
return joinBusinessReplyLines(lines);
|
||||
}
|
||||
const boundaryLines = userFacingLines([
|
||||
...toStringList(draft.unknown_lines),
|
||||
...toStringList(draft.limitation_lines)
|
||||
])
|
||||
.filter((line) => /(?:резерв|неликвид|склад|товар|reserve|obsolete|inventory|stock)/iu.test(line))
|
||||
.slice(0, 2);
|
||||
if (boundaryLines.length > 0) {
|
||||
lines.push(...boundaryLines.map(localizeLine));
|
||||
}
|
||||
lines.push(
|
||||
"Проверить нужно отдельно: складской срез на дату, учетную политику резервов, списания и ликвидационную стоимость; косвенные признаки нельзя выдавать за доказанный факт резерва."
|
||||
);
|
||||
return joinBusinessReplyLines(lines);
|
||||
}
|
||||
|
||||
if (crossScopeExecutiveSummary && separateSubject && previousCounterpartySummary && (incomingAmount || outgoingAmount || netAmount)) {
|
||||
lines.push(
|
||||
|
|
@ -1116,7 +739,7 @@ function buildCompactBusinessOverviewReply(
|
|||
);
|
||||
lines.push(previousCounterpartySummary.line);
|
||||
lines.push(
|
||||
`Можно утверждать: по компании подтвержден операционный денежный сигнал по найденным строкам 1С; по ${separateSubject} отдельно подтверждены входящие/исходящие строки, расчетное нетто и документы из предыдущего контрагентского среза.`
|
||||
`Можно утверждать: по компании подтвержден operating-flow proxy по найденным строкам 1С; по ${separateSubject} отдельно подтверждены входящие/исходящие строки, расчетное нетто и документы из предыдущего контрагентского среза.`
|
||||
);
|
||||
lines.push(
|
||||
`Нельзя утверждать: это не чистая прибыль, не полный бухгалтерский оборот вне проверенного окна и не доказательство, что ${separateSubject} является главным клиентом или поставщиком как бизнес-роль.`
|
||||
|
|
@ -1124,7 +747,8 @@ function buildCompactBusinessOverviewReply(
|
|||
if (limitLine) {
|
||||
lines.push(limitLine);
|
||||
}
|
||||
return joinBusinessReplyLines(lines);
|
||||
const reply = lines.join("\n").trim();
|
||||
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
|
||||
}
|
||||
|
||||
if (rankingNeed) {
|
||||
|
|
@ -1137,7 +761,7 @@ function buildCompactBusinessOverviewReply(
|
|||
return null;
|
||||
}
|
||||
lines.push(
|
||||
`Коротко: ${organizationPrefix}в доступном проверенном срезе 1С по входящим денежным строкам лидирует ${leaderYear}: ${leaderAmount}${Number.isFinite(leaderRows) && leaderRows > 0 ? ` по ${leaderRows} строкам с суммой` : ""}; это не полный бухгалтерский рейтинг доходности.`
|
||||
`Коротко: в доступном проверенном MCP-срезе по входящим денежным строкам лидирует ${leaderYear}: ${leaderAmount}${Number.isFinite(leaderRows) && leaderRows > 0 ? ` по ${leaderRows} строкам с суммой` : ""}; это не полный бухгалтерский рейтинг доходности.`
|
||||
);
|
||||
const netYear = toNonEmptyString(netLeader?.year_bucket);
|
||||
const netYearAmount = moneyText(netLeader?.net_amount_human_ru);
|
||||
|
|
@ -1149,9 +773,6 @@ function buildCompactBusinessOverviewReply(
|
|||
if (incomingAmount && outgoingAmount && netAmount) {
|
||||
lines.push(`Сверка по окну: входящие ${incomingAmount}, исходящие ${outgoingAmount}, ${netDirection} ${sentenceAmount(netAmount) ?? netAmount}.`);
|
||||
}
|
||||
if (requestedFinancialBoundaryLine) {
|
||||
lines.push(requestedFinancialBoundaryLine);
|
||||
}
|
||||
const yearRows = businessOverviewYearRowsLine(overview);
|
||||
if (yearRows) {
|
||||
lines.push(yearRows);
|
||||
|
|
@ -1160,16 +781,9 @@ function buildCompactBusinessOverviewReply(
|
|||
lines.push(
|
||||
`Коротко: ${organizationPrefix}${period} по подтвержденным строкам 1С получили ${incomingAmount ?? "0 руб."}; исходящие платежи/списания ${outgoingAmount ?? "0 руб."}; ${netDirection} ${sentenceAmount(netAmount) ?? netAmount ?? "0 руб"}${topCustomerLead}${topSupplierLead}${roleBoundaryLead}${separateSubjectLead}.`
|
||||
);
|
||||
lines.push('Метод: "заработали" здесь считаю как операционный денежный показатель по 1С; это не чистая прибыль и не финрезультат.');
|
||||
lines.push('Метод: "заработали" здесь считаю как денежный operating-flow proxy по 1С; это не чистая прибыль и не финрезультат.');
|
||||
if (!directMoneyAnswer && customerName && customerAmount) {
|
||||
lines.push(
|
||||
topCustomerLooksFinancial
|
||||
? `Крупнейший входящий денежный источник в этом срезе: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}. По названию это банк/финансовая организация, поэтому без назначения платежа не называю это клиентской выручкой.${nonFinancialCustomer ? ` Крупнейший небанковский входящий контрагент: ${nonFinancialCustomer}.` : ""}`
|
||||
: `Крупнейший подтвержденный источник входящих денег в этом срезе: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}.`
|
||||
);
|
||||
}
|
||||
if (requestedFinancialBoundaryLine) {
|
||||
lines.push(requestedFinancialBoundaryLine);
|
||||
lines.push(`Крупнейший подтвержденный источник входящих денег в этом срезе: ${customerName} — ${sentenceAmount(customerAmount) ?? customerAmount}.`);
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
|
|
@ -1183,18 +797,10 @@ function buildCompactBusinessOverviewReply(
|
|||
}
|
||||
|
||||
if (!directMoneyAnswer && topSupplier) {
|
||||
lines.push(
|
||||
topSupplierLooksFinancial
|
||||
? `Крупнейший получатель исходящих денег: ${topSupplier}. По названию это банк/финансовая организация, поэтому без назначения платежа/договора не считаю это обычным поставщиком.${nonFinancialSupplier ? ` Крупнейший небанковский получатель исходящих денег: ${nonFinancialSupplier}.` : ""}`
|
||||
: `Крупнейший подтвержденный получатель исходящих денег: ${topSupplier}.`
|
||||
);
|
||||
lines.push(`Крупнейший подтвержденный получатель исходящих денег: ${topSupplier}.`);
|
||||
}
|
||||
if (!directMoneyAnswer && (topCustomer || topSupplier)) {
|
||||
lines.push(
|
||||
topCustomerLooksFinancial || topSupplierLooksFinancial
|
||||
? "Важно по ролям: текущий денежный срез подтверждает источники и получателей денег, но банковские контрагенты требуют проверки назначения платежа/счетов и не доказывают роль клиента или поставщика."
|
||||
: "Важно по ролям: текущий денежный срез подтверждает денежные источники и получателей, но не доказывает, что это главный клиент или главный поставщик как бизнес-роль."
|
||||
);
|
||||
lines.push("Важно по ролям: текущий денежный срез подтверждает денежные источники и получателей, но не доказывает, что это главный клиент или главный поставщик как бизнес-роль.");
|
||||
}
|
||||
if (!directMoneyAnswer) {
|
||||
lines.push(
|
||||
|
|
@ -1233,7 +839,8 @@ function buildCompactBusinessOverviewReply(
|
|||
lines.push(limitLine);
|
||||
}
|
||||
lines.push("Для ответа именно про чистую прибыль нужно отдельно считать себестоимость, расходы и закрытие периода.");
|
||||
return joinBusinessReplyLines(lines);
|
||||
const reply = lines.join("\n").trim();
|
||||
return reply.length > 0 && !hasInternalMechanics(reply) ? reply : null;
|
||||
}
|
||||
|
||||
function statusFrom(entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null): AssistantMcpDiscoveryResponseCandidateStatus {
|
||||
|
|
|
|||
|
|
@ -255,26 +255,6 @@ 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 ?? "")
|
||||
|
|
@ -294,9 +274,6 @@ function hasMetadataDiscoveryPriority(
|
|||
if (!isMetadataDiscoveryTurn(entryPoint)) {
|
||||
return false;
|
||||
}
|
||||
if (!isExplicitMetadataDiscoveryTurn(entryPoint)) {
|
||||
return false;
|
||||
}
|
||||
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
|
||||
return !isInventoryExactAddressIntent(detectedIntent);
|
||||
}
|
||||
|
|
@ -386,111 +363,6 @@ 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 hasExactBankOperationsAddressReply(
|
||||
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
|
||||
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
|
||||
): boolean {
|
||||
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
|
||||
return false;
|
||||
}
|
||||
if (!hasEffectivelyFactualAddressReply(input)) {
|
||||
return false;
|
||||
}
|
||||
const source = String(input.currentReplySource ?? input.livingChatSource ?? "").trim().toLowerCase();
|
||||
if (source !== "address_query_runtime_v1" && source !== "address_exact" && source !== "address_lane") {
|
||||
return false;
|
||||
}
|
||||
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
|
||||
const selectedRecipe = toNonEmptyString(input.addressRuntimeMeta?.selected_recipe);
|
||||
const isBankIntent =
|
||||
detectedIntent === "bank_operations_by_counterparty" || detectedIntent === "bank_operations_by_contract";
|
||||
const isBankRecipe =
|
||||
selectedRecipe === "address_bank_operations_by_counterparty_v1" ||
|
||||
selectedRecipe === "address_bank_operations_by_contract_v1";
|
||||
if (!isBankIntent || !isBankRecipe) {
|
||||
return false;
|
||||
}
|
||||
const grounding = toRecordObject(input.addressRuntimeMeta?.answer_grounding_check);
|
||||
const groundingStatus = toNonEmptyString(grounding?.status);
|
||||
const mcpCallStatus = toNonEmptyString(input.addressRuntimeMeta?.mcp_call_status);
|
||||
const routeMode = toNonEmptyString(input.addressRuntimeMeta?.capability_route_mode);
|
||||
return Boolean(
|
||||
mcpCallStatus === "matched_non_empty" ||
|
||||
groundingStatus === "grounded" ||
|
||||
routeMode === "exact" ||
|
||||
hasFullConfirmedTruth(input)
|
||||
);
|
||||
}
|
||||
|
||||
function hasValueFlowActionConflictWithDiscoveryTurnMeaning(
|
||||
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
|
||||
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
|
||||
|
|
@ -507,9 +379,6 @@ function hasValueFlowActionConflictWithDiscoveryTurnMeaning(
|
|||
if (askedDomain !== "counterparty_value") {
|
||||
return false;
|
||||
}
|
||||
if (hasExactBankOperationsAddressReply(input, entryPoint)) {
|
||||
return false;
|
||||
}
|
||||
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
|
||||
if (askedAction === "payout") {
|
||||
return detectedIntent !== "supplier_payouts_profile";
|
||||
|
|
@ -574,7 +443,18 @@ function hasExactMatchedFactualAddressReply(
|
|||
return false;
|
||||
}
|
||||
}
|
||||
return 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 hasOpenScopeValueFlowDiscoveryPriority(
|
||||
|
|
@ -686,9 +566,6 @@ function hasSemanticConflictWithDiscoveryTurnMeaning(
|
|||
if (hasRuntimeMatchedExactReply(input, entryPoint)) {
|
||||
return false;
|
||||
}
|
||||
if (hasExactBankOperationsAddressReply(input, entryPoint)) {
|
||||
return false;
|
||||
}
|
||||
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
|
||||
const turnMeaning = readDiscoveryTurnMeaning(entryPoint);
|
||||
const askedDomain = toNonEmptyString(turnMeaning?.asked_domain_family);
|
||||
|
|
@ -805,15 +682,6 @@ 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 exactBankOperationsAddressReply = hasExactBankOperationsAddressReply(input, entryPoint);
|
||||
const openScopeValueFlowDiscoveryPriority = hasOpenScopeValueFlowDiscoveryPriority(input, entryPoint);
|
||||
const metadataDiscoveryPriority = hasMetadataDiscoveryPriority(input, entryPoint);
|
||||
const valueFlowActionConflictWithDiscoveryTurnMeaning = hasValueFlowActionConflictWithDiscoveryTurnMeaning(
|
||||
|
|
@ -882,21 +750,6 @@ 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 (exactBankOperationsAddressReply) {
|
||||
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_exact_bank_operations_address_reply");
|
||||
}
|
||||
if (deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") {
|
||||
pushReason(
|
||||
reasonCodes,
|
||||
|
|
@ -926,9 +779,6 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
|
|||
!exactMatchedFactualAddressReply &&
|
||||
!runtimeAdjustedExactReply &&
|
||||
!runtimeMatchedExactReply &&
|
||||
!staleMetadataDiscoveryFallbackAgainstExactAddressReply &&
|
||||
!exactValueFlowReplyForBusinessOverviewDirectMoneyNeed &&
|
||||
!exactBankOperationsAddressReply &&
|
||||
!(deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") &&
|
||||
ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) &&
|
||||
candidate.eligible_for_future_hot_runtime &&
|
||||
|
|
|
|||
|
|
@ -20,8 +20,6 @@ export const ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION =
|
|||
"assistant_mcp_discovery_runtime_bridge_v1" as const;
|
||||
export const ASSISTANT_MCP_DISCOVERY_LOOP_STATE_SCHEMA_VERSION =
|
||||
"assistant_mcp_discovery_loop_state_v1" as const;
|
||||
export const ASSISTANT_MCP_ROUTE_CANDIDATE_SCHEMA_VERSION =
|
||||
"assistant_mcp_route_candidate_v1" as const;
|
||||
|
||||
export type AssistantMcpDiscoveryRuntimeBridgeStatus =
|
||||
| "answer_draft_ready"
|
||||
|
|
@ -33,11 +31,6 @@ export type AssistantMcpDiscoveryLoopStatus =
|
|||
| "awaiting_clarification"
|
||||
| "ready_for_next_hop"
|
||||
| "blocked";
|
||||
export type AssistantMcpRouteCandidateStatus =
|
||||
| "ready_for_reviewed_execution"
|
||||
| "needs_user_scope"
|
||||
| "needs_route_enablement"
|
||||
| "blocked";
|
||||
|
||||
export interface AssistantMcpDiscoveryRuntimeBridgeInput {
|
||||
semanticDataNeed?: string | null;
|
||||
|
|
@ -68,26 +61,6 @@ export interface AssistantMcpDiscoveryLoopStateContract {
|
|||
explicit_date_scope: string | null;
|
||||
}
|
||||
|
||||
export interface AssistantMcpRouteCandidateContract {
|
||||
schema_version: typeof ASSISTANT_MCP_ROUTE_CANDIDATE_SCHEMA_VERSION;
|
||||
policy_owner: "assistantMcpDiscoveryRuntimeBridge";
|
||||
candidate_status: AssistantMcpRouteCandidateStatus;
|
||||
selected_chain_id: AssistantMcpDiscoveryChainId;
|
||||
selected_chain_summary: string;
|
||||
nearest_catalog_chain_template: AssistantMcpDiscoveryPlannerContract["catalog_chain_template_alignment"]["top_chain_template_match"];
|
||||
catalog_alignment_status: AssistantMcpDiscoveryPlannerContract["catalog_chain_template_alignment"]["alignment_status"];
|
||||
business_fact_family: string | null;
|
||||
action_family: string | null;
|
||||
proof_expectation: string | null;
|
||||
required_axes: string[];
|
||||
provided_axes: string[];
|
||||
missing_axes: string[];
|
||||
executable_now: boolean;
|
||||
enablement_reason: string | null;
|
||||
recommended_next_action: string;
|
||||
forbidden_overclaim_flags: string[];
|
||||
}
|
||||
|
||||
export interface AssistantMcpDiscoveryRuntimeBridgeContract {
|
||||
schema_version: typeof ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION;
|
||||
policy_owner: "assistantMcpDiscoveryRuntimeBridge";
|
||||
|
|
@ -97,7 +70,6 @@ export interface AssistantMcpDiscoveryRuntimeBridgeContract {
|
|||
pilot: AssistantMcpDiscoveryPilotExecutionContract;
|
||||
answer_draft: AssistantMcpDiscoveryAnswerDraftContract;
|
||||
loop_state: AssistantMcpDiscoveryLoopStateContract;
|
||||
route_candidate: AssistantMcpRouteCandidateContract;
|
||||
user_facing_response_allowed: boolean;
|
||||
business_fact_answer_allowed: boolean;
|
||||
requires_user_clarification: boolean;
|
||||
|
|
@ -166,26 +138,6 @@ function loopStatusFor(
|
|||
return "ready_for_next_hop";
|
||||
}
|
||||
|
||||
function routeCandidateStatusFor(
|
||||
bridgeStatus: AssistantMcpDiscoveryRuntimeBridgeStatus,
|
||||
pilot: AssistantMcpDiscoveryPilotExecutionContract,
|
||||
missingProofFamily: AssistantMcpDiscoveryBusinessOverviewMissingProofFamily | null
|
||||
): AssistantMcpRouteCandidateStatus {
|
||||
if (bridgeStatus === "blocked" || pilot.pilot_status === "blocked") {
|
||||
return "blocked";
|
||||
}
|
||||
if (bridgeStatus === "needs_clarification" || pilot.pilot_status === "skipped_needs_clarification") {
|
||||
return "needs_user_scope";
|
||||
}
|
||||
if (bridgeStatus === "unsupported" || pilot.pilot_status === "unsupported") {
|
||||
return "needs_route_enablement";
|
||||
}
|
||||
if (missingProofFamily) {
|
||||
return "needs_route_enablement";
|
||||
}
|
||||
return "ready_for_reviewed_execution";
|
||||
}
|
||||
|
||||
function flattenAxes(
|
||||
pilot: AssistantMcpDiscoveryPilotExecutionContract,
|
||||
source: "provided_axes" | "missing_axis_options"
|
||||
|
|
@ -216,144 +168,6 @@ function entityCandidatesFromPlanner(planner: AssistantMcpDiscoveryPlannerContra
|
|||
return uniqueStrings(values);
|
||||
}
|
||||
|
||||
function firstNonEmpty(values: Array<string | null | undefined>): string | null {
|
||||
for (const value of values) {
|
||||
const text = String(value ?? "").trim();
|
||||
if (text) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
type AssistantMcpDiscoveryBusinessOverviewMissingProofFamily = NonNullable<
|
||||
AssistantMcpDiscoveryPilotExecutionContract["derived_business_overview"]
|
||||
>["missing_proof_families"][number];
|
||||
|
||||
function routeCandidateProofFamiliesFor(actionFamily: string | null, proofExpectation: string | null): string[] {
|
||||
const combined = `${actionFamily ?? ""} ${proofExpectation ?? ""}`.trim().toLowerCase();
|
||||
const result: string[] = [];
|
||||
const add = (family: string) => {
|
||||
if (!result.includes(family)) {
|
||||
result.push(family);
|
||||
}
|
||||
};
|
||||
if (!combined || combined === "broad_evaluation bounded_inference") {
|
||||
return result;
|
||||
}
|
||||
if (/(?:inventory|stock|warehouse|reserve|liquidation|write[-_ ]?off|obsolete|obsolescence)/iu.test(combined)) {
|
||||
add("inventory_reserve_liquidation_quality");
|
||||
}
|
||||
if (/(?:debt|due[-_ ]?date|overdue|aging|credit[-_ ]?risk)/iu.test(combined)) {
|
||||
add("debt_due_date_aging_quality");
|
||||
}
|
||||
if (/(?:vendor|supplier|procurement|sourcing)/iu.test(combined)) {
|
||||
add("vendor_risk_procurement_quality");
|
||||
}
|
||||
if (/(?:profit|margin|pnl|p&l|financial[-_ ]?result)/iu.test(combined)) {
|
||||
add("accounting_profit_margin");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function routeCandidateMissingProofFamily(
|
||||
planner: AssistantMcpDiscoveryPlannerContract,
|
||||
pilot: AssistantMcpDiscoveryPilotExecutionContract
|
||||
): AssistantMcpDiscoveryBusinessOverviewMissingProofFamily | null {
|
||||
if (planner.data_need_graph?.business_fact_family !== "business_overview") {
|
||||
return null;
|
||||
}
|
||||
const wantedFamilies = routeCandidateProofFamiliesFor(
|
||||
planner.data_need_graph?.action_family ?? null,
|
||||
planner.data_need_graph?.proof_expectation ?? null
|
||||
);
|
||||
if (wantedFamilies.length <= 0) {
|
||||
return null;
|
||||
}
|
||||
const missingProofFamilies = pilot.derived_business_overview?.missing_proof_families ?? [];
|
||||
return missingProofFamilies.find((item) => wantedFamilies.includes(item.family)) ?? null;
|
||||
}
|
||||
|
||||
function routeCandidateEnablementReason(
|
||||
status: AssistantMcpRouteCandidateStatus,
|
||||
pilot: AssistantMcpDiscoveryPilotExecutionContract,
|
||||
missingAxes: string[],
|
||||
missingProofFamily: AssistantMcpDiscoveryBusinessOverviewMissingProofFamily | null
|
||||
): string | null {
|
||||
if (status === "ready_for_reviewed_execution") {
|
||||
return null;
|
||||
}
|
||||
if (status === "needs_user_scope") {
|
||||
return missingAxes.length > 0
|
||||
? `Missing scope axes: ${missingAxes.join(", ")}`
|
||||
: "Selected chain needs user clarification before MCP execution";
|
||||
}
|
||||
if (missingProofFamily) {
|
||||
return [
|
||||
`Missing reviewed proof family: ${missingProofFamily.family}`,
|
||||
`next_required_evidence=${missingProofFamily.next_required_evidence}`,
|
||||
missingProofFamily.current_supported_evidence
|
||||
? `current_supported_evidence=${missingProofFamily.current_supported_evidence}`
|
||||
: null,
|
||||
`must_not_claim=${missingProofFamily.must_not_claim}`
|
||||
]
|
||||
.filter((item): item is string => Boolean(item))
|
||||
.join("; ");
|
||||
}
|
||||
return firstNonEmpty([
|
||||
...pilot.query_limitations,
|
||||
...pilot.evidence.unknown_facts,
|
||||
"Selected chain is not safely executable by the reviewed MCP runtime yet"
|
||||
]);
|
||||
}
|
||||
|
||||
function routeCandidateNextAction(status: AssistantMcpRouteCandidateStatus): string {
|
||||
if (status === "ready_for_reviewed_execution") {
|
||||
return "Execute through the reviewed runtime bridge and truth gate.";
|
||||
}
|
||||
if (status === "needs_user_scope") {
|
||||
return "Ask the user for the missing scope axes before MCP execution.";
|
||||
}
|
||||
if (status === "needs_route_enablement") {
|
||||
return "Create or wire a reviewed exact route for the selected chain before treating the fact as answerable.";
|
||||
}
|
||||
return "Do not execute until the blocking reason is resolved.";
|
||||
}
|
||||
|
||||
function buildRouteCandidate(
|
||||
planner: AssistantMcpDiscoveryPlannerContract,
|
||||
pilot: AssistantMcpDiscoveryPilotExecutionContract,
|
||||
bridgeStatus: AssistantMcpDiscoveryRuntimeBridgeStatus
|
||||
): AssistantMcpRouteCandidateContract {
|
||||
const plannerClarificationGaps = planner.discovery_plan.clarification_gaps ?? [];
|
||||
const providedAxes = flattenAxes(pilot, "provided_axes");
|
||||
const missingAxes = plannerClarificationGaps.length > 0 ? plannerClarificationGaps : flattenAxes(pilot, "missing_axis_options");
|
||||
const missingProofFamily = routeCandidateMissingProofFamily(planner, pilot);
|
||||
const candidateStatus = routeCandidateStatusFor(bridgeStatus, pilot, missingProofFamily);
|
||||
return {
|
||||
schema_version: ASSISTANT_MCP_ROUTE_CANDIDATE_SCHEMA_VERSION,
|
||||
policy_owner: "assistantMcpDiscoveryRuntimeBridge",
|
||||
candidate_status: candidateStatus,
|
||||
selected_chain_id: planner.selected_chain_id,
|
||||
selected_chain_summary: planner.selected_chain_summary,
|
||||
nearest_catalog_chain_template: planner.catalog_chain_template_alignment.top_chain_template_match,
|
||||
catalog_alignment_status: planner.catalog_chain_template_alignment.alignment_status,
|
||||
business_fact_family: planner.data_need_graph?.business_fact_family ?? null,
|
||||
action_family: planner.data_need_graph?.action_family ?? null,
|
||||
proof_expectation: planner.data_need_graph?.proof_expectation ?? null,
|
||||
required_axes: [...planner.required_axes],
|
||||
provided_axes: providedAxes,
|
||||
missing_axes: missingAxes,
|
||||
executable_now: candidateStatus === "ready_for_reviewed_execution",
|
||||
enablement_reason: routeCandidateEnablementReason(candidateStatus, pilot, missingAxes, missingProofFamily),
|
||||
recommended_next_action: routeCandidateNextAction(candidateStatus),
|
||||
forbidden_overclaim_flags: uniqueStrings([
|
||||
...(planner.data_need_graph?.forbidden_overclaim_flags ?? []),
|
||||
...(missingProofFamily ? [missingProofFamily.must_not_claim] : [])
|
||||
])
|
||||
};
|
||||
}
|
||||
|
||||
function buildLoopState(
|
||||
planner: AssistantMcpDiscoveryPlannerContract,
|
||||
pilot: AssistantMcpDiscoveryPilotExecutionContract,
|
||||
|
|
@ -401,14 +215,11 @@ export async function runAssistantMcpDiscoveryRuntimeBridge(
|
|||
const answerDraft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
|
||||
const bridgeStatus = bridgeStatusFor(pilot, answerDraft);
|
||||
const loopState = buildLoopState(planner, pilot, bridgeStatus);
|
||||
const routeCandidate = buildRouteCandidate(planner, pilot, bridgeStatus);
|
||||
const reasonCodes = uniqueStrings([...planner.reason_codes, ...pilot.reason_codes, ...answerDraft.reason_codes]);
|
||||
|
||||
pushReason(reasonCodes, `runtime_bridge_status_${bridgeStatus}`);
|
||||
pushReason(reasonCodes, "runtime_bridge_not_wired_to_hot_assistant_answer");
|
||||
pushReason(reasonCodes, `runtime_bridge_loop_state_${loopState.loop_status}`);
|
||||
pushReason(reasonCodes, "runtime_bridge_route_candidate_built");
|
||||
pushReason(reasonCodes, `runtime_bridge_route_candidate_${routeCandidate.candidate_status}`);
|
||||
|
||||
return {
|
||||
schema_version: ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION,
|
||||
|
|
@ -419,7 +230,6 @@ export async function runAssistantMcpDiscoveryRuntimeBridge(
|
|||
pilot,
|
||||
answer_draft: answerDraft,
|
||||
loop_state: loopState,
|
||||
route_candidate: routeCandidate,
|
||||
user_facing_response_allowed: bridgeStatus !== "blocked",
|
||||
business_fact_answer_allowed: businessFactAnswerAllowed(answerDraft),
|
||||
requires_user_clarification: bridgeStatus === "needs_clarification",
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue