Стабилизировать маржинальность номенклатуры
This commit is contained in:
parent
f5d86d4bc1
commit
09c6d1aa0e
|
|
@ -44,6 +44,27 @@ Fresh validation cut:
|
|||
- `npm.cmd run build` passed;
|
||||
- graphify rebuilt to `6371` nodes, `14048` edges, `141` communities.
|
||||
|
||||
## 2026-05-18 Overlay - Context Entry And Latest Semantic Integrity Closure
|
||||
|
||||
The current short handoff document is now `41 - assistant_context_entry_2026-05-18.md`.
|
||||
|
||||
The latest saved-session semantic replay closure is:
|
||||
|
||||
- source saved session: `gen-mo1t93wq-jy0453e`;
|
||||
- final replay artifact: `artifacts/domain_runs/saved_session_gen_mo1t93wq_jy0453e_rerun_final_semantic_20260518`;
|
||||
- result: `accepted`, `31/31 passed`, `0 failed`, `execution_status=exact`;
|
||||
- commit: `9c86407 Зафиксировать семантическую целостность VAT, debt mirror и trace-ответов`;
|
||||
- graphify after the code cut: `6490 nodes`, `14412 edges`, `141 communities`.
|
||||
|
||||
This May-18 closure does not reopen Post-F. It reinforces Post-F as a regression gate and records four extra live-session seams:
|
||||
|
||||
- same-period VAT follow-up now preserves the prior requested tax period instead of drifting to current date;
|
||||
- stale MCP discovery counterparty no longer contaminates short debt mirror follow-ups such as `а нам?`;
|
||||
- inventory sale trace answers now distinguish sale trace by nomenclature from exact selected lot/batch proof;
|
||||
- broad best-year answers no longer rank unreliable yearly operating net when one direction is row-limit constrained.
|
||||
|
||||
Use this overlay when starting a new chat or preparing a Tasker handoff card.
|
||||
|
||||
## Current Module Map
|
||||
|
||||
- `Post-F Semantic Integrity Hardening`: `99%`, operationally closed as a hardening slice and now used as a regression gate.
|
||||
|
|
@ -96,6 +117,7 @@ Fresh validation cut:
|
|||
- 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`).
|
||||
- Current saved-session semantic replay closure: `saved_session_gen_mo1t93wq_jy0453e_rerun_final_semantic_20260518` accepted `31/31`.
|
||||
- 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.
|
||||
|
|
@ -167,28 +189,29 @@ After any code or documentation sync that changes the map, rebuild graphify and
|
|||
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`
|
||||
2. `41 - assistant_context_entry_2026-05-18.md`
|
||||
3. this document
|
||||
4. `31 - inventory_reserve_liquidation_quality_reviewed_route_2026-05-12.md`
|
||||
5. `33 - limit_honesty_business_language_2026-05-13.md`
|
||||
6. `32 - financial_counterparty_flow_hints_2026-05-13.md`
|
||||
7. `30 - vendor_procurement_quality_reviewed_route_2026-05-12.md`
|
||||
8. `29 - debt_due_date_aging_reviewed_route_2026-05-10.md`
|
||||
9. `28 - accounting_profit_margin_reviewed_route_2026-05-10.md`
|
||||
10. `27 - proof_family_enablement_candidates_2026-05-10.md`
|
||||
11. `26 - route_candidate_driven_enablement_loop_2026-05-10.md`
|
||||
12. `25 - open_world_route_candidate_promotion_2026-05-10.md`
|
||||
13. `34 - large_query_budget_continuation_2026-05-13.md`
|
||||
14. `35 - large_query_continuation_ux_2026-05-13.md`
|
||||
15. `36 - inventory_root_scope_no_warehouse_clarification_2026-05-13.md`
|
||||
16. `37 - debt_mirror_clean_scope_polarity_2026-05-13.md`
|
||||
17. `24 - agentic_semantic_development_loop_and_autorun_hygiene_2026-05-10.md`
|
||||
18. `23 - current_execution_spine_and_semantic_control_gate_2026-05-05.md`
|
||||
19. `22 - open_world_bounded_autonomy_breadth_2026-05-01.md`
|
||||
20. `20 - planner_autonomy_consolidation_2026-05-01.md`
|
||||
21. `19 - inventory_stock_open_world_breadth_proof_2026-05-01.md`
|
||||
22. `40 - mixed_schema_primitive_closure_replay_2026-05-13.md`
|
||||
23. `39 - generic_role_tail_anchor_hygiene_2026-05-13.md`
|
||||
24. `17 - post_f_semantic_integrity_hardening_2026-04-23.md`
|
||||
25. `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.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,178 @@
|
|||
# 41 - Вход в контекст разработки ассистента 1C (2026-05-18)
|
||||
|
||||
## Назначение
|
||||
|
||||
Этот документ является короткой, но полной точкой входа для нового чата, нового Codex-сеанса или нового инженера.
|
||||
|
||||
Он фиксирует актуальное состояние архитектуры после закрытия последнего semantic replay по saved-session `gen-mo1t93wq-jy0453e` и коммита:
|
||||
|
||||
- `9c86407 Зафиксировать семантическую целостность VAT, debt mirror и trace-ответов`
|
||||
|
||||
Главная цель проекта не изменилась:
|
||||
|
||||
- построить 1C-ассистента, который сам выбирает безопасные маршруты по MCP/1C evidence;
|
||||
- не превращать систему в набор хардкодных доменных скрепок;
|
||||
- отвечать бизнесово полезно, прямо и честно;
|
||||
- не выдавать неподтвержденные inference как confirmed 1C fact.
|
||||
|
||||
## Что читать первым
|
||||
|
||||
Для быстрого входа читать в таком порядке:
|
||||
|
||||
1. `AGENTS.md`
|
||||
2. `graphify-out/GRAPH_REPORT.md`
|
||||
3. `docs/ARCH/11 - architecture_turnaround/README.md`
|
||||
4. `docs/ARCH/11 - architecture_turnaround/21 - current_status_canon_2026-05-01.md`
|
||||
5. `docs/ARCH/11 - architecture_turnaround/24 - agentic_semantic_development_loop_and_autorun_hygiene_2026-05-10.md`
|
||||
6. `docs/ARCH/11 - architecture_turnaround/40 - mixed_schema_primitive_closure_replay_2026-05-13.md`
|
||||
7. этот документ
|
||||
8. актуальные Tasker-карточки с label `1C assistant` и `Архитектура`
|
||||
|
||||
Исторические документы `01`-`20` важны как trail решений, но текущий рабочий статус берется из `21`, `24`, `40`, этого документа и Tasker.
|
||||
|
||||
## Текущий статус модулей
|
||||
|
||||
Post-F Semantic Integrity Hardening:
|
||||
|
||||
- статус: `99%`, operationally closed;
|
||||
- теперь это regression gate, а не активный denominator;
|
||||
- защищает stale scope, wrong focus_object, repeated pivots, post-pivot arbitration, VAT materialization, debt mirror polarity, selected-object continuity и answer-shape truth.
|
||||
|
||||
Planner Autonomy Consolidation:
|
||||
|
||||
- статус: `100%` для declared phase83 slice;
|
||||
- planner-brain, catalog alignment, live-readiness gate и mixed replay приняты;
|
||||
- это не означает arbitrary 1C autonomy, но это закрывает первый мозг маршрутов.
|
||||
|
||||
Open-World Schema/Primitive Discovery:
|
||||
|
||||
- phases97-105 приняты и сохранены как canaries;
|
||||
- phase105 mixed schema/primitive closure replay accepted `13/13`;
|
||||
- модуль находится на manual-review readiness, а не на blind coding stage.
|
||||
|
||||
Agentic Semantic Development Loop:
|
||||
|
||||
- статус: `99%`;
|
||||
- stage loop, business-audit handoff, save-after-acceptance gate и autorun hygiene работают;
|
||||
- human GUI checkpoint остается финальным high-signal подтверждением.
|
||||
|
||||
Последний semantic integrity cut:
|
||||
|
||||
- saved session: `gen-mo1t93wq-jy0453e`;
|
||||
- final replay: `artifacts/domain_runs/saved_session_gen_mo1t93wq_jy0453e_rerun_final_semantic_20260518`;
|
||||
- результат: `accepted`, `31/31 passed`, `0 failed`, `execution_status=exact`;
|
||||
- закрытые seams: VAT same-period carryover, stale MCP discovery counterparty in short debt mirror, sale-trace lot/batch honesty, broad best-year net-ranking honesty.
|
||||
|
||||
## Архитектурная концепция
|
||||
|
||||
Система состоит из нескольких связанных слоев.
|
||||
|
||||
Assistant runtime:
|
||||
|
||||
- внешний фасад живого ассистента, GUI и runtime-сессий;
|
||||
- основной pressure center все еще вокруг `assistantService.ts`, но бизнесовая логика постепенно вынесена в специализированные модули;
|
||||
- runtime обязан сохранять session state, но не должен позволять старой памяти победить explicit current-turn meaning.
|
||||
|
||||
Exact address lane:
|
||||
|
||||
- `AddressQueryService`, `addressRecipeCatalog`, `addressIntentResolver`, `addressFilterExtractor`, `address_runtime/*`;
|
||||
- отвечает за VAT, receivables/payables, inventory, value-flow, bank operations, accounting result, procurement, debt aging, inventory quality events и связанные factual replies;
|
||||
- exact route может быть fast path, но не может обходить truth gate, scope gate и answer-shape gate.
|
||||
|
||||
MCP discovery/planner lane:
|
||||
|
||||
- `assistantMcpDiscoveryPlanner`, `assistantMcpCatalogIndex`, `assistantMcpDiscoveryPilotExecutor`, `assistantMcpDiscoveryRuntimeBridge`;
|
||||
- выбирает reviewed primitive chains через metadata, entity grounding, documents, movements, value-flow, route candidates;
|
||||
- не должен превращать unknown или proxy-only evidence в confirmed fact.
|
||||
|
||||
Data-need graph and route-candidate layer:
|
||||
|
||||
- описывает вопрос пользователя как бизнесовую потребность, а не только как route id;
|
||||
- хранит subject, fact family, action family, period, aggregation, ranking, comparison, proof expectation, missing axes и forbidden-overclaim flags;
|
||||
- `route_candidate` превращает missing proof family в конкретный enablement target, а не в размытое “не умеем”.
|
||||
|
||||
Continuity and transition layer:
|
||||
|
||||
- `assistantTransitionPolicy`, `assistantContinuityPolicy`, `assistantMcpDiscoveryTurnInputAdapter`, navigation state и focus/answer object helpers;
|
||||
- решает, что переносится между turn-ами: organization, counterparty, period, selected object, answer_object, provenance bundle;
|
||||
- explicit current-turn entity/period/action должны побеждать stale organization, stale focus_object и старые discovery candidates.
|
||||
|
||||
Answer shaping and response policy:
|
||||
|
||||
- `assistantMcpDiscoveryAnswerAdapter`, `assistantMcpDiscoveryResponseCandidate`, `assistantMcpDiscoveryResponsePolicy`, factual reply builders;
|
||||
- пользовательский ответ должен начинаться с прямого business answer;
|
||||
- proof, caveats, row limits и method notes идут после ответа;
|
||||
- internal route ids, capability ids, raw debug enums и service mechanics не должны попадать в финальный ответ.
|
||||
|
||||
GUI, autoruns and runtime artifacts:
|
||||
|
||||
- `autoRuns`, `eval`, `assistantService`, `addressTextRepair`;
|
||||
- GUI autoruns являются human checkpoint и replay history;
|
||||
- сохранение вопросов не равно AGENT replay;
|
||||
- saved AGENT pack допустим только после live replay and review;
|
||||
- UTF-8 без BOM и отсутствие mojibake являются acceptance surface, а не косметикой.
|
||||
|
||||
## Главные инварианты
|
||||
|
||||
- Сначала проверяется человеческий смысл вопроса и ответа, потом debug.
|
||||
- Live replay важнее зеленых unit tests.
|
||||
- Explicit текущий субъект сильнее stale scope.
|
||||
- Valid clarification is not a bug.
|
||||
- Debt mirror должен различать `мы должны` и `нам должны`.
|
||||
- Bank-like counterparty не является обычным поставщиком или клиентом без purpose/operation evidence.
|
||||
- VAT period carryover должен сохранять тот же период, если пользователь говорит `за этот период`.
|
||||
- Inventory sale trace может подтвердить sale trace by nomenclature, но не exact selected lot без batch/lot proof.
|
||||
- Broad business overview не должен ранжировать unreliable net, если один из потоков ограничен row cap.
|
||||
- No route/proxy/MCP/debug garbage in final answer.
|
||||
|
||||
## Текущие canary/replay anchors
|
||||
|
||||
Ключевые canaries:
|
||||
|
||||
- `phase83_planner_brain_alignment_live_20260501_readygate_rerun3`
|
||||
- `address_truth_harness_post_f_cross_stage_canary_agent_20260424_live7`
|
||||
- `inventory_stock_open_world_breadth_rerun_semantic_integrity_20260501_fix5`
|
||||
- `phase90`-`phase96` route-candidate/proof-family acceptance chain
|
||||
- `phase97_financial_counterparty_flow_hints_live4`
|
||||
- `phase98_limit_honesty_business_language_live3`
|
||||
- `phase99_large_query_budget_continuation_live2`
|
||||
- `phase100_large_query_continuation_ux_live2`
|
||||
- `phase101_inventory_root_scope_no_warehouse_clarification_live1`
|
||||
- `phase102_debt_mirror_clean_scope_polarity_live3`
|
||||
- `phase103_financial_role_purpose_arbitration_live3`
|
||||
- `phase104_generic_role_tail_anchor_hygiene`
|
||||
- `phase105_mixed_schema_primitive_closure_live3`
|
||||
- `saved_session_gen_mo1t93wq_jy0453e_rerun_final_semantic_20260518`
|
||||
|
||||
Если новый фикс касается соседней зоны, соответствующий replay нужно использовать как semantic regression gate.
|
||||
|
||||
## Как продолжать в новом чате
|
||||
|
||||
Новый Codex-сеанс должен начинать так:
|
||||
|
||||
1. прочитать `AGENTS.md`;
|
||||
2. прочитать `graphify-out/GRAPH_REPORT.md`;
|
||||
3. прочитать этот документ, README, `21`, `24`, `40`;
|
||||
4. проверить `git status --short`;
|
||||
5. не коммитить runtime_job artifacts без явного решения;
|
||||
6. определить текущий active module и его denominator;
|
||||
7. если пользователь принес run id, сначала читать human Q/A, затем artifacts/debug;
|
||||
8. если внесены code changes, запускать targeted tests, build по необходимости, semantic replay, graphify rebuild;
|
||||
9. после accepted replay обновлять docs/Tasker, если меняется архитектурный статус.
|
||||
|
||||
## Текущее направление после этого среза
|
||||
|
||||
Следующий крупный фокус:
|
||||
|
||||
- закрепить agentic semantic replay loop как регулярный gate после крупных фиксов;
|
||||
- минимизировать ручную роль пользователя до финального GUI checkpoint;
|
||||
- продолжать движение к open-world bounded autonomy, где ассистент не ждет хардкодного route-per-domain, а выбирает reviewed MCP primitive path по data-need graph, route_candidate и evidence gates.
|
||||
|
||||
При этом нельзя объявлять проект universal arbitrary-1C agent.
|
||||
|
||||
Честная формулировка текущего состояния:
|
||||
|
||||
- система уже умеет много устойчивых 1C-контуров;
|
||||
- bounded MCP autonomy substrate реально работает;
|
||||
- route-candidate-driven enablement показал, как превращать gaps в reviewed routes;
|
||||
- но широкая arbitrary schema traversal все еще должна расширяться через replay-backed slices, а не через свободную импровизацию.
|
||||
|
|
@ -0,0 +1,175 @@
|
|||
# 42 - Project Audit Milestone And Next Vector (2026-05-18)
|
||||
|
||||
## Purpose
|
||||
|
||||
This note records the May-18 audit milestone after a repo-first review of:
|
||||
|
||||
- current runtime code shape;
|
||||
- graphify pressure centers;
|
||||
- current architecture canon in `21`, `24`, `40`, and `41`;
|
||||
- Tasker handoff cards for `1C assistant` / `1С-ассистент`;
|
||||
- latest accepted live replay and saved-session closure artifacts.
|
||||
|
||||
Its purpose is not to reopen old slices.
|
||||
|
||||
Its purpose is to:
|
||||
|
||||
- freeze the current project map in one place;
|
||||
- state what is actually closed, what is still open, and what is merely pressure-tested;
|
||||
- define the next unified development vector after the current sync pass.
|
||||
|
||||
## Audit Result
|
||||
|
||||
The project is materially beyond the old "build first exact routes" stage.
|
||||
|
||||
The real current architecture is a bounded MCP-first assistant built from these active layers:
|
||||
|
||||
- assistant runtime and orchestration facade;
|
||||
- exact address lane;
|
||||
- MCP discovery / planner lane;
|
||||
- data-need graph and route-candidate layer;
|
||||
- continuity / transition / selected-object state layer;
|
||||
- truth gate and answer-shaping layer;
|
||||
- AGENT semantic replay operating loop;
|
||||
- GUI / autorun / runtime artifact layer.
|
||||
|
||||
The most connected code pressure centers remain:
|
||||
|
||||
- `executeAssistantMcpDiscoveryPilot()`
|
||||
- `buildAssistantMcpDiscoveryTurnInput()`
|
||||
- `ChannelRegistry`
|
||||
- `composeFactualReplyBody()`
|
||||
- `extractAddressFilters()`
|
||||
- `repairAddressMojibake()`
|
||||
|
||||
This confirms that the current blast radius is not only in one route or one wording family. The highest-risk seams still sit at:
|
||||
|
||||
- runtime input assembly;
|
||||
- continuity and stale-scope arbitration;
|
||||
- MCP execution and evidence shaping;
|
||||
- final answer truthfulness and text hygiene.
|
||||
|
||||
## Current Module Map
|
||||
|
||||
### Operationally closed or used as regression gates
|
||||
|
||||
- `Post-F Semantic Integrity Hardening`: operationally closed at `99%`
|
||||
- `Planner Autonomy Consolidation`: closed at `100%`
|
||||
- `Route-Candidate-Driven Enablement Loop`: closed at `100%`
|
||||
- reviewed proof-family routes through phases `93-96`
|
||||
- business-overview breadth slices through the currently accepted reviewed families
|
||||
|
||||
### Active but already implementation-heavy
|
||||
|
||||
- `Open-World Schema/Primitive Discovery`: `95%`
|
||||
- phases `97-105` are accepted and saved
|
||||
- current closure replay: `phase105_mixed_schema_primitive_closure_live3`, `13/13`
|
||||
- module status: manual-review readiness, not blind coding stage
|
||||
|
||||
### Active operating layer
|
||||
|
||||
- `Agentic Semantic Development Loop`: `99%`
|
||||
- dogfood loop accepted
|
||||
- autorun/runtime Cyrillic hygiene accepted
|
||||
- manual GUI confirmation still required before treating fat packs as fully human-accepted
|
||||
|
||||
## Latest Accepted Semantic Closure
|
||||
|
||||
Latest saved-session closure:
|
||||
|
||||
- source saved session: `gen-mo1t93wq-jy0453e`
|
||||
- replay artifact: `artifacts/domain_runs/saved_session_gen_mo1t93wq_jy0453e_rerun_final_semantic_20260518`
|
||||
- result: `accepted`
|
||||
- score: `31/31 passed`
|
||||
- execution status: `exact`
|
||||
- commit anchor: `9c86407 Зафиксировать семантическую целостность VAT, debt mirror и trace-ответов`
|
||||
|
||||
This closure additionally hardened:
|
||||
|
||||
- same-period VAT carryover;
|
||||
- stale MCP discovery counterparty contamination in short debt-mirror follow-ups;
|
||||
- sale-trace honesty between nomenclature trace and exact lot/batch proof;
|
||||
- broad best-year net-ranking honesty under row-cap asymmetry.
|
||||
|
||||
## Important Accepted Breadth Boundary
|
||||
|
||||
`phase86` must be treated as a real accepted contour in the current map:
|
||||
|
||||
- spec: `docs/orchestration/address_truth_harness_phase86_business_overview_debt_position.json`
|
||||
- accepted replay: `artifacts/domain_runs/address_truth_harness_phase86_business_overview_debt_position_live_20260504_debt2`
|
||||
|
||||
Meaning:
|
||||
|
||||
- explicit-period `business_overview` may include confirmed receivables/payables snapshot on an explicit as-of date;
|
||||
- all-time follow-up must not reuse the old debt snapshot as current or general debt position;
|
||||
- debt quality, overdue debt, credit risk, profit, and margin remain bounded unless separately proven.
|
||||
|
||||
This boundary matters because it is a clean example of the current project style:
|
||||
|
||||
- expand breadth through reviewed evidence;
|
||||
- widen answer usefulness;
|
||||
- keep stale-scope and overclaim protection intact.
|
||||
|
||||
## Documentation And Tasker Audit Findings
|
||||
|
||||
### What was already good
|
||||
|
||||
- the repo canon in `21`, `24`, `40`, and `41` is coherent and materially up to date;
|
||||
- the high-level Tasker card set `01-17` already covers the architecture spine well;
|
||||
- the dedicated context-entry card `[1С-ассистент] Вход в контекст разработки ассистента 1С` already works as the right top-level handoff card.
|
||||
|
||||
### What was missing or underrepresented
|
||||
|
||||
- latest May-18 saved-session closure was not reflected everywhere;
|
||||
- the current honest `95%` status for `Open-World Schema/Primitive Discovery` needed to be stated more explicitly in Tasker;
|
||||
- the accepted `phase86` debt-position breadth boundary was present in repo docs and artifacts, but was not explicit enough in the card layer;
|
||||
- the practical review order `human Q/A first -> replay artifacts -> debug later` needed to be restated in the main handoff card layer;
|
||||
- the current "active gate" reality around semantic-control / fat GUI review was still more explicit in repo docs than in the Tasker map.
|
||||
|
||||
## Unified Next Development Vector
|
||||
|
||||
The project should not fork into unrelated planning threads.
|
||||
|
||||
The current unified vector is:
|
||||
|
||||
1. Close the current human acceptance gate cleanly.
|
||||
2. Institutionalize the AGENT semantic replay loop as the normal post-fix gate.
|
||||
3. Expand open-world bounded autonomy only through replay-backed breadth slices.
|
||||
4. Reduce pressure on central continuity and intent seams while preserving existing regression gates.
|
||||
|
||||
In practical terms this means:
|
||||
|
||||
- first finish `phase105` GUI/manual review or the equivalent fat user pack review;
|
||||
- if the review is clean, close the current `Open-World Schema/Primitive Discovery` module honestly;
|
||||
- if the review exposes a real semantic defect, convert it into a narrow `phase106 repair/replay`, not a broad architecture rewrite;
|
||||
- continue future breadth through reviewed primitive descriptors, data-need coverage, route-candidate enablement, and replay-backed acceptance;
|
||||
- do not regress into domain-hardcode-first growth.
|
||||
|
||||
## What Should Not Become The Next Vector
|
||||
|
||||
The next vector should not be:
|
||||
|
||||
- "rewrite the runtime again";
|
||||
- "add routes opportunistically without replay discipline";
|
||||
- "treat green tests or route ids as primary acceptance";
|
||||
- "expand arbitrary schema traversal before the current closure gate is honestly reviewed";
|
||||
- "replace the AGENT loop with ad hoc manual debugging."
|
||||
|
||||
## Recommended Immediate Work Order
|
||||
|
||||
1. Keep the main context-entry card and current module cards synchronized with repo canon.
|
||||
2. Add or maintain one explicit Tasker card for the current semantic-control / GUI/manual acceptance gate if that gate is not clearly represented.
|
||||
3. Use `phase105` and the May-18 saved-session closure as the current top replay anchors.
|
||||
4. After the card sync, plan the next slice only from the audited state above, not from older percentage snapshots or stale docs.
|
||||
|
||||
## Honest Project Status Statement
|
||||
|
||||
The project already has a real bounded MCP autonomy substrate and a strong semantic hardening backbone.
|
||||
|
||||
It is not a universal arbitrary-1C agent yet.
|
||||
|
||||
The correct current status is:
|
||||
|
||||
- many important 1C contours are already stable and replay-backed;
|
||||
- the main remaining risk is not absence of architecture, but semantic drift on mixed human-style pressure surfaces;
|
||||
- the next development denominator is controlled breadth expansion after an honest current acceptance closure, not a reset.
|
||||
|
|
@ -0,0 +1,360 @@
|
|||
# 43 - Business Answer Contract And Semantic Audit Uplift (2026-05-18)
|
||||
|
||||
## Purpose
|
||||
|
||||
This note freezes the next real project module after the May-18 project audit.
|
||||
|
||||
Its purpose is to turn a high-quality human audit of real runs into:
|
||||
|
||||
- a runtime answer-shaping module;
|
||||
- a stronger semantic replay audit contract;
|
||||
- a concrete execution order for the next development stage.
|
||||
|
||||
The core conclusion is:
|
||||
|
||||
- the assistant already finds many real facts correctly;
|
||||
- the current weak point is not raw retrieval;
|
||||
- the weak point is the conversion from confirmed facts into a business-useful answer and a business-grade replay verdict.
|
||||
|
||||
In short:
|
||||
|
||||
`truth/retrieval is materially ahead of user-facing answer quality and semantic audit quality`
|
||||
|
||||
## Canonical Diagnosis
|
||||
|
||||
The current assistant too often behaves like a careful technical reporter instead of a strong 1C business analyst.
|
||||
|
||||
This means:
|
||||
|
||||
- confirmed numbers are found;
|
||||
- organization/date context is often preserved;
|
||||
- truth and anti-overclaim guards are active;
|
||||
- but the final answer is still overloaded with caveats, low on management interpretation, weak on next action, and not stable enough on semantic disambiguation.
|
||||
|
||||
The current failure pattern is:
|
||||
|
||||
1. the system proves a fact;
|
||||
2. the truth layer leaks directly into the user answer;
|
||||
3. the user receives a cautious technical block instead of a business answer;
|
||||
4. the replay evaluator notices this only partially;
|
||||
5. the run remains technically respectable but business-weak.
|
||||
|
||||
## What The Human Audit Proved
|
||||
|
||||
The human audit established two linked facts.
|
||||
|
||||
### 1. The current audit layer is too weak
|
||||
|
||||
If the AGENT semantic replay audit were as strong as the human review, more runs would be rejected or repaired before being treated as healthy.
|
||||
|
||||
The current replay stack can already see some of this through metrics such as:
|
||||
|
||||
- `accountant_actionability_score`
|
||||
- `mechanism_specificity_score`
|
||||
- `followup_context_retention_score`
|
||||
|
||||
But the practical business reading is still underpowered.
|
||||
|
||||
The system can tell that the answer is weak.
|
||||
It is still not good enough at explaining why it is weak in the same business language a strong human reviewer uses.
|
||||
|
||||
### 2. The runtime answer layer is the main product gap
|
||||
|
||||
The main user-facing problem is not "the system cannot compute".
|
||||
|
||||
The main problem is:
|
||||
|
||||
`the system cannot consistently convert computed truths into a concise, useful, accountant-grade answer`
|
||||
|
||||
This gap shows up as:
|
||||
|
||||
- dirty answer shape;
|
||||
- too much defensive wording in the main body;
|
||||
- weak distinction between cashflow, profit, debt, bank contour, inventory snapshot, and historical inventory;
|
||||
- weak business takeaway;
|
||||
- missing next action;
|
||||
- technical or truth-gate wording leaking into the user-facing answer.
|
||||
|
||||
## Architectural Reading Of The Gap
|
||||
|
||||
This audit should be read as a split between two layers.
|
||||
|
||||
### Layer A: Truth Layer
|
||||
|
||||
This layer answers:
|
||||
|
||||
- what is confirmed;
|
||||
- by which evidence;
|
||||
- within which date window;
|
||||
- with which limitations;
|
||||
- whether overclaim protection should block or soften the answer.
|
||||
|
||||
This layer is already substantial and should remain strict.
|
||||
|
||||
### Layer B: Business Answer Layer
|
||||
|
||||
This layer answers:
|
||||
|
||||
- what the result means for the business user;
|
||||
- which figures matter first;
|
||||
- what ambiguity should be surfaced explicitly;
|
||||
- what should be checked next;
|
||||
- how to keep the answer readable without lying.
|
||||
|
||||
This layer is the main missing product denominator.
|
||||
|
||||
Current problem:
|
||||
|
||||
`Layer A is too visible; Layer B is too weak`
|
||||
|
||||
## Exact Code Seams
|
||||
|
||||
The current implementation seams match this diagnosis closely.
|
||||
|
||||
### Runtime answer shaping seams
|
||||
|
||||
- `llm_normalizer/backend/src/services/answerComposer.ts`
|
||||
- `llm_normalizer/backend/src/services/address_runtime/composeStage.ts`
|
||||
- `llm_normalizer/backend/src/services/assistantTruthAnswerPolicyRuntimeAdapter.ts`
|
||||
- `llm_normalizer/backend/src/services/assistantDebugPayloadAssembler.ts`
|
||||
|
||||
Relevant active contracts already exist:
|
||||
|
||||
- `answer_contract_stage4_v1`
|
||||
- `answer_structure_v11`
|
||||
- `direct_answer`
|
||||
- `mechanism_block`
|
||||
- `uncertainty_block`
|
||||
- `next_step_block`
|
||||
|
||||
This means the next step is not a greenfield rewrite.
|
||||
It is a targeted strengthening of the answer contract and reply renderer.
|
||||
|
||||
### Replay / audit seams
|
||||
|
||||
- `llm_normalizer/backend/src/services/evalService.ts`
|
||||
- `llm_normalizer/backend/src/eval/p0_eval_runner.ts`
|
||||
- `llm_normalizer/backend/src/eval/p0_metric_definitions.ts`
|
||||
- `llm_normalizer/backend/src/eval/p0_acceptance_gate.ts`
|
||||
|
||||
These seams already compute weak/strong answer signals, but they do not yet encode the full business-grade audit logic demonstrated by the human review.
|
||||
|
||||
## Next Module Definition
|
||||
|
||||
The next module should be named:
|
||||
|
||||
`Business Answer Contract + Semantic Audit Uplift`
|
||||
|
||||
It must be treated as one module with two subtracks, not as unrelated work.
|
||||
|
||||
### Track 1. Runtime Business Answer Contract
|
||||
|
||||
Goal:
|
||||
|
||||
- make the assistant answer like a usable 1C analyst without weakening truthfulness.
|
||||
|
||||
Required outcomes:
|
||||
|
||||
1. Introduce stable answer shapes for business question families.
|
||||
2. Move caveats out of the main answer body and compress them into one honest boundary block.
|
||||
3. Force a management-first direct answer before evidence details.
|
||||
4. Always provide a concrete next action when the contour allows it.
|
||||
5. Prevent technical leakage into the user-facing answer.
|
||||
|
||||
### Track 2. Semantic Audit Uplift
|
||||
|
||||
Goal:
|
||||
|
||||
- make AGENT replay review detect the same answer-quality failures that a strong human auditor detects.
|
||||
|
||||
Required outcomes:
|
||||
|
||||
1. Detect when the answer is factually grounded but business-useless.
|
||||
2. Detect when ambiguity was handled weakly or silently.
|
||||
3. Detect when caveats dominate the main answer instead of being bounded cleanly.
|
||||
4. Detect when heterogeneous quantities are surfaced as misleading business totals.
|
||||
5. Detect when the answer lacks a management takeaway or next step.
|
||||
|
||||
## Required Runtime Changes
|
||||
|
||||
### 1. Introduce answer intent shapes
|
||||
|
||||
The runtime should stop relying only on broad reply classes such as:
|
||||
|
||||
- `factual`
|
||||
- `factual_with_explanation`
|
||||
- `partial_coverage`
|
||||
- `clarification_required`
|
||||
|
||||
It should add stable business answer shapes such as:
|
||||
|
||||
- `inventory_snapshot`
|
||||
- `historical_inventory_snapshot`
|
||||
- `cashflow_overview`
|
||||
- `profit_vs_cashflow_disambiguation`
|
||||
- `counterparty_value_flow`
|
||||
- `bank_flow_classification`
|
||||
- `tax_payable`
|
||||
- `accounts_payable`
|
||||
- `accounts_receivable`
|
||||
- `business_overview_summary`
|
||||
|
||||
Each shape should have its own user-facing render contract.
|
||||
|
||||
### 2. Introduce ambiguity-first handling for overloaded business words
|
||||
|
||||
Critical words such as:
|
||||
|
||||
- "заработала"
|
||||
- "прибыль"
|
||||
- "деньги"
|
||||
- "должны"
|
||||
- "остатки"
|
||||
- "оборот"
|
||||
- "выручка"
|
||||
|
||||
must not silently collapse into one interpretation.
|
||||
|
||||
The answer should explicitly split the meaning when needed:
|
||||
|
||||
- cashflow meaning;
|
||||
- accounting profit meaning;
|
||||
- debt snapshot meaning;
|
||||
- turnover meaning.
|
||||
|
||||
### 3. Force management-first answer order
|
||||
|
||||
The default order should become:
|
||||
|
||||
1. business conclusion;
|
||||
2. 3-5 key figures;
|
||||
3. one short boundary/limitation line;
|
||||
4. next useful step.
|
||||
|
||||
This order should be preferred over:
|
||||
|
||||
- raw table-first answers;
|
||||
- long limitation-first answers;
|
||||
- technical explanation-first answers.
|
||||
|
||||
### 4. Suppress misleading aggregate quantities
|
||||
|
||||
For heterogeneous inventory and similar mixed lists, the runtime should avoid presenting meaningless aggregate counts as if they were strong management metrics.
|
||||
|
||||
This is especially important when a count mixes radically different unit types.
|
||||
|
||||
### 5. Add actionability by contract
|
||||
|
||||
Every relevant answer family should end with a next useful option, for example:
|
||||
|
||||
- show document drilldown;
|
||||
- split by months;
|
||||
- separate bank from clients;
|
||||
- compute pure accounting profit;
|
||||
- show overdue aging;
|
||||
- open top counterparties;
|
||||
- explain why this debt remains open.
|
||||
|
||||
This should be contract-driven, not an optional wording flourish.
|
||||
|
||||
## Required Audit Changes
|
||||
|
||||
### 1. Upgrade replay verdict language
|
||||
|
||||
The replay review should explicitly distinguish:
|
||||
|
||||
- factually wrong;
|
||||
- factually right but business-wrong;
|
||||
- factually right but ambiguity-handled-poorly;
|
||||
- factually right but too technical/leaky;
|
||||
- factually right but low-actionability.
|
||||
|
||||
### 2. Add stronger answer-quality probes
|
||||
|
||||
The semantic audit should explicitly score:
|
||||
|
||||
- management conclusion present or absent;
|
||||
- key figure prioritization quality;
|
||||
- limitation compression quality;
|
||||
- ambiguity handling quality;
|
||||
- next-step usefulness;
|
||||
- bank/business contour classification quality;
|
||||
- follow-up interpretation continuity.
|
||||
|
||||
### 3. Treat “technical cleanliness” as a first-class acceptance surface
|
||||
|
||||
If the answer leaks technical mechanics into the business response, that must be treated as a real quality defect, not as harmless debug residue.
|
||||
|
||||
### 4. Strengthen human-style post-run findings
|
||||
|
||||
The replay artifacts should move closer to the human audit style:
|
||||
|
||||
- what worked;
|
||||
- what the user actually wanted;
|
||||
- what was answered instead;
|
||||
- why the answer is still weak;
|
||||
- what exact runtime behavior should be changed.
|
||||
|
||||
## Proposed Execution Order
|
||||
|
||||
The next execution order should be:
|
||||
|
||||
1. freeze this module and Tasker it;
|
||||
2. implement runtime answer-shape uplift first;
|
||||
3. rerun focused semantic packs against the same business questions;
|
||||
4. then tighten replay audit scoring and narrative findings using the new denominator;
|
||||
5. only after that widen breadth again.
|
||||
|
||||
Reason:
|
||||
|
||||
- if audit is improved before answer contracts, we mostly get stronger failure reporting;
|
||||
- if answer contracts are improved first, the next audit pass can judge a better target surface.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
The module should be accepted only when the following become true on focused replay packs.
|
||||
|
||||
### Runtime acceptance
|
||||
|
||||
- direct answers start with business meaning, not caveat noise;
|
||||
- ambiguous business terms are split honestly when needed;
|
||||
- cashflow vs profit confusion is handled explicitly;
|
||||
- bank-like contours are classified without pretending they are ordinary customer revenue;
|
||||
- mixed inventory counts are not surfaced as misleading management totals;
|
||||
- next-step guidance is present and useful.
|
||||
|
||||
### Audit acceptance
|
||||
|
||||
- weak business answers are called out even when retrieval is correct;
|
||||
- replay findings read closer to a senior analyst review than to a metric dump;
|
||||
- `accountant_actionability_score`, `mechanism_specificity_score`, and `followup_context_retention_score` stop being chronically weak on the target pack;
|
||||
- accepted runs no longer pass merely because the truth layer is clean.
|
||||
|
||||
## What This Module Is Not
|
||||
|
||||
This module is not:
|
||||
|
||||
- a new domain-route spree;
|
||||
- a planner rewrite;
|
||||
- a truth-gate rollback;
|
||||
- a generic UX polish task.
|
||||
|
||||
It is the missing business-answer contract between the already-strong retrieval core and the user.
|
||||
|
||||
## Recommended Immediate Tasker Shape
|
||||
|
||||
One active card should represent this module explicitly, with subtasks for:
|
||||
|
||||
1. answer intent shapes;
|
||||
2. ambiguity handler for overloaded business words;
|
||||
3. business-first render order;
|
||||
4. actionability contract;
|
||||
5. semantic audit uplift and replay rubric alignment.
|
||||
|
||||
## Honest Status Line
|
||||
|
||||
The project is now at the point where:
|
||||
|
||||
- retrieval competence is no longer the main differentiator;
|
||||
- answer usefulness and replay audit quality are the next denominator;
|
||||
- if this module lands well, the assistant will stop sounding like a debug log and start sounding like a real 1C analyst.
|
||||
|
|
@ -58,13 +58,15 @@ This package answers the next question:
|
|||
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)
|
||||
41. [41 - assistant_context_entry_2026-05-18.md](./41%20-%20assistant_context_entry_2026-05-18.md)
|
||||
|
||||
## Current Status Snapshot (2026-05-13)
|
||||
## Current Status Snapshot (2026-05-18)
|
||||
|
||||
This package is no longer planning-only.
|
||||
|
||||
Status canon for planning:
|
||||
|
||||
- The fastest current handoff document is now [41 - assistant_context_entry_2026-05-18.md](./41%20-%20assistant_context_entry_2026-05-18.md).
|
||||
- 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.
|
||||
|
|
@ -153,6 +155,7 @@ Status canon for planning:
|
|||
- 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.
|
||||
- The latest saved-session semantic integrity closure is `saved_session_gen_mo1t93wq_jy0453e_rerun_final_semantic_20260518`, accepted `31/31` after repairing VAT same-period carryover, stale MCP-discovery counterparty carryover in short debt mirror turns, sale-trace lot/batch honesty, and broad best-year net-ranking honesty.
|
||||
|
||||
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:
|
||||
|
||||
|
|
@ -423,6 +426,7 @@ Read in this order:
|
|||
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`
|
||||
42. `41 - assistant_context_entry_2026-05-18.md`
|
||||
|
||||
## Planning Rules
|
||||
|
||||
|
|
|
|||
|
|
@ -50,6 +50,12 @@
|
|||
"expected_requested_result_modes": ["confirmed_balance"],
|
||||
"expected_result_modes": ["confirmed_balance"]
|
||||
},
|
||||
{
|
||||
"intent": "inventory_margin_ranking_for_nomenclature",
|
||||
"expected_selected_recipes": ["address_inventory_margin_ranking_for_nomenclature_v1"],
|
||||
"expected_requested_result_modes": ["confirmed_balance"],
|
||||
"expected_result_modes": ["confirmed_balance"]
|
||||
},
|
||||
{
|
||||
"intent": "inventory_purchase_to_sale_chain",
|
||||
"expected_selected_recipes": ["address_inventory_purchase_to_sale_chain_v1"],
|
||||
|
|
|
|||
|
|
@ -0,0 +1,109 @@
|
|||
{
|
||||
"schema_version": "domain_scenario_manifest_v1",
|
||||
"scenario_id": "inventory_margin_ranking_agent_loop_20260522",
|
||||
"domain": "inventory_margin_ranking",
|
||||
"title": "Inventory nomenclature margin ranking limited-answer loop",
|
||||
"description": "Shared-session replay for nomenclature profitability ranking: missing period clarification, period-scoped insufficient realization/cost evidence, and follow-up action quality.",
|
||||
"acceptance_canon": {
|
||||
"root_step_id": "step_01_margin_root_needs_period",
|
||||
"primary_user_path": [
|
||||
"step_01_margin_root_needs_period",
|
||||
"step_02_september_2017_period",
|
||||
"step_03_show_cost_base_lines",
|
||||
"step_04_expand_to_2017",
|
||||
"step_05_account_41_not_01"
|
||||
],
|
||||
"required_paraphrase_families": [
|
||||
"colloquial",
|
||||
"canonical"
|
||||
],
|
||||
"required_carryover_invariants": [
|
||||
"intent_scope",
|
||||
"period_scope",
|
||||
"organization_scope",
|
||||
"answer_shape"
|
||||
]
|
||||
},
|
||||
"analysis_context": {
|
||||
"as_of_date": "2026-05-22",
|
||||
"source": "manual_export_and_codex_agent_loop"
|
||||
},
|
||||
"steps": [
|
||||
{
|
||||
"step_id": "step_01_margin_root_needs_period",
|
||||
"title": "Root asks nomenclature high/low profit without period",
|
||||
"question": "Какая номеклатура товара реализована с высокой прибылью какая с низкой",
|
||||
"node_role": "root",
|
||||
"paraphrase_family": "colloquial",
|
||||
"expected_capability": "inventory_inventory_margin_ranking_for_nomenclature",
|
||||
"expected_result_mode": "clarification_required"
|
||||
},
|
||||
{
|
||||
"step_id": "step_02_september_2017_period",
|
||||
"title": "User provides period",
|
||||
"question": "сентябрь 2017",
|
||||
"node_role": "critical_child",
|
||||
"paraphrase_family": "colloquial",
|
||||
"depends_on": [
|
||||
"step_01_margin_root_needs_period"
|
||||
],
|
||||
"expected_capability": "inventory_inventory_margin_ranking_for_nomenclature",
|
||||
"expected_result_mode": "limited_accounting_answer",
|
||||
"required_carryover_invariants": [
|
||||
"intent_scope",
|
||||
"period_scope",
|
||||
"organization_scope"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_03_show_cost_base_lines",
|
||||
"title": "Follow-up asks for found evidence after no sales",
|
||||
"question": "покажи найденные строки себестоимостной базы",
|
||||
"node_role": "critical_child",
|
||||
"paraphrase_family": "canonical",
|
||||
"depends_on": [
|
||||
"step_02_september_2017_period"
|
||||
],
|
||||
"expected_result_mode": "evidence_or_honest_boundary",
|
||||
"required_carryover_invariants": [
|
||||
"intent_scope",
|
||||
"period_scope",
|
||||
"organization_scope"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_04_expand_to_2017",
|
||||
"title": "User expands period to full year",
|
||||
"question": "расширь до 2017 года",
|
||||
"node_role": "critical_child",
|
||||
"paraphrase_family": "colloquial",
|
||||
"depends_on": [
|
||||
"step_02_september_2017_period"
|
||||
],
|
||||
"expected_capability": "inventory_inventory_margin_ranking_for_nomenclature",
|
||||
"expected_result_mode": "ranking_or_limited_accounting_answer",
|
||||
"required_carryover_invariants": [
|
||||
"intent_scope",
|
||||
"period_scope",
|
||||
"organization_scope"
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_id": "step_05_account_41_not_01",
|
||||
"title": "User corrects account family to 41 not fixed assets",
|
||||
"question": "анализ по 41 счету а не 01",
|
||||
"node_role": "critical_child",
|
||||
"paraphrase_family": "colloquial",
|
||||
"depends_on": [
|
||||
"step_04_expand_to_2017"
|
||||
],
|
||||
"expected_capability": "inventory_inventory_margin_ranking_for_nomenclature",
|
||||
"expected_result_mode": "same_inventory_margin_context_or_clarification",
|
||||
"required_carryover_invariants": [
|
||||
"intent_scope",
|
||||
"period_scope",
|
||||
"organization_scope"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ const COMPUTE_EXACT_INTENTS = new Set([
|
|||
"inventory_purchase_documents_for_item",
|
||||
"inventory_supplier_stock_overlap_as_of_date",
|
||||
"inventory_sale_trace_for_item",
|
||||
"inventory_margin_ranking_for_nomenclature",
|
||||
"inventory_profitability_for_item",
|
||||
"inventory_purchase_to_sale_chain",
|
||||
"inventory_aging_by_purchase_date",
|
||||
|
|
@ -68,6 +69,7 @@ function defaultCapabilityId(intent) {
|
|||
intent === "inventory_purchase_documents_for_item" ||
|
||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||
intent === "inventory_sale_trace_for_item" ||
|
||||
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
intent === "inventory_profitability_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date") {
|
||||
|
|
@ -151,12 +153,15 @@ function resolveCapabilityEnabled(intent) {
|
|||
if (intent === "inventory_purchase_provenance_for_item" ||
|
||||
intent === "inventory_purchase_documents_for_item" ||
|
||||
intent === "inventory_sale_trace_for_item" ||
|
||||
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
intent === "inventory_profitability_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain") {
|
||||
if (intent === "inventory_profitability_for_item") {
|
||||
if (intent === "inventory_profitability_for_item" || intent === "inventory_margin_ranking_for_nomenclature") {
|
||||
return {
|
||||
enabled: true,
|
||||
reason: "inventory_profitability_route_enabled"
|
||||
reason: intent === "inventory_margin_ranking_for_nomenclature"
|
||||
? "inventory_margin_ranking_route_enabled"
|
||||
: "inventory_profitability_route_enabled"
|
||||
};
|
||||
}
|
||||
if (intent === "inventory_purchase_to_sale_chain") {
|
||||
|
|
@ -249,6 +254,7 @@ function resolveShadowRouteIntent(intent, requestedResultMode) {
|
|||
intent === "inventory_purchase_documents_for_item" ||
|
||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||
intent === "inventory_sale_trace_for_item" ||
|
||||
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
intent === "inventory_profitability_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date") {
|
||||
|
|
|
|||
|
|
@ -101,6 +101,7 @@ function isConfirmedBalanceIntent(intent) {
|
|||
intent === "inventory_purchase_provenance_for_item" ||
|
||||
intent === "inventory_purchase_documents_for_item" ||
|
||||
intent === "inventory_sale_trace_for_item" ||
|
||||
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
intent === "inventory_profitability_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "open_contracts_confirmed_as_of_date" ||
|
||||
|
|
|
|||
|
|
@ -971,6 +971,7 @@ function isInventoryTraceIntent(intent) {
|
|||
intent === "inventory_purchase_documents_for_item" ||
|
||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||
intent === "inventory_sale_trace_for_item" ||
|
||||
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
intent === "inventory_profitability_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date");
|
||||
|
|
@ -989,6 +990,7 @@ function usesRecipeDefaultLimit(intent) {
|
|||
intent === "inventory_purchase_documents_for_item" ||
|
||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||
intent === "inventory_sale_trace_for_item" ||
|
||||
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
intent === "inventory_profitability_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date");
|
||||
|
|
@ -1407,6 +1409,9 @@ function requiredFiltersByIntent(intent) {
|
|||
intent === "inventory_purchase_to_sale_chain") {
|
||||
return ["item"];
|
||||
}
|
||||
if (intent === "inventory_margin_ranking_for_nomenclature") {
|
||||
return ["period_from", "period_to"];
|
||||
}
|
||||
if (intent === "payables_confirmed_as_of_date") {
|
||||
return ["as_of_date"];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1664,6 +1664,17 @@ function hasBidirectionalValueFlowComparisonSignal(text) {
|
|||
const hasNetAmountCue = /(?:сколько|сумм|итог|нетто|сальдо|минус|net|total|sum)/iu.test(normalized);
|
||||
return hasIncomingCue && hasOutgoingCue && hasComparisonCue && (hasValueFlowCue || hasNetAmountCue);
|
||||
}
|
||||
function hasNomenclatureMarginRankingSignal(text) {
|
||||
const normalized = String(text ?? "").trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
const hasNomenclatureCue = /(?:номенклатур|товар|позици|ассортимент|sku|item|product|goods)/iu.test(normalized);
|
||||
const hasRealizationCue = /(?:реализован|реализац|продан|продаж|отгруж|41(?:[.,]0?1)?|90(?:[.,]\d{1,2})?|sales?|sold)/iu.test(normalized);
|
||||
const hasMarginCue = /(?:прибыл|марж|рентаб|наценк|себестоим|выручк|profit|margin|profitability|gross\s+spread|cogs)/iu.test(normalized);
|
||||
const hasRankingCue = /(?:высок|низк|топ|сам(?:ая|ый|ое|ые)|больш|меньш|ранж|рейтинг|high|low|top|rank|best|worst)/iu.test(normalized);
|
||||
return hasNomenclatureCue && hasRealizationCue && hasMarginCue && hasRankingCue;
|
||||
}
|
||||
function hasVatPeriodInspectionBridgeSignal(text) {
|
||||
const normalized = String(text ?? "").trim().toLowerCase();
|
||||
if (!/(?:ндс|vat)/iu.test(normalized)) {
|
||||
|
|
@ -1736,6 +1747,9 @@ function resolveUnicodeAddressIntentBridge(text) {
|
|||
if (hasSelectedObjectProfitabilityCue) {
|
||||
return unicodeBridgeResolution("inventory_profitability_for_item", "high", "unicode_selected_object_profitability_bridge_signal_detected");
|
||||
}
|
||||
if (hasNomenclatureMarginRankingSignal(normalized)) {
|
||||
return unicodeBridgeResolution("inventory_margin_ranking_for_nomenclature", "high", "unicode_nomenclature_margin_ranking_bridge_signal_detected");
|
||||
}
|
||||
const hasOpenItemsAccountCue = /(?:хвост|долг|незакрыт|вис)/iu.test(normalized) &&
|
||||
/(?:сч(?:е|ё)т(?:а|у|ом|е|ов)?\s*(?:№|#)?\s*(?:60|62|76)(?:[.,]\d{1,2})?|\b(?:60|62|76)(?:[.,]\d{1,2})?\b\s*сч(?:е|ё)т)/iu.test(normalized);
|
||||
if (hasOpenItemsAccountCue) {
|
||||
|
|
|
|||
|
|
@ -1647,6 +1647,7 @@ function isOrganizationScopedInventoryIntent(intent) {
|
|||
intent === "inventory_purchase_documents_for_item" ||
|
||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||
intent === "inventory_sale_trace_for_item" ||
|
||||
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
intent === "inventory_profitability_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date");
|
||||
|
|
@ -1680,6 +1681,7 @@ function shouldDeferInventoryOrganizationClarification(intent, filters, semantic
|
|||
return (intent === "inventory_purchase_provenance_for_item" ||
|
||||
intent === "inventory_purchase_documents_for_item" ||
|
||||
intent === "inventory_sale_trace_for_item" ||
|
||||
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
intent === "inventory_profitability_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date");
|
||||
|
|
@ -1981,6 +1983,7 @@ function canAutoBroadenPeriodWindow(intent, filters) {
|
|||
intent === "inventory_purchase_documents_for_item" ||
|
||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||
intent === "inventory_sale_trace_for_item" ||
|
||||
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
intent === "inventory_profitability_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date");
|
||||
|
|
@ -1990,6 +1993,7 @@ function shouldBoostAutoBroadenedLimit(intent) {
|
|||
intent === "inventory_purchase_documents_for_item" ||
|
||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||
intent === "inventory_sale_trace_for_item" ||
|
||||
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
intent === "inventory_profitability_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date");
|
||||
|
|
@ -2520,6 +2524,9 @@ async function tryComposeLlmLimitedReply(input) {
|
|||
if (process.env.VITEST === "true" || process.env.NODE_ENV === "test") {
|
||||
return null;
|
||||
}
|
||||
if (input.intent === "inventory_margin_ranking_for_nomenclature" && input.category === "missing_anchor") {
|
||||
return null;
|
||||
}
|
||||
if (!shouldUseLlmLimitedReply(input.category)) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -2580,6 +2587,13 @@ function composeLimitedReply(input) {
|
|||
.map((item) => normalizeMissingAnchorLabel(String(item ?? "").trim()))
|
||||
.filter((item) => item.length > 0)));
|
||||
const missingAnchorPhrase = missingAnchorLabels.length > 0 ? missingAnchorLabels.join(", ") : "контрагент, договор, счет или период";
|
||||
if (input.intent === "inventory_margin_ranking_for_nomenclature" && input.category === "missing_anchor") {
|
||||
return [
|
||||
"Для рейтинга прибыльности номенклатуры нужен период.",
|
||||
"Могу посчитать по номенклатуре: выручку без НДС, себестоимость реализации, валовую прибыль и маржинальность.",
|
||||
"Уточните период: месяц, квартал, год или весь доступный период."
|
||||
].join("\n\n");
|
||||
}
|
||||
const heading = input.category === "empty_match"
|
||||
? pickDeterministicVariant(headingSeed, [
|
||||
"По текущим условиям в доступном срезе данных совпадений не нашлось.",
|
||||
|
|
|
|||
|
|
@ -966,6 +966,17 @@ const BASE_RECIPES = [
|
|||
account_scope_mode: "strict",
|
||||
query_template: "inventory_trading_margin_proxy_profile"
|
||||
},
|
||||
{
|
||||
recipe_id: "address_inventory_margin_ranking_for_nomenclature_v1",
|
||||
intent: "inventory_margin_ranking_for_nomenclature",
|
||||
purpose: "Rank realized nomenclature by bounded gross margin proxy for an explicit period using 41.01 purchase and sale document rows",
|
||||
required_filters: ["period_from", "period_to"],
|
||||
optional_filters: ["organization", "warehouse", "limit", "sort"],
|
||||
default_limit: 800,
|
||||
account_scope: ["41.01"],
|
||||
account_scope_mode: "strict",
|
||||
query_template: "inventory_margin_ranking_profile"
|
||||
},
|
||||
{
|
||||
recipe_id: "address_inventory_purchase_to_sale_chain_v1",
|
||||
intent: "inventory_purchase_to_sale_chain",
|
||||
|
|
@ -1714,6 +1725,7 @@ function maxLimitForIntent(intent) {
|
|||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||
intent === "inventory_sale_trace_for_item" ||
|
||||
intent === "inventory_trading_margin_proxy_for_organization" ||
|
||||
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
intent === "inventory_profitability_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date" ||
|
||||
|
|
@ -1905,33 +1917,17 @@ function buildAddressRecipePlan(recipe, filters) {
|
|||
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
||||
: recipe.query_template === "inventory_trading_margin_proxy_profile"
|
||||
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
||||
: recipe.query_template === "inventory_purchase_to_sale_chain_profile"
|
||||
: recipe.query_template === "inventory_margin_ranking_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)
|
||||
: 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"
|
||||
: 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)
|
||||
|
|
@ -1946,10 +1942,10 @@ function buildAddressRecipePlan(recipe, filters) {
|
|||
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("__OPEN_CONTRACT_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["60", "62", "76"]))
|
||||
.replaceAll("__ORDER_DIRECTION__", resolveOrderDirection(filters.sort));
|
||||
})()
|
||||
: recipe.query_template === "receivables_confirmed_as_of_balance_profile"
|
||||
: 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)
|
||||
|
|
@ -1964,20 +1960,38 @@ function buildAddressRecipePlan(recipe, filters) {
|
|||
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("__OPEN_CONTRACT_ACCOUNTS_MATCH__", buildAccountPrefixPredicate("Остатки.Счет", ["60", "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));
|
||||
: 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));
|
||||
return {
|
||||
recipe,
|
||||
query,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ exports.hasBareInventoryPurchaseDateFollowupCue = hasBareInventoryPurchaseDateFo
|
|||
exports.hasInventorySaleFollowupCue = hasInventorySaleFollowupCue;
|
||||
exports.hasInventoryPurchaseToSaleChainFollowupCue = hasInventoryPurchaseToSaleChainFollowupCue;
|
||||
exports.hasInventoryPurchaseDateVatBridgeCue = hasInventoryPurchaseDateVatBridgeCue;
|
||||
exports.hasInventoryMarginRankingFollowupCue = hasInventoryMarginRankingFollowupCue;
|
||||
exports.hasAddressFollowupContextSignal = hasAddressFollowupContextSignal;
|
||||
exports.runAddressDecomposeStage = runAddressDecomposeStage;
|
||||
const addressQueryClassifier_1 = require("../addressQueryClassifier");
|
||||
|
|
@ -321,6 +322,7 @@ function isInventoryIntent(intent) {
|
|||
intent === "inventory_purchase_documents_for_item" ||
|
||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||
intent === "inventory_sale_trace_for_item" ||
|
||||
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
intent === "inventory_profitability_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date");
|
||||
|
|
@ -332,6 +334,7 @@ function isInventoryDrilldownFrameIntent(intent) {
|
|||
return (intent === "inventory_purchase_provenance_for_item" ||
|
||||
intent === "inventory_purchase_documents_for_item" ||
|
||||
intent === "inventory_sale_trace_for_item" ||
|
||||
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
intent === "inventory_profitability_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date");
|
||||
|
|
@ -340,6 +343,7 @@ function isInventoryLifecycleHistoryIntent(intent) {
|
|||
return (intent === "inventory_purchase_provenance_for_item" ||
|
||||
intent === "inventory_purchase_documents_for_item" ||
|
||||
intent === "inventory_sale_trace_for_item" ||
|
||||
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
intent === "inventory_profitability_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain");
|
||||
}
|
||||
|
|
@ -625,11 +629,29 @@ function hasInventoryPurchaseDateVatBridgeCue(text) {
|
|||
return (/(?:ндс|vat)/iu.test(normalized) &&
|
||||
/(?:на\s+дат[ауеы]\s+покупк|на\s+дат[ауеы]\s+закупк|по\s+дат[еу]\s+покупк|по\s+дат[еу]\s+закупк|дата\s+покупк|дата\s+закупк|purchase\s+date)/iu.test(normalized));
|
||||
}
|
||||
function hasInventoryMarginRankingFollowupCue(text) {
|
||||
const normalized = textWithRepairedVariant(String(text ?? ""))
|
||||
.toLowerCase()
|
||||
.replace(/ё/g, "е");
|
||||
if (!normalized.trim()) {
|
||||
return false;
|
||||
}
|
||||
const wantsFoundRows = /(?:покажи|показать|выведи|дай|раскрой|show|list|покажи|показать|выведи|дай|раскрой)/iu.test(normalized) &&
|
||||
/(?:найденн|строк|реализац|себестоимостн|баз|найденн|строк|реализац|себестоимостн|баз)/iu.test(normalized) &&
|
||||
/(?:себестоимостн|реализац|марж|прибыл|номенклатур|себестоимостн|реализац|марж|прибыл|номенклат)/iu.test(normalized);
|
||||
const account41Not01 = /\b41(?:[.,]\d{1,2})?\b/iu.test(normalized) &&
|
||||
/\b01(?:[.,]\d{1,2})?\b/iu.test(normalized) &&
|
||||
/(?:\bне\b|вместо|а\s+не|not|instead|РЅРµ|вместо|Р°\s+РЅРµ)/iu.test(normalized);
|
||||
return wantsFoundRows || account41Not01;
|
||||
}
|
||||
function hasAddressFollowupContextSignal(text) {
|
||||
const normalized = String(text ?? "").trim();
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
if (hasInventoryMarginRankingFollowupCue(normalized)) {
|
||||
return true;
|
||||
}
|
||||
if (/(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+ней|по\s+нему|по\s+ним|for\s+selected\s+object|selected\s+object)/iu.test(normalized)) {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -867,6 +889,7 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
|||
intent === "inventory_purchase_documents_for_item" ||
|
||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||
intent === "inventory_sale_trace_for_item" ||
|
||||
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
intent === "inventory_profitability_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date" ||
|
||||
|
|
@ -919,6 +942,7 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
|||
if ((intent === "inventory_purchase_provenance_for_item" ||
|
||||
intent === "inventory_purchase_documents_for_item" ||
|
||||
intent === "inventory_sale_trace_for_item" ||
|
||||
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
intent === "inventory_profitability_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date")) {
|
||||
|
|
@ -1122,6 +1146,7 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
|||
intent === "inventory_purchase_documents_for_item" ||
|
||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||
intent === "inventory_sale_trace_for_item" ||
|
||||
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
intent === "inventory_profitability_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date" ||
|
||||
|
|
@ -1134,6 +1159,8 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
|||
const currentContractExplicit = toNonEmptyString(merged.contract);
|
||||
const currentItemExplicit = toNonEmptyString(merged.item);
|
||||
const currentAccountExplicit = toNonEmptyString(merged.account);
|
||||
const currentAccountRefinesMarginDomain = intent === "inventory_margin_ranking_for_nomenclature" &&
|
||||
hasInventoryMarginRankingFollowupCue(userMessage);
|
||||
const shouldSuppressGenericPeriodCarryover = (Boolean(currentCounterpartyExplicit) &&
|
||||
!isLowQualityCounterpartyAnchor(currentCounterpartyExplicit) &&
|
||||
currentCounterpartyExplicit !== previousCounterparty) ||
|
||||
|
|
@ -1141,7 +1168,7 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
|||
!isLowQualityContractAnchor(currentContractExplicit) &&
|
||||
currentContractExplicit !== previousContract) ||
|
||||
(Boolean(currentItemExplicit) && currentItemExplicit !== previousItem) ||
|
||||
(Boolean(currentAccountExplicit) && currentAccountExplicit !== previousAccount);
|
||||
(Boolean(currentAccountExplicit) && currentAccountExplicit !== previousAccount && !currentAccountRefinesMarginDomain);
|
||||
const vatRelativeMonthFollowup = relativeMonthFromFollowupYear &&
|
||||
(intent === "vat_payable_confirmed_as_of_date" ||
|
||||
intent === "vat_payable_forecast" ||
|
||||
|
|
@ -1189,6 +1216,19 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
|||
}
|
||||
reasons.push("period_from_followup_context");
|
||||
}
|
||||
if (intent === "inventory_margin_ranking_for_nomenclature" &&
|
||||
previousHasPeriod &&
|
||||
hasInventoryMarginRankingFollowupCue(userMessage) &&
|
||||
!hasExplicitPeriodInMessage &&
|
||||
!hasExplicitCurrentDateInMessage) {
|
||||
if (previousPeriodFrom && merged.period_from !== previousPeriodFrom) {
|
||||
merged.period_from = previousPeriodFrom;
|
||||
}
|
||||
if (previousPeriodTo && merged.period_to !== previousPeriodTo) {
|
||||
merged.period_to = previousPeriodTo;
|
||||
}
|
||||
reasons.push("period_from_followup_context");
|
||||
}
|
||||
if (!currentHasPeriod &&
|
||||
previousHasPeriod &&
|
||||
hasFollowupSignal &&
|
||||
|
|
@ -1251,6 +1291,7 @@ function resolveMissingRequiredFilters(intent, filters) {
|
|||
account_balance_snapshot: ["account", "as_of_date"],
|
||||
documents_forming_balance: ["account", "as_of_date"],
|
||||
inventory_on_hand_as_of_date: ["as_of_date"],
|
||||
inventory_margin_ranking_for_nomenclature: ["period_from", "period_to"],
|
||||
inventory_profitability_for_item: ["item"],
|
||||
open_contracts_confirmed_as_of_date: ["as_of_date"],
|
||||
payables_confirmed_as_of_date: ["as_of_date"],
|
||||
|
|
@ -1319,6 +1360,22 @@ function deriveIntentWithFollowupContext(detectedIntent, userMessage, followupCo
|
|||
const hasExplicitInventoryItemReference = /(?:товар|номенклатур|позици|склад|остат|sku|item|product|товар|номенклатур|позици|склад|остат)/iu.test(normalizedMessage) || hasSelectedObjectInlineSnapshotMetadata(normalizedMessage);
|
||||
const staleInventoryLineageCanYieldToCounterparty = previousCounterpartyLaneActive && !hasExplicitInventoryItemReference;
|
||||
const inventoryPurchaseDateVatBridge = inventorySelectedObjectFollowup && hasInventoryPurchaseDateVatBridgeCue(normalizedMessage);
|
||||
const marginRankingLineageActive = sourceIntent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
fallbackIntent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
followupContext.root_intent === "inventory_margin_ranking_for_nomenclature";
|
||||
if (marginRankingLineageActive &&
|
||||
hasInventoryMarginRankingFollowupCue(normalizedMessage) &&
|
||||
(detectedIntent.intent === "unknown" ||
|
||||
detectedIntent.intent === "account_balance_snapshot" ||
|
||||
detectedIntent.intent === "documents_forming_balance" ||
|
||||
detectedIntent.intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
detectedIntent.intent === sourceIntent)) {
|
||||
return {
|
||||
intent: "inventory_margin_ranking_for_nomenclature",
|
||||
confidence: "low",
|
||||
reasons: [...detectedIntent.reasons, "intent_adjusted_to_inventory_margin_ranking_followup_context"]
|
||||
};
|
||||
}
|
||||
if (inventoryPurchaseDateVatBridge &&
|
||||
(detectedIntent.intent === "unknown" ||
|
||||
detectedIntent.intent === sourceIntent ||
|
||||
|
|
|
|||
|
|
@ -81,6 +81,48 @@ function inventoryProfitabilityPeriodLabel(options, deps) {
|
|||
const asOfDate = typeof options.asOfDate === "string" && options.asOfDate.trim().length > 0 ? options.asOfDate : null;
|
||||
return asOfDate ? `до ${deps.formatDateRu(asOfDate)}` : "по доступной выборке";
|
||||
}
|
||||
function inventoryRowItemLabel(row, deps) {
|
||||
return deps.summarizeInventoryTraceRows([row]).item;
|
||||
}
|
||||
function buildInventoryMarginRankingEntries(rows, deps) {
|
||||
const byItem = new Map();
|
||||
for (const row of rows) {
|
||||
const item = inventoryRowItemLabel(row, deps);
|
||||
if (!item) {
|
||||
continue;
|
||||
}
|
||||
const key = item.trim().toLocaleLowerCase("ru");
|
||||
const current = byItem.get(key) ?? { item, saleRows: [], purchaseRows: [] };
|
||||
if (deps.isInventorySaleMovement(row)) {
|
||||
current.saleRows.push(row);
|
||||
}
|
||||
if (deps.isInventoryPurchaseMovement(row)) {
|
||||
current.purchaseRows.push(row);
|
||||
}
|
||||
byItem.set(key, current);
|
||||
}
|
||||
return Array.from(byItem.values())
|
||||
.map((entry) => {
|
||||
const revenue = sumInventoryRowAmount(entry.saleRows);
|
||||
const costProxy = sumInventoryRowAmount(entry.purchaseRows);
|
||||
const spread = revenue - costProxy;
|
||||
return {
|
||||
item: entry.item,
|
||||
revenue,
|
||||
costProxy,
|
||||
spread,
|
||||
marginPct: revenue > 0 ? (spread / revenue) * 100 : null,
|
||||
saleQuantity: sumInventoryRowQuantity(entry.saleRows),
|
||||
purchaseQuantity: sumInventoryRowQuantity(entry.purchaseRows),
|
||||
saleDocuments: entry.saleRows.length,
|
||||
purchaseDocuments: entry.purchaseRows.length
|
||||
};
|
||||
})
|
||||
.filter((entry) => entry.revenue > 0 || entry.costProxy > 0);
|
||||
}
|
||||
function formatInventoryMarginRankingLine(entry, index, deps) {
|
||||
return `${index + 1}. ${entry.item} — выручка ${deps.formatMoneyRub(entry.revenue)}, себестоимостная база ${deps.formatMoneyRub(entry.costProxy)}, валовая разница ${deps.formatMoneyRub(entry.spread)}, маржа ${formatInventoryPercent(entry.marginPct, deps.formatNumberWithDots)}.`;
|
||||
}
|
||||
function composeInventoryReply(intent, rows, options, deps) {
|
||||
if (intent === "inventory_on_hand_as_of_date") {
|
||||
const asOfDate = deps.resolvePayablesAsOfDate(options);
|
||||
|
|
@ -401,6 +443,83 @@ function composeInventoryReply(intent, rows, options, deps) {
|
|||
? (0, replyContracts_1.buildFactualListReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)(summary.counterparties.length > 0 ? "strong" : "medium", true))
|
||||
: (0, replyContracts_1.buildFactualSummaryReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)("medium", false));
|
||||
}
|
||||
if (intent === "inventory_margin_ranking_for_nomenclature") {
|
||||
const entries = buildInventoryMarginRankingEntries(rows, deps);
|
||||
const confirmedEntries = entries.filter((entry) => entry.revenue > 0 && entry.costProxy > 0);
|
||||
const highMargin = [...confirmedEntries]
|
||||
.sort((left, right) => right.spread - left.spread || (right.marginPct ?? -Infinity) - (left.marginPct ?? -Infinity))
|
||||
.slice(0, 5);
|
||||
const lowMargin = [...confirmedEntries]
|
||||
.sort((left, right) => left.spread - right.spread || (left.marginPct ?? Infinity) - (right.marginPct ?? Infinity))
|
||||
.slice(0, 5);
|
||||
const salesWithoutCost = entries.filter((entry) => entry.revenue > 0 && entry.costProxy <= 0);
|
||||
const purchasesWithoutSales = entries.filter((entry) => entry.costProxy > 0 && entry.revenue <= 0);
|
||||
const periodLabel = inventoryProfitabilityPeriodLabel(options, deps);
|
||||
const totalRevenue = entries.reduce((sum, entry) => sum + entry.revenue, 0);
|
||||
const totalCostProxy = entries.reduce((sum, entry) => sum + entry.costProxy, 0);
|
||||
const totalSpread = totalRevenue - totalCostProxy;
|
||||
if (confirmedEntries.length === 0) {
|
||||
const lines = [`За период ${periodLabel} рейтинг прибыльности номенклатуры построить нельзя.`];
|
||||
const findings = [];
|
||||
if (salesWithoutCost.length > 0) {
|
||||
const salesCount = deps.formatNumberWithDots(salesWithoutCost.length);
|
||||
const salesItemPhrase = salesWithoutCost.length === 1 ? "1 номенклатурной позиции" : `${salesCount} номенклатурным позициям`;
|
||||
findings.push(`Есть реализация по ${salesItemPhrase}.`);
|
||||
findings.push(salesWithoutCost.length === 1
|
||||
? "Подтвержденной себестоимости реализации по этой позиции не найдено."
|
||||
: "Подтвержденной себестоимости реализации по этим позициям не найдено.");
|
||||
findings.push("Поэтому валовую прибыль и маржинальность честно посчитать нельзя.");
|
||||
}
|
||||
if (purchasesWithoutSales.length > 0) {
|
||||
const purchaseCount = deps.formatNumberWithDots(purchasesWithoutSales.length);
|
||||
const purchaseItemPhrase = purchasesWithoutSales.length === 1 ? "1 позиции" : `${purchaseCount} позициям`;
|
||||
findings.push(purchasesWithoutSales.length === 1
|
||||
? `Есть себестоимостная база по ${purchaseItemPhrase}, но реализации по ней в периоде не найдено.`
|
||||
: `Есть себестоимостная база по ${purchaseItemPhrase}, но реализации по ним в периоде не найдено.`);
|
||||
}
|
||||
if (entries.length === 0) {
|
||||
findings.push("В доступной выборке нет достаточных строк реализации и себестоимости по номенклатуре.");
|
||||
}
|
||||
(0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Что нашлось:", findings);
|
||||
lines.push(`Вывод: за период ${periodLabel} нет достаточной базы для рейтинга «высокая / низкая прибыль» по номенклатуре.`);
|
||||
const nextActions = [];
|
||||
if (salesWithoutCost.length > 0) {
|
||||
nextActions.push("показать найденные реализации за этот период;");
|
||||
}
|
||||
if (purchasesWithoutSales.length > 0) {
|
||||
nextActions.push("показать найденные строки себестоимостной базы за этот период;");
|
||||
}
|
||||
nextActions.push("расширить период до квартала или года;", "попробовать строгий расчет по проводкам 90.01 / 90.02;", "построить управленческий proxy по закупочным документам, если такой способ допустим для вашей проверки.");
|
||||
(0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Что можно сделать дальше:", nextActions);
|
||||
(0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Граница ответа:", [
|
||||
"Прибыльность номенклатуры считаю только когда есть реализация и подтвержденная себестоимость реализации.",
|
||||
"Это не чистая прибыль компании и не замена закрытию месяца."
|
||||
]);
|
||||
return (0, replyContracts_1.buildFactualSummaryReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)(entries.length > 0 ? "medium" : "weak", false));
|
||||
}
|
||||
const directAnswerLine = confirmedEntries.length > 0
|
||||
? `За период ${periodLabel} собран рейтинг реализованной номенклатуры по валовой маржинальности: выручка ${deps.formatMoneyRub(totalRevenue)}, себестоимостная база ${deps.formatMoneyRub(totalCostProxy)}, расчетная валовая разница ${deps.formatMoneyRub(totalSpread)}.`
|
||||
: `За период ${periodLabel} не удалось подтвердить рейтинг прибыльности номенклатуры: нужны одновременно строки реализации и закупочного/себестоимостного следа по товарам.`;
|
||||
const lines = [directAnswerLine];
|
||||
if (highMargin.length > 0) {
|
||||
(0, inventoryReplyPresentation_1.appendInventorySection)(lines, "Высокая валовая маржинальность:", highMargin.map((entry, index) => formatInventoryMarginRankingLine(entry, index, deps)));
|
||||
}
|
||||
if (lowMargin.length > 0) {
|
||||
(0, inventoryReplyPresentation_1.appendInventorySection)(lines, "Низкая или отрицательная валовая маржинальность:", lowMargin.map((entry, index) => formatInventoryMarginRankingLine(entry, index, deps)));
|
||||
}
|
||||
const boundaryLines = [
|
||||
"Это управленческий расчет валовой маржинальности по реализации и доступной себестоимостной базе, не чистая прибыль компании.",
|
||||
"Для строгого бухгалтерского расчета нужны проводки 90.01 / 90.02 и закрытие себестоимости; этот ответ не подменяет закрытие месяца."
|
||||
];
|
||||
if (salesWithoutCost.length > 0) {
|
||||
boundaryLines.push(`По ${deps.formatNumberWithDots(salesWithoutCost.length)} позициям есть продажи, но нет подтвержденной себестоимости реализации — их нельзя честно ранжировать по прибыли.`);
|
||||
}
|
||||
if (purchasesWithoutSales.length > 0) {
|
||||
boundaryLines.push(`По ${deps.formatNumberWithDots(purchasesWithoutSales.length)} позициям есть себестоимостная база без реализации в этом периоде.`);
|
||||
}
|
||||
(0, inventoryReplyPresentation_1.appendInventoryBulletSection)(lines, "Граница ответа:", boundaryLines);
|
||||
return (0, replyContracts_1.buildFactualSummaryReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)(confirmedEntries.length > 0 ? "strong" : entries.length > 0 ? "medium" : "weak", confirmedEntries.length > 0));
|
||||
}
|
||||
if (intent === "inventory_profitability_for_item") {
|
||||
const purchaseRows = rows.filter((row) => deps.isInventoryPurchaseMovement(row));
|
||||
const saleRows = rows.filter((row) => deps.isInventorySaleMovement(row));
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ function isInventorySelectedObjectOrRootIntent(intent) {
|
|||
intent === "inventory_purchase_provenance_for_item" ||
|
||||
intent === "inventory_purchase_documents_for_item" ||
|
||||
intent === "inventory_sale_trace_for_item" ||
|
||||
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
intent === "inventory_profitability_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date");
|
||||
|
|
@ -74,6 +75,15 @@ function isGenericCanonicalDriftIntent(intent) {
|
|||
intent === "bank_operations_by_contract" ||
|
||||
intent === "documents_forming_balance");
|
||||
}
|
||||
function hasInventoryMarginRankingAccountCorrectionCue(text) {
|
||||
const value = String(text ?? "").toLowerCase();
|
||||
if (!value.trim()) {
|
||||
return false;
|
||||
}
|
||||
return (/\b41(?:[.,]\d{1,2})?\b/iu.test(value) &&
|
||||
/\b01(?:[.,]\d{1,2})?\b/iu.test(value) &&
|
||||
/(?:\u0430\s+\u043d\u0435|\u043d\u0435|\u0432\u043c\u0435\u0441\u0442\u043e|not|instead)/iu.test(value));
|
||||
}
|
||||
function hasSameDateFollowupSignal(text) {
|
||||
return /(?:эту\s+же\s+дат(?:у|е|ой)|ту\s+же\s+дат(?:у|е|ой)|same\s+date)/iu.test(String(text ?? ""));
|
||||
}
|
||||
|
|
@ -117,6 +127,10 @@ function shouldPreferRawFollowupMessage(userMessage, addressInputMessage, carryo
|
|||
const hasInventoryItemCarryover = previousAnchorType === "item" && isInventorySelectedObjectOrRootIntent(previousIntent);
|
||||
const hasInventoryFrameCarryover = isInventorySelectedObjectOrRootIntent(previousIntent) ||
|
||||
isInventorySelectedObjectOrRootIntent(rootIntent);
|
||||
const hasInventoryMarginRankingCarryover = previousIntent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
rootIntent === "inventory_margin_ranking_for_nomenclature";
|
||||
const hasInventoryMarginRankingAccountCorrection = hasInventoryMarginRankingCarryover &&
|
||||
[rawMessage, canonicalMessage].some((message) => hasInventoryMarginRankingAccountCorrectionCue(message));
|
||||
const hasDocumentCarryover = previousIntent === "list_documents_by_counterparty" || previousIntent === "list_documents_by_contract";
|
||||
if (mode === "unsupported" && intent === "unknown") {
|
||||
return true;
|
||||
|
|
@ -132,6 +146,10 @@ function shouldPreferRawFollowupMessage(userMessage, addressInputMessage, carryo
|
|||
(intent === "account_balance_snapshot" || intent === "documents_forming_balance" || intent === "unknown")) {
|
||||
return true;
|
||||
}
|
||||
if (hasInventoryMarginRankingAccountCorrection &&
|
||||
(intent === "account_balance_snapshot" || intent === "documents_forming_balance" || intent === "unknown")) {
|
||||
return true;
|
||||
}
|
||||
return ((hasSelectedObjectInventorySignal(rawMessage) || hasInventoryItemCarryover) &&
|
||||
(hasSelectedObjectInventoryActionCue(rawMessage) || hasShortInventoryPurchaseFollowupCue(rawMessage)) &&
|
||||
(isGenericCanonicalDriftIntent(intent) || intent === "unknown"));
|
||||
|
|
|
|||
|
|
@ -8,6 +8,13 @@ function formatMissingAnchors(anchors) {
|
|||
return anchors.join(", ");
|
||||
}
|
||||
function buildClarificationReply(binding) {
|
||||
if (binding.capability_contract_id === "inventory_inventory_margin_ranking_for_nomenclature") {
|
||||
return [
|
||||
"Для рейтинга прибыльности номенклатуры нужен период.",
|
||||
"Могу посчитать по номенклатуре: выручку без НДС, себестоимость реализации, валовую прибыль и маржинальность.",
|
||||
"Уточните период: месяц, квартал, год или весь доступный период."
|
||||
].join("\n\n");
|
||||
}
|
||||
return [
|
||||
"Нужно уточнение, чтобы не подставить неподтвержденный объект в расчет.",
|
||||
`Не хватает: ${formatMissingAnchors(binding.missing_anchors)}.`,
|
||||
|
|
|
|||
|
|
@ -110,6 +110,13 @@ function anchorSatisfied(requiredAnchor, providedAnchors, debug) {
|
|||
if (providedAnchors.includes(requiredAnchor)) {
|
||||
return true;
|
||||
}
|
||||
if (requiredAnchor === "period") {
|
||||
return ((hasValue(filters?.period_from) && hasValue(filters?.period_to)) ||
|
||||
hasValue(filters?.as_of_date) ||
|
||||
providedAnchors.includes("period_from") ||
|
||||
providedAnchors.includes("period_to") ||
|
||||
providedAnchors.includes("as_of_date"));
|
||||
}
|
||||
if (requiredAnchor === "item") {
|
||||
return (providedAnchors.includes("selected_object") ||
|
||||
providedAnchors.includes("anchor_item") ||
|
||||
|
|
|
|||
|
|
@ -141,6 +141,7 @@ function isDetectedIntentAlignedWithTurnMeaning(detectedIntent, turnMeaning) {
|
|||
if (normalizedIntent === "inventory_purchase_provenance_for_item" ||
|
||||
normalizedIntent === "inventory_purchase_documents_for_item" ||
|
||||
normalizedIntent === "inventory_sale_trace_for_item" ||
|
||||
normalizedIntent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
normalizedIntent === "inventory_profitability_for_item" ||
|
||||
normalizedIntent === "inventory_purchase_to_sale_chain") {
|
||||
return askedDomain === "inventory";
|
||||
|
|
@ -183,7 +184,7 @@ function isExplicitMetadataDiscoveryTurn(entryPoint) {
|
|||
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 ?? ""));
|
||||
return /^(?:inventory_purchase_provenance_for_item|inventory_purchase_documents_for_item|inventory_sale_trace_for_item|inventory_margin_ranking_for_nomenclature|inventory_profitability_for_item|inventory_purchase_to_sale_chain|inventory_aging_by_purchase_date|inventory_on_hand_as_of_date)$/u.test(String(intent ?? ""));
|
||||
}
|
||||
function hasMetadataDiscoveryPriority(input, entryPoint) {
|
||||
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ const ADDRESS_INTENTS_KEEP_ADDRESS_LANE = new Set([
|
|||
"inventory_purchase_documents_for_item",
|
||||
"inventory_supplier_stock_overlap_as_of_date",
|
||||
"inventory_sale_trace_for_item",
|
||||
"inventory_margin_ranking_for_nomenclature",
|
||||
"inventory_profitability_for_item",
|
||||
"inventory_purchase_to_sale_chain",
|
||||
"inventory_aging_by_purchase_date",
|
||||
|
|
@ -40,6 +41,7 @@ const ADDRESS_INTENTS_ALLOW_STRICT_DEEP_INVESTIGATION_BYPASS = new Set([
|
|||
"inventory_purchase_provenance_for_item",
|
||||
"inventory_purchase_documents_for_item",
|
||||
"inventory_sale_trace_for_item",
|
||||
"inventory_margin_ranking_for_nomenclature",
|
||||
"inventory_profitability_for_item",
|
||||
"inventory_purchase_to_sale_chain"
|
||||
]);
|
||||
|
|
@ -204,7 +206,7 @@ function createAssistantRoutePolicy(deps) {
|
|||
: null;
|
||||
const semanticCanonicalRecommended = semanticExtractionContract?.apply_canonical_recommended !== false;
|
||||
const llmSupportedDeepAddressIntentSignal = llmContractMode === "deep_analysis" &&
|
||||
/^(?:inventory_purchase_provenance_for_item|inventory_purchase_documents_for_item|inventory_sale_trace_for_item|inventory_profitability_for_item|inventory_purchase_to_sale_chain)$/u.test(llmContractIntent ?? "") &&
|
||||
/^(?:inventory_purchase_provenance_for_item|inventory_purchase_documents_for_item|inventory_sale_trace_for_item|inventory_margin_ranking_for_nomenclature|inventory_profitability_for_item|inventory_purchase_to_sale_chain)$/u.test(llmContractIntent ?? "") &&
|
||||
semanticCanonicalRecommended;
|
||||
const llmCanonicalEntitySignal = /(?:заказчик|поставщик|контрагент|компан|customer|supplier|counterparty|company|vendor|client)/iu.test(compactWhitespace(repairedInputMessage.toLowerCase()));
|
||||
const llmCanonicalAppliedSignal = Boolean(llmPreDecomposeMeta?.applied) && llmContractMode !== "deep_analysis";
|
||||
|
|
|
|||
|
|
@ -310,6 +310,18 @@ exports.INVENTORY_CAPABILITY_CONTRACTS = [
|
|||
answerObjectShape: "inventory_profitability_bundle",
|
||||
bundleReusePolicy: "sale_trace_bundle_preferred"
|
||||
}),
|
||||
inventoryExactCapability({
|
||||
capability_id: "inventory_inventory_margin_ranking_for_nomenclature",
|
||||
intent_ids: ["inventory_margin_ranking_for_nomenclature"],
|
||||
entry_modes: ["root_entry", "root_followup", "clarification_resume"],
|
||||
transitions: ["T1", "T2", "T7"],
|
||||
requiresFocusObject: false,
|
||||
requiredAnchors: ["period"],
|
||||
resultShape: "nomenclature_margin_ranking",
|
||||
answerObjectShape: "inventory_margin_ranking",
|
||||
bundleReusePolicy: "none",
|
||||
scenarioFamilies: ["canonical", "colloquial", "account_41_correction"]
|
||||
}),
|
||||
inventoryExactCapability({
|
||||
capability_id: "inventory_inventory_purchase_to_sale_chain",
|
||||
intent_ids: ["inventory_purchase_to_sale_chain"],
|
||||
|
|
|
|||
|
|
@ -7,6 +7,26 @@ function createAssistantTransitionPolicy(deps) {
|
|||
function normalizeFollowupText(value) {
|
||||
return deps.compactWhitespace(deps.repairAddressMojibake(String(value ?? "")).toLowerCase()).replace(/ё/g, "е");
|
||||
}
|
||||
function hasInventoryMarginRankingFollowupSignal(userMessage, alternateMessage = null, sourceIntentHint = null) {
|
||||
if (sourceIntentHint !== "inventory_margin_ranking_for_nomenclature") {
|
||||
return false;
|
||||
}
|
||||
return [userMessage, alternateMessage]
|
||||
.filter((value) => deps.toNonEmptyString(value))
|
||||
.map((value) => deps.compactWhitespace(deps.repairAddressMojibake(String(value ?? "")).toLowerCase()).replace(/ё/g, "е"))
|
||||
.some((normalized) => {
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
const wantsFoundRows = /(?:покажи|показать|выведи|дай|раскрой|show|list|покажи|показать|выведи|дай|раскрой)/iu.test(normalized) &&
|
||||
/(?:найденн|строк|реализац|себестоимостн|баз|найденн|строк|реализац|себестоимостн|баз)/iu.test(normalized) &&
|
||||
/(?:себестоимостн|реализац|марж|прибыл|номенклатур|себестоимостн|реализац|марж|прибыл|номенклат)/iu.test(normalized);
|
||||
const account41Not01 = /\b41(?:[.,]\d{1,2})?\b/iu.test(normalized) &&
|
||||
/\b01(?:[.,]\d{1,2})?\b/iu.test(normalized) &&
|
||||
/(?:\bне\b|вместо|а\s+не|not|instead|РЅРµ|вместо|Р°\s+РЅРµ)/iu.test(normalized);
|
||||
return wantsFoundRows || account41Not01;
|
||||
});
|
||||
}
|
||||
function hasSamePeriodReferenceCue(...values) {
|
||||
return values
|
||||
.map((value) => normalizeFollowupText(value))
|
||||
|
|
@ -545,6 +565,7 @@ function createAssistantTransitionPolicy(deps) {
|
|||
sourceIntentHint === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||
deps.isInventorySelectedObjectIntent(sourceIntentHint)));
|
||||
const inventoryPurchaseDateVatBridge = hasInventoryPurchaseDateVatBridgeSignal(userMessage, alternateMessage, sourceIntentHint, hasNavigationInventoryItemFocusHint);
|
||||
const inventoryMarginRankingFollowup = hasInventoryMarginRankingFollowupSignal(userMessage, alternateMessage, sourceIntentHint);
|
||||
let inventoryShortFollowupPrimary = (deps.isInventorySelectedObjectIntent(sourceIntentHint) || hasNavigationInventoryItemFocusHint) &&
|
||||
deps.hasShortInventoryObjectFollowupSignal(userMessage);
|
||||
let inventoryShortFollowupAlternate = (deps.isInventorySelectedObjectIntent(sourceIntentHint) || hasNavigationInventoryItemFocusHint) &&
|
||||
|
|
@ -573,6 +594,7 @@ function createAssistantTransitionPolicy(deps) {
|
|||
businessOverviewBoundaryFollowupPrimary ||
|
||||
inventoryShortFollowupPrimary ||
|
||||
inventoryPurchaseDateVatBridge ||
|
||||
inventoryMarginRankingFollowup ||
|
||||
explicitSummaryBundleReuseSignal ||
|
||||
mcpDiscoveryOrganizationClarificationContinuation;
|
||||
let hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage)
|
||||
|
|
@ -582,6 +604,7 @@ function createAssistantTransitionPolicy(deps) {
|
|||
businessOverviewBoundaryFollowupAlternate ||
|
||||
inventoryShortFollowupAlternate ||
|
||||
inventoryPurchaseDateVatBridge ||
|
||||
inventoryMarginRankingFollowup ||
|
||||
explicitSummaryBundleReuseSignal ||
|
||||
mcpDiscoveryOrganizationClarificationContinuation
|
||||
: false;
|
||||
|
|
@ -622,6 +645,7 @@ function createAssistantTransitionPolicy(deps) {
|
|||
shortValueFlowRetargetAlternate ||
|
||||
businessOverviewBoundaryFollowupPrimary ||
|
||||
businessOverviewBoundaryFollowupAlternate ||
|
||||
inventoryMarginRankingFollowup ||
|
||||
deps.hasFollowupMarker(userMessage) ||
|
||||
deps.hasReferentialPointer(userMessage) ||
|
||||
(deps.toNonEmptyString(alternateMessage)
|
||||
|
|
@ -645,6 +669,7 @@ function createAssistantTransitionPolicy(deps) {
|
|||
shortValueFlowRetargetAlternate ||
|
||||
businessOverviewBoundaryFollowupPrimary ||
|
||||
businessOverviewBoundaryFollowupAlternate ||
|
||||
inventoryMarginRankingFollowup ||
|
||||
deps.hasFollowupMarker(userMessage) ||
|
||||
deps.hasReferentialPointer(userMessage) ||
|
||||
(deps.toNonEmptyString(alternateMessage)
|
||||
|
|
@ -757,6 +782,7 @@ function createAssistantTransitionPolicy(deps) {
|
|||
!inventoryShortFollowupAlternate &&
|
||||
!businessOverviewBoundaryFollowupPrimary &&
|
||||
!businessOverviewBoundaryFollowupAlternate &&
|
||||
!inventoryMarginRankingFollowup &&
|
||||
!foreignAccountingPivotOverInventory &&
|
||||
!deps.hasFollowupMarker(userMessage) &&
|
||||
!deps.hasReferentialPointer(userMessage) &&
|
||||
|
|
@ -816,6 +842,7 @@ function createAssistantTransitionPolicy(deps) {
|
|||
businessOverviewBoundaryFollowupPrimary ||
|
||||
inventoryShortFollowupPrimary ||
|
||||
inventoryPurchaseDateVatBridge ||
|
||||
inventoryMarginRankingFollowup ||
|
||||
explicitSummaryBundleReuseSignal ||
|
||||
hasInventoryRootTemporalFollowupPrimary ||
|
||||
mcpDiscoveryOrganizationClarificationContinuation;
|
||||
|
|
@ -827,6 +854,7 @@ function createAssistantTransitionPolicy(deps) {
|
|||
businessOverviewBoundaryFollowupAlternate ||
|
||||
inventoryShortFollowupAlternate ||
|
||||
inventoryPurchaseDateVatBridge ||
|
||||
inventoryMarginRankingFollowup ||
|
||||
explicitSummaryBundleReuseSignal ||
|
||||
hasInventoryRootTemporalFollowupAlternate ||
|
||||
mcpDiscoveryOrganizationClarificationContinuation
|
||||
|
|
@ -849,6 +877,7 @@ function createAssistantTransitionPolicy(deps) {
|
|||
shortValueFlowRetargetAlternate ||
|
||||
businessOverviewBoundaryFollowupPrimary ||
|
||||
businessOverviewBoundaryFollowupAlternate ||
|
||||
inventoryMarginRankingFollowup ||
|
||||
deps.hasFollowupMarker(userMessage) ||
|
||||
deps.hasReferentialPointer(userMessage) ||
|
||||
(deps.toNonEmptyString(alternateMessage)
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ const SUPPORTED_ADDRESS_INTENTS = new Set([
|
|||
"inventory_purchase_documents_for_item",
|
||||
"inventory_supplier_stock_overlap_as_of_date",
|
||||
"inventory_sale_trace_for_item",
|
||||
"inventory_margin_ranking_for_nomenclature",
|
||||
"inventory_profitability_for_item",
|
||||
"inventory_purchase_to_sale_chain",
|
||||
"inventory_aging_by_purchase_date",
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ const COMPUTE_EXACT_INTENTS = new Set<AddressIntent>([
|
|||
"inventory_purchase_documents_for_item",
|
||||
"inventory_supplier_stock_overlap_as_of_date",
|
||||
"inventory_sale_trace_for_item",
|
||||
"inventory_margin_ranking_for_nomenclature",
|
||||
"inventory_profitability_for_item",
|
||||
"inventory_purchase_to_sale_chain",
|
||||
"inventory_aging_by_purchase_date",
|
||||
|
|
@ -92,6 +93,7 @@ function defaultCapabilityId(intent: AddressIntent): string {
|
|||
intent === "inventory_purchase_documents_for_item" ||
|
||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||
intent === "inventory_sale_trace_for_item" ||
|
||||
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
intent === "inventory_profitability_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date"
|
||||
|
|
@ -178,13 +180,17 @@ function resolveCapabilityEnabled(intent: AddressIntent): { enabled: boolean; re
|
|||
intent === "inventory_purchase_provenance_for_item" ||
|
||||
intent === "inventory_purchase_documents_for_item" ||
|
||||
intent === "inventory_sale_trace_for_item" ||
|
||||
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
intent === "inventory_profitability_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain"
|
||||
) {
|
||||
if (intent === "inventory_profitability_for_item") {
|
||||
if (intent === "inventory_profitability_for_item" || intent === "inventory_margin_ranking_for_nomenclature") {
|
||||
return {
|
||||
enabled: true,
|
||||
reason: "inventory_profitability_route_enabled"
|
||||
reason:
|
||||
intent === "inventory_margin_ranking_for_nomenclature"
|
||||
? "inventory_margin_ranking_route_enabled"
|
||||
: "inventory_profitability_route_enabled"
|
||||
};
|
||||
}
|
||||
if (intent === "inventory_purchase_to_sale_chain") {
|
||||
|
|
@ -284,6 +290,7 @@ export function resolveShadowRouteIntent(
|
|||
intent === "inventory_purchase_documents_for_item" ||
|
||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||
intent === "inventory_sale_trace_for_item" ||
|
||||
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
intent === "inventory_profitability_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date"
|
||||
|
|
|
|||
|
|
@ -149,6 +149,7 @@ export function isConfirmedBalanceIntent(intent: AddressIntent): boolean {
|
|||
intent === "inventory_purchase_provenance_for_item" ||
|
||||
intent === "inventory_purchase_documents_for_item" ||
|
||||
intent === "inventory_sale_trace_for_item" ||
|
||||
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
intent === "inventory_profitability_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "open_contracts_confirmed_as_of_date" ||
|
||||
|
|
|
|||
|
|
@ -1111,6 +1111,7 @@ function isInventoryTraceIntent(intent: AddressIntent): boolean {
|
|||
intent === "inventory_purchase_documents_for_item" ||
|
||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||
intent === "inventory_sale_trace_for_item" ||
|
||||
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
intent === "inventory_profitability_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date"
|
||||
|
|
@ -1135,6 +1136,7 @@ function usesRecipeDefaultLimit(intent: AddressIntent): boolean {
|
|||
intent === "inventory_purchase_documents_for_item" ||
|
||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||
intent === "inventory_sale_trace_for_item" ||
|
||||
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
intent === "inventory_profitability_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date"
|
||||
|
|
@ -1628,6 +1630,9 @@ function requiredFiltersByIntent(intent: AddressIntent): Array<keyof AddressFilt
|
|||
) {
|
||||
return ["item"];
|
||||
}
|
||||
if (intent === "inventory_margin_ranking_for_nomenclature") {
|
||||
return ["period_from", "period_to"];
|
||||
}
|
||||
if (intent === "payables_confirmed_as_of_date") {
|
||||
return ["as_of_date"];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2152,6 +2152,28 @@ function hasBidirectionalValueFlowComparisonSignal(text: string): boolean {
|
|||
return hasIncomingCue && hasOutgoingCue && hasComparisonCue && (hasValueFlowCue || hasNetAmountCue);
|
||||
}
|
||||
|
||||
function hasNomenclatureMarginRankingSignal(text: string): boolean {
|
||||
const normalized = String(text ?? "").trim().toLowerCase();
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
const hasNomenclatureCue =
|
||||
/(?:номенклатур|товар|позици|ассортимент|sku|item|product|goods)/iu.test(normalized);
|
||||
const hasRealizationCue =
|
||||
/(?:реализован|реализац|продан|продаж|отгруж|41(?:[.,]0?1)?|90(?:[.,]\d{1,2})?|sales?|sold)/iu.test(
|
||||
normalized
|
||||
);
|
||||
const hasMarginCue =
|
||||
/(?:прибыл|марж|рентаб|наценк|себестоим|выручк|profit|margin|profitability|gross\s+spread|cogs)/iu.test(
|
||||
normalized
|
||||
);
|
||||
const hasRankingCue =
|
||||
/(?:высок|низк|топ|сам(?:ая|ый|ое|ые)|больш|меньш|ранж|рейтинг|high|low|top|rank|best|worst)/iu.test(
|
||||
normalized
|
||||
);
|
||||
return hasNomenclatureCue && hasRealizationCue && hasMarginCue && hasRankingCue;
|
||||
}
|
||||
|
||||
function hasVatPeriodInspectionBridgeSignal(text: string): boolean {
|
||||
const normalized = String(text ?? "").trim().toLowerCase();
|
||||
if (!/(?:ндс|vat)/iu.test(normalized)) {
|
||||
|
|
@ -2263,6 +2285,14 @@ function resolveUnicodeAddressIntentBridge(text: string): AddressIntentResolutio
|
|||
);
|
||||
}
|
||||
|
||||
if (hasNomenclatureMarginRankingSignal(normalized)) {
|
||||
return unicodeBridgeResolution(
|
||||
"inventory_margin_ranking_for_nomenclature",
|
||||
"high",
|
||||
"unicode_nomenclature_margin_ranking_bridge_signal_detected"
|
||||
);
|
||||
}
|
||||
|
||||
const hasOpenItemsAccountCue =
|
||||
/(?:хвост|долг|незакрыт|вис)/iu.test(normalized) &&
|
||||
/(?:сч(?:е|ё)т(?:а|у|ом|е|ов)?\s*(?:№|#)?\s*(?:60|62|76)(?:[.,]\d{1,2})?|\b(?:60|62|76)(?:[.,]\d{1,2})?\b\s*сч(?:е|ё)т)/iu.test(
|
||||
|
|
|
|||
|
|
@ -2034,6 +2034,7 @@ function isOrganizationScopedInventoryIntent(intent: AddressIntent): boolean {
|
|||
intent === "inventory_purchase_documents_for_item" ||
|
||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||
intent === "inventory_sale_trace_for_item" ||
|
||||
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
intent === "inventory_profitability_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date"
|
||||
|
|
@ -2085,6 +2086,7 @@ function shouldDeferInventoryOrganizationClarification(
|
|||
intent === "inventory_purchase_provenance_for_item" ||
|
||||
intent === "inventory_purchase_documents_for_item" ||
|
||||
intent === "inventory_sale_trace_for_item" ||
|
||||
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
intent === "inventory_profitability_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date"
|
||||
|
|
@ -2455,6 +2457,7 @@ function canAutoBroadenPeriodWindow(intent: AddressIntent, filters: AddressFilte
|
|||
intent === "inventory_purchase_documents_for_item" ||
|
||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||
intent === "inventory_sale_trace_for_item" ||
|
||||
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
intent === "inventory_profitability_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date"
|
||||
|
|
@ -2467,6 +2470,7 @@ function shouldBoostAutoBroadenedLimit(intent: AddressIntent): boolean {
|
|||
intent === "inventory_purchase_documents_for_item" ||
|
||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||
intent === "inventory_sale_trace_for_item" ||
|
||||
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
intent === "inventory_profitability_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date"
|
||||
|
|
@ -3132,6 +3136,9 @@ async function tryComposeLlmLimitedReply(input: {
|
|||
if (process.env.VITEST === "true" || process.env.NODE_ENV === "test") {
|
||||
return null;
|
||||
}
|
||||
if (input.intent === "inventory_margin_ranking_for_nomenclature" && input.category === "missing_anchor") {
|
||||
return null;
|
||||
}
|
||||
if (!shouldUseLlmLimitedReply(input.category)) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -3206,6 +3213,13 @@ function composeLimitedReply(input: {
|
|||
)
|
||||
);
|
||||
const missingAnchorPhrase = missingAnchorLabels.length > 0 ? missingAnchorLabels.join(", ") : "контрагент, договор, счет или период";
|
||||
if (input.intent === "inventory_margin_ranking_for_nomenclature" && input.category === "missing_anchor") {
|
||||
return [
|
||||
"Для рейтинга прибыльности номенклатуры нужен период.",
|
||||
"Могу посчитать по номенклатуре: выручку без НДС, себестоимость реализации, валовую прибыль и маржинальность.",
|
||||
"Уточните период: месяц, квартал, год или весь доступный период."
|
||||
].join("\n\n");
|
||||
}
|
||||
const heading =
|
||||
input.category === "empty_match"
|
||||
? pickDeterministicVariant(headingSeed, [
|
||||
|
|
|
|||
|
|
@ -992,6 +992,17 @@ const BASE_RECIPES: AddressRecipeDefinition[] = [
|
|||
account_scope_mode: "strict",
|
||||
query_template: "inventory_trading_margin_proxy_profile"
|
||||
},
|
||||
{
|
||||
recipe_id: "address_inventory_margin_ranking_for_nomenclature_v1",
|
||||
intent: "inventory_margin_ranking_for_nomenclature",
|
||||
purpose: "Rank realized nomenclature by bounded gross margin proxy for an explicit period using 41.01 purchase and sale document rows",
|
||||
required_filters: ["period_from", "period_to"],
|
||||
optional_filters: ["organization", "warehouse", "limit", "sort"],
|
||||
default_limit: 800,
|
||||
account_scope: ["41.01"],
|
||||
account_scope_mode: "strict",
|
||||
query_template: "inventory_margin_ranking_profile"
|
||||
},
|
||||
{
|
||||
recipe_id: "address_inventory_purchase_to_sale_chain_v1",
|
||||
intent: "inventory_purchase_to_sale_chain",
|
||||
|
|
@ -1928,6 +1939,7 @@ function maxLimitForIntent(intent: AddressIntent): number {
|
|||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||
intent === "inventory_sale_trace_for_item" ||
|
||||
intent === "inventory_trading_margin_proxy_for_organization" ||
|
||||
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
intent === "inventory_profitability_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date" ||
|
||||
|
|
@ -2182,6 +2194,8 @@ export function buildAddressRecipePlan(
|
|||
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
||||
: recipe.query_template === "inventory_trading_margin_proxy_profile"
|
||||
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
||||
: recipe.query_template === "inventory_margin_ranking_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"
|
||||
|
|
|
|||
|
|
@ -423,6 +423,7 @@ function isInventoryIntent(intent: AddressIntent | undefined): boolean {
|
|||
intent === "inventory_purchase_documents_for_item" ||
|
||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||
intent === "inventory_sale_trace_for_item" ||
|
||||
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
intent === "inventory_profitability_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date"
|
||||
|
|
@ -438,6 +439,7 @@ function isInventoryDrilldownFrameIntent(intent: AddressIntent | undefined): boo
|
|||
intent === "inventory_purchase_provenance_for_item" ||
|
||||
intent === "inventory_purchase_documents_for_item" ||
|
||||
intent === "inventory_sale_trace_for_item" ||
|
||||
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
intent === "inventory_profitability_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date"
|
||||
|
|
@ -449,6 +451,7 @@ function isInventoryLifecycleHistoryIntent(intent: AddressIntent | undefined): b
|
|||
intent === "inventory_purchase_provenance_for_item" ||
|
||||
intent === "inventory_purchase_documents_for_item" ||
|
||||
intent === "inventory_sale_trace_for_item" ||
|
||||
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
intent === "inventory_profitability_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain"
|
||||
);
|
||||
|
|
@ -798,11 +801,32 @@ export function hasInventoryPurchaseDateVatBridgeCue(text: string): boolean {
|
|||
);
|
||||
}
|
||||
|
||||
export function hasInventoryMarginRankingFollowupCue(text: string): boolean {
|
||||
const normalized = textWithRepairedVariant(String(text ?? ""))
|
||||
.toLowerCase()
|
||||
.replace(/ё/g, "е");
|
||||
if (!normalized.trim()) {
|
||||
return false;
|
||||
}
|
||||
const wantsFoundRows =
|
||||
/(?:покажи|показать|выведи|дай|раскрой|show|list|покажи|показать|выведи|дай|раскрой)/iu.test(normalized) &&
|
||||
/(?:найденн|строк|реализац|себестоимостн|баз|найденн|строк|реализац|себестоимостн|баз)/iu.test(normalized) &&
|
||||
/(?:себестоимостн|реализац|марж|прибыл|номенклатур|себестоимостн|реализац|марж|прибыл|номенклат)/iu.test(normalized);
|
||||
const account41Not01 =
|
||||
/\b41(?:[.,]\d{1,2})?\b/iu.test(normalized) &&
|
||||
/\b01(?:[.,]\d{1,2})?\b/iu.test(normalized) &&
|
||||
/(?:\bне\b|вместо|а\s+не|not|instead|РЅРµ|вместо|Р°\s+РЅРµ)/iu.test(normalized);
|
||||
return wantsFoundRows || account41Not01;
|
||||
}
|
||||
|
||||
export function hasAddressFollowupContextSignal(text: string): boolean {
|
||||
const normalized = String(text ?? "").trim();
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
if (hasInventoryMarginRankingFollowupCue(normalized)) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
/(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+ней|по\s+нему|по\s+ним|for\s+selected\s+object|selected\s+object)/iu.test(
|
||||
normalized
|
||||
|
|
@ -1115,6 +1139,7 @@ function mergeFollowupFilters(
|
|||
intent === "inventory_purchase_documents_for_item" ||
|
||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||
intent === "inventory_sale_trace_for_item" ||
|
||||
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
intent === "inventory_profitability_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date" ||
|
||||
|
|
@ -1175,6 +1200,7 @@ function mergeFollowupFilters(
|
|||
(intent === "inventory_purchase_provenance_for_item" ||
|
||||
intent === "inventory_purchase_documents_for_item" ||
|
||||
intent === "inventory_sale_trace_for_item" ||
|
||||
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
intent === "inventory_profitability_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date")
|
||||
|
|
@ -1412,6 +1438,7 @@ function mergeFollowupFilters(
|
|||
intent === "inventory_purchase_documents_for_item" ||
|
||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||
intent === "inventory_sale_trace_for_item" ||
|
||||
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
intent === "inventory_profitability_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date" ||
|
||||
|
|
@ -1424,6 +1451,9 @@ function mergeFollowupFilters(
|
|||
const currentContractExplicit = toNonEmptyString(merged.contract);
|
||||
const currentItemExplicit = toNonEmptyString(merged.item);
|
||||
const currentAccountExplicit = toNonEmptyString(merged.account);
|
||||
const currentAccountRefinesMarginDomain =
|
||||
intent === "inventory_margin_ranking_for_nomenclature" &&
|
||||
hasInventoryMarginRankingFollowupCue(userMessage);
|
||||
const shouldSuppressGenericPeriodCarryover =
|
||||
(Boolean(currentCounterpartyExplicit) &&
|
||||
!isLowQualityCounterpartyAnchor(currentCounterpartyExplicit) &&
|
||||
|
|
@ -1432,7 +1462,7 @@ function mergeFollowupFilters(
|
|||
!isLowQualityContractAnchor(currentContractExplicit) &&
|
||||
currentContractExplicit !== previousContract) ||
|
||||
(Boolean(currentItemExplicit) && currentItemExplicit !== previousItem) ||
|
||||
(Boolean(currentAccountExplicit) && currentAccountExplicit !== previousAccount);
|
||||
(Boolean(currentAccountExplicit) && currentAccountExplicit !== previousAccount && !currentAccountRefinesMarginDomain);
|
||||
const vatRelativeMonthFollowup =
|
||||
relativeMonthFromFollowupYear &&
|
||||
(intent === "vat_payable_confirmed_as_of_date" ||
|
||||
|
|
@ -1488,6 +1518,22 @@ function mergeFollowupFilters(
|
|||
reasons.push("period_from_followup_context");
|
||||
}
|
||||
|
||||
if (
|
||||
intent === "inventory_margin_ranking_for_nomenclature" &&
|
||||
previousHasPeriod &&
|
||||
hasInventoryMarginRankingFollowupCue(userMessage) &&
|
||||
!hasExplicitPeriodInMessage &&
|
||||
!hasExplicitCurrentDateInMessage
|
||||
) {
|
||||
if (previousPeriodFrom && merged.period_from !== previousPeriodFrom) {
|
||||
merged.period_from = previousPeriodFrom;
|
||||
}
|
||||
if (previousPeriodTo && merged.period_to !== previousPeriodTo) {
|
||||
merged.period_to = previousPeriodTo;
|
||||
}
|
||||
reasons.push("period_from_followup_context");
|
||||
}
|
||||
|
||||
if (
|
||||
!currentHasPeriod &&
|
||||
previousHasPeriod &&
|
||||
|
|
@ -1563,6 +1609,7 @@ function resolveMissingRequiredFilters(intent: AddressIntent, filters: AddressFi
|
|||
account_balance_snapshot: ["account", "as_of_date"],
|
||||
documents_forming_balance: ["account", "as_of_date"],
|
||||
inventory_on_hand_as_of_date: ["as_of_date"],
|
||||
inventory_margin_ranking_for_nomenclature: ["period_from", "period_to"],
|
||||
inventory_profitability_for_item: ["item"],
|
||||
open_contracts_confirmed_as_of_date: ["as_of_date"],
|
||||
payables_confirmed_as_of_date: ["as_of_date"],
|
||||
|
|
@ -1648,6 +1695,26 @@ function deriveIntentWithFollowupContext(
|
|||
previousCounterpartyLaneActive && !hasExplicitInventoryItemReference;
|
||||
const inventoryPurchaseDateVatBridge =
|
||||
inventorySelectedObjectFollowup && hasInventoryPurchaseDateVatBridgeCue(normalizedMessage);
|
||||
const marginRankingLineageActive =
|
||||
sourceIntent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
fallbackIntent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
followupContext.root_intent === "inventory_margin_ranking_for_nomenclature";
|
||||
|
||||
if (
|
||||
marginRankingLineageActive &&
|
||||
hasInventoryMarginRankingFollowupCue(normalizedMessage) &&
|
||||
(detectedIntent.intent === "unknown" ||
|
||||
detectedIntent.intent === "account_balance_snapshot" ||
|
||||
detectedIntent.intent === "documents_forming_balance" ||
|
||||
detectedIntent.intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
detectedIntent.intent === sourceIntent)
|
||||
) {
|
||||
return {
|
||||
intent: "inventory_margin_ranking_for_nomenclature",
|
||||
confidence: "low",
|
||||
reasons: [...detectedIntent.reasons, "intent_adjusted_to_inventory_margin_ranking_followup_context"]
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
inventoryPurchaseDateVatBridge &&
|
||||
|
|
|
|||
|
|
@ -162,6 +162,73 @@ function inventoryProfitabilityPeriodLabel(options: InventoryComposeOptions, dep
|
|||
return asOfDate ? `до ${deps.formatDateRu(asOfDate)}` : "по доступной выборке";
|
||||
}
|
||||
|
||||
interface InventoryMarginRankingEntry {
|
||||
item: string;
|
||||
revenue: number;
|
||||
costProxy: number;
|
||||
spread: number;
|
||||
marginPct: number | null;
|
||||
saleQuantity: number;
|
||||
purchaseQuantity: number;
|
||||
saleDocuments: number;
|
||||
purchaseDocuments: number;
|
||||
}
|
||||
|
||||
function inventoryRowItemLabel(row: ComposeStageRow, deps: InventoryReplyDeps): string | null {
|
||||
return deps.summarizeInventoryTraceRows([row]).item;
|
||||
}
|
||||
|
||||
function buildInventoryMarginRankingEntries(rows: ComposeStageRow[], deps: InventoryReplyDeps): InventoryMarginRankingEntry[] {
|
||||
const byItem = new Map<string, { item: string; saleRows: ComposeStageRow[]; purchaseRows: ComposeStageRow[] }>();
|
||||
for (const row of rows) {
|
||||
const item = inventoryRowItemLabel(row, deps);
|
||||
if (!item) {
|
||||
continue;
|
||||
}
|
||||
const key = item.trim().toLocaleLowerCase("ru");
|
||||
const current = byItem.get(key) ?? { item, saleRows: [], purchaseRows: [] };
|
||||
if (deps.isInventorySaleMovement(row)) {
|
||||
current.saleRows.push(row);
|
||||
}
|
||||
if (deps.isInventoryPurchaseMovement(row)) {
|
||||
current.purchaseRows.push(row);
|
||||
}
|
||||
byItem.set(key, current);
|
||||
}
|
||||
|
||||
return Array.from(byItem.values())
|
||||
.map((entry) => {
|
||||
const revenue = sumInventoryRowAmount(entry.saleRows);
|
||||
const costProxy = sumInventoryRowAmount(entry.purchaseRows);
|
||||
const spread = revenue - costProxy;
|
||||
return {
|
||||
item: entry.item,
|
||||
revenue,
|
||||
costProxy,
|
||||
spread,
|
||||
marginPct: revenue > 0 ? (spread / revenue) * 100 : null,
|
||||
saleQuantity: sumInventoryRowQuantity(entry.saleRows),
|
||||
purchaseQuantity: sumInventoryRowQuantity(entry.purchaseRows),
|
||||
saleDocuments: entry.saleRows.length,
|
||||
purchaseDocuments: entry.purchaseRows.length
|
||||
};
|
||||
})
|
||||
.filter((entry) => entry.revenue > 0 || entry.costProxy > 0);
|
||||
}
|
||||
|
||||
function formatInventoryMarginRankingLine(
|
||||
entry: InventoryMarginRankingEntry,
|
||||
index: number,
|
||||
deps: InventoryReplyDeps
|
||||
): string {
|
||||
return `${index + 1}. ${entry.item} — выручка ${deps.formatMoneyRub(entry.revenue)}, себестоимостная база ${deps.formatMoneyRub(
|
||||
entry.costProxy
|
||||
)}, валовая разница ${deps.formatMoneyRub(entry.spread)}, маржа ${formatInventoryPercent(
|
||||
entry.marginPct,
|
||||
deps.formatNumberWithDots
|
||||
)}.`;
|
||||
}
|
||||
|
||||
export function composeInventoryReply(
|
||||
intent: AddressIntent,
|
||||
rows: ComposeStageRow[],
|
||||
|
|
@ -548,6 +615,121 @@ export function composeInventoryReply(
|
|||
: buildFactualSummaryReply(lines, buildConfirmedBalanceSemantics("medium", false));
|
||||
}
|
||||
|
||||
if (intent === "inventory_margin_ranking_for_nomenclature") {
|
||||
const entries = buildInventoryMarginRankingEntries(rows, deps);
|
||||
const confirmedEntries = entries.filter((entry) => entry.revenue > 0 && entry.costProxy > 0);
|
||||
const highMargin = [...confirmedEntries]
|
||||
.sort((left, right) => right.spread - left.spread || (right.marginPct ?? -Infinity) - (left.marginPct ?? -Infinity))
|
||||
.slice(0, 5);
|
||||
const lowMargin = [...confirmedEntries]
|
||||
.sort((left, right) => left.spread - right.spread || (left.marginPct ?? Infinity) - (right.marginPct ?? Infinity))
|
||||
.slice(0, 5);
|
||||
const salesWithoutCost = entries.filter((entry) => entry.revenue > 0 && entry.costProxy <= 0);
|
||||
const purchasesWithoutSales = entries.filter((entry) => entry.costProxy > 0 && entry.revenue <= 0);
|
||||
const periodLabel = inventoryProfitabilityPeriodLabel(options, deps);
|
||||
const totalRevenue = entries.reduce((sum, entry) => sum + entry.revenue, 0);
|
||||
const totalCostProxy = entries.reduce((sum, entry) => sum + entry.costProxy, 0);
|
||||
const totalSpread = totalRevenue - totalCostProxy;
|
||||
if (confirmedEntries.length === 0) {
|
||||
const lines: string[] = [`За период ${periodLabel} рейтинг прибыльности номенклатуры построить нельзя.`];
|
||||
const findings: string[] = [];
|
||||
if (salesWithoutCost.length > 0) {
|
||||
const salesCount = deps.formatNumberWithDots(salesWithoutCost.length);
|
||||
const salesItemPhrase =
|
||||
salesWithoutCost.length === 1 ? "1 номенклатурной позиции" : `${salesCount} номенклатурным позициям`;
|
||||
findings.push(
|
||||
`Есть реализация по ${salesItemPhrase}.`
|
||||
);
|
||||
findings.push(
|
||||
salesWithoutCost.length === 1
|
||||
? "Подтвержденной себестоимости реализации по этой позиции не найдено."
|
||||
: "Подтвержденной себестоимости реализации по этим позициям не найдено."
|
||||
);
|
||||
findings.push("Поэтому валовую прибыль и маржинальность честно посчитать нельзя.");
|
||||
}
|
||||
if (purchasesWithoutSales.length > 0) {
|
||||
const purchaseCount = deps.formatNumberWithDots(purchasesWithoutSales.length);
|
||||
const purchaseItemPhrase =
|
||||
purchasesWithoutSales.length === 1 ? "1 позиции" : `${purchaseCount} позициям`;
|
||||
findings.push(
|
||||
purchasesWithoutSales.length === 1
|
||||
? `Есть себестоимостная база по ${purchaseItemPhrase}, но реализации по ней в периоде не найдено.`
|
||||
: `Есть себестоимостная база по ${purchaseItemPhrase}, но реализации по ним в периоде не найдено.`
|
||||
);
|
||||
}
|
||||
if (entries.length === 0) {
|
||||
findings.push("В доступной выборке нет достаточных строк реализации и себестоимости по номенклатуре.");
|
||||
}
|
||||
appendInventoryBulletSection(lines, "Что нашлось:", findings);
|
||||
lines.push(
|
||||
`Вывод: за период ${periodLabel} нет достаточной базы для рейтинга «высокая / низкая прибыль» по номенклатуре.`
|
||||
);
|
||||
const nextActions: string[] = [];
|
||||
if (salesWithoutCost.length > 0) {
|
||||
nextActions.push("показать найденные реализации за этот период;");
|
||||
}
|
||||
if (purchasesWithoutSales.length > 0) {
|
||||
nextActions.push("показать найденные строки себестоимостной базы за этот период;");
|
||||
}
|
||||
nextActions.push(
|
||||
"расширить период до квартала или года;",
|
||||
"попробовать строгий расчет по проводкам 90.01 / 90.02;",
|
||||
"построить управленческий proxy по закупочным документам, если такой способ допустим для вашей проверки."
|
||||
);
|
||||
appendInventoryBulletSection(lines, "Что можно сделать дальше:", nextActions);
|
||||
appendInventoryBulletSection(lines, "Граница ответа:", [
|
||||
"Прибыльность номенклатуры считаю только когда есть реализация и подтвержденная себестоимость реализации.",
|
||||
"Это не чистая прибыль компании и не замена закрытию месяца."
|
||||
]);
|
||||
return buildFactualSummaryReply(lines, buildConfirmedBalanceSemantics(entries.length > 0 ? "medium" : "weak", false));
|
||||
}
|
||||
const directAnswerLine =
|
||||
confirmedEntries.length > 0
|
||||
? `За период ${periodLabel} собран рейтинг реализованной номенклатуры по валовой маржинальности: выручка ${deps.formatMoneyRub(
|
||||
totalRevenue
|
||||
)}, себестоимостная база ${deps.formatMoneyRub(totalCostProxy)}, расчетная валовая разница ${deps.formatMoneyRub(
|
||||
totalSpread
|
||||
)}.`
|
||||
: `За период ${periodLabel} не удалось подтвердить рейтинг прибыльности номенклатуры: нужны одновременно строки реализации и закупочного/себестоимостного следа по товарам.`;
|
||||
const lines: string[] = [directAnswerLine];
|
||||
|
||||
if (highMargin.length > 0) {
|
||||
appendInventorySection(
|
||||
lines,
|
||||
"Высокая валовая маржинальность:",
|
||||
highMargin.map((entry, index) => formatInventoryMarginRankingLine(entry, index, deps))
|
||||
);
|
||||
}
|
||||
if (lowMargin.length > 0) {
|
||||
appendInventorySection(
|
||||
lines,
|
||||
"Низкая или отрицательная валовая маржинальность:",
|
||||
lowMargin.map((entry, index) => formatInventoryMarginRankingLine(entry, index, deps))
|
||||
);
|
||||
}
|
||||
|
||||
const boundaryLines = [
|
||||
"Это управленческий расчет валовой маржинальности по реализации и доступной себестоимостной базе, не чистая прибыль компании.",
|
||||
"Для строгого бухгалтерского расчета нужны проводки 90.01 / 90.02 и закрытие себестоимости; этот ответ не подменяет закрытие месяца."
|
||||
];
|
||||
if (salesWithoutCost.length > 0) {
|
||||
boundaryLines.push(
|
||||
`По ${deps.formatNumberWithDots(salesWithoutCost.length)} позициям есть продажи, но нет подтвержденной себестоимости реализации — их нельзя честно ранжировать по прибыли.`
|
||||
);
|
||||
}
|
||||
if (purchasesWithoutSales.length > 0) {
|
||||
boundaryLines.push(
|
||||
`По ${deps.formatNumberWithDots(purchasesWithoutSales.length)} позициям есть себестоимостная база без реализации в этом периоде.`
|
||||
);
|
||||
}
|
||||
appendInventoryBulletSection(lines, "Граница ответа:", boundaryLines);
|
||||
|
||||
return buildFactualSummaryReply(
|
||||
lines,
|
||||
buildConfirmedBalanceSemantics(confirmedEntries.length > 0 ? "strong" : entries.length > 0 ? "medium" : "weak", confirmedEntries.length > 0)
|
||||
);
|
||||
}
|
||||
|
||||
if (intent === "inventory_profitability_for_item") {
|
||||
const purchaseRows = rows.filter((row) => deps.isInventoryPurchaseMovement(row));
|
||||
const saleRows = rows.filter((row) => deps.isInventorySaleMovement(row));
|
||||
|
|
|
|||
|
|
@ -157,6 +157,7 @@ function isInventorySelectedObjectOrRootIntent(intent: string | null): boolean {
|
|||
intent === "inventory_purchase_provenance_for_item" ||
|
||||
intent === "inventory_purchase_documents_for_item" ||
|
||||
intent === "inventory_sale_trace_for_item" ||
|
||||
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
intent === "inventory_profitability_for_item" ||
|
||||
intent === "inventory_purchase_to_sale_chain" ||
|
||||
intent === "inventory_aging_by_purchase_date"
|
||||
|
|
@ -175,6 +176,18 @@ function isGenericCanonicalDriftIntent(intent: string | null): boolean {
|
|||
);
|
||||
}
|
||||
|
||||
function hasInventoryMarginRankingAccountCorrectionCue(text: string | null): boolean {
|
||||
const value = String(text ?? "").toLowerCase();
|
||||
if (!value.trim()) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
/\b41(?:[.,]\d{1,2})?\b/iu.test(value) &&
|
||||
/\b01(?:[.,]\d{1,2})?\b/iu.test(value) &&
|
||||
/(?:\u0430\s+\u043d\u0435|\u043d\u0435|\u0432\u043c\u0435\u0441\u0442\u043e|not|instead)/iu.test(value)
|
||||
);
|
||||
}
|
||||
|
||||
function hasSameDateFollowupSignal(text: string | null): boolean {
|
||||
return /(?:эту\s+же\s+дат(?:у|е|ой)|ту\s+же\s+дат(?:у|е|ой)|same\s+date)/iu.test(String(text ?? ""));
|
||||
}
|
||||
|
|
@ -240,6 +253,12 @@ function shouldPreferRawFollowupMessage(
|
|||
const hasInventoryFrameCarryover =
|
||||
isInventorySelectedObjectOrRootIntent(previousIntent) ||
|
||||
isInventorySelectedObjectOrRootIntent(rootIntent);
|
||||
const hasInventoryMarginRankingCarryover =
|
||||
previousIntent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
rootIntent === "inventory_margin_ranking_for_nomenclature";
|
||||
const hasInventoryMarginRankingAccountCorrection =
|
||||
hasInventoryMarginRankingCarryover &&
|
||||
[rawMessage, canonicalMessage].some((message) => hasInventoryMarginRankingAccountCorrectionCue(message));
|
||||
const hasDocumentCarryover =
|
||||
previousIntent === "list_documents_by_counterparty" || previousIntent === "list_documents_by_contract";
|
||||
|
||||
|
|
@ -263,6 +282,13 @@ function shouldPreferRawFollowupMessage(
|
|||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
hasInventoryMarginRankingAccountCorrection &&
|
||||
(intent === "account_balance_snapshot" || intent === "documents_forming_balance" || intent === "unknown")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (
|
||||
(hasSelectedObjectInventorySignal(rawMessage) || hasInventoryItemCarryover) &&
|
||||
(hasSelectedObjectInventoryActionCue(rawMessage) || hasShortInventoryPurchaseFollowupCue(rawMessage)) &&
|
||||
|
|
|
|||
|
|
@ -34,6 +34,13 @@ function formatMissingAnchors(anchors: string[]): string {
|
|||
}
|
||||
|
||||
function buildClarificationReply(binding: AssistantCapabilityRuntimeBindingContract): string {
|
||||
if (binding.capability_contract_id === "inventory_inventory_margin_ranking_for_nomenclature") {
|
||||
return [
|
||||
"Для рейтинга прибыльности номенклатуры нужен период.",
|
||||
"Могу посчитать по номенклатуре: выручку без НДС, себестоимость реализации, валовую прибыль и маржинальность.",
|
||||
"Уточните период: месяц, квартал, год или весь доступный период."
|
||||
].join("\n\n");
|
||||
}
|
||||
return [
|
||||
"Нужно уточнение, чтобы не подставить неподтвержденный объект в расчет.",
|
||||
`Не хватает: ${formatMissingAnchors(binding.missing_anchors)}.`,
|
||||
|
|
|
|||
|
|
@ -161,6 +161,15 @@ function anchorSatisfied(requiredAnchor: string, providedAnchors: string[], debu
|
|||
if (providedAnchors.includes(requiredAnchor)) {
|
||||
return true;
|
||||
}
|
||||
if (requiredAnchor === "period") {
|
||||
return (
|
||||
(hasValue(filters?.period_from) && hasValue(filters?.period_to)) ||
|
||||
hasValue(filters?.as_of_date) ||
|
||||
providedAnchors.includes("period_from") ||
|
||||
providedAnchors.includes("period_to") ||
|
||||
providedAnchors.includes("as_of_date")
|
||||
);
|
||||
}
|
||||
if (requiredAnchor === "item") {
|
||||
return (
|
||||
providedAnchors.includes("selected_object") ||
|
||||
|
|
|
|||
|
|
@ -216,6 +216,7 @@ function isDetectedIntentAlignedWithTurnMeaning(
|
|||
normalizedIntent === "inventory_purchase_provenance_for_item" ||
|
||||
normalizedIntent === "inventory_purchase_documents_for_item" ||
|
||||
normalizedIntent === "inventory_sale_trace_for_item" ||
|
||||
normalizedIntent === "inventory_margin_ranking_for_nomenclature" ||
|
||||
normalizedIntent === "inventory_profitability_for_item" ||
|
||||
normalizedIntent === "inventory_purchase_to_sale_chain"
|
||||
) {
|
||||
|
|
@ -276,7 +277,7 @@ function isExplicitMetadataDiscoveryTurn(
|
|||
}
|
||||
|
||||
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(
|
||||
return /^(?:inventory_purchase_provenance_for_item|inventory_purchase_documents_for_item|inventory_sale_trace_for_item|inventory_margin_ranking_for_nomenclature|inventory_profitability_for_item|inventory_purchase_to_sale_chain|inventory_aging_by_purchase_date|inventory_on_hand_as_of_date)$/u.test(
|
||||
String(intent ?? "")
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ const ADDRESS_INTENTS_KEEP_ADDRESS_LANE = new Set([
|
|||
"inventory_purchase_documents_for_item",
|
||||
"inventory_supplier_stock_overlap_as_of_date",
|
||||
"inventory_sale_trace_for_item",
|
||||
"inventory_margin_ranking_for_nomenclature",
|
||||
"inventory_profitability_for_item",
|
||||
"inventory_purchase_to_sale_chain",
|
||||
"inventory_aging_by_purchase_date",
|
||||
|
|
@ -41,6 +42,7 @@ const ADDRESS_INTENTS_ALLOW_STRICT_DEEP_INVESTIGATION_BYPASS = new Set([
|
|||
"inventory_purchase_provenance_for_item",
|
||||
"inventory_purchase_documents_for_item",
|
||||
"inventory_sale_trace_for_item",
|
||||
"inventory_margin_ranking_for_nomenclature",
|
||||
"inventory_profitability_for_item",
|
||||
"inventory_purchase_to_sale_chain"
|
||||
]);
|
||||
|
|
@ -286,7 +288,7 @@ export function createAssistantRoutePolicy(deps) {
|
|||
: null;
|
||||
const semanticCanonicalRecommended = semanticExtractionContract?.apply_canonical_recommended !== false;
|
||||
const llmSupportedDeepAddressIntentSignal = llmContractMode === "deep_analysis" &&
|
||||
/^(?:inventory_purchase_provenance_for_item|inventory_purchase_documents_for_item|inventory_sale_trace_for_item|inventory_profitability_for_item|inventory_purchase_to_sale_chain)$/u.test(llmContractIntent ?? "") &&
|
||||
/^(?:inventory_purchase_provenance_for_item|inventory_purchase_documents_for_item|inventory_sale_trace_for_item|inventory_margin_ranking_for_nomenclature|inventory_profitability_for_item|inventory_purchase_to_sale_chain)$/u.test(llmContractIntent ?? "") &&
|
||||
semanticCanonicalRecommended;
|
||||
const llmCanonicalEntitySignal = /(?:заказчик|поставщик|контрагент|компан|customer|supplier|counterparty|company|vendor|client)/iu.test(compactWhitespace(repairedInputMessage.toLowerCase()));
|
||||
const llmCanonicalAppliedSignal = Boolean(llmPreDecomposeMeta?.applied) && llmContractMode !== "deep_analysis";
|
||||
|
|
|
|||
|
|
@ -337,6 +337,18 @@ export const INVENTORY_CAPABILITY_CONTRACTS: readonly AssistantCapabilityContrac
|
|||
answerObjectShape: "inventory_profitability_bundle",
|
||||
bundleReusePolicy: "sale_trace_bundle_preferred"
|
||||
}),
|
||||
inventoryExactCapability({
|
||||
capability_id: "inventory_inventory_margin_ranking_for_nomenclature",
|
||||
intent_ids: ["inventory_margin_ranking_for_nomenclature"],
|
||||
entry_modes: ["root_entry", "root_followup", "clarification_resume"],
|
||||
transitions: ["T1", "T2", "T7"],
|
||||
requiresFocusObject: false,
|
||||
requiredAnchors: ["period"],
|
||||
resultShape: "nomenclature_margin_ranking",
|
||||
answerObjectShape: "inventory_margin_ranking",
|
||||
bundleReusePolicy: "none",
|
||||
scenarioFamilies: ["canonical", "colloquial", "account_41_correction"]
|
||||
}),
|
||||
inventoryExactCapability({
|
||||
capability_id: "inventory_inventory_purchase_to_sale_chain",
|
||||
intent_ids: ["inventory_purchase_to_sale_chain"],
|
||||
|
|
|
|||
|
|
@ -47,6 +47,31 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
return deps.compactWhitespace(deps.repairAddressMojibake(String(value ?? "")).toLowerCase()).replace(/ё/g, "е");
|
||||
}
|
||||
|
||||
function hasInventoryMarginRankingFollowupSignal(userMessage, alternateMessage = null, sourceIntentHint = null) {
|
||||
if (sourceIntentHint !== "inventory_margin_ranking_for_nomenclature") {
|
||||
return false;
|
||||
}
|
||||
return [userMessage, alternateMessage]
|
||||
.filter((value) => deps.toNonEmptyString(value))
|
||||
.map((value) =>
|
||||
deps.compactWhitespace(deps.repairAddressMojibake(String(value ?? "")).toLowerCase()).replace(/ё/g, "е")
|
||||
)
|
||||
.some((normalized) => {
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
const wantsFoundRows =
|
||||
/(?:покажи|показать|выведи|дай|раскрой|show|list|покажи|показать|выведи|дай|раскрой)/iu.test(normalized) &&
|
||||
/(?:найденн|строк|реализац|себестоимостн|баз|найденн|строк|реализац|себестоимостн|баз)/iu.test(normalized) &&
|
||||
/(?:себестоимостн|реализац|марж|прибыл|номенклатур|себестоимостн|реализац|марж|прибыл|номенклат)/iu.test(normalized);
|
||||
const account41Not01 =
|
||||
/\b41(?:[.,]\d{1,2})?\b/iu.test(normalized) &&
|
||||
/\b01(?:[.,]\d{1,2})?\b/iu.test(normalized) &&
|
||||
/(?:\bне\b|вместо|а\s+не|not|instead|РЅРµ|вместо|Р°\s+РЅРµ)/iu.test(normalized);
|
||||
return wantsFoundRows || account41Not01;
|
||||
});
|
||||
}
|
||||
|
||||
function hasSamePeriodReferenceCue(...values) {
|
||||
return values
|
||||
.map((value) => normalizeFollowupText(value))
|
||||
|
|
@ -760,6 +785,11 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
sourceIntentHint,
|
||||
hasNavigationInventoryItemFocusHint
|
||||
);
|
||||
const inventoryMarginRankingFollowup = hasInventoryMarginRankingFollowupSignal(
|
||||
userMessage,
|
||||
alternateMessage,
|
||||
sourceIntentHint
|
||||
);
|
||||
let inventoryShortFollowupPrimary =
|
||||
(deps.isInventorySelectedObjectIntent(sourceIntentHint) || hasNavigationInventoryItemFocusHint) &&
|
||||
deps.hasShortInventoryObjectFollowupSignal(userMessage);
|
||||
|
|
@ -796,6 +826,7 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
businessOverviewBoundaryFollowupPrimary ||
|
||||
inventoryShortFollowupPrimary ||
|
||||
inventoryPurchaseDateVatBridge ||
|
||||
inventoryMarginRankingFollowup ||
|
||||
explicitSummaryBundleReuseSignal ||
|
||||
mcpDiscoveryOrganizationClarificationContinuation;
|
||||
let hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage)
|
||||
|
|
@ -805,6 +836,7 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
businessOverviewBoundaryFollowupAlternate ||
|
||||
inventoryShortFollowupAlternate ||
|
||||
inventoryPurchaseDateVatBridge ||
|
||||
inventoryMarginRankingFollowup ||
|
||||
explicitSummaryBundleReuseSignal ||
|
||||
mcpDiscoveryOrganizationClarificationContinuation
|
||||
: false;
|
||||
|
|
@ -862,6 +894,7 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
shortValueFlowRetargetAlternate ||
|
||||
businessOverviewBoundaryFollowupPrimary ||
|
||||
businessOverviewBoundaryFollowupAlternate ||
|
||||
inventoryMarginRankingFollowup ||
|
||||
deps.hasFollowupMarker(userMessage) ||
|
||||
deps.hasReferentialPointer(userMessage) ||
|
||||
(deps.toNonEmptyString(alternateMessage)
|
||||
|
|
@ -886,6 +919,7 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
shortValueFlowRetargetAlternate ||
|
||||
businessOverviewBoundaryFollowupPrimary ||
|
||||
businessOverviewBoundaryFollowupAlternate ||
|
||||
inventoryMarginRankingFollowup ||
|
||||
deps.hasFollowupMarker(userMessage) ||
|
||||
deps.hasReferentialPointer(userMessage) ||
|
||||
(deps.toNonEmptyString(alternateMessage)
|
||||
|
|
@ -1054,8 +1088,9 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
!inventoryShortFollowupPrimary &&
|
||||
!inventoryShortFollowupAlternate &&
|
||||
!businessOverviewBoundaryFollowupPrimary &&
|
||||
!businessOverviewBoundaryFollowupAlternate &&
|
||||
!foreignAccountingPivotOverInventory &&
|
||||
!businessOverviewBoundaryFollowupAlternate &&
|
||||
!inventoryMarginRankingFollowup &&
|
||||
!foreignAccountingPivotOverInventory &&
|
||||
!deps.hasFollowupMarker(userMessage) &&
|
||||
!deps.hasReferentialPointer(userMessage) &&
|
||||
(!deps.toNonEmptyString(alternateMessage)
|
||||
|
|
@ -1118,6 +1153,7 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
businessOverviewBoundaryFollowupPrimary ||
|
||||
inventoryShortFollowupPrimary ||
|
||||
inventoryPurchaseDateVatBridge ||
|
||||
inventoryMarginRankingFollowup ||
|
||||
explicitSummaryBundleReuseSignal ||
|
||||
hasInventoryRootTemporalFollowupPrimary ||
|
||||
mcpDiscoveryOrganizationClarificationContinuation;
|
||||
|
|
@ -1129,6 +1165,7 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
businessOverviewBoundaryFollowupAlternate ||
|
||||
inventoryShortFollowupAlternate ||
|
||||
inventoryPurchaseDateVatBridge ||
|
||||
inventoryMarginRankingFollowup ||
|
||||
explicitSummaryBundleReuseSignal ||
|
||||
hasInventoryRootTemporalFollowupAlternate ||
|
||||
mcpDiscoveryOrganizationClarificationContinuation
|
||||
|
|
@ -1151,6 +1188,7 @@ export function createAssistantTransitionPolicy(deps) {
|
|||
shortValueFlowRetargetAlternate ||
|
||||
businessOverviewBoundaryFollowupPrimary ||
|
||||
businessOverviewBoundaryFollowupAlternate ||
|
||||
inventoryMarginRankingFollowup ||
|
||||
deps.hasFollowupMarker(userMessage) ||
|
||||
deps.hasReferentialPointer(userMessage) ||
|
||||
(deps.toNonEmptyString(alternateMessage)
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ const SUPPORTED_ADDRESS_INTENTS = new Set([
|
|||
"inventory_purchase_documents_for_item",
|
||||
"inventory_supplier_stock_overlap_as_of_date",
|
||||
"inventory_sale_trace_for_item",
|
||||
"inventory_margin_ranking_for_nomenclature",
|
||||
"inventory_profitability_for_item",
|
||||
"inventory_purchase_to_sale_chain",
|
||||
"inventory_aging_by_purchase_date",
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ export type AddressIntent =
|
|||
| "inventory_supplier_stock_overlap_as_of_date"
|
||||
| "inventory_sale_trace_for_item"
|
||||
| "inventory_trading_margin_proxy_for_organization"
|
||||
| "inventory_margin_ranking_for_nomenclature"
|
||||
| "inventory_profitability_for_item"
|
||||
| "inventory_purchase_to_sale_chain"
|
||||
| "inventory_aging_by_purchase_date"
|
||||
|
|
@ -203,6 +204,7 @@ export interface AddressRecipeDefinition {
|
|||
| "inventory_supplier_stock_overlap_profile"
|
||||
| "inventory_sale_trace_profile"
|
||||
| "inventory_trading_margin_proxy_profile"
|
||||
| "inventory_margin_ranking_profile"
|
||||
| "inventory_profitability_profile"
|
||||
| "inventory_purchase_to_sale_chain_profile"
|
||||
| "inventory_aging_by_purchase_date_profile"
|
||||
|
|
|
|||
|
|
@ -104,6 +104,15 @@ describe("addressIntentResolver regression bridges", () => {
|
|||
|
||||
expect(result.intent).toBe("inventory_aging_by_purchase_date");
|
||||
});
|
||||
it("routes nomenclature margin ranking away from OS, bank, and settlement fallbacks", () => {
|
||||
const result = resolveAddressIntent(
|
||||
"\u041a\u0430\u043a\u0430\u044f \u043d\u043e\u043c\u0435\u043a\u043b\u0430\u0442\u0443\u0440\u0430 \u0442\u043e\u0432\u0430\u0440\u0430 \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d\u0430 \u0441 \u0432\u044b\u0441\u043e\u043a\u043e\u0439 \u043f\u0440\u0438\u0431\u044b\u043b\u044c\u044e \u043a\u0430\u043a\u0430\u044f \u0441 \u043d\u0438\u0437\u043a\u043e\u0439"
|
||||
);
|
||||
|
||||
expect(result.intent).toBe("inventory_margin_ranking_for_nomenclature");
|
||||
expect(result.reasons).toContain("unicode_nomenclature_margin_ranking_bridge_signal_detected");
|
||||
});
|
||||
|
||||
it("detects bare historical inventory root with explicit month-year", () => {
|
||||
const result = resolveAddressIntent("остатки на март 2016");
|
||||
|
||||
|
|
|
|||
|
|
@ -104,4 +104,292 @@ describe("inventory profitability selected-object regressions", () => {
|
|||
expect(reply).toContain("не чистая прибыль компании");
|
||||
expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("asks for a period before ranking nomenclature by margin", async () => {
|
||||
const service = new AddressQueryService();
|
||||
const result = await service.tryHandle(
|
||||
"\u041a\u0430\u043a\u0430\u044f \u043d\u043e\u043c\u0435\u043a\u043b\u0430\u0442\u0443\u0440\u0430 \u0442\u043e\u0432\u0430\u0440\u0430 \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d\u0430 \u0441 \u0432\u044b\u0441\u043e\u043a\u043e\u0439 \u043f\u0440\u0438\u0431\u044b\u043b\u044c\u044e \u043a\u0430\u043a\u0430\u044f \u0441 \u043d\u0438\u0437\u043a\u043e\u0439"
|
||||
);
|
||||
|
||||
expect(result?.handled).toBe(true);
|
||||
expect(result?.response_type).toBe("LIMITED_WITH_REASON");
|
||||
expect(result?.debug.detected_intent).toBe("inventory_margin_ranking_for_nomenclature");
|
||||
expect(result?.debug.selected_recipe).toBe("address_inventory_margin_ranking_for_nomenclature_v1");
|
||||
expect(result?.debug.capability_id).toBe("inventory_inventory_margin_ranking_for_nomenclature");
|
||||
expect(result?.debug.missing_required_filters).toEqual(["period_from", "period_to"]);
|
||||
const reply = String(result?.reply_text ?? "");
|
||||
expect(reply).toContain("\u043d\u0443\u0436\u0435\u043d \u043f\u0435\u0440\u0438\u043e\u0434");
|
||||
expect(reply).toContain("\u0441\u0435\u0431\u0435\u0441\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u044c \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438");
|
||||
expect(reply).not.toContain("\u041e\u0421");
|
||||
expect(reply).not.toContain("\u0430\u043c\u043e\u0440\u0442\u0438\u0437");
|
||||
expect(reply).not.toContain("\u0437\u0430\u043a\u0443\u043f\u043e\u0447\u043d\u044b\u0439/\u0441\u0435\u0431\u0435\u0441\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u043d\u044b\u0439 \u0441\u043b\u0435\u0434");
|
||||
expect(reply).not.toContain("settlement");
|
||||
expect(executeAddressMcpQueryMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("gives a useful accounting limited answer when sales exist but cost is missing", async () => {
|
||||
executeAddressMcpQueryMock.mockResolvedValueOnce({
|
||||
fetched_rows: 1,
|
||||
matched_rows: 1,
|
||||
raw_rows: [
|
||||
{
|
||||
Period: "2020-05-20T00:00:00Z",
|
||||
Registrator: "Sales document 1",
|
||||
AccountDt: "62.01",
|
||||
AccountKt: "41.01",
|
||||
Amount: 1500,
|
||||
Quantity: 10,
|
||||
SubcontoKt1: "Item A",
|
||||
Organization: "OOO Alternative Plus"
|
||||
}
|
||||
],
|
||||
rows: [],
|
||||
error: null
|
||||
});
|
||||
|
||||
const service = new AddressQueryService();
|
||||
const result = await service.tryHandle(
|
||||
"\u041a\u0430\u043a\u0430\u044f \u043d\u043e\u043c\u0435\u043a\u043b\u0430\u0442\u0443\u0440\u0430 \u0442\u043e\u0432\u0430\u0440\u0430 \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d\u0430 \u0441 \u0432\u044b\u0441\u043e\u043a\u043e\u0439 \u043f\u0440\u0438\u0431\u044b\u043b\u044c\u044e \u043a\u0430\u043a\u0430\u044f \u0441 \u043d\u0438\u0437\u043a\u043e\u0439 \u0437\u0430 \u043c\u0430\u0439 2020"
|
||||
);
|
||||
|
||||
expect(result?.handled).toBe(true);
|
||||
expect(result?.response_type).toBe("FACTUAL_SUMMARY");
|
||||
expect(result?.debug.detected_intent).toBe("inventory_margin_ranking_for_nomenclature");
|
||||
expect(result?.debug.mcp_call_status).toBe("matched_non_empty");
|
||||
const reply = String(result?.reply_text ?? "");
|
||||
expect(reply).toContain("\u0440\u0435\u0439\u0442\u0438\u043d\u0433 \u043f\u0440\u0438\u0431\u044b\u043b\u044c\u043d\u043e\u0441\u0442\u0438 \u043d\u043e\u043c\u0435\u043d\u043a\u043b\u0430\u0442\u0443\u0440\u044b \u043f\u043e\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043d\u0435\u043b\u044c\u0437\u044f");
|
||||
expect(reply).toContain("\u0415\u0441\u0442\u044c \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u044f");
|
||||
expect(reply).toContain("\u0441\u0435\u0431\u0435\u0441\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u0438 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438");
|
||||
expect(reply).toContain("\u0447\u0435\u0441\u0442\u043d\u043e \u043f\u043e\u0441\u0447\u0438\u0442\u0430\u0442\u044c \u043d\u0435\u043b\u044c\u0437\u044f");
|
||||
expect(reply).toContain("\u0427\u0442\u043e \u043c\u043e\u0436\u043d\u043e \u0441\u0434\u0435\u043b\u0430\u0442\u044c \u0434\u0430\u043b\u044c\u0448\u0435");
|
||||
expect(reply).toContain("90.01 / 90.02");
|
||||
expect(reply).not.toContain("\u041e\u0421");
|
||||
expect(reply).not.toContain("\u0430\u043c\u043e\u0440\u0442\u0438\u0437");
|
||||
expect(reply).not.toContain("\u0437\u0430\u043a\u0443\u043f\u043e\u0447\u043d\u044b\u0439/\u0441\u0435\u0431\u0435\u0441\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u043d\u044b\u0439 \u0441\u043b\u0435\u0434");
|
||||
expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does not offer to show realizations when only cost base exists in the period", async () => {
|
||||
executeAddressMcpQueryMock.mockResolvedValueOnce({
|
||||
fetched_rows: 1,
|
||||
matched_rows: 1,
|
||||
raw_rows: [
|
||||
{
|
||||
Period: "2017-09-12T00:00:00Z",
|
||||
Registrator: "Purchase document 1",
|
||||
AccountDt: "41.01",
|
||||
AccountKt: "60.01",
|
||||
Amount: 700,
|
||||
Quantity: 2,
|
||||
SubcontoDt1: "Item C",
|
||||
Organization: "OOO Alternative Plus"
|
||||
}
|
||||
],
|
||||
rows: [],
|
||||
error: null
|
||||
});
|
||||
|
||||
const service = new AddressQueryService();
|
||||
const result = await service.tryHandle(
|
||||
"\u041a\u0430\u043a\u0430\u044f \u043d\u043e\u043c\u0435\u043a\u043b\u0430\u0442\u0443\u0440\u0430 \u0442\u043e\u0432\u0430\u0440\u0430 \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d\u0430 \u0441 \u0432\u044b\u0441\u043e\u043a\u043e\u0439 \u043f\u0440\u0438\u0431\u044b\u043b\u044c\u044e \u043a\u0430\u043a\u0430\u044f \u0441 \u043d\u0438\u0437\u043a\u043e\u0439 \u0437\u0430 \u0441\u0435\u043d\u0442\u044f\u0431\u0440\u044c 2017"
|
||||
);
|
||||
|
||||
expect(result?.handled).toBe(true);
|
||||
expect(result?.response_type).toBe("FACTUAL_SUMMARY");
|
||||
const reply = String(result?.reply_text ?? "");
|
||||
expect(reply).toContain("\u0441\u0435\u0431\u0435\u0441\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u043d\u0430\u044f \u0431\u0430\u0437\u0430");
|
||||
expect(reply).toContain("\u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438 \u043f\u043e \u043d\u0435\u0439 \u0432 \u043f\u0435\u0440\u0438\u043e\u0434\u0435 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e");
|
||||
expect(reply).toContain("\u043f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u043d\u0430\u0439\u0434\u0435\u043d\u043d\u044b\u0435 \u0441\u0442\u0440\u043e\u043a\u0438 \u0441\u0435\u0431\u0435\u0441\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u043d\u043e\u0439 \u0431\u0430\u0437\u044b");
|
||||
expect(reply).not.toContain("\u043f\u043e\u043a\u0430\u0437\u0430\u0442\u044c \u043d\u0430\u0439\u0434\u0435\u043d\u043d\u044b\u0435 \u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0438");
|
||||
expect(reply).not.toContain("\u0441\u0435\u0431\u0435\u0441\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u043d\u0430\u044f/\u0437\u0430\u043a\u0443\u043f\u043e\u0447\u043d\u0430\u044f");
|
||||
expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("keeps cost-base line drilldown inside the nomenclature margin route", () => {
|
||||
const marginFollowupContext = {
|
||||
previous_intent: "inventory_margin_ranking_for_nomenclature" as const,
|
||||
target_intent: "inventory_margin_ranking_for_nomenclature" as const,
|
||||
root_intent: "inventory_margin_ranking_for_nomenclature" as const,
|
||||
previous_filters: {
|
||||
organization: "OOO Alternative Plus",
|
||||
period_from: "2017-09-01",
|
||||
period_to: "2017-09-30"
|
||||
},
|
||||
previous_anchor_type: "unknown" as const,
|
||||
previous_anchor_value: null
|
||||
};
|
||||
|
||||
const result = runAddressDecomposeStage(
|
||||
"\u043f\u043e\u043a\u0430\u0436\u0438 \u043d\u0430\u0439\u0434\u0435\u043d\u043d\u044b\u0435 \u0441\u0442\u0440\u043e\u043a\u0438 \u0441\u0435\u0431\u0435\u0441\u0442\u043e\u0438\u043c\u043e\u0441\u0442\u043d\u043e\u0439 \u0431\u0430\u0437\u044b",
|
||||
marginFollowupContext
|
||||
);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.intent.intent).toBe("inventory_margin_ranking_for_nomenclature");
|
||||
expect(result?.filters.extracted_filters.period_from).toBe("2017-09-01");
|
||||
expect(result?.filters.extracted_filters.period_to).toBe("2017-09-30");
|
||||
expect(result?.filters.missing_required_filters).toEqual([]);
|
||||
expect(result?.intent.reasons).toContain("intent_adjusted_to_inventory_margin_ranking_followup_context");
|
||||
});
|
||||
|
||||
it("does not pivot margin follow-up account-41 correction into a balance snapshot", () => {
|
||||
const marginFollowupContext = {
|
||||
previous_intent: "inventory_margin_ranking_for_nomenclature" as const,
|
||||
target_intent: "inventory_margin_ranking_for_nomenclature" as const,
|
||||
root_intent: "inventory_margin_ranking_for_nomenclature" as const,
|
||||
previous_filters: {
|
||||
organization: "OOO Alternative Plus",
|
||||
period_from: "2017-09-01",
|
||||
period_to: "2017-09-30"
|
||||
},
|
||||
previous_anchor_type: "unknown" as const,
|
||||
previous_anchor_value: null
|
||||
};
|
||||
|
||||
for (const message of [
|
||||
"\u0430\u043d\u0430\u043b\u0438\u0437 \u043f\u043e 41 \u0441\u0447\u0435\u0442\u0443 \u0430 \u043d\u0435 01",
|
||||
"\u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0430\u043d\u0430\u043b\u0438\u0437 \u043f\u043e \u0441\u0447\u0435\u0442\u0443 41 \u0432\u043c\u0435\u0441\u0442\u043e \u0441\u0447\u0435\u0442\u0430 01"
|
||||
]) {
|
||||
const result = runAddressDecomposeStage(message, marginFollowupContext);
|
||||
|
||||
expect(result).not.toBeNull();
|
||||
expect(result?.intent.intent).toBe("inventory_margin_ranking_for_nomenclature");
|
||||
expect(result?.intent.intent).not.toBe("account_balance_snapshot");
|
||||
expect(result?.filters.extracted_filters.period_from).toBe("2017-09-01");
|
||||
expect(result?.filters.extracted_filters.period_to).toBe("2017-09-30");
|
||||
expect(result?.filters.missing_required_filters).toEqual([]);
|
||||
expect(result?.intent.reasons).toContain("intent_adjusted_to_inventory_margin_ranking_followup_context");
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps carried period when executing margin account-41 correction", async () => {
|
||||
executeAddressMcpQueryMock.mockResolvedValueOnce({
|
||||
fetched_rows: 2,
|
||||
matched_rows: 2,
|
||||
raw_rows: [
|
||||
{
|
||||
Period: "2017-02-10T00:00:00Z",
|
||||
Registrator: "Purchase document 2017",
|
||||
AccountDt: "41.01",
|
||||
AccountKt: "60.01",
|
||||
Amount: 500,
|
||||
Quantity: 1,
|
||||
SubcontoDt1: "Item A",
|
||||
Organization: "OOO Alternative Plus"
|
||||
},
|
||||
{
|
||||
Period: "2017-03-10T00:00:00Z",
|
||||
Registrator: "Sales document 2017",
|
||||
AccountDt: "62.01",
|
||||
AccountKt: "41.01",
|
||||
Amount: 900,
|
||||
Quantity: 1,
|
||||
SubcontoKt1: "Item A",
|
||||
Organization: "OOO Alternative Plus"
|
||||
}
|
||||
],
|
||||
rows: [],
|
||||
error: null
|
||||
});
|
||||
const marginFollowupContext = {
|
||||
previous_intent: "inventory_margin_ranking_for_nomenclature" as const,
|
||||
target_intent: "inventory_margin_ranking_for_nomenclature" as const,
|
||||
root_intent: "inventory_margin_ranking_for_nomenclature" as const,
|
||||
previous_filters: {
|
||||
organization: "OOO Alternative Plus",
|
||||
period_from: "2017-01-01",
|
||||
period_to: "2017-12-31"
|
||||
},
|
||||
previous_anchor_type: "organization" as const,
|
||||
previous_anchor_value: "OOO Alternative Plus"
|
||||
};
|
||||
|
||||
const service = new AddressQueryService();
|
||||
const result = await service.tryHandle(
|
||||
"\u0430\u043d\u0430\u043b\u0438\u0437 \u043f\u043e 41 \u0441\u0447\u0435\u0442\u0443 \u0430 \u043d\u0435 01",
|
||||
{ followupContext: marginFollowupContext }
|
||||
);
|
||||
|
||||
expect(result?.handled).toBe(true);
|
||||
expect(result?.debug.detected_intent).toBe("inventory_margin_ranking_for_nomenclature");
|
||||
expect(result?.debug.selected_recipe).toBe("address_inventory_margin_ranking_for_nomenclature_v1");
|
||||
expect(result?.debug.capability_id).toBe("inventory_inventory_margin_ranking_for_nomenclature");
|
||||
expect(result?.debug.extracted_filters?.period_from).toBe("2017-01-01");
|
||||
expect(result?.debug.extracted_filters?.period_to).toBe("2017-12-31");
|
||||
expect(result?.debug.missing_required_filters).toEqual([]);
|
||||
expect(result?.debug.mcp_call_status).toBe("matched_non_empty");
|
||||
expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("answers period-scoped nomenclature margin ranking with high and low gross-margin buckets", async () => {
|
||||
executeAddressMcpQueryMock.mockResolvedValueOnce({
|
||||
fetched_rows: 4,
|
||||
matched_rows: 4,
|
||||
raw_rows: [
|
||||
{
|
||||
Period: "2020-01-10T00:00:00Z",
|
||||
Registrator: "Purchase document 1",
|
||||
AccountDt: "41.01",
|
||||
AccountKt: "60.01",
|
||||
Amount: 500,
|
||||
Quantity: 10,
|
||||
SubcontoDt1: "Item A",
|
||||
Organization: "OOO Alternative Plus"
|
||||
},
|
||||
{
|
||||
Period: "2020-02-10T00:00:00Z",
|
||||
Registrator: "Sales document 1",
|
||||
AccountDt: "62.01",
|
||||
AccountKt: "41.01",
|
||||
Amount: 1500,
|
||||
Quantity: 10,
|
||||
SubcontoKt1: "Item A",
|
||||
Organization: "OOO Alternative Plus"
|
||||
},
|
||||
{
|
||||
Period: "2020-03-10T00:00:00Z",
|
||||
Registrator: "Purchase document 2",
|
||||
AccountDt: "41.01",
|
||||
AccountKt: "60.01",
|
||||
Amount: 1000,
|
||||
Quantity: 5,
|
||||
SubcontoDt1: "Item B",
|
||||
Organization: "OOO Alternative Plus"
|
||||
},
|
||||
{
|
||||
Period: "2020-04-10T00:00:00Z",
|
||||
Registrator: "Sales document 2",
|
||||
AccountDt: "62.01",
|
||||
AccountKt: "41.01",
|
||||
Amount: 900,
|
||||
Quantity: 5,
|
||||
SubcontoKt1: "Item B",
|
||||
Organization: "OOO Alternative Plus"
|
||||
}
|
||||
],
|
||||
rows: [],
|
||||
error: null
|
||||
});
|
||||
|
||||
const service = new AddressQueryService();
|
||||
const result = await service.tryHandle(
|
||||
"\u041a\u0430\u043a\u0430\u044f \u043d\u043e\u043c\u0435\u043a\u043b\u0430\u0442\u0443\u0440\u0430 \u0442\u043e\u0432\u0430\u0440\u0430 \u0440\u0435\u0430\u043b\u0438\u0437\u043e\u0432\u0430\u043d\u0430 \u0441 \u0432\u044b\u0441\u043e\u043a\u043e\u0439 \u043f\u0440\u0438\u0431\u044b\u043b\u044c\u044e \u043a\u0430\u043a\u0430\u044f \u0441 \u043d\u0438\u0437\u043a\u043e\u0439 \u0437\u0430 2020 \u0433\u043e\u0434"
|
||||
);
|
||||
|
||||
expect(result?.handled).toBe(true);
|
||||
expect(result?.response_type).toBe("FACTUAL_SUMMARY");
|
||||
expect(result?.debug.detected_intent).toBe("inventory_margin_ranking_for_nomenclature");
|
||||
expect(result?.debug.selected_recipe).toBe("address_inventory_margin_ranking_for_nomenclature_v1");
|
||||
expect(result?.debug.capability_id).toBe("inventory_inventory_margin_ranking_for_nomenclature");
|
||||
expect(result?.debug.mcp_call_status).toBe("matched_non_empty");
|
||||
const reply = String(result?.reply_text ?? "");
|
||||
expect(reply).toContain("\u0412\u044b\u0441\u043e\u043a\u0430\u044f \u0432\u0430\u043b\u043e\u0432\u0430\u044f \u043c\u0430\u0440\u0436\u0438\u043d\u0430\u043b\u044c\u043d\u043e\u0441\u0442\u044c");
|
||||
expect(reply).toContain("\u041d\u0438\u0437\u043a\u0430\u044f \u0438\u043b\u0438 \u043e\u0442\u0440\u0438\u0446\u0430\u0442\u0435\u043b\u044c\u043d\u0430\u044f");
|
||||
expect(reply).toContain("Item A");
|
||||
expect(reply).toContain("Item B");
|
||||
expect(reply).toContain("\u043d\u0435 \u0447\u0438\u0441\u0442\u0430\u044f \u043f\u0440\u0438\u0431\u044b\u043b\u044c \u043a\u043e\u043c\u043f\u0430\u043d\u0438\u0438");
|
||||
expect(reply).not.toContain("\u041e\u0421");
|
||||
expect(reply).not.toContain("\u0430\u043c\u043e\u0440\u0442\u0438\u0437");
|
||||
expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -499,6 +499,70 @@ describe("assistant address orchestration runtime adapter", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("prefers raw margin-ranking account correction over account-balance canonical drift", async () => {
|
||||
const resolveAddressFollowupCarryoverContext = vi.fn(() => ({
|
||||
followupContext: {
|
||||
previous_intent: "inventory_margin_ranking_for_nomenclature",
|
||||
target_intent: "inventory_margin_ranking_for_nomenclature",
|
||||
root_intent: "inventory_margin_ranking_for_nomenclature",
|
||||
previous_filters: {
|
||||
period_from: "2017-01-01",
|
||||
period_to: "2017-12-31",
|
||||
organization: "OOO Alternative Plus"
|
||||
},
|
||||
previous_anchor_type: "organization",
|
||||
previous_anchor_value: "OOO Alternative Plus"
|
||||
}
|
||||
}));
|
||||
const resolveAssistantOrchestrationDecision = vi.fn(() => ({
|
||||
runAddressLane: true,
|
||||
livingMode: "address_data",
|
||||
livingReason: "address_lane_triggered",
|
||||
toolGateDecision: "run_address_lane",
|
||||
toolGateReason: "followup_context_detected",
|
||||
orchestrationContract: { schema_version: "assistant_orchestration_contract_v1" }
|
||||
}));
|
||||
const buildAddressLlmPredecomposeContractV1 = vi.fn(({ sourceMessage, canonicalMessage }: { sourceMessage: string; canonicalMessage: string }) => ({
|
||||
schema_version: "address_llm_predecompose_contract_v1",
|
||||
source_message: sourceMessage,
|
||||
canonical_message: canonicalMessage,
|
||||
mode: canonicalMessage === sourceMessage ? "unsupported" : "address_query",
|
||||
intent: canonicalMessage === sourceMessage ? "unknown" : "account_balance_snapshot"
|
||||
}));
|
||||
const rawMessage = "\u0430\u043d\u0430\u043b\u0438\u0437 \u043f\u043e 41 \u0441\u0447\u0435\u0442\u0443 \u0430 \u043d\u0435 01";
|
||||
|
||||
const output = await buildAssistantAddressOrchestrationRuntime(
|
||||
buildInput({
|
||||
userMessage: rawMessage,
|
||||
runAddressLlmPreDecompose: vi.fn(async () => ({
|
||||
attempted: true,
|
||||
applied: true,
|
||||
effectiveMessage:
|
||||
"\u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0430\u043d\u0430\u043b\u0438\u0437 \u043f\u043e \u0441\u0447\u0435\u0442\u0443 41 \u0432\u043c\u0435\u0441\u0442\u043e \u0441\u0447\u0435\u0442\u0430 01",
|
||||
reason: "normalized_fragment_applied",
|
||||
predecomposeContract: {
|
||||
mode: "address_query",
|
||||
intent: "account_balance_snapshot"
|
||||
}
|
||||
})),
|
||||
buildAddressLlmPredecomposeContractV1,
|
||||
resolveAddressFollowupCarryoverContext,
|
||||
resolveAssistantOrchestrationDecision
|
||||
})
|
||||
);
|
||||
|
||||
expect(output.addressInputMessage).toBe(rawMessage);
|
||||
expect(output.addressPreDecompose.applied).toBe(false);
|
||||
expect(output.addressPreDecompose.reason).toBe("followup_raw_message_preferred_over_llm_rewrite");
|
||||
expect(resolveAddressFollowupCarryoverContext).toHaveBeenCalledTimes(2);
|
||||
expect(resolveAssistantOrchestrationDecision).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
rawUserMessage: rawMessage,
|
||||
effectiveAddressUserMessage: rawMessage
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("prefers raw selected-object inventory action over generic canonical drift intent", async () => {
|
||||
const resolveAddressFollowupCarryoverContext = vi.fn(() => ({
|
||||
followupContext: {
|
||||
|
|
|
|||
|
|
@ -66,6 +66,35 @@ describe("assistant capability binding response guard", () => {
|
|||
expect(output.audit.reason_codes).toContain("capability_binding_guard_clarification_reply");
|
||||
});
|
||||
|
||||
it("uses business clarification for nomenclature margin ranking period", () => {
|
||||
const output = applyAssistantCapabilityBindingResponseGuard({
|
||||
assistantReply: "unsafe answer",
|
||||
replyType: "partial_coverage",
|
||||
capabilityBinding: binding({
|
||||
capability_id: "inventory_inventory_margin_ranking_for_nomenclature",
|
||||
capability_contract_id: "inventory_inventory_margin_ranking_for_nomenclature",
|
||||
binding_status: "blocked",
|
||||
binding_action: "clarify",
|
||||
required_anchors: ["period"],
|
||||
provided_anchors: [],
|
||||
missing_anchors: ["period"],
|
||||
requires_focus_object: false,
|
||||
focus_object_binding_status: "not_required",
|
||||
result_shape: "nomenclature_margin_ranking",
|
||||
answer_object_shape: "inventory_margin_ranking",
|
||||
violations: ["required_anchor_missing"],
|
||||
reason_codes: ["required_anchor_missing"]
|
||||
})
|
||||
});
|
||||
|
||||
expect(output.replyType).toBe("partial_coverage");
|
||||
expect(output.assistantReply).toContain("Для рейтинга прибыльности номенклатуры нужен период");
|
||||
expect(output.assistantReply).toContain("выручку");
|
||||
expect(output.assistantReply).toContain("себестоимость реализации");
|
||||
expect(output.assistantReply).not.toContain("period");
|
||||
expect(output.audit.applied).toBe(true);
|
||||
});
|
||||
|
||||
it("turns blocked incompatible transitions into bounded replies", () => {
|
||||
const output = applyAssistantCapabilityBindingResponseGuard({
|
||||
assistantReply: "unsafe answer",
|
||||
|
|
|
|||
|
|
@ -130,6 +130,34 @@ describe("assistant capability runtime binding adapter", () => {
|
|||
expect(binding.violations).toEqual([]);
|
||||
});
|
||||
|
||||
it("treats period_from and period_to as satisfying period anchor", () => {
|
||||
const binding = resolveAssistantCapabilityRuntimeBinding({
|
||||
addressDebug: {
|
||||
capability_id: "inventory_inventory_margin_ranking_for_nomenclature",
|
||||
detected_intent: "inventory_margin_ranking_for_nomenclature",
|
||||
detected_mode: "address_query",
|
||||
capability_layer: "compute",
|
||||
capability_route_mode: "exact",
|
||||
extracted_filters: {
|
||||
period_from: "2020-05-01",
|
||||
period_to: "2020-05-31",
|
||||
organization: "ООО Альтернатива Плюс"
|
||||
},
|
||||
rows_matched: 1,
|
||||
route_expectation_status: "matched"
|
||||
},
|
||||
groundingStatus: "grounded",
|
||||
replyType: "factual"
|
||||
});
|
||||
|
||||
expect(binding.binding_status).toBe("bound");
|
||||
expect(binding.binding_action).toBe("allow");
|
||||
expect(binding.required_anchors).toEqual(["period"]);
|
||||
expect(binding.provided_anchors).toEqual(expect.arrayContaining(["period_from", "period_to"]));
|
||||
expect(binding.missing_anchors).toEqual([]);
|
||||
expect(binding.violations).toEqual([]);
|
||||
});
|
||||
|
||||
it("blocks selected-object capabilities when required anchors are missing", () => {
|
||||
const binding = resolveAssistantCapabilityRuntimeBinding({
|
||||
addressDebug: {
|
||||
|
|
|
|||
Loading…
Reference in New Issue