Стабилизировать маржинальность номенклатуры
This commit is contained in:
parent
f5d86d4bc1
commit
09c6d1aa0e
|
|
@ -44,6 +44,27 @@ Fresh validation cut:
|
||||||
- `npm.cmd run build` passed;
|
- `npm.cmd run build` passed;
|
||||||
- graphify rebuilt to `6371` nodes, `14048` edges, `141` communities.
|
- 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
|
## Current Module Map
|
||||||
|
|
||||||
- `Post-F Semantic Integrity Hardening`: `99%`, operationally closed as a hardening slice and now used as a regression gate.
|
- `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.
|
- 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 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 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)`.
|
- 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.
|
- 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.
|
- 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:
|
For current planning, read:
|
||||||
|
|
||||||
1. `README.md`
|
1. `README.md`
|
||||||
2. this document
|
2. `41 - assistant_context_entry_2026-05-18.md`
|
||||||
3. `31 - inventory_reserve_liquidation_quality_reviewed_route_2026-05-12.md`
|
3. this document
|
||||||
4. `33 - limit_honesty_business_language_2026-05-13.md`
|
4. `31 - inventory_reserve_liquidation_quality_reviewed_route_2026-05-12.md`
|
||||||
5. `32 - financial_counterparty_flow_hints_2026-05-13.md`
|
5. `33 - limit_honesty_business_language_2026-05-13.md`
|
||||||
6. `30 - vendor_procurement_quality_reviewed_route_2026-05-12.md`
|
6. `32 - financial_counterparty_flow_hints_2026-05-13.md`
|
||||||
7. `29 - debt_due_date_aging_reviewed_route_2026-05-10.md`
|
7. `30 - vendor_procurement_quality_reviewed_route_2026-05-12.md`
|
||||||
8. `28 - accounting_profit_margin_reviewed_route_2026-05-10.md`
|
8. `29 - debt_due_date_aging_reviewed_route_2026-05-10.md`
|
||||||
9. `27 - proof_family_enablement_candidates_2026-05-10.md`
|
9. `28 - accounting_profit_margin_reviewed_route_2026-05-10.md`
|
||||||
10. `26 - route_candidate_driven_enablement_loop_2026-05-10.md`
|
10. `27 - proof_family_enablement_candidates_2026-05-10.md`
|
||||||
11. `25 - open_world_route_candidate_promotion_2026-05-10.md`
|
11. `26 - route_candidate_driven_enablement_loop_2026-05-10.md`
|
||||||
12. `34 - large_query_budget_continuation_2026-05-13.md`
|
12. `25 - open_world_route_candidate_promotion_2026-05-10.md`
|
||||||
13. `35 - large_query_continuation_ux_2026-05-13.md`
|
13. `34 - large_query_budget_continuation_2026-05-13.md`
|
||||||
14. `36 - inventory_root_scope_no_warehouse_clarification_2026-05-13.md`
|
14. `35 - large_query_continuation_ux_2026-05-13.md`
|
||||||
15. `37 - debt_mirror_clean_scope_polarity_2026-05-13.md`
|
15. `36 - inventory_root_scope_no_warehouse_clarification_2026-05-13.md`
|
||||||
16. `24 - agentic_semantic_development_loop_and_autorun_hygiene_2026-05-10.md`
|
16. `37 - debt_mirror_clean_scope_polarity_2026-05-13.md`
|
||||||
17. `23 - current_execution_spine_and_semantic_control_gate_2026-05-05.md`
|
17. `24 - agentic_semantic_development_loop_and_autorun_hygiene_2026-05-10.md`
|
||||||
18. `22 - open_world_bounded_autonomy_breadth_2026-05-01.md`
|
18. `23 - current_execution_spine_and_semantic_control_gate_2026-05-05.md`
|
||||||
19. `20 - planner_autonomy_consolidation_2026-05-01.md`
|
19. `22 - open_world_bounded_autonomy_breadth_2026-05-01.md`
|
||||||
20. `19 - inventory_stock_open_world_breadth_proof_2026-05-01.md`
|
20. `20 - planner_autonomy_consolidation_2026-05-01.md`
|
||||||
21. `40 - mixed_schema_primitive_closure_replay_2026-05-13.md`
|
21. `19 - inventory_stock_open_world_breadth_proof_2026-05-01.md`
|
||||||
22. `39 - generic_role_tail_anchor_hygiene_2026-05-13.md`
|
22. `40 - mixed_schema_primitive_closure_replay_2026-05-13.md`
|
||||||
23. `17 - post_f_semantic_integrity_hardening_2026-04-23.md`
|
23. `39 - generic_role_tail_anchor_hygiene_2026-05-13.md`
|
||||||
24. `16 - data_need_graph_and_open_world_mcp_plan_2026-04-22.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.
|
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)
|
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)
|
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)
|
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.
|
This package is no longer planning-only.
|
||||||
|
|
||||||
Status canon for planning:
|
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 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 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.
|
- 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 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 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 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:
|
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`
|
39. `38 - financial_role_purpose_arbitration_2026-05-13.md`
|
||||||
40. `39 - generic_role_tail_anchor_hygiene_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`
|
41. `40 - mixed_schema_primitive_closure_replay_2026-05-13.md`
|
||||||
|
42. `41 - assistant_context_entry_2026-05-18.md`
|
||||||
|
|
||||||
## Planning Rules
|
## Planning Rules
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,12 @@
|
||||||
"expected_requested_result_modes": ["confirmed_balance"],
|
"expected_requested_result_modes": ["confirmed_balance"],
|
||||||
"expected_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",
|
"intent": "inventory_purchase_to_sale_chain",
|
||||||
"expected_selected_recipes": ["address_inventory_purchase_to_sale_chain_v1"],
|
"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_purchase_documents_for_item",
|
||||||
"inventory_supplier_stock_overlap_as_of_date",
|
"inventory_supplier_stock_overlap_as_of_date",
|
||||||
"inventory_sale_trace_for_item",
|
"inventory_sale_trace_for_item",
|
||||||
|
"inventory_margin_ranking_for_nomenclature",
|
||||||
"inventory_profitability_for_item",
|
"inventory_profitability_for_item",
|
||||||
"inventory_purchase_to_sale_chain",
|
"inventory_purchase_to_sale_chain",
|
||||||
"inventory_aging_by_purchase_date",
|
"inventory_aging_by_purchase_date",
|
||||||
|
|
@ -68,6 +69,7 @@ function defaultCapabilityId(intent) {
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date") {
|
intent === "inventory_aging_by_purchase_date") {
|
||||||
|
|
@ -151,12 +153,15 @@ function resolveCapabilityEnabled(intent) {
|
||||||
if (intent === "inventory_purchase_provenance_for_item" ||
|
if (intent === "inventory_purchase_provenance_for_item" ||
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain") {
|
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 {
|
return {
|
||||||
enabled: true,
|
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") {
|
if (intent === "inventory_purchase_to_sale_chain") {
|
||||||
|
|
@ -249,6 +254,7 @@ function resolveShadowRouteIntent(intent, requestedResultMode) {
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date") {
|
intent === "inventory_aging_by_purchase_date") {
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,7 @@ function isConfirmedBalanceIntent(intent) {
|
||||||
intent === "inventory_purchase_provenance_for_item" ||
|
intent === "inventory_purchase_provenance_for_item" ||
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "open_contracts_confirmed_as_of_date" ||
|
intent === "open_contracts_confirmed_as_of_date" ||
|
||||||
|
|
|
||||||
|
|
@ -971,6 +971,7 @@ function isInventoryTraceIntent(intent) {
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date");
|
intent === "inventory_aging_by_purchase_date");
|
||||||
|
|
@ -989,6 +990,7 @@ function usesRecipeDefaultLimit(intent) {
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date");
|
intent === "inventory_aging_by_purchase_date");
|
||||||
|
|
@ -1407,6 +1409,9 @@ function requiredFiltersByIntent(intent) {
|
||||||
intent === "inventory_purchase_to_sale_chain") {
|
intent === "inventory_purchase_to_sale_chain") {
|
||||||
return ["item"];
|
return ["item"];
|
||||||
}
|
}
|
||||||
|
if (intent === "inventory_margin_ranking_for_nomenclature") {
|
||||||
|
return ["period_from", "period_to"];
|
||||||
|
}
|
||||||
if (intent === "payables_confirmed_as_of_date") {
|
if (intent === "payables_confirmed_as_of_date") {
|
||||||
return ["as_of_date"];
|
return ["as_of_date"];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1664,6 +1664,17 @@ function hasBidirectionalValueFlowComparisonSignal(text) {
|
||||||
const hasNetAmountCue = /(?:сколько|сумм|итог|нетто|сальдо|минус|net|total|sum)/iu.test(normalized);
|
const hasNetAmountCue = /(?:сколько|сумм|итог|нетто|сальдо|минус|net|total|sum)/iu.test(normalized);
|
||||||
return hasIncomingCue && hasOutgoingCue && hasComparisonCue && (hasValueFlowCue || hasNetAmountCue);
|
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) {
|
function hasVatPeriodInspectionBridgeSignal(text) {
|
||||||
const normalized = String(text ?? "").trim().toLowerCase();
|
const normalized = String(text ?? "").trim().toLowerCase();
|
||||||
if (!/(?:ндс|vat)/iu.test(normalized)) {
|
if (!/(?:ндс|vat)/iu.test(normalized)) {
|
||||||
|
|
@ -1736,6 +1747,9 @@ function resolveUnicodeAddressIntentBridge(text) {
|
||||||
if (hasSelectedObjectProfitabilityCue) {
|
if (hasSelectedObjectProfitabilityCue) {
|
||||||
return unicodeBridgeResolution("inventory_profitability_for_item", "high", "unicode_selected_object_profitability_bridge_signal_detected");
|
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) &&
|
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);
|
/(?:сч(?:е|ё)т(?:а|у|ом|е|ов)?\s*(?:№|#)?\s*(?:60|62|76)(?:[.,]\d{1,2})?|\b(?:60|62|76)(?:[.,]\d{1,2})?\b\s*сч(?:е|ё)т)/iu.test(normalized);
|
||||||
if (hasOpenItemsAccountCue) {
|
if (hasOpenItemsAccountCue) {
|
||||||
|
|
|
||||||
|
|
@ -1647,6 +1647,7 @@ function isOrganizationScopedInventoryIntent(intent) {
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date");
|
intent === "inventory_aging_by_purchase_date");
|
||||||
|
|
@ -1680,6 +1681,7 @@ function shouldDeferInventoryOrganizationClarification(intent, filters, semantic
|
||||||
return (intent === "inventory_purchase_provenance_for_item" ||
|
return (intent === "inventory_purchase_provenance_for_item" ||
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date");
|
intent === "inventory_aging_by_purchase_date");
|
||||||
|
|
@ -1981,6 +1983,7 @@ function canAutoBroadenPeriodWindow(intent, filters) {
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date");
|
intent === "inventory_aging_by_purchase_date");
|
||||||
|
|
@ -1990,6 +1993,7 @@ function shouldBoostAutoBroadenedLimit(intent) {
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date");
|
intent === "inventory_aging_by_purchase_date");
|
||||||
|
|
@ -2520,6 +2524,9 @@ async function tryComposeLlmLimitedReply(input) {
|
||||||
if (process.env.VITEST === "true" || process.env.NODE_ENV === "test") {
|
if (process.env.VITEST === "true" || process.env.NODE_ENV === "test") {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
if (input.intent === "inventory_margin_ranking_for_nomenclature" && input.category === "missing_anchor") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
if (!shouldUseLlmLimitedReply(input.category)) {
|
if (!shouldUseLlmLimitedReply(input.category)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -2580,6 +2587,13 @@ function composeLimitedReply(input) {
|
||||||
.map((item) => normalizeMissingAnchorLabel(String(item ?? "").trim()))
|
.map((item) => normalizeMissingAnchorLabel(String(item ?? "").trim()))
|
||||||
.filter((item) => item.length > 0)));
|
.filter((item) => item.length > 0)));
|
||||||
const missingAnchorPhrase = missingAnchorLabels.length > 0 ? missingAnchorLabels.join(", ") : "контрагент, договор, счет или период";
|
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"
|
const heading = input.category === "empty_match"
|
||||||
? pickDeterministicVariant(headingSeed, [
|
? pickDeterministicVariant(headingSeed, [
|
||||||
"По текущим условиям в доступном срезе данных совпадений не нашлось.",
|
"По текущим условиям в доступном срезе данных совпадений не нашлось.",
|
||||||
|
|
|
||||||
|
|
@ -966,6 +966,17 @@ const BASE_RECIPES = [
|
||||||
account_scope_mode: "strict",
|
account_scope_mode: "strict",
|
||||||
query_template: "inventory_trading_margin_proxy_profile"
|
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",
|
recipe_id: "address_inventory_purchase_to_sale_chain_v1",
|
||||||
intent: "inventory_purchase_to_sale_chain",
|
intent: "inventory_purchase_to_sale_chain",
|
||||||
|
|
@ -1714,6 +1725,7 @@ function maxLimitForIntent(intent) {
|
||||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
intent === "inventory_trading_margin_proxy_for_organization" ||
|
intent === "inventory_trading_margin_proxy_for_organization" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date" ||
|
intent === "inventory_aging_by_purchase_date" ||
|
||||||
|
|
@ -1904,6 +1916,8 @@ function buildAddressRecipePlan(recipe, filters) {
|
||||||
: recipe.query_template === "inventory_profitability_profile"
|
: recipe.query_template === "inventory_profitability_profile"
|
||||||
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
||||||
: recipe.query_template === "inventory_trading_margin_proxy_profile"
|
: recipe.query_template === "inventory_trading_margin_proxy_profile"
|
||||||
|
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
||||||
|
: recipe.query_template === "inventory_margin_ranking_profile"
|
||||||
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
||||||
: recipe.query_template === "inventory_purchase_to_sale_chain_profile"
|
: recipe.query_template === "inventory_purchase_to_sale_chain_profile"
|
||||||
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ exports.hasBareInventoryPurchaseDateFollowupCue = hasBareInventoryPurchaseDateFo
|
||||||
exports.hasInventorySaleFollowupCue = hasInventorySaleFollowupCue;
|
exports.hasInventorySaleFollowupCue = hasInventorySaleFollowupCue;
|
||||||
exports.hasInventoryPurchaseToSaleChainFollowupCue = hasInventoryPurchaseToSaleChainFollowupCue;
|
exports.hasInventoryPurchaseToSaleChainFollowupCue = hasInventoryPurchaseToSaleChainFollowupCue;
|
||||||
exports.hasInventoryPurchaseDateVatBridgeCue = hasInventoryPurchaseDateVatBridgeCue;
|
exports.hasInventoryPurchaseDateVatBridgeCue = hasInventoryPurchaseDateVatBridgeCue;
|
||||||
|
exports.hasInventoryMarginRankingFollowupCue = hasInventoryMarginRankingFollowupCue;
|
||||||
exports.hasAddressFollowupContextSignal = hasAddressFollowupContextSignal;
|
exports.hasAddressFollowupContextSignal = hasAddressFollowupContextSignal;
|
||||||
exports.runAddressDecomposeStage = runAddressDecomposeStage;
|
exports.runAddressDecomposeStage = runAddressDecomposeStage;
|
||||||
const addressQueryClassifier_1 = require("../addressQueryClassifier");
|
const addressQueryClassifier_1 = require("../addressQueryClassifier");
|
||||||
|
|
@ -321,6 +322,7 @@ function isInventoryIntent(intent) {
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date");
|
intent === "inventory_aging_by_purchase_date");
|
||||||
|
|
@ -332,6 +334,7 @@ function isInventoryDrilldownFrameIntent(intent) {
|
||||||
return (intent === "inventory_purchase_provenance_for_item" ||
|
return (intent === "inventory_purchase_provenance_for_item" ||
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date");
|
intent === "inventory_aging_by_purchase_date");
|
||||||
|
|
@ -340,6 +343,7 @@ function isInventoryLifecycleHistoryIntent(intent) {
|
||||||
return (intent === "inventory_purchase_provenance_for_item" ||
|
return (intent === "inventory_purchase_provenance_for_item" ||
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain");
|
intent === "inventory_purchase_to_sale_chain");
|
||||||
}
|
}
|
||||||
|
|
@ -625,11 +629,29 @@ function hasInventoryPurchaseDateVatBridgeCue(text) {
|
||||||
return (/(?:ндс|vat)/iu.test(normalized) &&
|
return (/(?:ндс|vat)/iu.test(normalized) &&
|
||||||
/(?:на\s+дат[ауеы]\s+покупк|на\s+дат[ауеы]\s+закупк|по\s+дат[еу]\s+покупк|по\s+дат[еу]\s+закупк|дата\s+покупк|дата\s+закупк|purchase\s+date)/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) {
|
function hasAddressFollowupContextSignal(text) {
|
||||||
const normalized = String(text ?? "").trim();
|
const normalized = String(text ?? "").trim();
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
return false;
|
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)) {
|
if (/(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+ней|по\s+нему|по\s+ним|for\s+selected\s+object|selected\s+object)/iu.test(normalized)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -867,6 +889,7 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date" ||
|
intent === "inventory_aging_by_purchase_date" ||
|
||||||
|
|
@ -919,6 +942,7 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||||
if ((intent === "inventory_purchase_provenance_for_item" ||
|
if ((intent === "inventory_purchase_provenance_for_item" ||
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date")) {
|
intent === "inventory_aging_by_purchase_date")) {
|
||||||
|
|
@ -1122,6 +1146,7 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date" ||
|
intent === "inventory_aging_by_purchase_date" ||
|
||||||
|
|
@ -1134,6 +1159,8 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||||
const currentContractExplicit = toNonEmptyString(merged.contract);
|
const currentContractExplicit = toNonEmptyString(merged.contract);
|
||||||
const currentItemExplicit = toNonEmptyString(merged.item);
|
const currentItemExplicit = toNonEmptyString(merged.item);
|
||||||
const currentAccountExplicit = toNonEmptyString(merged.account);
|
const currentAccountExplicit = toNonEmptyString(merged.account);
|
||||||
|
const currentAccountRefinesMarginDomain = intent === "inventory_margin_ranking_for_nomenclature" &&
|
||||||
|
hasInventoryMarginRankingFollowupCue(userMessage);
|
||||||
const shouldSuppressGenericPeriodCarryover = (Boolean(currentCounterpartyExplicit) &&
|
const shouldSuppressGenericPeriodCarryover = (Boolean(currentCounterpartyExplicit) &&
|
||||||
!isLowQualityCounterpartyAnchor(currentCounterpartyExplicit) &&
|
!isLowQualityCounterpartyAnchor(currentCounterpartyExplicit) &&
|
||||||
currentCounterpartyExplicit !== previousCounterparty) ||
|
currentCounterpartyExplicit !== previousCounterparty) ||
|
||||||
|
|
@ -1141,7 +1168,7 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||||
!isLowQualityContractAnchor(currentContractExplicit) &&
|
!isLowQualityContractAnchor(currentContractExplicit) &&
|
||||||
currentContractExplicit !== previousContract) ||
|
currentContractExplicit !== previousContract) ||
|
||||||
(Boolean(currentItemExplicit) && currentItemExplicit !== previousItem) ||
|
(Boolean(currentItemExplicit) && currentItemExplicit !== previousItem) ||
|
||||||
(Boolean(currentAccountExplicit) && currentAccountExplicit !== previousAccount);
|
(Boolean(currentAccountExplicit) && currentAccountExplicit !== previousAccount && !currentAccountRefinesMarginDomain);
|
||||||
const vatRelativeMonthFollowup = relativeMonthFromFollowupYear &&
|
const vatRelativeMonthFollowup = relativeMonthFromFollowupYear &&
|
||||||
(intent === "vat_payable_confirmed_as_of_date" ||
|
(intent === "vat_payable_confirmed_as_of_date" ||
|
||||||
intent === "vat_payable_forecast" ||
|
intent === "vat_payable_forecast" ||
|
||||||
|
|
@ -1189,6 +1216,19 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
|
||||||
}
|
}
|
||||||
reasons.push("period_from_followup_context");
|
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 &&
|
if (!currentHasPeriod &&
|
||||||
previousHasPeriod &&
|
previousHasPeriod &&
|
||||||
hasFollowupSignal &&
|
hasFollowupSignal &&
|
||||||
|
|
@ -1251,6 +1291,7 @@ function resolveMissingRequiredFilters(intent, filters) {
|
||||||
account_balance_snapshot: ["account", "as_of_date"],
|
account_balance_snapshot: ["account", "as_of_date"],
|
||||||
documents_forming_balance: ["account", "as_of_date"],
|
documents_forming_balance: ["account", "as_of_date"],
|
||||||
inventory_on_hand_as_of_date: ["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"],
|
inventory_profitability_for_item: ["item"],
|
||||||
open_contracts_confirmed_as_of_date: ["as_of_date"],
|
open_contracts_confirmed_as_of_date: ["as_of_date"],
|
||||||
payables_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 hasExplicitInventoryItemReference = /(?:товар|номенклатур|позици|склад|остат|sku|item|product|товар|номенклатур|позици|склад|остат)/iu.test(normalizedMessage) || hasSelectedObjectInlineSnapshotMetadata(normalizedMessage);
|
||||||
const staleInventoryLineageCanYieldToCounterparty = previousCounterpartyLaneActive && !hasExplicitInventoryItemReference;
|
const staleInventoryLineageCanYieldToCounterparty = previousCounterpartyLaneActive && !hasExplicitInventoryItemReference;
|
||||||
const inventoryPurchaseDateVatBridge = inventorySelectedObjectFollowup && hasInventoryPurchaseDateVatBridgeCue(normalizedMessage);
|
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 &&
|
if (inventoryPurchaseDateVatBridge &&
|
||||||
(detectedIntent.intent === "unknown" ||
|
(detectedIntent.intent === "unknown" ||
|
||||||
detectedIntent.intent === sourceIntent ||
|
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;
|
const asOfDate = typeof options.asOfDate === "string" && options.asOfDate.trim().length > 0 ? options.asOfDate : null;
|
||||||
return asOfDate ? `до ${deps.formatDateRu(asOfDate)}` : "по доступной выборке";
|
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) {
|
function composeInventoryReply(intent, rows, options, deps) {
|
||||||
if (intent === "inventory_on_hand_as_of_date") {
|
if (intent === "inventory_on_hand_as_of_date") {
|
||||||
const asOfDate = deps.resolvePayablesAsOfDate(options);
|
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.buildFactualListReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)(summary.counterparties.length > 0 ? "strong" : "medium", true))
|
||||||
: (0, replyContracts_1.buildFactualSummaryReply)(lines, (0, replyContracts_1.buildConfirmedBalanceSemantics)("medium", false));
|
: (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") {
|
if (intent === "inventory_profitability_for_item") {
|
||||||
const purchaseRows = rows.filter((row) => deps.isInventoryPurchaseMovement(row));
|
const purchaseRows = rows.filter((row) => deps.isInventoryPurchaseMovement(row));
|
||||||
const saleRows = rows.filter((row) => deps.isInventorySaleMovement(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_provenance_for_item" ||
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date");
|
intent === "inventory_aging_by_purchase_date");
|
||||||
|
|
@ -74,6 +75,15 @@ function isGenericCanonicalDriftIntent(intent) {
|
||||||
intent === "bank_operations_by_contract" ||
|
intent === "bank_operations_by_contract" ||
|
||||||
intent === "documents_forming_balance");
|
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) {
|
function hasSameDateFollowupSignal(text) {
|
||||||
return /(?:эту\s+же\s+дат(?:у|е|ой)|ту\s+же\s+дат(?:у|е|ой)|same\s+date)/iu.test(String(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 hasInventoryItemCarryover = previousAnchorType === "item" && isInventorySelectedObjectOrRootIntent(previousIntent);
|
||||||
const hasInventoryFrameCarryover = isInventorySelectedObjectOrRootIntent(previousIntent) ||
|
const hasInventoryFrameCarryover = isInventorySelectedObjectOrRootIntent(previousIntent) ||
|
||||||
isInventorySelectedObjectOrRootIntent(rootIntent);
|
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";
|
const hasDocumentCarryover = previousIntent === "list_documents_by_counterparty" || previousIntent === "list_documents_by_contract";
|
||||||
if (mode === "unsupported" && intent === "unknown") {
|
if (mode === "unsupported" && intent === "unknown") {
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -132,6 +146,10 @@ function shouldPreferRawFollowupMessage(userMessage, addressInputMessage, carryo
|
||||||
(intent === "account_balance_snapshot" || intent === "documents_forming_balance" || intent === "unknown")) {
|
(intent === "account_balance_snapshot" || intent === "documents_forming_balance" || intent === "unknown")) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (hasInventoryMarginRankingAccountCorrection &&
|
||||||
|
(intent === "account_balance_snapshot" || intent === "documents_forming_balance" || intent === "unknown")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return ((hasSelectedObjectInventorySignal(rawMessage) || hasInventoryItemCarryover) &&
|
return ((hasSelectedObjectInventorySignal(rawMessage) || hasInventoryItemCarryover) &&
|
||||||
(hasSelectedObjectInventoryActionCue(rawMessage) || hasShortInventoryPurchaseFollowupCue(rawMessage)) &&
|
(hasSelectedObjectInventoryActionCue(rawMessage) || hasShortInventoryPurchaseFollowupCue(rawMessage)) &&
|
||||||
(isGenericCanonicalDriftIntent(intent) || intent === "unknown"));
|
(isGenericCanonicalDriftIntent(intent) || intent === "unknown"));
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,13 @@ function formatMissingAnchors(anchors) {
|
||||||
return anchors.join(", ");
|
return anchors.join(", ");
|
||||||
}
|
}
|
||||||
function buildClarificationReply(binding) {
|
function buildClarificationReply(binding) {
|
||||||
|
if (binding.capability_contract_id === "inventory_inventory_margin_ranking_for_nomenclature") {
|
||||||
|
return [
|
||||||
|
"Для рейтинга прибыльности номенклатуры нужен период.",
|
||||||
|
"Могу посчитать по номенклатуре: выручку без НДС, себестоимость реализации, валовую прибыль и маржинальность.",
|
||||||
|
"Уточните период: месяц, квартал, год или весь доступный период."
|
||||||
|
].join("\n\n");
|
||||||
|
}
|
||||||
return [
|
return [
|
||||||
"Нужно уточнение, чтобы не подставить неподтвержденный объект в расчет.",
|
"Нужно уточнение, чтобы не подставить неподтвержденный объект в расчет.",
|
||||||
`Не хватает: ${formatMissingAnchors(binding.missing_anchors)}.`,
|
`Не хватает: ${formatMissingAnchors(binding.missing_anchors)}.`,
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,13 @@ function anchorSatisfied(requiredAnchor, providedAnchors, debug) {
|
||||||
if (providedAnchors.includes(requiredAnchor)) {
|
if (providedAnchors.includes(requiredAnchor)) {
|
||||||
return true;
|
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") {
|
if (requiredAnchor === "item") {
|
||||||
return (providedAnchors.includes("selected_object") ||
|
return (providedAnchors.includes("selected_object") ||
|
||||||
providedAnchors.includes("anchor_item") ||
|
providedAnchors.includes("anchor_item") ||
|
||||||
|
|
|
||||||
|
|
@ -141,6 +141,7 @@ function isDetectedIntentAlignedWithTurnMeaning(detectedIntent, turnMeaning) {
|
||||||
if (normalizedIntent === "inventory_purchase_provenance_for_item" ||
|
if (normalizedIntent === "inventory_purchase_provenance_for_item" ||
|
||||||
normalizedIntent === "inventory_purchase_documents_for_item" ||
|
normalizedIntent === "inventory_purchase_documents_for_item" ||
|
||||||
normalizedIntent === "inventory_sale_trace_for_item" ||
|
normalizedIntent === "inventory_sale_trace_for_item" ||
|
||||||
|
normalizedIntent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
normalizedIntent === "inventory_profitability_for_item" ||
|
normalizedIntent === "inventory_profitability_for_item" ||
|
||||||
normalizedIntent === "inventory_purchase_to_sale_chain") {
|
normalizedIntent === "inventory_purchase_to_sale_chain") {
|
||||||
return askedDomain === "inventory";
|
return askedDomain === "inventory";
|
||||||
|
|
@ -183,7 +184,7 @@ function isExplicitMetadataDiscoveryTurn(entryPoint) {
|
||||||
reasonCodes.some((reason) => toNonEmptyString(reason) === "mcp_discovery_metadata_signal_detected"));
|
reasonCodes.some((reason) => toNonEmptyString(reason) === "mcp_discovery_metadata_signal_detected"));
|
||||||
}
|
}
|
||||||
function isInventoryExactAddressIntent(intent) {
|
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) {
|
function hasMetadataDiscoveryPriority(input, entryPoint) {
|
||||||
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
|
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ const ADDRESS_INTENTS_KEEP_ADDRESS_LANE = new Set([
|
||||||
"inventory_purchase_documents_for_item",
|
"inventory_purchase_documents_for_item",
|
||||||
"inventory_supplier_stock_overlap_as_of_date",
|
"inventory_supplier_stock_overlap_as_of_date",
|
||||||
"inventory_sale_trace_for_item",
|
"inventory_sale_trace_for_item",
|
||||||
|
"inventory_margin_ranking_for_nomenclature",
|
||||||
"inventory_profitability_for_item",
|
"inventory_profitability_for_item",
|
||||||
"inventory_purchase_to_sale_chain",
|
"inventory_purchase_to_sale_chain",
|
||||||
"inventory_aging_by_purchase_date",
|
"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_provenance_for_item",
|
||||||
"inventory_purchase_documents_for_item",
|
"inventory_purchase_documents_for_item",
|
||||||
"inventory_sale_trace_for_item",
|
"inventory_sale_trace_for_item",
|
||||||
|
"inventory_margin_ranking_for_nomenclature",
|
||||||
"inventory_profitability_for_item",
|
"inventory_profitability_for_item",
|
||||||
"inventory_purchase_to_sale_chain"
|
"inventory_purchase_to_sale_chain"
|
||||||
]);
|
]);
|
||||||
|
|
@ -204,7 +206,7 @@ function createAssistantRoutePolicy(deps) {
|
||||||
: null;
|
: null;
|
||||||
const semanticCanonicalRecommended = semanticExtractionContract?.apply_canonical_recommended !== false;
|
const semanticCanonicalRecommended = semanticExtractionContract?.apply_canonical_recommended !== false;
|
||||||
const llmSupportedDeepAddressIntentSignal = llmContractMode === "deep_analysis" &&
|
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;
|
semanticCanonicalRecommended;
|
||||||
const llmCanonicalEntitySignal = /(?:заказчик|поставщик|контрагент|компан|customer|supplier|counterparty|company|vendor|client)/iu.test(compactWhitespace(repairedInputMessage.toLowerCase()));
|
const llmCanonicalEntitySignal = /(?:заказчик|поставщик|контрагент|компан|customer|supplier|counterparty|company|vendor|client)/iu.test(compactWhitespace(repairedInputMessage.toLowerCase()));
|
||||||
const llmCanonicalAppliedSignal = Boolean(llmPreDecomposeMeta?.applied) && llmContractMode !== "deep_analysis";
|
const llmCanonicalAppliedSignal = Boolean(llmPreDecomposeMeta?.applied) && llmContractMode !== "deep_analysis";
|
||||||
|
|
|
||||||
|
|
@ -310,6 +310,18 @@ exports.INVENTORY_CAPABILITY_CONTRACTS = [
|
||||||
answerObjectShape: "inventory_profitability_bundle",
|
answerObjectShape: "inventory_profitability_bundle",
|
||||||
bundleReusePolicy: "sale_trace_bundle_preferred"
|
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({
|
inventoryExactCapability({
|
||||||
capability_id: "inventory_inventory_purchase_to_sale_chain",
|
capability_id: "inventory_inventory_purchase_to_sale_chain",
|
||||||
intent_ids: ["inventory_purchase_to_sale_chain"],
|
intent_ids: ["inventory_purchase_to_sale_chain"],
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,26 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
function normalizeFollowupText(value) {
|
function normalizeFollowupText(value) {
|
||||||
return deps.compactWhitespace(deps.repairAddressMojibake(String(value ?? "")).toLowerCase()).replace(/ё/g, "е");
|
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) {
|
function hasSamePeriodReferenceCue(...values) {
|
||||||
return values
|
return values
|
||||||
.map((value) => normalizeFollowupText(value))
|
.map((value) => normalizeFollowupText(value))
|
||||||
|
|
@ -545,6 +565,7 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
sourceIntentHint === "inventory_supplier_stock_overlap_as_of_date" ||
|
sourceIntentHint === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
deps.isInventorySelectedObjectIntent(sourceIntentHint)));
|
deps.isInventorySelectedObjectIntent(sourceIntentHint)));
|
||||||
const inventoryPurchaseDateVatBridge = hasInventoryPurchaseDateVatBridgeSignal(userMessage, alternateMessage, sourceIntentHint, hasNavigationInventoryItemFocusHint);
|
const inventoryPurchaseDateVatBridge = hasInventoryPurchaseDateVatBridgeSignal(userMessage, alternateMessage, sourceIntentHint, hasNavigationInventoryItemFocusHint);
|
||||||
|
const inventoryMarginRankingFollowup = hasInventoryMarginRankingFollowupSignal(userMessage, alternateMessage, sourceIntentHint);
|
||||||
let inventoryShortFollowupPrimary = (deps.isInventorySelectedObjectIntent(sourceIntentHint) || hasNavigationInventoryItemFocusHint) &&
|
let inventoryShortFollowupPrimary = (deps.isInventorySelectedObjectIntent(sourceIntentHint) || hasNavigationInventoryItemFocusHint) &&
|
||||||
deps.hasShortInventoryObjectFollowupSignal(userMessage);
|
deps.hasShortInventoryObjectFollowupSignal(userMessage);
|
||||||
let inventoryShortFollowupAlternate = (deps.isInventorySelectedObjectIntent(sourceIntentHint) || hasNavigationInventoryItemFocusHint) &&
|
let inventoryShortFollowupAlternate = (deps.isInventorySelectedObjectIntent(sourceIntentHint) || hasNavigationInventoryItemFocusHint) &&
|
||||||
|
|
@ -573,6 +594,7 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
businessOverviewBoundaryFollowupPrimary ||
|
businessOverviewBoundaryFollowupPrimary ||
|
||||||
inventoryShortFollowupPrimary ||
|
inventoryShortFollowupPrimary ||
|
||||||
inventoryPurchaseDateVatBridge ||
|
inventoryPurchaseDateVatBridge ||
|
||||||
|
inventoryMarginRankingFollowup ||
|
||||||
explicitSummaryBundleReuseSignal ||
|
explicitSummaryBundleReuseSignal ||
|
||||||
mcpDiscoveryOrganizationClarificationContinuation;
|
mcpDiscoveryOrganizationClarificationContinuation;
|
||||||
let hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage)
|
let hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage)
|
||||||
|
|
@ -582,6 +604,7 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
businessOverviewBoundaryFollowupAlternate ||
|
businessOverviewBoundaryFollowupAlternate ||
|
||||||
inventoryShortFollowupAlternate ||
|
inventoryShortFollowupAlternate ||
|
||||||
inventoryPurchaseDateVatBridge ||
|
inventoryPurchaseDateVatBridge ||
|
||||||
|
inventoryMarginRankingFollowup ||
|
||||||
explicitSummaryBundleReuseSignal ||
|
explicitSummaryBundleReuseSignal ||
|
||||||
mcpDiscoveryOrganizationClarificationContinuation
|
mcpDiscoveryOrganizationClarificationContinuation
|
||||||
: false;
|
: false;
|
||||||
|
|
@ -622,6 +645,7 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
shortValueFlowRetargetAlternate ||
|
shortValueFlowRetargetAlternate ||
|
||||||
businessOverviewBoundaryFollowupPrimary ||
|
businessOverviewBoundaryFollowupPrimary ||
|
||||||
businessOverviewBoundaryFollowupAlternate ||
|
businessOverviewBoundaryFollowupAlternate ||
|
||||||
|
inventoryMarginRankingFollowup ||
|
||||||
deps.hasFollowupMarker(userMessage) ||
|
deps.hasFollowupMarker(userMessage) ||
|
||||||
deps.hasReferentialPointer(userMessage) ||
|
deps.hasReferentialPointer(userMessage) ||
|
||||||
(deps.toNonEmptyString(alternateMessage)
|
(deps.toNonEmptyString(alternateMessage)
|
||||||
|
|
@ -645,6 +669,7 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
shortValueFlowRetargetAlternate ||
|
shortValueFlowRetargetAlternate ||
|
||||||
businessOverviewBoundaryFollowupPrimary ||
|
businessOverviewBoundaryFollowupPrimary ||
|
||||||
businessOverviewBoundaryFollowupAlternate ||
|
businessOverviewBoundaryFollowupAlternate ||
|
||||||
|
inventoryMarginRankingFollowup ||
|
||||||
deps.hasFollowupMarker(userMessage) ||
|
deps.hasFollowupMarker(userMessage) ||
|
||||||
deps.hasReferentialPointer(userMessage) ||
|
deps.hasReferentialPointer(userMessage) ||
|
||||||
(deps.toNonEmptyString(alternateMessage)
|
(deps.toNonEmptyString(alternateMessage)
|
||||||
|
|
@ -757,6 +782,7 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
!inventoryShortFollowupAlternate &&
|
!inventoryShortFollowupAlternate &&
|
||||||
!businessOverviewBoundaryFollowupPrimary &&
|
!businessOverviewBoundaryFollowupPrimary &&
|
||||||
!businessOverviewBoundaryFollowupAlternate &&
|
!businessOverviewBoundaryFollowupAlternate &&
|
||||||
|
!inventoryMarginRankingFollowup &&
|
||||||
!foreignAccountingPivotOverInventory &&
|
!foreignAccountingPivotOverInventory &&
|
||||||
!deps.hasFollowupMarker(userMessage) &&
|
!deps.hasFollowupMarker(userMessage) &&
|
||||||
!deps.hasReferentialPointer(userMessage) &&
|
!deps.hasReferentialPointer(userMessage) &&
|
||||||
|
|
@ -816,6 +842,7 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
businessOverviewBoundaryFollowupPrimary ||
|
businessOverviewBoundaryFollowupPrimary ||
|
||||||
inventoryShortFollowupPrimary ||
|
inventoryShortFollowupPrimary ||
|
||||||
inventoryPurchaseDateVatBridge ||
|
inventoryPurchaseDateVatBridge ||
|
||||||
|
inventoryMarginRankingFollowup ||
|
||||||
explicitSummaryBundleReuseSignal ||
|
explicitSummaryBundleReuseSignal ||
|
||||||
hasInventoryRootTemporalFollowupPrimary ||
|
hasInventoryRootTemporalFollowupPrimary ||
|
||||||
mcpDiscoveryOrganizationClarificationContinuation;
|
mcpDiscoveryOrganizationClarificationContinuation;
|
||||||
|
|
@ -827,6 +854,7 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
businessOverviewBoundaryFollowupAlternate ||
|
businessOverviewBoundaryFollowupAlternate ||
|
||||||
inventoryShortFollowupAlternate ||
|
inventoryShortFollowupAlternate ||
|
||||||
inventoryPurchaseDateVatBridge ||
|
inventoryPurchaseDateVatBridge ||
|
||||||
|
inventoryMarginRankingFollowup ||
|
||||||
explicitSummaryBundleReuseSignal ||
|
explicitSummaryBundleReuseSignal ||
|
||||||
hasInventoryRootTemporalFollowupAlternate ||
|
hasInventoryRootTemporalFollowupAlternate ||
|
||||||
mcpDiscoveryOrganizationClarificationContinuation
|
mcpDiscoveryOrganizationClarificationContinuation
|
||||||
|
|
@ -849,6 +877,7 @@ function createAssistantTransitionPolicy(deps) {
|
||||||
shortValueFlowRetargetAlternate ||
|
shortValueFlowRetargetAlternate ||
|
||||||
businessOverviewBoundaryFollowupPrimary ||
|
businessOverviewBoundaryFollowupPrimary ||
|
||||||
businessOverviewBoundaryFollowupAlternate ||
|
businessOverviewBoundaryFollowupAlternate ||
|
||||||
|
inventoryMarginRankingFollowup ||
|
||||||
deps.hasFollowupMarker(userMessage) ||
|
deps.hasFollowupMarker(userMessage) ||
|
||||||
deps.hasReferentialPointer(userMessage) ||
|
deps.hasReferentialPointer(userMessage) ||
|
||||||
(deps.toNonEmptyString(alternateMessage)
|
(deps.toNonEmptyString(alternateMessage)
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ const SUPPORTED_ADDRESS_INTENTS = new Set([
|
||||||
"inventory_purchase_documents_for_item",
|
"inventory_purchase_documents_for_item",
|
||||||
"inventory_supplier_stock_overlap_as_of_date",
|
"inventory_supplier_stock_overlap_as_of_date",
|
||||||
"inventory_sale_trace_for_item",
|
"inventory_sale_trace_for_item",
|
||||||
|
"inventory_margin_ranking_for_nomenclature",
|
||||||
"inventory_profitability_for_item",
|
"inventory_profitability_for_item",
|
||||||
"inventory_purchase_to_sale_chain",
|
"inventory_purchase_to_sale_chain",
|
||||||
"inventory_aging_by_purchase_date",
|
"inventory_aging_by_purchase_date",
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ const COMPUTE_EXACT_INTENTS = new Set<AddressIntent>([
|
||||||
"inventory_purchase_documents_for_item",
|
"inventory_purchase_documents_for_item",
|
||||||
"inventory_supplier_stock_overlap_as_of_date",
|
"inventory_supplier_stock_overlap_as_of_date",
|
||||||
"inventory_sale_trace_for_item",
|
"inventory_sale_trace_for_item",
|
||||||
|
"inventory_margin_ranking_for_nomenclature",
|
||||||
"inventory_profitability_for_item",
|
"inventory_profitability_for_item",
|
||||||
"inventory_purchase_to_sale_chain",
|
"inventory_purchase_to_sale_chain",
|
||||||
"inventory_aging_by_purchase_date",
|
"inventory_aging_by_purchase_date",
|
||||||
|
|
@ -92,6 +93,7 @@ function defaultCapabilityId(intent: AddressIntent): string {
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date"
|
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_provenance_for_item" ||
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain"
|
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 {
|
return {
|
||||||
enabled: true,
|
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") {
|
if (intent === "inventory_purchase_to_sale_chain") {
|
||||||
|
|
@ -284,6 +290,7 @@ export function resolveShadowRouteIntent(
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date"
|
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_provenance_for_item" ||
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "open_contracts_confirmed_as_of_date" ||
|
intent === "open_contracts_confirmed_as_of_date" ||
|
||||||
|
|
|
||||||
|
|
@ -1111,6 +1111,7 @@ function isInventoryTraceIntent(intent: AddressIntent): boolean {
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date"
|
intent === "inventory_aging_by_purchase_date"
|
||||||
|
|
@ -1135,6 +1136,7 @@ function usesRecipeDefaultLimit(intent: AddressIntent): boolean {
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date"
|
intent === "inventory_aging_by_purchase_date"
|
||||||
|
|
@ -1628,6 +1630,9 @@ function requiredFiltersByIntent(intent: AddressIntent): Array<keyof AddressFilt
|
||||||
) {
|
) {
|
||||||
return ["item"];
|
return ["item"];
|
||||||
}
|
}
|
||||||
|
if (intent === "inventory_margin_ranking_for_nomenclature") {
|
||||||
|
return ["period_from", "period_to"];
|
||||||
|
}
|
||||||
if (intent === "payables_confirmed_as_of_date") {
|
if (intent === "payables_confirmed_as_of_date") {
|
||||||
return ["as_of_date"];
|
return ["as_of_date"];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2152,6 +2152,28 @@ function hasBidirectionalValueFlowComparisonSignal(text: string): boolean {
|
||||||
return hasIncomingCue && hasOutgoingCue && hasComparisonCue && (hasValueFlowCue || hasNetAmountCue);
|
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 {
|
function hasVatPeriodInspectionBridgeSignal(text: string): boolean {
|
||||||
const normalized = String(text ?? "").trim().toLowerCase();
|
const normalized = String(text ?? "").trim().toLowerCase();
|
||||||
if (!/(?:ндс|vat)/iu.test(normalized)) {
|
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 =
|
const hasOpenItemsAccountCue =
|
||||||
/(?:хвост|долг|незакрыт|вис)/iu.test(normalized) &&
|
/(?:хвост|долг|незакрыт|вис)/iu.test(normalized) &&
|
||||||
/(?:сч(?:е|ё)т(?:а|у|ом|е|ов)?\s*(?:№|#)?\s*(?:60|62|76)(?:[.,]\d{1,2})?|\b(?:60|62|76)(?:[.,]\d{1,2})?\b\s*сч(?:е|ё)т)/iu.test(
|
/(?:сч(?:е|ё)т(?:а|у|ом|е|ов)?\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_purchase_documents_for_item" ||
|
||||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date"
|
intent === "inventory_aging_by_purchase_date"
|
||||||
|
|
@ -2085,6 +2086,7 @@ function shouldDeferInventoryOrganizationClarification(
|
||||||
intent === "inventory_purchase_provenance_for_item" ||
|
intent === "inventory_purchase_provenance_for_item" ||
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date"
|
intent === "inventory_aging_by_purchase_date"
|
||||||
|
|
@ -2455,6 +2457,7 @@ function canAutoBroadenPeriodWindow(intent: AddressIntent, filters: AddressFilte
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date"
|
intent === "inventory_aging_by_purchase_date"
|
||||||
|
|
@ -2467,6 +2470,7 @@ function shouldBoostAutoBroadenedLimit(intent: AddressIntent): boolean {
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date"
|
intent === "inventory_aging_by_purchase_date"
|
||||||
|
|
@ -3132,6 +3136,9 @@ async function tryComposeLlmLimitedReply(input: {
|
||||||
if (process.env.VITEST === "true" || process.env.NODE_ENV === "test") {
|
if (process.env.VITEST === "true" || process.env.NODE_ENV === "test") {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
if (input.intent === "inventory_margin_ranking_for_nomenclature" && input.category === "missing_anchor") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
if (!shouldUseLlmLimitedReply(input.category)) {
|
if (!shouldUseLlmLimitedReply(input.category)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -3206,6 +3213,13 @@ function composeLimitedReply(input: {
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
const missingAnchorPhrase = missingAnchorLabels.length > 0 ? missingAnchorLabels.join(", ") : "контрагент, договор, счет или период";
|
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 =
|
const heading =
|
||||||
input.category === "empty_match"
|
input.category === "empty_match"
|
||||||
? pickDeterministicVariant(headingSeed, [
|
? pickDeterministicVariant(headingSeed, [
|
||||||
|
|
|
||||||
|
|
@ -992,6 +992,17 @@ const BASE_RECIPES: AddressRecipeDefinition[] = [
|
||||||
account_scope_mode: "strict",
|
account_scope_mode: "strict",
|
||||||
query_template: "inventory_trading_margin_proxy_profile"
|
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",
|
recipe_id: "address_inventory_purchase_to_sale_chain_v1",
|
||||||
intent: "inventory_purchase_to_sale_chain",
|
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_supplier_stock_overlap_as_of_date" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
intent === "inventory_trading_margin_proxy_for_organization" ||
|
intent === "inventory_trading_margin_proxy_for_organization" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date" ||
|
intent === "inventory_aging_by_purchase_date" ||
|
||||||
|
|
@ -2182,6 +2194,8 @@ export function buildAddressRecipePlan(
|
||||||
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
||||||
: recipe.query_template === "inventory_trading_margin_proxy_profile"
|
: recipe.query_template === "inventory_trading_margin_proxy_profile"
|
||||||
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
||||||
|
: recipe.query_template === "inventory_margin_ranking_profile"
|
||||||
|
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
||||||
: recipe.query_template === "inventory_purchase_to_sale_chain_profile"
|
: recipe.query_template === "inventory_purchase_to_sale_chain_profile"
|
||||||
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
? buildInventoryPurchaseToSaleDocumentQuery(filters, resolvedLimit)
|
||||||
: recipe.query_template === "inventory_aging_by_purchase_date_profile"
|
: 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_purchase_documents_for_item" ||
|
||||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date"
|
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_provenance_for_item" ||
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date"
|
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_provenance_for_item" ||
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain"
|
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 {
|
export function hasAddressFollowupContextSignal(text: string): boolean {
|
||||||
const normalized = String(text ?? "").trim();
|
const normalized = String(text ?? "").trim();
|
||||||
if (!normalized) {
|
if (!normalized) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (hasInventoryMarginRankingFollowupCue(normalized)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
/(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+ней|по\s+нему|по\s+ним|for\s+selected\s+object|selected\s+object)/iu.test(
|
/(?:по\s+выбранному\s+объекту|по\s+этой\s+позиции|по\s+этому\s+товару|по\s+ней|по\s+нему|по\s+ним|for\s+selected\s+object|selected\s+object)/iu.test(
|
||||||
normalized
|
normalized
|
||||||
|
|
@ -1115,6 +1139,7 @@ function mergeFollowupFilters(
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date" ||
|
intent === "inventory_aging_by_purchase_date" ||
|
||||||
|
|
@ -1175,6 +1200,7 @@ function mergeFollowupFilters(
|
||||||
(intent === "inventory_purchase_provenance_for_item" ||
|
(intent === "inventory_purchase_provenance_for_item" ||
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date")
|
intent === "inventory_aging_by_purchase_date")
|
||||||
|
|
@ -1412,6 +1438,7 @@ function mergeFollowupFilters(
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
intent === "inventory_supplier_stock_overlap_as_of_date" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date" ||
|
intent === "inventory_aging_by_purchase_date" ||
|
||||||
|
|
@ -1424,6 +1451,9 @@ function mergeFollowupFilters(
|
||||||
const currentContractExplicit = toNonEmptyString(merged.contract);
|
const currentContractExplicit = toNonEmptyString(merged.contract);
|
||||||
const currentItemExplicit = toNonEmptyString(merged.item);
|
const currentItemExplicit = toNonEmptyString(merged.item);
|
||||||
const currentAccountExplicit = toNonEmptyString(merged.account);
|
const currentAccountExplicit = toNonEmptyString(merged.account);
|
||||||
|
const currentAccountRefinesMarginDomain =
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" &&
|
||||||
|
hasInventoryMarginRankingFollowupCue(userMessage);
|
||||||
const shouldSuppressGenericPeriodCarryover =
|
const shouldSuppressGenericPeriodCarryover =
|
||||||
(Boolean(currentCounterpartyExplicit) &&
|
(Boolean(currentCounterpartyExplicit) &&
|
||||||
!isLowQualityCounterpartyAnchor(currentCounterpartyExplicit) &&
|
!isLowQualityCounterpartyAnchor(currentCounterpartyExplicit) &&
|
||||||
|
|
@ -1432,7 +1462,7 @@ function mergeFollowupFilters(
|
||||||
!isLowQualityContractAnchor(currentContractExplicit) &&
|
!isLowQualityContractAnchor(currentContractExplicit) &&
|
||||||
currentContractExplicit !== previousContract) ||
|
currentContractExplicit !== previousContract) ||
|
||||||
(Boolean(currentItemExplicit) && currentItemExplicit !== previousItem) ||
|
(Boolean(currentItemExplicit) && currentItemExplicit !== previousItem) ||
|
||||||
(Boolean(currentAccountExplicit) && currentAccountExplicit !== previousAccount);
|
(Boolean(currentAccountExplicit) && currentAccountExplicit !== previousAccount && !currentAccountRefinesMarginDomain);
|
||||||
const vatRelativeMonthFollowup =
|
const vatRelativeMonthFollowup =
|
||||||
relativeMonthFromFollowupYear &&
|
relativeMonthFromFollowupYear &&
|
||||||
(intent === "vat_payable_confirmed_as_of_date" ||
|
(intent === "vat_payable_confirmed_as_of_date" ||
|
||||||
|
|
@ -1488,6 +1518,22 @@ function mergeFollowupFilters(
|
||||||
reasons.push("period_from_followup_context");
|
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 (
|
if (
|
||||||
!currentHasPeriod &&
|
!currentHasPeriod &&
|
||||||
previousHasPeriod &&
|
previousHasPeriod &&
|
||||||
|
|
@ -1563,6 +1609,7 @@ function resolveMissingRequiredFilters(intent: AddressIntent, filters: AddressFi
|
||||||
account_balance_snapshot: ["account", "as_of_date"],
|
account_balance_snapshot: ["account", "as_of_date"],
|
||||||
documents_forming_balance: ["account", "as_of_date"],
|
documents_forming_balance: ["account", "as_of_date"],
|
||||||
inventory_on_hand_as_of_date: ["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"],
|
inventory_profitability_for_item: ["item"],
|
||||||
open_contracts_confirmed_as_of_date: ["as_of_date"],
|
open_contracts_confirmed_as_of_date: ["as_of_date"],
|
||||||
payables_confirmed_as_of_date: ["as_of_date"],
|
payables_confirmed_as_of_date: ["as_of_date"],
|
||||||
|
|
@ -1648,6 +1695,26 @@ function deriveIntentWithFollowupContext(
|
||||||
previousCounterpartyLaneActive && !hasExplicitInventoryItemReference;
|
previousCounterpartyLaneActive && !hasExplicitInventoryItemReference;
|
||||||
const inventoryPurchaseDateVatBridge =
|
const inventoryPurchaseDateVatBridge =
|
||||||
inventorySelectedObjectFollowup && hasInventoryPurchaseDateVatBridgeCue(normalizedMessage);
|
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 (
|
if (
|
||||||
inventoryPurchaseDateVatBridge &&
|
inventoryPurchaseDateVatBridge &&
|
||||||
|
|
|
||||||
|
|
@ -162,6 +162,73 @@ function inventoryProfitabilityPeriodLabel(options: InventoryComposeOptions, dep
|
||||||
return asOfDate ? `до ${deps.formatDateRu(asOfDate)}` : "по доступной выборке";
|
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(
|
export function composeInventoryReply(
|
||||||
intent: AddressIntent,
|
intent: AddressIntent,
|
||||||
rows: ComposeStageRow[],
|
rows: ComposeStageRow[],
|
||||||
|
|
@ -548,6 +615,121 @@ export function composeInventoryReply(
|
||||||
: buildFactualSummaryReply(lines, buildConfirmedBalanceSemantics("medium", false));
|
: 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") {
|
if (intent === "inventory_profitability_for_item") {
|
||||||
const purchaseRows = rows.filter((row) => deps.isInventoryPurchaseMovement(row));
|
const purchaseRows = rows.filter((row) => deps.isInventoryPurchaseMovement(row));
|
||||||
const saleRows = rows.filter((row) => deps.isInventorySaleMovement(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_provenance_for_item" ||
|
||||||
intent === "inventory_purchase_documents_for_item" ||
|
intent === "inventory_purchase_documents_for_item" ||
|
||||||
intent === "inventory_sale_trace_for_item" ||
|
intent === "inventory_sale_trace_for_item" ||
|
||||||
|
intent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
intent === "inventory_profitability_for_item" ||
|
intent === "inventory_profitability_for_item" ||
|
||||||
intent === "inventory_purchase_to_sale_chain" ||
|
intent === "inventory_purchase_to_sale_chain" ||
|
||||||
intent === "inventory_aging_by_purchase_date"
|
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 {
|
function hasSameDateFollowupSignal(text: string | null): boolean {
|
||||||
return /(?:эту\s+же\s+дат(?:у|е|ой)|ту\s+же\s+дат(?:у|е|ой)|same\s+date)/iu.test(String(text ?? ""));
|
return /(?:эту\s+же\s+дат(?:у|е|ой)|ту\s+же\s+дат(?:у|е|ой)|same\s+date)/iu.test(String(text ?? ""));
|
||||||
}
|
}
|
||||||
|
|
@ -240,6 +253,12 @@ function shouldPreferRawFollowupMessage(
|
||||||
const hasInventoryFrameCarryover =
|
const hasInventoryFrameCarryover =
|
||||||
isInventorySelectedObjectOrRootIntent(previousIntent) ||
|
isInventorySelectedObjectOrRootIntent(previousIntent) ||
|
||||||
isInventorySelectedObjectOrRootIntent(rootIntent);
|
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 =
|
const hasDocumentCarryover =
|
||||||
previousIntent === "list_documents_by_counterparty" || previousIntent === "list_documents_by_contract";
|
previousIntent === "list_documents_by_counterparty" || previousIntent === "list_documents_by_contract";
|
||||||
|
|
||||||
|
|
@ -263,6 +282,13 @@ function shouldPreferRawFollowupMessage(
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
hasInventoryMarginRankingAccountCorrection &&
|
||||||
|
(intent === "account_balance_snapshot" || intent === "documents_forming_balance" || intent === "unknown")
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
(hasSelectedObjectInventorySignal(rawMessage) || hasInventoryItemCarryover) &&
|
(hasSelectedObjectInventorySignal(rawMessage) || hasInventoryItemCarryover) &&
|
||||||
(hasSelectedObjectInventoryActionCue(rawMessage) || hasShortInventoryPurchaseFollowupCue(rawMessage)) &&
|
(hasSelectedObjectInventoryActionCue(rawMessage) || hasShortInventoryPurchaseFollowupCue(rawMessage)) &&
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,13 @@ function formatMissingAnchors(anchors: string[]): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildClarificationReply(binding: AssistantCapabilityRuntimeBindingContract): string {
|
function buildClarificationReply(binding: AssistantCapabilityRuntimeBindingContract): string {
|
||||||
|
if (binding.capability_contract_id === "inventory_inventory_margin_ranking_for_nomenclature") {
|
||||||
|
return [
|
||||||
|
"Для рейтинга прибыльности номенклатуры нужен период.",
|
||||||
|
"Могу посчитать по номенклатуре: выручку без НДС, себестоимость реализации, валовую прибыль и маржинальность.",
|
||||||
|
"Уточните период: месяц, квартал, год или весь доступный период."
|
||||||
|
].join("\n\n");
|
||||||
|
}
|
||||||
return [
|
return [
|
||||||
"Нужно уточнение, чтобы не подставить неподтвержденный объект в расчет.",
|
"Нужно уточнение, чтобы не подставить неподтвержденный объект в расчет.",
|
||||||
`Не хватает: ${formatMissingAnchors(binding.missing_anchors)}.`,
|
`Не хватает: ${formatMissingAnchors(binding.missing_anchors)}.`,
|
||||||
|
|
|
||||||
|
|
@ -161,6 +161,15 @@ function anchorSatisfied(requiredAnchor: string, providedAnchors: string[], debu
|
||||||
if (providedAnchors.includes(requiredAnchor)) {
|
if (providedAnchors.includes(requiredAnchor)) {
|
||||||
return true;
|
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") {
|
if (requiredAnchor === "item") {
|
||||||
return (
|
return (
|
||||||
providedAnchors.includes("selected_object") ||
|
providedAnchors.includes("selected_object") ||
|
||||||
|
|
|
||||||
|
|
@ -216,6 +216,7 @@ function isDetectedIntentAlignedWithTurnMeaning(
|
||||||
normalizedIntent === "inventory_purchase_provenance_for_item" ||
|
normalizedIntent === "inventory_purchase_provenance_for_item" ||
|
||||||
normalizedIntent === "inventory_purchase_documents_for_item" ||
|
normalizedIntent === "inventory_purchase_documents_for_item" ||
|
||||||
normalizedIntent === "inventory_sale_trace_for_item" ||
|
normalizedIntent === "inventory_sale_trace_for_item" ||
|
||||||
|
normalizedIntent === "inventory_margin_ranking_for_nomenclature" ||
|
||||||
normalizedIntent === "inventory_profitability_for_item" ||
|
normalizedIntent === "inventory_profitability_for_item" ||
|
||||||
normalizedIntent === "inventory_purchase_to_sale_chain"
|
normalizedIntent === "inventory_purchase_to_sale_chain"
|
||||||
) {
|
) {
|
||||||
|
|
@ -276,7 +277,7 @@ function isExplicitMetadataDiscoveryTurn(
|
||||||
}
|
}
|
||||||
|
|
||||||
function isInventoryExactAddressIntent(intent: string | null): boolean {
|
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 ?? "")
|
String(intent ?? "")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ const ADDRESS_INTENTS_KEEP_ADDRESS_LANE = new Set([
|
||||||
"inventory_purchase_documents_for_item",
|
"inventory_purchase_documents_for_item",
|
||||||
"inventory_supplier_stock_overlap_as_of_date",
|
"inventory_supplier_stock_overlap_as_of_date",
|
||||||
"inventory_sale_trace_for_item",
|
"inventory_sale_trace_for_item",
|
||||||
|
"inventory_margin_ranking_for_nomenclature",
|
||||||
"inventory_profitability_for_item",
|
"inventory_profitability_for_item",
|
||||||
"inventory_purchase_to_sale_chain",
|
"inventory_purchase_to_sale_chain",
|
||||||
"inventory_aging_by_purchase_date",
|
"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_provenance_for_item",
|
||||||
"inventory_purchase_documents_for_item",
|
"inventory_purchase_documents_for_item",
|
||||||
"inventory_sale_trace_for_item",
|
"inventory_sale_trace_for_item",
|
||||||
|
"inventory_margin_ranking_for_nomenclature",
|
||||||
"inventory_profitability_for_item",
|
"inventory_profitability_for_item",
|
||||||
"inventory_purchase_to_sale_chain"
|
"inventory_purchase_to_sale_chain"
|
||||||
]);
|
]);
|
||||||
|
|
@ -286,7 +288,7 @@ export function createAssistantRoutePolicy(deps) {
|
||||||
: null;
|
: null;
|
||||||
const semanticCanonicalRecommended = semanticExtractionContract?.apply_canonical_recommended !== false;
|
const semanticCanonicalRecommended = semanticExtractionContract?.apply_canonical_recommended !== false;
|
||||||
const llmSupportedDeepAddressIntentSignal = llmContractMode === "deep_analysis" &&
|
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;
|
semanticCanonicalRecommended;
|
||||||
const llmCanonicalEntitySignal = /(?:заказчик|поставщик|контрагент|компан|customer|supplier|counterparty|company|vendor|client)/iu.test(compactWhitespace(repairedInputMessage.toLowerCase()));
|
const llmCanonicalEntitySignal = /(?:заказчик|поставщик|контрагент|компан|customer|supplier|counterparty|company|vendor|client)/iu.test(compactWhitespace(repairedInputMessage.toLowerCase()));
|
||||||
const llmCanonicalAppliedSignal = Boolean(llmPreDecomposeMeta?.applied) && llmContractMode !== "deep_analysis";
|
const llmCanonicalAppliedSignal = Boolean(llmPreDecomposeMeta?.applied) && llmContractMode !== "deep_analysis";
|
||||||
|
|
|
||||||
|
|
@ -337,6 +337,18 @@ export const INVENTORY_CAPABILITY_CONTRACTS: readonly AssistantCapabilityContrac
|
||||||
answerObjectShape: "inventory_profitability_bundle",
|
answerObjectShape: "inventory_profitability_bundle",
|
||||||
bundleReusePolicy: "sale_trace_bundle_preferred"
|
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({
|
inventoryExactCapability({
|
||||||
capability_id: "inventory_inventory_purchase_to_sale_chain",
|
capability_id: "inventory_inventory_purchase_to_sale_chain",
|
||||||
intent_ids: ["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, "е");
|
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) {
|
function hasSamePeriodReferenceCue(...values) {
|
||||||
return values
|
return values
|
||||||
.map((value) => normalizeFollowupText(value))
|
.map((value) => normalizeFollowupText(value))
|
||||||
|
|
@ -760,6 +785,11 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
sourceIntentHint,
|
sourceIntentHint,
|
||||||
hasNavigationInventoryItemFocusHint
|
hasNavigationInventoryItemFocusHint
|
||||||
);
|
);
|
||||||
|
const inventoryMarginRankingFollowup = hasInventoryMarginRankingFollowupSignal(
|
||||||
|
userMessage,
|
||||||
|
alternateMessage,
|
||||||
|
sourceIntentHint
|
||||||
|
);
|
||||||
let inventoryShortFollowupPrimary =
|
let inventoryShortFollowupPrimary =
|
||||||
(deps.isInventorySelectedObjectIntent(sourceIntentHint) || hasNavigationInventoryItemFocusHint) &&
|
(deps.isInventorySelectedObjectIntent(sourceIntentHint) || hasNavigationInventoryItemFocusHint) &&
|
||||||
deps.hasShortInventoryObjectFollowupSignal(userMessage);
|
deps.hasShortInventoryObjectFollowupSignal(userMessage);
|
||||||
|
|
@ -796,6 +826,7 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
businessOverviewBoundaryFollowupPrimary ||
|
businessOverviewBoundaryFollowupPrimary ||
|
||||||
inventoryShortFollowupPrimary ||
|
inventoryShortFollowupPrimary ||
|
||||||
inventoryPurchaseDateVatBridge ||
|
inventoryPurchaseDateVatBridge ||
|
||||||
|
inventoryMarginRankingFollowup ||
|
||||||
explicitSummaryBundleReuseSignal ||
|
explicitSummaryBundleReuseSignal ||
|
||||||
mcpDiscoveryOrganizationClarificationContinuation;
|
mcpDiscoveryOrganizationClarificationContinuation;
|
||||||
let hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage)
|
let hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage)
|
||||||
|
|
@ -805,6 +836,7 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
businessOverviewBoundaryFollowupAlternate ||
|
businessOverviewBoundaryFollowupAlternate ||
|
||||||
inventoryShortFollowupAlternate ||
|
inventoryShortFollowupAlternate ||
|
||||||
inventoryPurchaseDateVatBridge ||
|
inventoryPurchaseDateVatBridge ||
|
||||||
|
inventoryMarginRankingFollowup ||
|
||||||
explicitSummaryBundleReuseSignal ||
|
explicitSummaryBundleReuseSignal ||
|
||||||
mcpDiscoveryOrganizationClarificationContinuation
|
mcpDiscoveryOrganizationClarificationContinuation
|
||||||
: false;
|
: false;
|
||||||
|
|
@ -862,6 +894,7 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
shortValueFlowRetargetAlternate ||
|
shortValueFlowRetargetAlternate ||
|
||||||
businessOverviewBoundaryFollowupPrimary ||
|
businessOverviewBoundaryFollowupPrimary ||
|
||||||
businessOverviewBoundaryFollowupAlternate ||
|
businessOverviewBoundaryFollowupAlternate ||
|
||||||
|
inventoryMarginRankingFollowup ||
|
||||||
deps.hasFollowupMarker(userMessage) ||
|
deps.hasFollowupMarker(userMessage) ||
|
||||||
deps.hasReferentialPointer(userMessage) ||
|
deps.hasReferentialPointer(userMessage) ||
|
||||||
(deps.toNonEmptyString(alternateMessage)
|
(deps.toNonEmptyString(alternateMessage)
|
||||||
|
|
@ -886,6 +919,7 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
shortValueFlowRetargetAlternate ||
|
shortValueFlowRetargetAlternate ||
|
||||||
businessOverviewBoundaryFollowupPrimary ||
|
businessOverviewBoundaryFollowupPrimary ||
|
||||||
businessOverviewBoundaryFollowupAlternate ||
|
businessOverviewBoundaryFollowupAlternate ||
|
||||||
|
inventoryMarginRankingFollowup ||
|
||||||
deps.hasFollowupMarker(userMessage) ||
|
deps.hasFollowupMarker(userMessage) ||
|
||||||
deps.hasReferentialPointer(userMessage) ||
|
deps.hasReferentialPointer(userMessage) ||
|
||||||
(deps.toNonEmptyString(alternateMessage)
|
(deps.toNonEmptyString(alternateMessage)
|
||||||
|
|
@ -1055,6 +1089,7 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
!inventoryShortFollowupAlternate &&
|
!inventoryShortFollowupAlternate &&
|
||||||
!businessOverviewBoundaryFollowupPrimary &&
|
!businessOverviewBoundaryFollowupPrimary &&
|
||||||
!businessOverviewBoundaryFollowupAlternate &&
|
!businessOverviewBoundaryFollowupAlternate &&
|
||||||
|
!inventoryMarginRankingFollowup &&
|
||||||
!foreignAccountingPivotOverInventory &&
|
!foreignAccountingPivotOverInventory &&
|
||||||
!deps.hasFollowupMarker(userMessage) &&
|
!deps.hasFollowupMarker(userMessage) &&
|
||||||
!deps.hasReferentialPointer(userMessage) &&
|
!deps.hasReferentialPointer(userMessage) &&
|
||||||
|
|
@ -1118,6 +1153,7 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
businessOverviewBoundaryFollowupPrimary ||
|
businessOverviewBoundaryFollowupPrimary ||
|
||||||
inventoryShortFollowupPrimary ||
|
inventoryShortFollowupPrimary ||
|
||||||
inventoryPurchaseDateVatBridge ||
|
inventoryPurchaseDateVatBridge ||
|
||||||
|
inventoryMarginRankingFollowup ||
|
||||||
explicitSummaryBundleReuseSignal ||
|
explicitSummaryBundleReuseSignal ||
|
||||||
hasInventoryRootTemporalFollowupPrimary ||
|
hasInventoryRootTemporalFollowupPrimary ||
|
||||||
mcpDiscoveryOrganizationClarificationContinuation;
|
mcpDiscoveryOrganizationClarificationContinuation;
|
||||||
|
|
@ -1129,6 +1165,7 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
businessOverviewBoundaryFollowupAlternate ||
|
businessOverviewBoundaryFollowupAlternate ||
|
||||||
inventoryShortFollowupAlternate ||
|
inventoryShortFollowupAlternate ||
|
||||||
inventoryPurchaseDateVatBridge ||
|
inventoryPurchaseDateVatBridge ||
|
||||||
|
inventoryMarginRankingFollowup ||
|
||||||
explicitSummaryBundleReuseSignal ||
|
explicitSummaryBundleReuseSignal ||
|
||||||
hasInventoryRootTemporalFollowupAlternate ||
|
hasInventoryRootTemporalFollowupAlternate ||
|
||||||
mcpDiscoveryOrganizationClarificationContinuation
|
mcpDiscoveryOrganizationClarificationContinuation
|
||||||
|
|
@ -1151,6 +1188,7 @@ export function createAssistantTransitionPolicy(deps) {
|
||||||
shortValueFlowRetargetAlternate ||
|
shortValueFlowRetargetAlternate ||
|
||||||
businessOverviewBoundaryFollowupPrimary ||
|
businessOverviewBoundaryFollowupPrimary ||
|
||||||
businessOverviewBoundaryFollowupAlternate ||
|
businessOverviewBoundaryFollowupAlternate ||
|
||||||
|
inventoryMarginRankingFollowup ||
|
||||||
deps.hasFollowupMarker(userMessage) ||
|
deps.hasFollowupMarker(userMessage) ||
|
||||||
deps.hasReferentialPointer(userMessage) ||
|
deps.hasReferentialPointer(userMessage) ||
|
||||||
(deps.toNonEmptyString(alternateMessage)
|
(deps.toNonEmptyString(alternateMessage)
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ const SUPPORTED_ADDRESS_INTENTS = new Set([
|
||||||
"inventory_purchase_documents_for_item",
|
"inventory_purchase_documents_for_item",
|
||||||
"inventory_supplier_stock_overlap_as_of_date",
|
"inventory_supplier_stock_overlap_as_of_date",
|
||||||
"inventory_sale_trace_for_item",
|
"inventory_sale_trace_for_item",
|
||||||
|
"inventory_margin_ranking_for_nomenclature",
|
||||||
"inventory_profitability_for_item",
|
"inventory_profitability_for_item",
|
||||||
"inventory_purchase_to_sale_chain",
|
"inventory_purchase_to_sale_chain",
|
||||||
"inventory_aging_by_purchase_date",
|
"inventory_aging_by_purchase_date",
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ export type AddressIntent =
|
||||||
| "inventory_supplier_stock_overlap_as_of_date"
|
| "inventory_supplier_stock_overlap_as_of_date"
|
||||||
| "inventory_sale_trace_for_item"
|
| "inventory_sale_trace_for_item"
|
||||||
| "inventory_trading_margin_proxy_for_organization"
|
| "inventory_trading_margin_proxy_for_organization"
|
||||||
|
| "inventory_margin_ranking_for_nomenclature"
|
||||||
| "inventory_profitability_for_item"
|
| "inventory_profitability_for_item"
|
||||||
| "inventory_purchase_to_sale_chain"
|
| "inventory_purchase_to_sale_chain"
|
||||||
| "inventory_aging_by_purchase_date"
|
| "inventory_aging_by_purchase_date"
|
||||||
|
|
@ -203,6 +204,7 @@ export interface AddressRecipeDefinition {
|
||||||
| "inventory_supplier_stock_overlap_profile"
|
| "inventory_supplier_stock_overlap_profile"
|
||||||
| "inventory_sale_trace_profile"
|
| "inventory_sale_trace_profile"
|
||||||
| "inventory_trading_margin_proxy_profile"
|
| "inventory_trading_margin_proxy_profile"
|
||||||
|
| "inventory_margin_ranking_profile"
|
||||||
| "inventory_profitability_profile"
|
| "inventory_profitability_profile"
|
||||||
| "inventory_purchase_to_sale_chain_profile"
|
| "inventory_purchase_to_sale_chain_profile"
|
||||||
| "inventory_aging_by_purchase_date_profile"
|
| "inventory_aging_by_purchase_date_profile"
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,15 @@ describe("addressIntentResolver regression bridges", () => {
|
||||||
|
|
||||||
expect(result.intent).toBe("inventory_aging_by_purchase_date");
|
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", () => {
|
it("detects bare historical inventory root with explicit month-year", () => {
|
||||||
const result = resolveAddressIntent("остатки на март 2016");
|
const result = resolveAddressIntent("остатки на март 2016");
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -104,4 +104,292 @@ describe("inventory profitability selected-object regressions", () => {
|
||||||
expect(reply).toContain("не чистая прибыль компании");
|
expect(reply).toContain("не чистая прибыль компании");
|
||||||
expect(executeAddressMcpQueryMock).toHaveBeenCalledTimes(1);
|
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 () => {
|
it("prefers raw selected-object inventory action over generic canonical drift intent", async () => {
|
||||||
const resolveAddressFollowupCarryoverContext = vi.fn(() => ({
|
const resolveAddressFollowupCarryoverContext = vi.fn(() => ({
|
||||||
followupContext: {
|
followupContext: {
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,35 @@ describe("assistant capability binding response guard", () => {
|
||||||
expect(output.audit.reason_codes).toContain("capability_binding_guard_clarification_reply");
|
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", () => {
|
it("turns blocked incompatible transitions into bounded replies", () => {
|
||||||
const output = applyAssistantCapabilityBindingResponseGuard({
|
const output = applyAssistantCapabilityBindingResponseGuard({
|
||||||
assistantReply: "unsafe answer",
|
assistantReply: "unsafe answer",
|
||||||
|
|
|
||||||
|
|
@ -130,6 +130,34 @@ describe("assistant capability runtime binding adapter", () => {
|
||||||
expect(binding.violations).toEqual([]);
|
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", () => {
|
it("blocks selected-object capabilities when required anchors are missing", () => {
|
||||||
const binding = resolveAssistantCapabilityRuntimeBinding({
|
const binding = resolveAssistantCapabilityRuntimeBinding({
|
||||||
addressDebug: {
|
addressDebug: {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue