Compare commits

...

10 Commits

71 changed files with 21722 additions and 365 deletions

View File

@ -199,3 +199,50 @@ The next move is larger:
- make the assistant able to look into 1C through bounded MCP discovery,
- choose its path through reviewed primitives,
- and answer from proved evidence instead of memorized route scripts.
## Status Update - 2026-04-22
The reset above is no longer only directional.
Its first three large blocks are now considered closed:
- `Big Block A. Metadata-First Self-Navigation`
- `Big Block B. Entity And Schema Grounding`
- `Big Block C. Planner-Selected Primitive Chains`
What is now materially real in code and replay-backed:
- metadata wording can start a live `inspect_1c_metadata` discovery path instead of collapsing into a hand-written route;
- metadata ambiguity can be held as a bounded clarification and resumed into `documents` or `movements`;
- grounded entity resolution can continue into `incoming`, `payout`, `net`, `documents`, and `movements` without repeating the entity name on every turn;
- planner-selected chains now survive year-switch, lane-switch, and direct follow-up pivots under the guarded evidence gate.
Current replay anchors for this closure:
- `address_truth_harness_phase24_metadata_lane_choice_loop_live_rerun5` accepted;
- `address_truth_harness_phase25_entity_resolution_chain_live_rerun_full_chain` accepted;
- `address_truth_harness_phase32_planner_selected_chain_end_to_end_live_rerun2` accepted `6/6`.
What is **not** complete yet:
- a general open-world understanding layer for structurally new 1C questions;
- planner selection from a broader primitive search space without pre-reviewed family scaffolding;
- a multi-hop evidence loop that can keep exploring, pause for clarification, and resume on unfamiliar contours.
That means the reset succeeded at building the bounded autonomy foundation.
It does **not** yet mean:
- unrestricted navigation through arbitrary 1C schemas;
- no-more-domain-thinking;
- "ask anything about 1C and the system will figure it out".
The next mainline now moves to:
- [16 - data_need_graph_and_open_world_mcp_plan_2026-04-22.md](./16%20-%20data_need_graph_and_open_world_mcp_plan_2026-04-22.md)
That document formalizes the next three blocks:
- `D. Question -> Data Need Graph`
- `E. Dynamic Schema Traversal And Primitive Search`
- `F. Multi-Hop Evidence Loop`

View File

@ -0,0 +1,223 @@
# 16 - Data Need Graph And Open-World MCP Plan (2026-04-22)
## Purpose
This note opens the next architecture phase after the completion of bounded-autonomy foundation blocks `A/B/C`.
It is not a restart.
It is the formal hand-off from:
- metadata-first self-navigation;
- entity/schema grounding;
- planner-selected primitive chains;
to the next problem:
- how the assistant starts understanding structurally new 1C questions without waiting for one more domain-specific route.
## Baseline Entering This Phase
The following is now treated as implemented baseline rather than future intent:
- live metadata inspection through reviewed MCP primitives;
- bounded schema grounding with honest ambiguity handling;
- planner-selected reviewed chains across entity resolution, value flow, documents, and movements;
- continuity that can preserve grounded entity and period context across the validated chain families.
Replay-backed anchors for that baseline include:
- `address_truth_harness_phase24_metadata_lane_choice_loop_live_rerun5`;
- `address_truth_harness_phase25_entity_resolution_chain_live_rerun_full_chain`;
- `address_truth_harness_phase32_planner_selected_chain_end_to_end_live_rerun2`.
This is enough to say:
- the assistant is no longer only a deterministic route bundle;
- the system already has a bounded MCP discovery substrate.
This is **not** enough to say:
- the assistant already understands arbitrary 1C questions;
- the planner already walks open-world 1C structure on its own;
- the system can already derive the correct probe sequence for unfamiliar asks without more architecture.
## Main Remaining Gap
The current system still understands many turns through a reviewed family scaffold.
That scaffold is much healthier than old route hardcoding, but it is still a scaffold.
The next large gap is:
- new questions must stop depending on whether their wording already resembles one of the preworked families;
- the runtime must first understand the user's data need as a machine-readable object;
- then search for a safe path through the MCP primitive catalog and observed 1C surface;
- then keep iterating until the answer is either proved, honestly bounded, or blocked for a concrete reason.
In short:
- move from `reviewed chain families`
- toward `open-world bounded evidence planning`.
## Big Block D. Question -> Data Need Graph
### Goal
Create one runtime object that represents the user's business data need independently from any one preworked domain route.
### Why This Block Comes First
Without this layer the planner still starts too low:
- it knows primitive families;
- it knows reviewed chains;
- but it does not yet have one explicit object saying what kind of business fact the user is asking to prove.
That means unfamiliar wording still risks falling back into:
- a nearby known family;
- a shallow unsupported verdict;
- or a hand-added route patch.
### Target Object
`assistant_data_need_graph_v1`
### Minimum Axes
- `subject_candidates`
- `business_fact_family`
- `action_family`
- `aggregation_need`
- `time_scope_need`
- `comparison_need`
- `ranking_need`
- `proof_expectation`
- `clarification_gaps`
- `decomposition_candidates`
- `forbidden_overclaim_flags`
### Scope
- normalize user wording into a business-meaning graph before primitive selection;
- distinguish direct fact asks from ranking, comparison, trend, list, drilldown, and explanation asks;
- carry explicit versus inherited axes separately;
- represent "understood but not yet grounded" without forcing a wrong domain route;
- give the planner one shared contract that survives wording variation.
### Acceptance
- structurally similar questions with different wording build equivalent data-need graphs;
- unfamiliar but intelligible questions stop collapsing into nearest-route guessing;
- the planner consumes the graph rather than ad hoc route-only hints;
- unsupported cases become "understood but missing evidence path", not "question not understood".
## Big Block E. Dynamic Schema Traversal And Primitive Search
### Goal
Teach the planner to search a broader reviewed primitive space against the observed 1C surface instead of depending on a fixed set of precomposed chain families.
### Why This Block Matters
Today the system can chain reviewed primitives well inside validated families.
What it still cannot do broadly enough is:
- look at the current data-need graph;
- inspect the schema surface;
- score plausible objects and primitives;
- assemble the next safe probe path dynamically.
That is the step from `good bounded recipes` to `bounded open-world navigation`.
### Scope
- introduce machine-readable primitive capability descriptors and prerequisites;
- rank candidate schema objects against the data-need graph;
- score document/register/catalog surfaces dynamically;
- let the planner build candidate chains from the catalog instead of picking from a short reviewed list only;
- preserve ambiguity when the schema does not yet justify a single path;
- keep traversal bounded by budget, proof rules, and reviewed primitive availability.
### Acceptance
- the planner can assemble a safe next chain without a dedicated per-family recipe;
- new questions can move from metadata into plausible schema traversal without route-per-wording work;
- ambiguity is retained honestly when multiple schema paths compete;
- no chain may bypass the evidence gate or invent unsupported primitives.
## Big Block F. Multi-Hop Evidence Loop And Clarifying Recovery
### Goal
Turn one-shot chain execution into a bounded evidence loop that can probe, detect missing axes, ask for clarification, resume, and stop honestly.
### Why This Block Is Separate
Even with a strong data-need graph and better primitive search, the assistant will still fail on unfamiliar asks if it cannot keep iterating safely.
Open-world bounded autonomy is not one magic route choice.
It is:
- execute;
- inspect what is still missing;
- clarify if needed;
- resume the same proof path;
- answer only with the evidence that survived the loop.
### Scope
- multi-step probe execution with explicit stop conditions;
- gap detection for missing organization, period, subject, or surface choice;
- clarification turns that preserve the active data-need graph and current probe state;
- resume logic that continues the same evidence loop rather than restarting from scratch;
- final answer shaping that separates:
- proved;
- inferred from evidence;
- still unknown.
### Acceptance
- a new question can survive more than one evidence hop without losing its meaning;
- clarification resumes the same proof path instead of resetting into local heuristics;
- the assistant can explain what is proved, what is inferred, and what remains unproved;
- unfamiliar asks no longer fail just because the first probe was incomplete.
## Execution Order
The next module should now be executed in this order:
1. `D. Question -> Data Need Graph`
2. `E. Dynamic Schema Traversal And Primitive Search`
3. `F. Multi-Hop Evidence Loop And Clarifying Recovery`
The order matters.
Doing `E` without `D` would create smarter probing without a stable representation of the user ask.
Doing `F` before `D/E` would create a more complex loop still attached to narrow reviewed families.
## Non-Goals
This phase should **not** be implemented as:
- another wave of domain-by-domain route stitching;
- a hidden prompt wrapper that pretends to be agentic;
- unrestricted free exploration of 1C without reviewed primitive boundaries;
- answer-time improvisation that bypasses the evidence gate;
- a stealth rewrite of the whole orchestration stack.
## Success Condition
This phase is successful only when a new human user can ask a structurally new but intelligible 1C data question and the assistant can:
1. understand the data need in machine-readable form;
2. search for a safe path through reviewed MCP primitives and observed schema surface;
3. iterate through bounded evidence steps;
4. ask a bounded clarification when needed;
5. answer honestly from proved evidence without pretending certainty it does not have.
That is the first point where the assistant will start to feel like it can actually walk 1C on its own within the reviewed MCP boundaries.

View File

@ -32,38 +32,46 @@ This package answers the next question:
12. [12 - manual_run_system_analysis_3NilqwT1G2_2026-04-18.md](./12%20-%20manual_run_system_analysis_3NilqwT1G2_2026-04-18.md)
13. [13 - pre_multidomain_readiness_audit_2026-04-18.md](./13%20-%20pre_multidomain_readiness_audit_2026-04-18.md)
14. [14 - semantic_dialog_authority_recovery_plan_2026-04-19.md](./14%20-%20semantic_dialog_authority_recovery_plan_2026-04-19.md)
15. [15 - mcp_bounded_autonomy_reset_plan_2026-04-21.md](./15%20-%20mcp_bounded_autonomy_reset_plan_2026-04-21.md)
16. [16 - data_need_graph_and_open_world_mcp_plan_2026-04-22.md](./16%20-%20data_need_graph_and_open_world_mcp_plan_2026-04-22.md)
## Current Status Snapshot (2026-04-19)
## Current Status Snapshot (2026-04-22)
This package is no longer planning-only.
It now documents a turnaround that is already operational in code, already materially past the acute regression breakpoint, but still not ready for wide multi-domain expansion:
It now documents a turnaround that is already operational in code, already materially past the acute regression breakpoint, and already moved into bounded MCP autonomy work beyond the first stabilization wave:
- route, transition, boundary, meta, memory, and provider policy owners exist as separate modules;
- exact-lane truth and coverage/evidence contracts exist as explicit runtime artifacts;
- scenario acceptance writes machine-readable `scenario_acceptance_matrix.json` and `pack_state.json`;
- AGENT semantic packs and source catalogs already exist for mixed domain/meta validation.
- the reset toward `MCP-first bounded autonomy` is now formalized;
- `Big Block A/B/C` of that reset are now closed in runtime code and replay-backed;
- the next architecture mainline is no longer continuity polishing, but `D/E/F`:
- `Question -> Data Need Graph`
- dynamic schema traversal and primitive search
- multi-hop evidence loop with bounded clarification recovery
Current honest status:
- turnaround implementation progress: `~96%`
- exit-from-danger-zone readiness: `~91%`
- pre-multidomain readiness: `~78%`
- graph snapshot after latest rebuild: `5372 nodes`, `11525 edges`, `135 communities`
- bounded-autonomy foundation readiness: `~60%`
- graph snapshot after latest rebuild: `5741 nodes`, `12385 edges`, `137 communities`
- current breakpoint:
- the validated hot paths are no longer structurally broken;
- flagship continuity collapse is no longer the primary risk;
- the main remaining risk is no longer clarification-resume collapse, but the unfinished final convergence toward one true runtime authority plus replay breadth still below the intended multi-domain blast radius;
- pure wording polish is now secondary debt, but semantic robustness of user-facing answers is now a first-class blocker;
- the practical product risk is no longer "the route collapsed", but "a new user can still feel that the assistant is glitchy, misses intent, or answers the wrong thing on short live wording".
- the main remaining risk is no longer clarification-resume collapse, but the unfinished shift from bounded reviewed chains toward open-world data-need-driven MCP planning;
- pure wording polish is now secondary debt, but semantic robustness plus open-world evidence navigation is now a first-class blocker;
- the practical product risk is no longer only "the route collapsed", but "the assistant still cannot yet understand and explore many non-preworked 1C questions on its own".
- main remaining architectural pressure:
- no single fully authoritative continuity contract consumed by every hot runtime owner
- residual coordinator/legacy pressure inside `assistantService.ts`
- no general `Question -> Data Need Graph` authority yet
- planner chain selection is still reviewed-family bounded rather than open-world over the primitive catalog
- schema traversal is still narrower than the intended arbitrary 1C blast radius
- multi-hop evidence recovery is still too shallow for unfamiliar asks
- central domain-intent pressure inside `resolveAddressIntent()`
- replay breadth still narrower than the intended multi-domain rollout surface beyond the flagship and late-switch families
- remaining answer-semantics pressure inside `composeStage.ts` / `answerComposer.ts`
- insufficient semantic robustness on live user wording, especially short follow-up retarget, typo tolerance, and intent-faithful human answers
- no guarded MCP semantic discovery lane yet for understood long-tail 1C questions that should not require one-off route hardcoding
- replay breadth is still below the future open-world autonomy surface
Latest live proof now includes:
@ -72,14 +80,17 @@ Latest live proof now includes:
- `address_truth_harness_phase15_answer_inspection_followup_live_20260419_rerun11` accepted `9/9`
- `address_truth_harness_phase16_multicompany_late_pivot_live_20260419_rerun10` accepted
- `address_truth_harness_phase17_clarification_resume_and_counterparty_tail_live_20260419_rerun5` accepted `10/10`
- `address_truth_harness_phase24_metadata_lane_choice_loop_live_rerun5` accepted
- `address_truth_harness_phase25_entity_resolution_chain_live_rerun_full_chain` accepted
- `address_truth_harness_phase32_planner_selected_chain_end_to_end_live_rerun2` accepted `6/6`
Current architectural reading:
- the system is already materially past the dangerous regression breakpoint;
- it is now safe for continued architecture hardening and controlled domain-by-domain enablement under replay gates;
- it is now materially closer to pre-multidomain stability, but still not safe to declare broad low-risk multi-domain expansion.
- the practical next target is now `90%+ pre-multidomain readiness`, and the remaining gap should be treated as five large architecture iterations rather than as cosmetic cleanup.
- from this point onward, readiness must be judged not only by route truth and replay pass rate, but also by whether a new human user would feel that the assistant understands the intent and responds meaningfully in live wording.
- the practical next target is no longer only `90%+ pre-multidomain readiness`, but the first believable `open-world bounded autonomy` over 1C evidence.
- from this point onward, readiness must be judged not only by route truth and replay pass rate, but also by whether a new human user can ask a structurally new 1C data question and still get a bounded, evidence-honest answer path.
For the detailed audit, current percentages, and remaining debt, read:
@ -90,6 +101,8 @@ For the detailed audit, current percentages, and remaining debt, read:
- [12 - manual_run_system_analysis_3NilqwT1G2_2026-04-18.md](./12%20-%20manual_run_system_analysis_3NilqwT1G2_2026-04-18.md)
- [13 - pre_multidomain_readiness_audit_2026-04-18.md](./13%20-%20pre_multidomain_readiness_audit_2026-04-18.md)
- [14 - semantic_dialog_authority_recovery_plan_2026-04-19.md](./14%20-%20semantic_dialog_authority_recovery_plan_2026-04-19.md)
- [15 - mcp_bounded_autonomy_reset_plan_2026-04-21.md](./15%20-%20mcp_bounded_autonomy_reset_plan_2026-04-21.md)
- [16 - data_need_graph_and_open_world_mcp_plan_2026-04-22.md](./16%20-%20data_need_graph_and_open_world_mcp_plan_2026-04-22.md)
## Architectural Objects Of Planning
@ -122,6 +135,8 @@ Read in this order:
13. `12 - manual_run_system_analysis_3NilqwT1G2_2026-04-18.md`
14. `13 - pre_multidomain_readiness_audit_2026-04-18.md`
15. `14 - semantic_dialog_authority_recovery_plan_2026-04-19.md`
16. `15 - mcp_bounded_autonomy_reset_plan_2026-04-21.md`
17. `16 - data_need_graph_and_open_world_mcp_plan_2026-04-22.md`
## Planning Rules
@ -141,15 +156,14 @@ and start being described as:
- "a stateful exact-data assistant with explicit transition contracts and isolated truth gating."
As of `2026-04-19`, the project is already materially closer to the target description and is no longer in the same acute collapse state. The remaining blocker is no longer the original continuity failure itself, but the unfinished convergence toward one runtime authority plus still-insufficient replay breadth for low-risk multi-domain expansion.
As of `2026-04-22`, the project is already materially closer to the target description and is no longer in the same acute collapse state. The remaining blocker is no longer the original continuity failure itself, but the unfinished convergence from reviewed bounded MCP chains toward open-world data-need-driven autonomy with replay breadth still below the future blast radius.
The biggest remaining blockers are:
- split continuity ownership across route / transition / recap / coordinator glue;
- saved-session acceptance still too narrow compared with the intended domain-expansion blast radius outside the repaired flagship + late-pivot families;
- clarification precedence is much better than before, but still not yet proven widely enough outside the repaired replay family;
- no general `Question -> Data Need Graph` runtime authority yet;
- planner-selected primitive chains are real, but still narrower than open-world primitive search;
- dynamic schema traversal is not yet broad enough for unfamiliar 1C asks outside the repaired families;
- multi-hop evidence recovery still depends on bounded reviewed seams and not yet on a general exploration loop;
- residual `assistantService` overload;
- central intent pressure in `resolveAddressIntent()`;
- remaining answer-semantics pressure in `composeStage.ts` and `answerComposer.ts`.
- semantic robustness gaps where already-supported questions can still look broken to a human user because of typo sensitivity, short follow-up retarget loss, or human-answer mismatch.
- missing MCP semantic data-discovery layer where Qwen3 can help plan controlled 1C evidence search without bypassing runtime truth gates.

View File

@ -0,0 +1,77 @@
{
"schema_version": "domain_truth_harness_spec_v1",
"scenario_id": "address_truth_harness_phase23_broad_eval_short_followup",
"domain": "address_phase23_broad_eval_short_followup",
"title": "Phase 23 broad evaluation short follow-up replay",
"description": "Targeted AGENT replay for the seam where a broad business evaluation must not erase the last grounded MCP discovery context before a short year-switch follow-up.",
"bindings": {},
"steps": [
{
"step_id": "step_01_net_flow_2020",
"title": "Grounded net-flow answer seeds reusable discovery context",
"question": "какое нетто по деньгам с Группа СВК за 2020 год: сколько получили и сколько заплатили?",
"allowed_reply_types": [
"partial_coverage",
"factual_with_explanation"
],
"required_answer_patterns_all": [
"(?i)свк",
"(?i)получил|входящ|поступ",
"(?i)заплат|исходящ|списан|плат[её]ж",
"(?i)нетто|сальдо|разниц",
"(?i)2020|период",
"(?i)руб"
],
"criticality": "critical",
"semantic_tags": [
"counterparty_net_cash_flow",
"grounded_discovery_seed"
]
},
{
"step_id": "step_02_broad_business_evaluation",
"title": "Broad business evaluation reframes without erasing the grounded context",
"question": "Как ты оценишь деятельность компании?",
"required_answer_patterns_all": [
"(?i)коротко|оценк|частичн",
"(?i)1с|подтвержд|контекст",
"(?i)денежн|долг|ндс|контрагент"
],
"forbidden_answer_patterns": [
"(?i)активных заказчиков",
"(?i)последняя активность",
"(?i)^\\s*1\\."
],
"criticality": "critical",
"semantic_tags": [
"broad_business_evaluation",
"context_reframe"
]
},
{
"step_id": "step_03_short_year_switch_after_broad_eval",
"title": "Short year-switch follow-up still revives the last net-flow contour",
"question": "а теперь за 2021?",
"allowed_reply_types": [
"partial_coverage",
"factual_with_explanation"
],
"required_answer_patterns_all": [
"(?i)свк",
"(?i)2021|период",
"(?i)получил|входящ|поступ|заплат|исходящ|нетто|сальдо|разниц",
"(?i)руб|не удалось|не подтвержд|лимит"
],
"forbidden_answer_patterns": [
"(?i)активных заказчиков",
"(?i)последняя активность",
"(?i)уточните контрагент"
],
"criticality": "critical",
"semantic_tags": [
"broad_eval_followup_continuity",
"year_switch_after_reframe"
]
}
]
}

View File

@ -0,0 +1,84 @@
{
"schema_version": "domain_truth_harness_spec_v1",
"scenario_id": "address_truth_harness_phase24_metadata_lane_choice_loop",
"domain": "address_phase24_metadata_lane_choice_loop",
"title": "Phase 24 metadata lane-choice clarification loop replay",
"description": "Targeted AGENT replay for the metadata ambiguity seam where a mixed metadata surface must stay ambiguous on a neutral follow-up, ask the user to choose a lane, and then continue the same bounded chain after the user says 'по движениям'.",
"bindings": {},
"steps": [
{
"step_id": "step_01_metadata_ambiguity_surface",
"title": "Mixed metadata surface is surfaced honestly for the VAT domain",
"question": "какие объекты 1С есть по НДС?",
"allowed_reply_types": [
"partial_coverage",
"factual_with_explanation"
],
"required_answer_patterns_all": [
"(?i)metadata|метадан",
"(?i)ндс",
"(?i)документ|регистр"
],
"forbidden_answer_patterns": [
"(?i)получили",
"(?i)заплатили",
"(?i)нетто"
],
"criticality": "critical",
"semantic_tags": [
"metadata_surface",
"mixed_ambiguity"
]
},
{
"step_id": "step_02_neutral_followup_requires_lane_choice",
"title": "Neutral downstream follow-up keeps ambiguity and explicitly asks for a lane choice",
"question": "давай дальше",
"allowed_reply_types": [
"clarification_required",
"partial_coverage"
],
"required_answer_patterns_all": [
"(?i)документ",
"(?i)движени|регистр",
"(?i)уточн|выб(ери|рать)|какой контур"
],
"forbidden_answer_patterns": [
"(?i)получили",
"(?i)заплатили",
"(?i)нетто",
"(?i)документные строки найдены",
"(?i)строки денежных движений найдены"
],
"criticality": "critical",
"semantic_tags": [
"metadata_lane_choice_clarification",
"neutral_followup"
]
},
{
"step_id": "step_03_choose_movement_lane",
"title": "Short user choice continues the same metadata-born chain into movement evidence",
"question": "по движениям",
"allowed_reply_types": [
"partial_coverage",
"factual_with_explanation"
],
"required_answer_patterns_any": [
"(?i)движени",
"(?i)операц",
"(?i)проводк",
"(?i)денежн"
],
"forbidden_answer_patterns": [
"(?i)уточните, в какой контур",
"(?i)документные строки найдены"
],
"criticality": "critical",
"semantic_tags": [
"lane_choice_reuse",
"movement_lane_after_clarification"
]
}
]
}

View File

@ -0,0 +1,97 @@
{
"schema_version": "domain_truth_harness_spec_v1",
"scenario_id": "address_truth_harness_phase30_value_flow_to_movements_chain",
"domain": "address_phase30_value_flow_to_movements_chain",
"title": "Phase 30 grounded value-flow to movement evidence replay",
"description": "Targeted AGENT replay for Big Block C where a grounded counterparty and carried period must survive a pivot from value-flow answers into movement evidence without forcing the user to restate the resolved name.",
"bindings": {},
"steps": [
{
"step_id": "step_01_resolve_counterparty_alias",
"title": "Entity resolution grounds the checked 1C counterparty from a loose alias",
"question": "найди в 1С контрагента СВК",
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
"required_answer_patterns_all": ["(?i)свк", "(?i)контрагент"],
"required_answer_patterns_any": [
"(?i)группа\\s+свк",
"(?i)каталог",
"(?i)найден",
"(?i)наиболее вероятн"
],
"forbidden_answer_patterns": [
"(?i)получили",
"(?i)заплатили",
"(?i)нетто",
"(?i)оборот",
"(?i)выручк",
"(?i)сумм(а|ы)"
],
"criticality": "critical",
"semantic_tags": ["entity_resolution", "alias_grounding", "followup_anchor"]
},
{
"step_id": "step_02_incoming_by_resolved_entity",
"title": "Incoming value-flow follow-up reuses the resolved counterparty anchor",
"question": "сколько получили по нему за 2020 год",
"allowed_reply_types": ["factual_with_explanation", "partial_coverage"],
"required_answer_patterns_all": ["(?i)2020", "(?i)получил|входящ|поступ", "(?i)руб"],
"required_answer_patterns_any": ["(?i)группа\\s+свк", "(?i)свк"],
"forbidden_answer_patterns": [
"(?i)не найден контрагент",
"(?i)уточните, какого контрагента",
"(?i)по какому контрагенту"
],
"criticality": "critical",
"semantic_tags": ["entity_resolution", "incoming_value_flow", "followup_reuse"]
},
{
"step_id": "step_03_payout_switch_by_resolved_entity",
"title": "Outgoing payment follow-up keeps the same grounded counterparty and checked year",
"question": "а теперь сколько заплатили?",
"allowed_reply_types": ["factual_with_explanation", "partial_coverage"],
"required_answer_patterns_all": ["(?i)2020", "(?i)заплатил|исходящ|списан|платеж", "(?i)руб"],
"required_answer_patterns_any": ["(?i)группа\\s+свк", "(?i)свк"],
"forbidden_answer_patterns": [
"(?i)не найден контрагент",
"(?i)уточните, какого контрагента",
"(?i)по какому контрагенту",
"(?i)за какой год"
],
"criticality": "critical",
"semantic_tags": ["entity_resolution", "payout_switch", "followup_reuse", "date_carryover"]
},
{
"step_id": "step_04_year_switch_on_payout",
"title": "Short year switch keeps the payout contour and grounded counterparty",
"question": "а за 2021?",
"allowed_reply_types": ["factual_with_explanation", "partial_coverage"],
"required_answer_patterns_all": ["(?i)2021", "(?i)заплатил|исходящ|списан|платеж", "(?i)руб"],
"required_answer_patterns_any": ["(?i)группа\\s+свк", "(?i)свк"],
"forbidden_answer_patterns": [
"(?i)не найден контрагент",
"(?i)уточните, какого контрагента",
"(?i)по какому контрагенту"
],
"criticality": "critical",
"semantic_tags": ["entity_resolution", "payout_year_switch", "followup_reuse"]
},
{
"step_id": "step_05_movements_after_value_flow",
"title": "Movement evidence follow-up keeps the same grounded counterparty after the money answer",
"question": "а по движениям?",
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
"required_answer_patterns_all": ["(?i)движени|операц|платеж|списан|поступ"],
"required_answer_patterns_any": ["(?i)группа\\s+свк", "(?i)свк", "(?i)2021"],
"forbidden_answer_patterns": [
"(?i)не найден контрагент",
"(?i)уточните, какого контрагента",
"(?i)по какому контрагенту",
"(?i)сколько получили",
"(?i)сколько заплатили",
"(?i)нетто"
],
"criticality": "critical",
"semantic_tags": ["entity_resolution", "movement_evidence", "value_flow_pivot", "followup_reuse"]
}
]
}

View File

@ -0,0 +1,43 @@
{
"schema_version": "domain_truth_harness_spec_v1",
"scenario_id": "address_truth_harness_phase31_entity_ambiguity_probe",
"domain": "address_phase31_entity_ambiguity_probe",
"title": "Phase 31 entity ambiguity probe",
"description": "Probe whether raw counterparty wording creates a stable real ambiguity loop for MCP entity-resolution in the current 1C data.",
"bindings": {},
"steps": [
{
"step_id": "step_01_probe_ambiguous_counterparty_search",
"title": "Probe raw entity search wording for an ambiguity-capable live response",
"question": "найди в 1С контрагента СВК",
"allowed_reply_types": [
"factual_with_explanation",
"partial_coverage"
],
"required_answer_patterns_all": [
"(?i)свк",
"(?i)контрагент"
],
"required_answer_patterns_any": [
"(?i)каталог",
"(?i)1с",
"(?i)наш[её]л",
"(?i)найден"
],
"forbidden_answer_patterns": [
"(?i)получили",
"(?i)заплатили",
"(?i)нетто",
"(?i)оборот",
"(?i)выручк",
"(?i)сумм(а|ы)"
],
"criticality": "critical",
"semantic_tags": [
"entity_resolution",
"ambiguity_probe",
"bounded_autonomy"
]
}
]
}

View File

@ -0,0 +1,35 @@
{
"schema_version": "domain_truth_harness_spec_v1",
"scenario_id": "address_truth_harness_phase33_open_scope_value_flow_comparison",
"domain": "address_phase33_open_scope_value_flow_comparison",
"title": "Phase 33 open-scope value-flow comparison replay",
"description": "Targeted AGENT replay for Big Block D where an open-scope incoming-vs-outgoing money question must be understood as a bounded comparison need, not as a missing-counterparty fact ask.",
"bindings": {},
"steps": [
{
"step_id": "step_01_compare_incoming_vs_outgoing_for_org",
"title": "Raw organization-scoped comparison wording produces a bounded incoming-vs-outgoing answer without inventing a counterparty",
"question": "что больше: входящие или исходящие деньги за 2020 год по ООО Альтернатива Плюс?",
"allowed_reply_types": ["factual_with_explanation", "partial_coverage"],
"required_answer_patterns_all": [
"(?i)2020",
"(?i)входящ|получили|поступ",
"(?i)исходящ|заплатили|списан|платеж",
"(?i)руб"
],
"required_answer_patterns_any": [
"(?i)нетто|сальдо",
"(?i)больше|превыш",
"(?i)альтернатива"
],
"forbidden_answer_patterns": [
"(?i)уточните контрагента",
"(?i)не найден контрагент",
"(?i)по какому контрагенту",
"(?i)не найдено контрагента"
],
"criticality": "critical",
"semantic_tags": ["value_flow_comparison", "open_scope", "organization_scoped", "bounded_autonomy"]
}
]
}

View File

@ -0,0 +1,56 @@
{
"schema_version": "domain_truth_harness_spec_v1",
"scenario_id": "address_truth_harness_phase34_open_scope_value_flow_totals",
"domain": "address_phase34_open_scope_value_flow_totals",
"title": "Phase 34 open-scope value-flow totals replay",
"description": "Targeted AGENT replay for Big Block D where organization-scoped incoming/outgoing money totals must be understood as bounded open-scope value-flow questions rather than as missing-counterparty fact asks.",
"bindings": {},
"steps": [
{
"step_id": "step_01_incoming_total_for_org",
"title": "Raw organization-scoped incoming wording produces a bounded incoming total without inventing a counterparty",
"question": "Сколько входящих денег за 2020 год по ООО Альтернатива Плюс?",
"allowed_reply_types": ["factual_with_explanation", "partial_coverage"],
"required_answer_patterns_all": [
"(?i)2020",
"(?i)входящ|получ|поступ",
"(?i)руб"
],
"required_answer_patterns_any": [
"(?i)альтернатива",
"(?i)проверенн|найденн"
],
"forbidden_answer_patterns": [
"(?i)уточните контрагента",
"(?i)не найден контрагент",
"(?i)по какому контрагенту",
"(?i)не найдено контрагента"
],
"criticality": "critical",
"semantic_tags": ["value_flow_total", "incoming", "open_scope", "organization_scoped", "bounded_autonomy"]
},
{
"step_id": "step_02_outgoing_total_for_org",
"title": "Raw organization-scoped outgoing wording produces a bounded payout total without inventing a counterparty",
"question": "Сколько исходящих денег за 2020 год по ООО Альтернатива Плюс?",
"allowed_reply_types": ["factual_with_explanation", "partial_coverage"],
"required_answer_patterns_all": [
"(?i)2020",
"(?i)исходящ|заплат|списан|платеж",
"(?i)руб"
],
"required_answer_patterns_any": [
"(?i)альтернатива",
"(?i)проверенн|найденн"
],
"forbidden_answer_patterns": [
"(?i)уточните контрагента",
"(?i)не найден контрагент",
"(?i)по какому контрагенту",
"(?i)не найдено контрагента"
],
"criticality": "critical",
"semantic_tags": ["value_flow_total", "outgoing", "open_scope", "organization_scoped", "bounded_autonomy"]
}
]
}

View File

@ -0,0 +1,75 @@
{
"schema_version": "domain_truth_harness_spec_v1",
"scenario_id": "address_truth_harness_phase35_open_scope_org_clarification",
"domain": "address_phase35_open_scope_org_clarification",
"title": "Phase 35 open-scope organization clarification replay",
"description": "Targeted AGENT replay for Big Block D where a generic one-sided money total must ask for organization, then continue the same bounded open-scope value-flow chain after the user clarifies only the organization.",
"bindings": {},
"steps": [
{
"step_id": "step_01_generic_incoming_total_requires_organization",
"title": "Generic incoming total asks for organization instead of inventing a counterparty",
"question": "Сколько входящих денег за 2020 год?",
"allowed_reply_types": ["clarification_required", "partial_coverage"],
"required_answer_patterns_all": [
"(?i)уточн|нужно",
"(?i)организац"
],
"forbidden_answer_patterns": [
"(?i)уточните контрагента",
"(?i)не найден контрагент",
"(?i)по какому контрагенту",
"(?i)не найдено контрагента"
],
"criticality": "critical",
"semantic_tags": ["value_flow_total", "incoming", "open_scope", "organization_clarification", "bounded_autonomy"]
},
{
"step_id": "step_02_org_clarification_resumes_incoming_total",
"title": "Organization-only clarification resumes the same open-scope incoming total chain",
"question": "по ООО Альтернатива Плюс",
"allowed_reply_types": ["factual_with_explanation", "partial_coverage"],
"required_answer_patterns_all": [
"(?i)2020",
"(?i)входящ|получ|поступ",
"(?i)руб"
],
"required_answer_patterns_any": [
"(?i)альтернатива",
"(?i)проверенн|найденн"
],
"forbidden_answer_patterns": [
"(?i)уточните контрагента",
"(?i)не найден контрагент",
"(?i)по какому контрагенту",
"(?i)не найдено контрагента"
],
"criticality": "critical",
"semantic_tags": ["value_flow_total", "incoming", "organization_followup_reuse", "bounded_autonomy"]
},
{
"step_id": "step_03_outgoing_followup_reuses_org_and_period",
"title": "Short outgoing follow-up reuses the same organization and period without reintroducing a counterparty",
"question": "а исходящих?",
"allowed_reply_types": ["factual_with_explanation", "partial_coverage"],
"required_answer_patterns_all": [
"(?i)2020",
"(?i)исходящ|заплат|списан|платеж",
"(?i)руб"
],
"required_answer_patterns_any": [
"(?i)альтернатива",
"(?i)проверенн|найденн"
],
"forbidden_answer_patterns": [
"(?i)уточните контрагента",
"(?i)не найден контрагент",
"(?i)по какому контрагенту",
"(?i)не найдено контрагента",
"(?i)уточните организацию"
],
"criticality": "critical",
"semantic_tags": ["value_flow_total", "outgoing", "organization_followup_reuse", "bounded_autonomy"]
}
]
}

View File

@ -0,0 +1,99 @@
{
"schema_version": "domain_truth_harness_spec_v1",
"scenario_id": "address_truth_harness_phase36_open_scope_year_switch_after_org_clarification",
"domain": "address_phase36_open_scope_year_switch_after_org_clarification",
"title": "Phase 36 open-scope year switch after organization clarification",
"description": "Targeted AGENT replay for Big Block D where an open-scope value-flow total asks for organization, then preserves the same organization and money-flow contour across a short year-switch follow-up and a one-sided outgoing follow-up.",
"bindings": {},
"steps": [
{
"step_id": "step_01_generic_incoming_total_requires_organization",
"title": "Generic incoming total asks for organization before execution",
"question": "Сколько входящих денег за 2020 год?",
"allowed_reply_types": ["clarification_required", "partial_coverage"],
"required_answer_patterns_all": [
"(?i)уточн|нужно",
"(?i)организац"
],
"forbidden_answer_patterns": [
"(?i)уточните контрагента",
"(?i)не найден контрагент",
"(?i)по какому контрагенту",
"(?i)не найдено контрагента"
],
"criticality": "critical",
"semantic_tags": ["value_flow_total", "incoming", "open_scope", "organization_clarification", "bounded_autonomy"]
},
{
"step_id": "step_02_org_clarification_resumes_incoming_total",
"title": "Organization-only clarification resumes the same incoming total for 2020",
"question": "по ООО Альтернатива Плюс",
"allowed_reply_types": ["factual_with_explanation", "partial_coverage"],
"required_answer_patterns_all": [
"(?i)2020",
"(?i)входящ|получ|поступ",
"(?i)руб"
],
"required_answer_patterns_any": [
"(?i)альтернатива",
"(?i)проверенн|найденн"
],
"forbidden_answer_patterns": [
"(?i)уточните контрагента",
"(?i)не найден контрагент",
"(?i)по какому контрагенту",
"(?i)не найдено контрагента"
],
"criticality": "critical",
"semantic_tags": ["value_flow_total", "incoming", "organization_followup_reuse", "bounded_autonomy"]
},
{
"step_id": "step_03_year_switch_reuses_org_and_axis",
"title": "Short year-switch follow-up keeps the same organization and incoming contour",
"question": "а за 2021?",
"allowed_reply_types": ["factual_with_explanation", "partial_coverage"],
"required_answer_patterns_all": [
"(?i)2021",
"(?i)входящ|получ|поступ",
"(?i)руб"
],
"required_answer_patterns_any": [
"(?i)альтернатива",
"(?i)проверенн|найденн"
],
"forbidden_answer_patterns": [
"(?i)уточните контрагента",
"(?i)не найден контрагент",
"(?i)по какому контрагенту",
"(?i)не найдено контрагента",
"(?i)уточните организацию"
],
"criticality": "critical",
"semantic_tags": ["value_flow_total", "incoming", "year_switch", "organization_followup_reuse", "bounded_autonomy"]
},
{
"step_id": "step_04_outgoing_followup_reuses_org_and_new_year",
"title": "Outgoing follow-up keeps the same organization and already switched year",
"question": "а исходящих?",
"allowed_reply_types": ["factual_with_explanation", "partial_coverage"],
"required_answer_patterns_all": [
"(?i)2021",
"(?i)исходящ|заплат|списан|платеж",
"(?i)руб"
],
"required_answer_patterns_any": [
"(?i)альтернатива",
"(?i)проверенн|найденн"
],
"forbidden_answer_patterns": [
"(?i)уточните контрагента",
"(?i)не найден контрагент",
"(?i)по какому контрагенту",
"(?i)не найдено контрагента",
"(?i)уточните организацию"
],
"criticality": "critical",
"semantic_tags": ["value_flow_total", "outgoing", "year_switch", "organization_followup_reuse", "bounded_autonomy"]
}
]
}

View File

@ -0,0 +1,77 @@
{
"schema_version": "domain_truth_harness_spec_v1",
"scenario_id": "address_truth_harness_phase37_open_scope_comparison_org_clarification",
"domain": "address_phase37_open_scope_comparison_org_clarification",
"title": "Phase 37 open-scope comparison organization clarification",
"description": "Targeted AGENT replay for Big Block D where an open-scope incoming-vs-outgoing comparison must ask for organization, then resume the same comparison contour after an organization-only clarification and preserve it across a short year switch.",
"bindings": {},
"steps": [
{
"step_id": "step_01_open_scope_comparison_requires_organization",
"title": "Generic incoming-vs-outgoing comparison asks for organization first",
"question": "Что больше за 2020 год: входящих денег или исходящих?",
"allowed_reply_types": ["clarification_required", "partial_coverage"],
"required_answer_patterns_all": [
"(?i)уточн|нужно",
"(?i)организац"
],
"forbidden_answer_patterns": [
"(?i)уточните контрагента",
"(?i)не найден контрагент",
"(?i)по какому контрагенту",
"(?i)не найдено контрагента"
],
"criticality": "critical",
"semantic_tags": ["value_flow_comparison", "open_scope", "organization_clarification", "bounded_autonomy"]
},
{
"step_id": "step_02_org_clarification_resumes_comparison",
"title": "Organization-only clarification resumes the same comparison contour for 2020",
"question": "по ООО Альтернатива Плюс",
"allowed_reply_types": ["factual_with_explanation", "partial_coverage"],
"required_answer_patterns_all": [
"(?i)2020",
"(?i)входящ|исходящ|получ|заплат|списан|платеж",
"(?i)руб"
],
"required_answer_patterns_any": [
"(?i)больше|меньше|превыш|разниц",
"(?i)альтернатива",
"(?i)проверенн|найденн"
],
"forbidden_answer_patterns": [
"(?i)уточните контрагента",
"(?i)не найден контрагент",
"(?i)по какому контрагенту",
"(?i)не найдено контрагента"
],
"criticality": "critical",
"semantic_tags": ["value_flow_comparison", "organization_followup_reuse", "bounded_autonomy"]
},
{
"step_id": "step_03_year_switch_reuses_org_and_comparison",
"title": "Short year-switch follow-up keeps the same organization and comparison contour",
"question": "а за 2021?",
"allowed_reply_types": ["factual_with_explanation", "partial_coverage"],
"required_answer_patterns_all": [
"(?i)2021",
"(?i)входящ|исходящ|получ|заплат|списан|платеж",
"(?i)руб"
],
"required_answer_patterns_any": [
"(?i)больше|меньше|превыш|разниц",
"(?i)альтернатива",
"(?i)проверенн|найденн"
],
"forbidden_answer_patterns": [
"(?i)уточните контрагента",
"(?i)не найден контрагент",
"(?i)по какому контрагенту",
"(?i)не найдено контрагента",
"(?i)уточните организацию"
],
"criticality": "critical",
"semantic_tags": ["value_flow_comparison", "year_switch", "organization_followup_reuse", "bounded_autonomy"]
}
]
}

View File

@ -0,0 +1,79 @@
{
"schema_version": "domain_truth_harness_spec_v1",
"scenario_id": "address_truth_harness_phase38_open_scope_net_org_clarification",
"domain": "address_phase38_open_scope_net_org_clarification",
"title": "Phase 38 open-scope net organization clarification",
"description": "Targeted AGENT replay for Big Block D where an open-scope net money-flow question must ask for organization, then resume the same bidirectional contour after an organization-only clarification and preserve it across a short year switch.",
"bindings": {},
"steps": [
{
"step_id": "step_01_open_scope_net_requires_organization",
"title": "Generic net value-flow question asks for organization first",
"question": "Какое нетто по деньгам за 2020 год: сколько получили и сколько заплатили?",
"allowed_reply_types": ["clarification_required", "partial_coverage"],
"required_answer_patterns_all": [
"(?i)уточн|нужно",
"(?i)организац"
],
"forbidden_answer_patterns": [
"(?i)уточните контрагента",
"(?i)не найден контрагент",
"(?i)по какому контрагенту",
"(?i)не найдено контрагента"
],
"criticality": "critical",
"semantic_tags": ["value_flow_net", "open_scope", "organization_clarification", "bounded_autonomy"]
},
{
"step_id": "step_02_org_clarification_resumes_net",
"title": "Organization-only clarification resumes the same net contour for 2020",
"question": "по ООО Альтернатива Плюс",
"allowed_reply_types": ["factual_with_explanation", "partial_coverage"],
"required_answer_patterns_all": [
"(?i)2020",
"(?i)получ|входящ|поступ",
"(?i)заплат|исходящ|списан|платеж",
"(?i)нетто|сальдо|разниц",
"(?i)руб"
],
"required_answer_patterns_any": [
"(?i)альтернатива",
"(?i)проверенн|найденн"
],
"forbidden_answer_patterns": [
"(?i)уточните контрагента",
"(?i)не найден контрагент",
"(?i)по какому контрагенту",
"(?i)не найдено контрагента"
],
"criticality": "critical",
"semantic_tags": ["value_flow_net", "organization_followup_reuse", "bounded_autonomy"]
},
{
"step_id": "step_03_year_switch_reuses_org_and_net",
"title": "Short year-switch follow-up keeps the same organization and bidirectional net contour",
"question": "а за 2021?",
"allowed_reply_types": ["factual_with_explanation", "partial_coverage"],
"required_answer_patterns_all": [
"(?i)2021",
"(?i)получ|входящ|поступ",
"(?i)заплат|исходящ|списан|платеж",
"(?i)нетто|сальдо|разниц",
"(?i)руб"
],
"required_answer_patterns_any": [
"(?i)альтернатива",
"(?i)проверенн|найденн"
],
"forbidden_answer_patterns": [
"(?i)уточните контрагента",
"(?i)не найден контрагент",
"(?i)по какому контрагенту",
"(?i)не найдено контрагента",
"(?i)уточните организацию"
],
"criticality": "critical",
"semantic_tags": ["value_flow_net", "year_switch", "organization_followup_reuse", "bounded_autonomy"]
}
]
}

View File

@ -0,0 +1,77 @@
{
"schema_version": "domain_truth_harness_spec_v1",
"scenario_id": "address_truth_harness_phase39_open_scope_ranking_org_clarification",
"domain": "address_phase39_open_scope_ranking_org_clarification",
"title": "Phase 39 open-scope ranking organization clarification",
"description": "Targeted AGENT replay for Big Block D where an open-scope top-value-flow question must ask for organization, then resume the same ranking contour after an organization-only clarification and preserve it across a short year switch.",
"bindings": {},
"steps": [
{
"step_id": "step_01_open_scope_ranking_requires_organization",
"title": "Generic top customer question asks for organization before ranking",
"question": "Кто больше всего принес денег за 2020 год?",
"allowed_reply_types": ["clarification_required", "partial_coverage"],
"required_answer_patterns_all": [
"(?i)уточн|нужно",
"(?i)организац"
],
"forbidden_answer_patterns": [
"(?i)уточните контрагента",
"(?i)не найден контрагент",
"(?i)по какому контрагенту",
"(?i)не найдено контрагента"
],
"criticality": "critical",
"semantic_tags": ["value_flow_ranking", "open_scope", "organization_clarification", "bounded_autonomy"]
},
{
"step_id": "step_02_org_clarification_resumes_ranking",
"title": "Organization-only clarification resumes the same ranking contour for 2020",
"question": "по ООО Альтернатива Плюс",
"allowed_reply_types": ["factual_with_explanation", "partial_coverage"],
"required_answer_patterns_all": [
"(?i)2020",
"(?i)клиент|контрагент|заказчик",
"(?i)руб"
],
"required_answer_patterns_any": [
"(?i)больше всего|топ|самый доходный|наибол",
"(?i)альтернатива",
"(?i)проверенн|найденн"
],
"forbidden_answer_patterns": [
"(?i)уточните контрагента",
"(?i)не найден контрагент",
"(?i)по какому контрагенту",
"(?i)не найдено контрагента"
],
"criticality": "critical",
"semantic_tags": ["value_flow_ranking", "organization_followup_reuse", "bounded_autonomy"]
},
{
"step_id": "step_03_year_switch_reuses_org_and_ranking",
"title": "Short year-switch follow-up keeps the same organization and ranking contour",
"question": "а за 2021?",
"allowed_reply_types": ["factual_with_explanation", "partial_coverage"],
"required_answer_patterns_all": [
"(?i)2021",
"(?i)клиент|контрагент|заказчик",
"(?i)руб"
],
"required_answer_patterns_any": [
"(?i)больше всего|топ|самый доходный|наибол",
"(?i)альтернатива",
"(?i)проверенн|найденн"
],
"forbidden_answer_patterns": [
"(?i)уточните контрагента",
"(?i)не найден контрагент",
"(?i)по какому контрагенту",
"(?i)не найдено контрагента",
"(?i)уточните организацию"
],
"criticality": "critical",
"semantic_tags": ["value_flow_ranking", "year_switch", "organization_followup_reuse", "bounded_autonomy"]
}
]
}

View File

@ -0,0 +1,78 @@
{
"schema_version": "domain_truth_harness_spec_v1",
"scenario_id": "address_truth_harness_phase40_open_scope_all_time_followup",
"domain": "address_phase40_open_scope_all_time_followup",
"title": "Phase 40 open-scope all-time follow-up after year total",
"description": "Targeted AGENT replay for Big Block D where a bounded organization-scoped yearly incoming total must pivot into an all-time open-scope total without carrying a stale follow-up date from the previous turn.",
"bindings": {},
"steps": [
{
"step_id": "step_01_year_total_for_org",
"title": "Organization-scoped yearly total returns a bounded incoming amount for 2017",
"question": "Сколько входящих денег за 2017 год по ООО Альтернатива Плюс?",
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
"required_answer_patterns_all": [
"(?i)2017",
"(?i)входящ|получ|поступ",
"(?i)руб|₽"
],
"required_answer_patterns_any": [
"(?i)альтернатива",
"(?i)проверенн|найденн"
],
"forbidden_answer_patterns": [
"(?i)уточните организацию",
"(?i)уточните контрагента",
"(?i)не найден контрагент",
"(?i)по какому контрагенту",
"(?i)не найдено контрагента"
],
"criticality": "critical",
"semantic_tags": [
"value_flow_total",
"incoming",
"organization_scoped",
"year_specific",
"bounded_autonomy"
]
},
{
"step_id": "step_02_all_time_followup_reuses_org_without_stale_year",
"title": "All-time follow-up keeps the organization but drops the stale year filter",
"question": "сколько вообще денег мы заработали за все время?",
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
"required_answer_patterns_all": [
"(?i)входящ|получ|поступ|заработ",
"(?i)руб|₽"
],
"required_answer_patterns_any": [
"(?i)за все время",
"(?i)за все доступное время",
"(?i)весь проверенн",
"(?i)подтвержден",
"(?i)проверенн|найденн",
"(?i)по данным 1с"
],
"forbidden_answer_patterns": [
"(?i)уточните организацию",
"(?i)уточните контрагента",
"(?i)не найден контрагент",
"(?i)по какому контрагенту",
"(?i)не найдено контрагента",
"(?i)не получил",
"(?i)не смог",
"(?i)не удалось",
"(?i)не подтвержденного факта"
],
"criticality": "critical",
"semantic_tags": [
"value_flow_total",
"incoming",
"open_scope",
"all_time_scope",
"organization_followup_reuse",
"bounded_autonomy"
]
}
]
}

View File

@ -0,0 +1,104 @@
{
"schema_version": "domain_truth_harness_spec_v1",
"scenario_id": "address_truth_harness_phase41_saved_chain_all_time_revenue_followup",
"domain": "address_phase41_saved_chain_all_time_revenue_followup",
"title": "Phase 41 saved-chain all-time revenue follow-up",
"description": "Targeted AGENT replay for the saved-session seam from assistant-stage1-RFWTAQ-aXR where the assistant must survive the natural chain 'самый доходный год -> а за 2017 мы скок заработали -> сколько вообще денег мы заработали за все время' without collapsing into an empty partial answer.",
"bindings": {},
"steps": [
{
"step_id": "step_01_top_revenue_year",
"title": "The assistant identifies the top confirmed revenue year in the active self-scope contour",
"question": "какой у нас самый доходный год",
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
"required_answer_patterns_all": [
"(?i)20\\d\\d",
"(?i)денежн|поступ|доходн|выручк",
"(?i)руб|₽"
],
"required_answer_patterns_any": [
"(?i)самый доходный год",
"(?i)топ-?3",
"(?i)подтвержден"
],
"forbidden_answer_patterns": [
"(?i)уточните организацию",
"(?i)уточните контрагента",
"(?i)не найден контрагент"
],
"criticality": "critical",
"semantic_tags": [
"value_flow_ranking",
"self_scope",
"saved_chain",
"bounded_autonomy"
]
},
{
"step_id": "step_02_year_followup_2017",
"title": "Short year follow-up reuses the same contour and returns a bounded 2017 incoming total",
"question": "а за 2017 мы скок заработали?",
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
"required_answer_patterns_all": [
"(?i)2017",
"(?i)входящ|получ|поступ|заработ",
"(?i)руб|₽"
],
"required_answer_patterns_any": [
"(?i)подтвержден",
"(?i)проверенн|найденн",
"(?i)по данным 1с",
"(?i)в рамках проверенного периода"
],
"forbidden_answer_patterns": [
"(?i)уточните организацию",
"(?i)уточните контрагента",
"(?i)не найден контрагент",
"(?i)не получил",
"(?i)не смог",
"(?i)не удалось"
],
"criticality": "critical",
"semantic_tags": [
"value_flow_total",
"year_switch",
"self_scope",
"saved_chain",
"bounded_autonomy"
]
},
{
"step_id": "step_03_all_time_followup",
"title": "All-time follow-up stays in the same money contour and does not fall into an empty partial answer",
"question": "сколько вообще денег мы заработали за все время?",
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
"required_answer_patterns_all": [
"(?i)входящ|получ|поступ|заработ",
"(?i)руб|₽"
],
"required_answer_patterns_any": [
"(?i)за все время",
"(?i)за все доступное время",
"(?i)подтвержден",
"(?i)по данным 1с"
],
"forbidden_answer_patterns": [
"(?i)уточните организацию",
"(?i)уточните контрагента",
"(?i)не найден контрагент",
"(?i)не получил",
"(?i)не смог",
"(?i)не удалось",
"(?i)подтвержденного факта для ответа не получил"
],
"criticality": "critical",
"semantic_tags": [
"value_flow_total",
"all_time_scope",
"self_scope",
"saved_chain",
"bounded_autonomy"
]
}
]
}

View File

@ -0,0 +1,63 @@
{
"schema_version": "domain_truth_harness_spec_v1",
"scenario_id": "address_truth_harness_phase42_catalog_metadata_drilldown",
"domain": "address_phase42_catalog_metadata_drilldown",
"title": "Phase 42 catalog metadata drilldown replay",
"description": "Targeted AGENT replay for the new Big Block E seam where a catalog-oriented metadata surface should continue on a neutral follow-up into a deeper catalog metadata probe instead of collapsing back into generic metadata or asking for an unrelated lane choice.",
"bindings": {},
"steps": [
{
"step_id": "step_01_catalog_metadata_surface",
"title": "Catalog-oriented metadata surface is surfaced honestly for counterparties",
"question": "какие справочники 1С есть по контрагентам?",
"allowed_reply_types": [
"partial_coverage",
"factual_with_explanation"
],
"required_answer_patterns_all": [
"(?i)metadata|метадан",
"(?i)справоч|catalog|directory",
"(?i)контрагент"
],
"forbidden_answer_patterns": [
"(?i)получили",
"(?i)заплатили",
"(?i)нетто",
"(?i)документные строки найдены",
"(?i)строки денежных движений найдены"
],
"criticality": "critical",
"semantic_tags": [
"catalog_metadata_surface",
"counterparty_catalog_scope"
]
},
{
"step_id": "step_02_neutral_followup_catalog_drilldown",
"title": "Neutral follow-up continues into deeper catalog metadata instead of asking for a documents-vs-movements lane choice",
"question": "давай дальше",
"allowed_reply_types": [
"partial_coverage",
"factual_with_explanation"
],
"required_answer_patterns_all": [
"(?i)metadata|метадан|схем",
"(?i)справоч|catalog|directory",
"(?i)контрагент|counterpart"
],
"forbidden_answer_patterns": [
"(?i)документ",
"(?i)движени|регистр",
"(?i)уточн.*контур",
"(?i)получили",
"(?i)заплатили",
"(?i)нетто"
],
"criticality": "critical",
"semantic_tags": [
"catalog_drilldown",
"neutral_followup"
]
}
]
}

2
external/FoxyLink vendored

@ -1 +1 @@
Subproject commit 344c5a46ddcb5fb774ec718f2d1bc7027ba20491
Subproject commit e18c514983189fd63e8650da01673c1231a97258

View File

@ -4,8 +4,12 @@ exports.readAssistantMcpDiscoveryEntityResolutionStatus = readAssistantMcpDiscov
exports.readAssistantMcpDiscoveryEntityAmbiguityCandidates = readAssistantMcpDiscoveryEntityAmbiguityCandidates;
exports.readAssistantMcpDiscoveryEntityCandidates = readAssistantMcpDiscoveryEntityCandidates;
exports.readAssistantMcpDiscoveryPilotScope = readAssistantMcpDiscoveryPilotScope;
exports.readAssistantMcpDiscoveryRankingNeed = readAssistantMcpDiscoveryRankingNeed;
exports.readAssistantMcpDiscoveryMetadataRouteFamily = readAssistantMcpDiscoveryMetadataRouteFamily;
exports.readAssistantMcpDiscoveryMetadataRouteFamilySelectionBasis = readAssistantMcpDiscoveryMetadataRouteFamilySelectionBasis;
exports.readAssistantMcpDiscoveryMetadataSelectedEntitySet = readAssistantMcpDiscoveryMetadataSelectedEntitySet;
exports.readAssistantMcpDiscoveryMetadataSelectedSurfaceObjects = readAssistantMcpDiscoveryMetadataSelectedSurfaceObjects;
exports.readAssistantMcpDiscoveryMetadataRecommendedNextPrimitive = readAssistantMcpDiscoveryMetadataRecommendedNextPrimitive;
exports.readAssistantMcpDiscoveryMetadataAmbiguityDetected = readAssistantMcpDiscoveryMetadataAmbiguityDetected;
exports.readAssistantMcpDiscoveryMetadataAmbiguityEntitySets = readAssistantMcpDiscoveryMetadataAmbiguityEntitySets;
exports.formatIsoDateForReply = formatIsoDateForReply;
@ -76,6 +80,11 @@ function readAssistantMcpDiscoveryTurnMeaning(debug) {
const turnInput = toRecordObject(entry?.turn_input);
return toRecordObject(turnInput?.turn_meaning_ref);
}
function readAssistantMcpDiscoveryDataNeedGraph(debug) {
const entry = readAssistantMcpDiscoveryEntry(debug);
const turnInput = toRecordObject(entry?.turn_input);
return toRecordObject(turnInput?.data_need_graph);
}
function readAssistantMcpDiscoveryTurnMeaningMetadataAmbiguityEntitySets(debug, toNonEmptyString = fallbackToNonEmptyString) {
const values = readAssistantMcpDiscoveryTurnMeaning(debug)?.metadata_ambiguity_entity_sets;
if (!Array.isArray(values)) {
@ -142,12 +151,28 @@ function readAssistantMcpDiscoveryPilotScope(debug, toNonEmptyString = fallbackT
const pilot = toRecordObject(bridge?.pilot);
return toNonEmptyString(pilot?.pilot_scope);
}
function readAssistantMcpDiscoveryRankingNeed(debug, toNonEmptyString = fallbackToNonEmptyString) {
return toNonEmptyString(readAssistantMcpDiscoveryDataNeedGraph(debug)?.ranking_need);
}
function readAssistantMcpDiscoveryMetadataRouteFamily(debug, toNonEmptyString = fallbackToNonEmptyString) {
return toNonEmptyString(readAssistantMcpDiscoveryDerivedMetadataSurface(debug)?.downstream_route_family);
}
function readAssistantMcpDiscoveryMetadataRouteFamilySelectionBasis(debug, toNonEmptyString = fallbackToNonEmptyString) {
return toNonEmptyString(readAssistantMcpDiscoveryDerivedMetadataSurface(debug)?.route_family_selection_basis);
}
function readAssistantMcpDiscoveryMetadataSelectedEntitySet(debug, toNonEmptyString = fallbackToNonEmptyString) {
return toNonEmptyString(readAssistantMcpDiscoveryDerivedMetadataSurface(debug)?.selected_entity_set);
}
function readAssistantMcpDiscoveryMetadataSelectedSurfaceObjects(debug, toNonEmptyString = fallbackToNonEmptyString) {
const values = readAssistantMcpDiscoveryDerivedMetadataSurface(debug)?.selected_surface_objects;
if (!Array.isArray(values)) {
return [];
}
return values.map((item) => toNonEmptyString(item)).filter((item) => Boolean(item));
}
function readAssistantMcpDiscoveryMetadataRecommendedNextPrimitive(debug, toNonEmptyString = fallbackToNonEmptyString) {
return toNonEmptyString(readAssistantMcpDiscoveryDerivedMetadataSurface(debug)?.recommended_next_primitive);
}
function readAssistantMcpDiscoveryMetadataAmbiguityDetected(debug) {
return (readAssistantMcpDiscoveryDerivedMetadataSurface(debug)?.ambiguity_detected === true ||
readAssistantMcpDiscoveryTurnMeaningMetadataAmbiguityEntitySets(debug).length > 0);

View File

@ -1,6 +1,9 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ASSISTANT_MCP_CATALOG_PLAN_REVIEW_SCHEMA_VERSION = exports.ASSISTANT_MCP_CATALOG_INDEX_SCHEMA_VERSION = void 0;
exports.searchAssistantMcpCatalogPrimitivesByDecompositionCandidates = searchAssistantMcpCatalogPrimitivesByDecompositionCandidates;
exports.searchAssistantMcpCatalogPrimitivesByFactAxis = searchAssistantMcpCatalogPrimitivesByFactAxis;
exports.searchAssistantMcpCatalogPrimitivesByMetadataSurface = searchAssistantMcpCatalogPrimitivesByMetadataSurface;
exports.buildAssistantMcpCatalogIndex = buildAssistantMcpCatalogIndex;
exports.getAssistantMcpCatalogPrimitive = getAssistantMcpCatalogPrimitive;
exports.reviewAssistantMcpDiscoveryPlanAgainstCatalog = reviewAssistantMcpDiscoveryPlanAgainstCatalog;
@ -11,6 +14,10 @@ const PRIMITIVE_CONTRACTS = [
{
primitive_id: "search_business_entity",
purpose: "Find candidate 1C business entities by user wording before a fact query is executed.",
decomposition_hints: ["search_business_entity"],
supported_fact_families: ["entity_grounding"],
supported_action_families: ["search_business_entity"],
planning_tags: ["subject_resolution"],
required_axes_any_of: [["business_entity"], ["counterparty"], ["organization"], ["contract"], ["item"]],
optional_axes: ["period", "document", "account"],
output_fact_kinds: ["entity_candidates", "entity_ambiguity"],
@ -21,6 +28,10 @@ const PRIMITIVE_CONTRACTS = [
{
primitive_id: "inspect_1c_metadata",
purpose: "Inspect available 1C schema/catalog/document/register surface before selecting a query lane.",
decomposition_hints: ["inspect_metadata_surface"],
supported_fact_families: ["schema_surface"],
supported_action_families: ["inspect_catalog", "inspect_documents", "inspect_registers", "inspect_fields", "inspect_surface"],
planning_tags: ["metadata", "surface_inspection"],
required_axes_any_of: [["metadata_scope"], ["domain_family"], ["document"], ["register"]],
optional_axes: ["business_entity", "account", "counterparty"],
output_fact_kinds: ["available_fields", "available_entity_sets", "known_limitations"],
@ -31,6 +42,10 @@ const PRIMITIVE_CONTRACTS = [
{
primitive_id: "resolve_entity_reference",
purpose: "Resolve a user-visible entity name to a concrete 1C reference candidate.",
decomposition_hints: ["resolve_entity_reference"],
supported_fact_families: ["entity_grounding", "value_flow", "document_evidence", "movement_evidence", "activity_lifecycle"],
supported_action_families: ["search_business_entity", "turnover", "payout", "net_value_flow", "list_documents", "list_movements", "activity_duration"],
planning_tags: ["subject_resolution"],
required_axes_any_of: [["business_entity"], ["counterparty"], ["organization"], ["contract"], ["item"]],
optional_axes: ["period", "inn", "document"],
output_fact_kinds: ["resolved_entity_ref", "entity_conflict"],
@ -41,6 +56,15 @@ const PRIMITIVE_CONTRACTS = [
{
primitive_id: "query_movements",
purpose: "Fetch or aggregate accounting/register movements for a scoped business question.",
decomposition_hints: [
"collect_scoped_movements",
"collect_incoming_movements",
"collect_outgoing_movements",
"fetch_scoped_movements"
],
supported_fact_families: ["value_flow", "movement_evidence"],
supported_action_families: ["turnover", "payout", "net_value_flow", "list_movements"],
planning_tags: ["movement", "comparison", "ranking", "aggregation", "monthly_aggregation"],
required_axes_any_of: [["period", "account"], ["period", "counterparty"], ["period", "organization"]],
optional_axes: ["contract", "document", "amount", "item", "warehouse"],
output_fact_kinds: ["movement_rows", "turnover", "balance_delta"],
@ -51,6 +75,10 @@ const PRIMITIVE_CONTRACTS = [
{
primitive_id: "query_documents",
purpose: "Fetch documents related to a scoped entity, period, contract, or movement explanation.",
decomposition_hints: ["fetch_scoped_documents", "fetch_supporting_documents"],
supported_fact_families: ["document_evidence", "activity_lifecycle"],
supported_action_families: ["list_documents", "activity_duration"],
planning_tags: ["document"],
required_axes_any_of: [["document"], ["counterparty"], ["contract"], ["period", "organization"]],
optional_axes: ["account", "amount", "item", "warehouse"],
output_fact_kinds: ["document_rows", "document_dates", "document_amounts"],
@ -61,6 +89,10 @@ const PRIMITIVE_CONTRACTS = [
{
primitive_id: "aggregate_by_axis",
purpose: "Aggregate already-scoped 1C evidence by a business axis such as counterparty, contract, or period.",
decomposition_hints: ["aggregate_checked_amounts", "aggregate_ranked_axis_values", "aggregate_by_month"],
supported_fact_families: ["value_flow"],
supported_action_families: ["turnover", "payout", "net_value_flow"],
planning_tags: ["aggregation", "ranking", "monthly_aggregation"],
required_axes_any_of: [["aggregate_axis", "period"], ["aggregate_axis", "counterparty"], ["aggregate_axis", "account"]],
optional_axes: ["organization", "contract", "document", "amount"],
output_fact_kinds: ["aggregate_totals", "ranked_axis_values"],
@ -71,6 +103,10 @@ const PRIMITIVE_CONTRACTS = [
{
primitive_id: "drilldown_related_objects",
purpose: "Drill from a known entity or document into related contracts, documents, movements, or payments.",
decomposition_hints: ["drilldown_related_objects"],
supported_fact_families: [],
supported_action_families: [],
planning_tags: ["drilldown"],
required_axes_any_of: [["business_entity"], ["document"], ["contract"], ["counterparty"]],
optional_axes: ["period", "account", "amount"],
output_fact_kinds: ["related_objects", "relationship_edges"],
@ -81,6 +117,30 @@ const PRIMITIVE_CONTRACTS = [
{
primitive_id: "probe_coverage",
purpose: "Check whether the selected MCP/schema route can prove the requested fact or only support a bounded inference.",
decomposition_hints: ["probe_coverage"],
supported_fact_families: [
"schema_surface",
"entity_grounding",
"value_flow",
"document_evidence",
"movement_evidence",
"activity_lifecycle"
],
supported_action_families: [
"inspect_catalog",
"inspect_documents",
"inspect_registers",
"inspect_fields",
"inspect_surface",
"search_business_entity",
"turnover",
"payout",
"net_value_flow",
"list_documents",
"list_movements",
"activity_duration"
],
planning_tags: ["coverage"],
required_axes_any_of: [["coverage_target"], ["domain_family"], ["primitive_id"]],
optional_axes: ["period", "organization", "counterparty", "document", "account"],
output_fact_kinds: ["coverage_status", "known_gaps"],
@ -91,6 +151,10 @@ const PRIMITIVE_CONTRACTS = [
{
primitive_id: "explain_evidence_basis",
purpose: "Produce a machine-readable explanation of which checked MCP evidence supports, limits, or fails the answer.",
decomposition_hints: ["explain_evidence_basis"],
supported_fact_families: ["activity_lifecycle"],
supported_action_families: ["activity_duration"],
planning_tags: ["explanation"],
required_axes_any_of: [["evidence_basis"], ["primitive_id"], ["source_rows_summary"]],
optional_axes: ["coverage_target", "domain_family"],
output_fact_kinds: ["confirmed_facts", "inferred_facts", "unknown_facts"],
@ -123,6 +187,307 @@ function hasAnyAxisGroup(axisSet, groups) {
function missingAxisGroups(axisSet, groups) {
return groups.filter((group) => !group.every((axis) => axisSet.has(axis)));
}
function normalizeDecompositionStep(value) {
return value.trim().toLowerCase();
}
function normalizePlanningToken(value) {
return value.trim().toLowerCase();
}
function countAxisOverlap(axisSet, groups) {
let best = 0;
for (const group of groups) {
const overlap = group.filter((axis) => axisSet.has(axis)).length;
if (overlap > best) {
best = overlap;
}
}
return best;
}
function tagSetFromFactAxisInput(input) {
const tags = new Set();
const requiredAxes = input.required_axes ?? [];
if (input.business_fact_family === "schema_surface") {
tags.add("metadata");
tags.add("surface_inspection");
}
if (input.business_fact_family === "entity_grounding" ||
input.has_subject_candidates ||
requiredAxes.some((axis) => ["business_entity", "counterparty", "contract", "item"].includes(axis))) {
tags.add("subject_resolution");
}
if (input.business_fact_family === "document_evidence") {
tags.add("document");
}
if (input.business_fact_family === "movement_evidence") {
tags.add("movement");
}
if (input.business_fact_family === "value_flow") {
tags.add("movement");
}
if (input.comparison_need) {
tags.add("comparison");
}
if (input.ranking_need) {
tags.add("ranking");
tags.add("aggregation");
}
if ((input.aggregation_need ?? "") === "by_month" || requiredAxes.includes("calendar_month")) {
tags.add("monthly_aggregation");
tags.add("aggregation");
}
if (requiredAxes.includes("aggregate_axis")) {
tags.add("aggregation");
}
if (requiredAxes.includes("coverage_target")) {
tags.add("coverage");
}
if (input.business_fact_family === "activity_lifecycle" || requiredAxes.includes("evidence_basis")) {
tags.add("explanation");
}
return tags;
}
function matchesPlanningToken(value, candidates) {
const normalizedValue = normalizePlanningToken(value ?? "");
return normalizedValue.length > 0 && candidates.some((candidate) => normalizePlanningToken(candidate) === normalizedValue);
}
function normalizeMetadataSurfaceToken(value) {
return value.trim().toLowerCase().replace(/[\s_.-]+/g, "");
}
function metadataSurfaceSuggestsDocument(value) {
const token = normalizeMetadataSurfaceToken(value);
return (token.includes("документ") ||
token.includes("document") ||
token.includes("invoice") ||
token.includes("waybill") ||
token.includes("накладн") ||
token.includes("счетфактур") ||
token.includes("счётфактур") ||
token.includes("акт"));
}
function metadataSurfaceSuggestsMovement(value) {
const token = normalizeMetadataSurfaceToken(value);
return (token.includes("регистр") ||
token.includes("register") ||
token.includes("movement") ||
token.includes("движени") ||
token.includes("операц") ||
token.includes("проводк") ||
token.includes("bank"));
}
function metadataSurfaceSuggestsCatalog(value) {
const token = normalizeMetadataSurfaceToken(value);
return (token.includes("справочник") ||
token.includes("catalog") ||
token.includes("directory"));
}
function tagSetFromMetadataSurfaceInput(input) {
const tags = new Set();
const routeFamily = normalizePlanningToken(input.downstream_route_family ?? "");
const recommendedPrimitive = normalizePlanningToken(input.recommended_next_primitive ?? "");
const surfaceValues = [
input.selected_entity_set ?? "",
...(input.selected_surface_objects ?? [])
];
if (routeFamily === "document_evidence" || recommendedPrimitive === "query_documents") {
tags.add("document");
}
if (routeFamily === "movement_evidence" || recommendedPrimitive === "query_movements") {
tags.add("movement");
}
if (routeFamily === "catalog_drilldown" || recommendedPrimitive === "drilldown_related_objects") {
tags.add("drilldown");
tags.add("metadata");
tags.add("surface_inspection");
}
for (const value of surfaceValues) {
if (metadataSurfaceSuggestsDocument(value)) {
tags.add("document");
}
if (metadataSurfaceSuggestsMovement(value)) {
tags.add("movement");
}
if (metadataSurfaceSuggestsCatalog(value)) {
tags.add("drilldown");
tags.add("metadata");
tags.add("surface_inspection");
}
}
const requiredAxisSet = toStringSet(input.required_axes ?? []);
if (requiredAxisSet.has("coverage_target")) {
tags.add("coverage");
}
if (requiredAxisSet.has("counterparty") ||
requiredAxisSet.has("business_entity") ||
requiredAxisSet.has("contract")) {
tags.add("subject_resolution");
}
return tags;
}
function factFamiliesFromMetadataSurfaceInput(input) {
const families = new Set();
const routeFamily = normalizePlanningToken(input.downstream_route_family ?? "");
if (routeFamily === "document_evidence") {
families.add("document_evidence");
}
if (routeFamily === "movement_evidence") {
families.add("movement_evidence");
}
if (routeFamily === "catalog_drilldown") {
families.add("schema_surface");
}
for (const value of [input.selected_entity_set ?? "", ...(input.selected_surface_objects ?? [])]) {
if (metadataSurfaceSuggestsCatalog(value)) {
families.add("schema_surface");
break;
}
}
return families;
}
function searchAssistantMcpCatalogPrimitivesByDecompositionCandidates(input) {
const allowAggregateByAxis = input.allow_aggregate_by_axis !== false;
const result = [];
for (const candidate of input.decomposition_candidates) {
const normalizedCandidate = normalizeDecompositionStep(candidate);
if (!normalizedCandidate) {
continue;
}
for (const contract of PRIMITIVE_CONTRACTS) {
if (contract.primitive_id === "aggregate_by_axis" &&
normalizedCandidate === "aggregate_by_month" &&
!allowAggregateByAxis) {
continue;
}
if (!contract.decomposition_hints.includes(normalizedCandidate)) {
continue;
}
if (!result.includes(contract.primitive_id)) {
result.push(contract.primitive_id);
}
}
}
return result;
}
function searchAssistantMcpCatalogPrimitivesByFactAxis(input) {
const allowAggregateByAxis = input.allow_aggregate_by_axis !== false;
const requiredAxisSet = toStringSet(input.required_axes ?? []);
const desiredTags = tagSetFromFactAxisInput(input);
const scored = [];
for (const contract of PRIMITIVE_CONTRACTS) {
if (contract.primitive_id === "aggregate_by_axis" && !allowAggregateByAxis) {
continue;
}
if (contract.primitive_id === "search_business_entity" && input.business_fact_family !== "entity_grounding") {
continue;
}
if (input.business_fact_family === "schema_surface" && contract.primitive_id !== "inspect_1c_metadata") {
continue;
}
const factMatch = matchesPlanningToken(input.business_fact_family, contract.supported_fact_families);
const actionMatch = matchesPlanningToken(input.action_family, contract.supported_action_families);
const tagMatches = contract.planning_tags.filter((tag) => desiredTags.has(normalizePlanningToken(tag)));
if (contract.primitive_id === "search_business_entity" && !desiredTags.has("subject_resolution")) {
continue;
}
if (contract.primitive_id === "resolve_entity_reference" && !desiredTags.has("subject_resolution")) {
continue;
}
if (contract.primitive_id === "aggregate_by_axis" && !desiredTags.has("aggregation")) {
continue;
}
if (contract.primitive_id === "explain_evidence_basis" && !desiredTags.has("explanation")) {
continue;
}
if (!factMatch && !actionMatch && tagMatches.length <= 0) {
continue;
}
const hasCompatibleAxisGroup = requiredAxisSet.size > 0 && hasAnyAxisGroup(requiredAxisSet, contract.required_axes_any_of);
const axisOverlap = requiredAxisSet.size > 0 ? countAxisOverlap(requiredAxisSet, contract.required_axes_any_of) : 0;
let score = 0;
if (factMatch) {
score += 5;
}
if (actionMatch) {
score += 3;
}
score += tagMatches.length * 2;
if (hasCompatibleAxisGroup) {
score += 2;
}
else if (axisOverlap > 0) {
score += 1;
}
if (score <= 0) {
continue;
}
scored.push({
primitive: contract.primitive_id,
score
});
}
return scored
.sort((left, right) => right.score - left.score)
.map((item) => item.primitive);
}
function searchAssistantMcpCatalogPrimitivesByMetadataSurface(input) {
const allowAggregateByAxis = input.allow_aggregate_by_axis !== false;
const requiredAxisSet = toStringSet(input.required_axes ?? []);
const desiredTags = tagSetFromMetadataSurfaceInput(input);
const desiredFactFamilies = factFamiliesFromMetadataSurfaceInput(input);
const recommendedPrimitive = normalizePlanningToken(input.recommended_next_primitive ?? "");
const scored = [];
for (const contract of PRIMITIVE_CONTRACTS) {
if (contract.primitive_id === "aggregate_by_axis" && !allowAggregateByAxis) {
continue;
}
if (contract.primitive_id === "search_business_entity") {
continue;
}
const tagMatches = contract.planning_tags.filter((tag) => desiredTags.has(normalizePlanningToken(tag)));
const factMatch = contract.supported_fact_families.some((family) => desiredFactFamilies.has(normalizePlanningToken(family)));
const primitiveRecommended = normalizePlanningToken(contract.primitive_id) === recommendedPrimitive;
if (contract.primitive_id === "resolve_entity_reference" && !desiredTags.has("subject_resolution")) {
continue;
}
if (contract.primitive_id === "aggregate_by_axis" && !desiredTags.has("aggregation")) {
continue;
}
if (!primitiveRecommended && !factMatch && tagMatches.length <= 0) {
continue;
}
const hasCompatibleAxisGroup = requiredAxisSet.size > 0 && hasAnyAxisGroup(requiredAxisSet, contract.required_axes_any_of);
const axisOverlap = requiredAxisSet.size > 0 ? countAxisOverlap(requiredAxisSet, contract.required_axes_any_of) : 0;
if (contract.primitive_id === "probe_coverage" && !desiredTags.has("coverage")) {
continue;
}
if (contract.primitive_id === "drilldown_related_objects" && !hasCompatibleAxisGroup && axisOverlap <= 0) {
continue;
}
let score = 0;
if (primitiveRecommended) {
score += 6;
}
if (factMatch) {
score += 5;
}
score += tagMatches.length * 2;
if (hasCompatibleAxisGroup) {
score += 2;
}
else if (axisOverlap > 0) {
score += 1;
}
if (score <= 0) {
continue;
}
scored.push({
primitive: contract.primitive_id,
score
});
}
return scored
.sort((left, right) => right.score - left.score)
.map((item) => item.primitive);
}
function buildAssistantMcpCatalogIndex() {
const reasonCodes = [];
const missingContracts = assistantMcpDiscoveryPolicy_1.ASSISTANT_MCP_DISCOVERY_PRIMITIVES.filter((primitive) => !PRIMITIVE_CONTRACT_MAP.has(primitive));

View File

@ -84,12 +84,20 @@ function isMovementPilot(pilot) {
function isMetadataPilot(pilot) {
return pilot.pilot_scope === "metadata_inspection_v1";
}
function isCatalogDrilldownPilot(pilot) {
return (isMetadataPilot(pilot) &&
(pilot.reason_codes.includes("planner_selected_catalog_drilldown_from_confirmed_metadata_surface_ref") ||
pilot.dry_run.reason_codes.includes("planner_selected_catalog_drilldown_from_confirmed_metadata_surface_ref") ||
pilot.reason_codes.includes("pilot_catalog_drilldown_metadata_scope_seeded_from_surface_ref")));
}
function isEntityResolutionPilot(pilot) {
return pilot.pilot_scope === "entity_resolution_search_v1";
}
function isMetadataLaneChoiceClarification(pilot) {
return (pilot.reason_codes.includes("planner_selected_metadata_lane_clarification_recipe") ||
pilot.dry_run.reason_codes.includes("planner_selected_metadata_lane_clarification_recipe"));
pilot.reason_codes.includes("planner_selected_metadata_lane_clarification_from_data_need_graph") ||
pilot.dry_run.reason_codes.includes("planner_selected_metadata_lane_clarification_recipe") ||
pilot.dry_run.reason_codes.includes("planner_selected_metadata_lane_clarification_from_data_need_graph"));
}
function askedActionFamily(pilot) {
const action = pilot.evidence.query_plan.turn_meaning_ref?.asked_action_family;
@ -127,6 +135,14 @@ function explicitDateScope(pilot) {
const normalized = value.trim();
return normalized.length > 0 ? normalized : null;
}
function explicitOrganizationScope(pilot) {
const value = pilot.evidence.query_plan.turn_meaning_ref?.explicit_organization_scope;
if (typeof value !== "string") {
return null;
}
const normalized = value.trim();
return normalized.length > 0 ? normalized : null;
}
function documentOrMovementScopeRu(pilot) {
const entity = firstEntityCandidate(pilot);
const period = explicitDateScope(pilot);
@ -141,6 +157,22 @@ function isMovementLaneClarification(pilot) {
askedActionFamily(pilot) === "list_movements" ||
unsupportedFamily(pilot) === "movement_evidence");
}
function isRankedValueFlowClarification(pilot) {
return (pilot.reason_codes.includes("planner_selected_top_ranked_value_flow_from_data_need_graph") ||
pilot.reason_codes.includes("planner_selected_bottom_ranked_value_flow_from_data_need_graph") ||
pilot.dry_run.reason_codes.includes("planner_selected_top_ranked_value_flow_from_data_need_graph") ||
pilot.dry_run.reason_codes.includes("planner_selected_bottom_ranked_value_flow_from_data_need_graph"));
}
function isBidirectionalValueFlowComparisonClarification(pilot) {
return (pilot.reason_codes.includes("planner_selected_bidirectional_value_flow_comparison_from_data_need_graph") ||
pilot.dry_run.reason_codes.includes("planner_selected_bidirectional_value_flow_comparison_from_data_need_graph"));
}
function isOpenScopeValueFlowClarification(pilot) {
return (pilot.reason_codes.includes("planner_selected_open_scope_value_flow_total_from_data_need_graph") ||
pilot.reason_codes.includes("planner_selected_monthly_open_scope_value_flow_total_from_data_need_graph") ||
pilot.dry_run.reason_codes.includes("planner_selected_open_scope_value_flow_total_from_data_need_graph") ||
pilot.dry_run.reason_codes.includes("planner_selected_monthly_open_scope_value_flow_total_from_data_need_graph"));
}
function isDocumentLaneClarification(pilot) {
return (isDocumentPilot(pilot) ||
pilot.reason_codes.includes("planner_selected_document_recipe") ||
@ -152,12 +184,30 @@ function laneScopeSuffix(pilot) {
const entity = firstEntityCandidate(pilot);
return entity ? ` по "${entity}"` : "";
}
function dryRunHasAxis(pilot, axis) {
return pilot.dry_run.execution_steps.some((step) => step.provided_axes.includes(axis));
}
function dryRunMissingAxis(pilot, axis) {
if (dryRunHasAxis(pilot, axis)) {
return false;
}
return pilot.dry_run.execution_steps.some((step) => step.missing_axis_options.some((option) => option.includes(axis)));
}
function clarificationNeedRu(pilot) {
const organizationScopedOpenTotal = pilot.reason_codes.includes("data_need_graph_open_scope_total_needs_organization") ||
pilot.dry_run.reason_codes.includes("data_need_graph_open_scope_total_needs_organization") ||
pilot.reason_codes.includes("planner_requires_organization_scope_from_data_need_graph") ||
pilot.dry_run.reason_codes.includes("planner_requires_organization_scope_from_data_need_graph");
if (organizationScopedOpenTotal) {
return {
subject: "\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\u0438\u044e",
verb: "\u043d\u0443\u0436\u043d\u043e"
};
}
const hasCounterparty = dryRunHasAxis(pilot, "counterparty");
const hasAccount = dryRunHasAxis(pilot, "account");
const needsPeriod = dryRunMissingAxis(pilot, "period");
const needsOrganization = dryRunMissingAxis(pilot, "organization");
const needsOrganization = !hasCounterparty && !hasAccount && dryRunMissingAxis(pilot, "organization");
if (needsPeriod && needsOrganization) {
return { subject: "проверяемый период и организацию", verb: "нужно" };
}
@ -170,9 +220,16 @@ function clarificationNeedRu(pilot) {
return { subject: "контекст проверки", verb: "нужно" };
}
function clarificationNextStepLine(pilot, laneLabel) {
const organizationScopedOpenTotal = pilot.reason_codes.includes("data_need_graph_open_scope_total_needs_organization") ||
pilot.dry_run.reason_codes.includes("data_need_graph_open_scope_total_needs_organization") ||
pilot.reason_codes.includes("planner_requires_organization_scope_from_data_need_graph") ||
pilot.dry_run.reason_codes.includes("planner_requires_organization_scope_from_data_need_graph");
const needsPeriod = dryRunMissingAxis(pilot, "period");
const needsOrganization = dryRunMissingAxis(pilot, "organization");
const scopeSuffix = laneScopeSuffix(pilot);
if (organizationScopedOpenTotal && !needsPeriod) {
return `Уточните организацию, и я продолжу поиск по ${laneLabel}${scopeSuffix} в 1С.`;
}
if (needsPeriod && needsOrganization) {
return `Уточните период и организацию, и я продолжу поиск по ${laneLabel}${scopeSuffix} в 1С.`;
}
@ -210,9 +267,15 @@ function headlineFor(mode, pilot) {
pilot.derived_entity_resolution?.resolution_status === "not_found") {
return "По текущему каталожному поиску 1С точный контрагент пока не подтвержден.";
}
if (pilot.derived_ranked_value_flow && mode === "confirmed_with_bounded_inference") {
return "По данным 1С можно построить ограниченный рейтинг по контрагентам на подтвержденных строках денежных движений.";
}
if (isMovementPilot(pilot) && mode === "confirmed_with_bounded_inference") {
return `По движениям${documentOrMovementScopeRu(pilot)} в 1С найдены подтвержденные строки; ответ ограничен проверенным окном и найденными строками.`;
}
if (isCatalogDrilldownPilot(pilot) && mode === "confirmed_with_bounded_inference") {
return "По метаданным 1С удалось углубиться в контур справочников и связанных объектов; это уже не общий обзор схемы, а следующий безопасный catalog drilldown.";
}
if (pilot.derived_metadata_surface && mode === "confirmed_with_bounded_inference") {
if (pilot.derived_metadata_surface.ambiguity_detected) {
return "По метаданным 1С найдены конкурирующие schema-поверхности; перед следующим шагом нужно удержать неоднозначность явно.";
@ -269,6 +332,18 @@ function headlineFor(mode, pilot) {
const need = clarificationNeedRu(pilot);
return `Могу идти дальше по документам${laneScopeSuffix(pilot)}, но для запуска поиска в 1С ${need.verb} ${need.subject}.`;
}
if (mode === "needs_clarification" && isBidirectionalValueFlowComparisonClarification(pilot)) {
const need = clarificationNeedRu(pilot);
return `Могу сравнить входящий и исходящий денежный поток, но для проверяемого поиска в 1С ${need.verb} ${need.subject}.`;
}
if (mode === "needs_clarification" && isRankedValueFlowClarification(pilot)) {
const need = clarificationNeedRu(pilot);
return `Могу посчитать рейтинг по денежному потоку между контрагентами, но для проверяемого поиска в 1С ${need.verb} ${need.subject}.`;
}
if (mode === "needs_clarification" && isOpenScopeValueFlowClarification(pilot)) {
const need = clarificationNeedRu(pilot);
return `Могу посчитать общий денежный поток в проверяемом окне, но для проверяемого поиска в 1С ${need.verb} ${need.subject}.`;
}
if (mode === "needs_clarification") {
return "Нужно уточнить контекст перед поиском в 1С.";
}
@ -302,6 +377,15 @@ function nextStepFor(mode, pilot) {
if (mode === "needs_clarification" && isDocumentLaneClarification(pilot)) {
return clarificationNextStepLine(pilot, "документам");
}
if (mode === "needs_clarification" && isBidirectionalValueFlowComparisonClarification(pilot)) {
return clarificationNextStepLine(pilot, "сравнению входящих и исходящих денежных потоков");
}
if (mode === "needs_clarification" && isRankedValueFlowClarification(pilot)) {
return clarificationNextStepLine(pilot, "рейтингу контрагентов по денежному потоку");
}
if (mode === "needs_clarification" && isOpenScopeValueFlowClarification(pilot)) {
return clarificationNextStepLine(pilot, "денежному потоку");
}
if (mode === "needs_clarification") {
return "Уточните контрагента, период или организацию, и я смогу выполнить проверку по 1С.";
}
@ -336,6 +420,10 @@ function buildMustNotClaim(pilot) {
claims.push("Do not claim full all-time turnover unless the checked period and coverage prove it.");
claims.push("Do not present a derived sum as a legal/accounting final total outside the checked 1C rows.");
}
if (pilot.derived_ranked_value_flow) {
claims.push("Do not present a bounded ranking as a complete all-time ranking outside the checked period and organization.");
claims.push("Do not imply the top-ranked counterparty is globally final when probe-limit or scope boundaries still exist.");
}
if (isDocumentPilot(pilot)) {
claims.push("Do not claim full document history outside the checked period.");
claims.push("Do not present the confirmed document rows as a complete document universe.");
@ -463,11 +551,47 @@ function derivedEntityResolutionInferenceLine(pilot) {
}
return null;
}
function derivedRankedValueFlowInferenceLine(pilot) {
const ranking = pilot.derived_ranked_value_flow;
if (!ranking) {
return null;
}
const organization = ranking.organization_scope ? ` по организации ${ranking.organization_scope}` : "";
const period = ranking.period_scope ? ` за период ${ranking.period_scope}` : " в проверенном окне";
return `Рейтинг по контрагентам${organization}${period} рассчитан только по подтвержденным строкам 1С и не доказывает полный исторический срез вне проверенного окна.`;
}
function derivedRankedValueFlowConfirmedLine(pilot) {
const ranking = pilot.derived_ranked_value_flow;
if (!ranking || ranking.ranked_values.length <= 0) {
return null;
}
const leader = ranking.ranked_values[0];
const organization = ranking.organization_scope ? ` по организации ${ranking.organization_scope}` : "";
const period = ranking.period_scope ? ` за период ${ranking.period_scope}` : " в проверенном окне";
const directionLead = ranking.ranking_need === "bottom_asc"
? ranking.value_flow_direction === "outgoing_supplier_payout"
? "Меньше всего заплатили контрагенту"
: "Меньше всего денег принёс контрагент"
: ranking.value_flow_direction === "outgoing_supplier_payout"
? "Больше всего заплатили контрагенту"
: "Больше всего денег принёс контрагент";
const tail = ranking.ranked_values
.slice(1, 3)
.map((bucket) => `${bucket.axis_value}${bucket.total_amount_human_ru}`)
.join("; ");
const trail = tail ? ` Следом: ${tail}.` : "";
const limitCaveat = ranking.coverage_limited_by_probe_limit
? " Лимит строк проверки достигнут; рейтинг может быть неполным."
: "";
return `${directionLead} ${leader.axis_value}${organization}${period}: ${leader.total_amount_human_ru} по ${leader.rows_with_amount} строкам с суммой.${trail}${limitCaveat}`;
}
function derivedValueFlowConfirmedLine(pilot) {
const flow = pilot.derived_value_flow;
if (!flow) {
return null;
}
const organizationScope = explicitOrganizationScope(pilot);
const organization = organizationScope ? ` по организации ${organizationScope}` : "";
const counterparty = flow.counterparty ? ` по контрагенту ${flow.counterparty}` : "";
const period = flow.period_scope ? ` за период ${flow.period_scope}` : " в проверенном окне";
const movementLabel = flow.value_flow_direction === "outgoing_supplier_payout"
@ -553,13 +677,16 @@ function buildAssistantMcpDiscoveryAnswerDraft(pilot) {
}
const derivedInferenceLine = derivedActivityInferenceLine(pilot) ??
derivedMetadataInferenceLine(pilot) ??
derivedRankedValueFlowInferenceLine(pilot) ??
derivedEntityResolutionInferenceLine(pilot);
const inferenceLines = derivedInferenceLine
? [derivedInferenceLine]
: pilot.evidence.inferred_facts;
const derivedMetadataLine = derivedMetadataConfirmedLine(pilot);
const derivedEntityResolutionLine = derivedEntityResolutionConfirmedLine(pilot);
const derivedValueLine = derivedBidirectionalValueFlowConfirmedLine(pilot) ?? derivedValueFlowConfirmedLine(pilot);
const derivedValueLine = derivedBidirectionalValueFlowConfirmedLine(pilot) ??
derivedRankedValueFlowConfirmedLine(pilot) ??
derivedValueFlowConfirmedLine(pilot);
const monthlyConfirmedLines = derivedBidirectionalValueFlowMonthlyLines(pilot).length > 0
? derivedBidirectionalValueFlowMonthlyLines(pilot)
: derivedValueFlowMonthlyLines(pilot);

View File

@ -0,0 +1,355 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ASSISTANT_MCP_DISCOVERY_DATA_NEED_GRAPH_SCHEMA_VERSION = void 0;
exports.buildAssistantMcpDiscoveryDataNeedGraph = buildAssistantMcpDiscoveryDataNeedGraph;
exports.ASSISTANT_MCP_DISCOVERY_DATA_NEED_GRAPH_SCHEMA_VERSION = "assistant_data_need_graph_v1";
function toNonEmptyString(value) {
if (value === null || value === undefined) {
return null;
}
const text = String(value).trim();
return text.length > 0 ? text : null;
}
function lower(value) {
return String(value ?? "").trim().toLowerCase();
}
function normalizeReasonCode(value) {
const normalized = value
.trim()
.replace(/[^\p{L}\p{N}_.:-]+/gu, "_")
.replace(/^_+|_+$/g, "")
.toLowerCase();
return normalized.length > 0 ? normalized.slice(0, 120) : null;
}
function pushReason(target, value) {
const normalized = normalizeReasonCode(value);
if (normalized && !target.includes(normalized)) {
target.push(normalized);
}
}
function pushUnique(target, value) {
const text = toNonEmptyString(value);
if (text && !target.includes(text)) {
target.push(text);
}
}
function businessFactFamilyFor(input) {
const combined = `${input.semanticDataNeed} ${input.domain} ${input.action} ${input.unsupported}`.trim();
if (combined.includes("metadata lane clarification")) {
return "schema_surface";
}
if (combined.includes("metadata")) {
return "schema_surface";
}
if (combined.includes("entity discovery") || combined.includes("entity_resolution")) {
return "entity_grounding";
}
if (combined.includes("lifecycle") || combined.includes("activity")) {
return "activity_lifecycle";
}
if (combined.includes("movement")) {
return "movement_evidence";
}
if (combined.includes("document")) {
return "document_evidence";
}
if (combined.includes("value-flow") || combined.includes("turnover") || combined.includes("payout") || combined.includes("net")) {
return "value_flow";
}
return null;
}
function aggregationNeedFor(axis) {
if (!axis) {
return null;
}
if (axis === "month") {
return "by_month";
}
return `by_${axis}`;
}
function hasAllTimeScopeHint(rawUtterance) {
if (!rawUtterance) {
return false;
}
return /(?:\u0437\u0430\s+\u0432\u0441[\u0435\u0451]\s+\u0432\u0440\u0435\u043c\u044f|\u0437\u0430\s+\u0432\u0435\u0441\u044c\s+\u043f\u0435\u0440\u0438\u043e\u0434|\u0437\u0430\s+\u0432\u0441\u044e\s+\u0438\u0441\u0442\u043e\u0440\u0438(?:\u044e|\u0438)|\u0437\u0430\s+\u043b\u044e\u0431\u043e\u0439\s+\u043f\u0435\u0440\u0438\u043e\u0434|for\s+all\s+time|all\s+time|entire\s+period|full\s+history|any\s+period)/iu.test(rawUtterance);
}
function timeScopeNeedFor(input) {
if (input.explicitDateScope) {
return "explicit_period";
}
if (input.allTimeScopeHint &&
(input.family === "value_flow" || input.family === "movement_evidence" || input.family === "document_evidence")) {
return "all_time_scope";
}
if (input.family === "value_flow" || input.family === "movement_evidence" || input.family === "document_evidence") {
return "period_required";
}
if (input.family === "activity_lifecycle") {
return "open_activity_window";
}
return null;
}
function comparisonNeedFor(action) {
if (action === "net_value_flow") {
return "incoming_vs_outgoing";
}
return null;
}
function hasOpenScopeOneSidedValueTotalHint(rawUtterance, action) {
if (!rawUtterance) {
return false;
}
if (action === "turnover") {
return /(?:\bсколько\s+(?:мы\s+)?(?:получили|получено|входящих(?:\s+денег)?|поступлений|денег\s+пришло)\b|(?:сумма|объем)\s+(?:входящих|поступлений)|поступлений\s+за\b)/iu.test(rawUtterance);
}
if (action === "payout") {
return /(?:\bсколько\s+(?:мы\s+)?(?:заплатили|выплатили|потратили|исходящих(?:\s+денег)?|платежей|списаний)\b|(?:сумма|объем)\s+(?:исходящих|платежей|списаний)|(?:платежей|списаний)\s+за\b)/iu.test(rawUtterance);
}
return false;
}
function hasOpenScopeOneSidedValueTotalHintUtf8Safe(rawUtterance, action) {
if (!rawUtterance) {
return false;
}
if (action === "turnover") {
return /(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e\s+(?:\u043c\u044b\s+)?(?:\u043f\u043e\u043b\u0443\u0447\u0438\u043b\u0438|\u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043e|\u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0445(?:\s+\u0434\u0435\u043d\u0435\u0433)?|\u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d\u0438\u0439|\u0434\u0435\u043d\u0435\u0433\s+\u043f\u0440\u0438\u0448\u043b\u043e)|(?:\u0441\u0443\u043c\u043c\u0430|\u043e\u0431\u044a\u0435\u043c)\s+(?:\u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0445|\u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d\u0438\u0439)|\u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d\u0438\u0439\s+\u0437\u0430)/u.test(rawUtterance);
}
if (action === "payout") {
return /(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e\s+(?:\u043c\u044b\s+)?(?:\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u043b\u0438|\u0432\u044b\u043f\u043b\u0430\u0442\u0438\u043b\u0438|\u043f\u043e\u0442\u0440\u0430\u0442\u0438\u043b\u0438|\u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0445(?:\s+\u0434\u0435\u043d\u0435\u0433)?|\u043f\u043b\u0430\u0442\u0435\u0436\u0435\u0439|\u0441\u043f\u0438\u0441\u0430\u043d\u0438\u0439)|(?:\u0441\u0443\u043c\u043c\u0430|\u043e\u0431\u044a\u0435\u043c)\s+(?:\u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0445|\u043f\u043b\u0430\u0442\u0435\u0436\u0435\u0439|\u0441\u043f\u0438\u0441\u0430\u043d\u0438\u0439)|(?:\u043f\u043b\u0430\u0442\u0435\u0436\u0435\u0439|\u0441\u043f\u0438\u0441\u0430\u043d\u0438\u0439)\s+\u0437\u0430)/u.test(rawUtterance);
}
return false;
}
function supportsOrganizationScopedOpenTotal(action) {
return action === "turnover" || action === "payout";
}
function allowsOpenScopeWithoutSubject(input) {
if (input.family !== "value_flow") {
return false;
}
if (input.rankingNeed || input.comparisonNeed === "incoming_vs_outgoing") {
return true;
}
return Boolean(supportsOrganizationScopedOpenTotal(input.action) && (input.organizationScope || input.oneSidedOpenScopeTotalHint));
}
function rankingNeedFromRawUtterance(value) {
const text = lower(value);
if (!text) {
return null;
}
if (/(?:\btop[-\s]?\d+\b|\btop\b|топ[-\s]?\d+|топ\b|сам(?:ый|ая|ое|ые)\b|больше\s+всего|наибол[её]е|highest|largest|most)/iu.test(text)) {
return "top_desc";
}
if (/(?:меньше\s+всего|наимен[ьш]е|lowest|smallest|least)/iu.test(text)) {
return "bottom_asc";
}
return null;
}
function proofExpectationFor(input) {
if (input.clarificationGaps.length > 0) {
return "clarification_required";
}
if (input.family === "schema_surface") {
return "schema_surface";
}
if (input.family === "entity_grounding") {
return "entity_grounding";
}
if (input.family === "activity_lifecycle") {
return "bounded_inference";
}
return "coverage_checked_fact";
}
function decompositionCandidatesFor(input) {
const result = [];
if (input.family === "schema_surface") {
pushUnique(result, "inspect_metadata_surface");
return result;
}
if (input.family === "entity_grounding") {
pushUnique(result, "search_business_entity");
pushUnique(result, "resolve_entity_reference");
pushUnique(result, "probe_coverage");
return result;
}
if (input.family === "value_flow") {
if (input.rankingNeed && input.openScopeWithoutSubject) {
pushUnique(result, "collect_scoped_movements");
pushUnique(result, "aggregate_ranked_axis_values");
pushUnique(result, "probe_coverage");
return result;
}
if (input.comparisonNeed === "incoming_vs_outgoing" && input.openScopeWithoutSubject) {
pushUnique(result, "collect_incoming_movements");
pushUnique(result, "collect_outgoing_movements");
if (input.aggregationNeed === "by_month") {
pushUnique(result, "aggregate_by_month");
}
pushUnique(result, "probe_coverage");
return result;
}
if (input.openScopeWithoutSubject) {
pushUnique(result, "collect_scoped_movements");
pushUnique(result, input.aggregationNeed === "by_month" ? "aggregate_by_month" : "aggregate_checked_amounts");
pushUnique(result, "probe_coverage");
return result;
}
pushUnique(result, "resolve_entity_reference");
if (input.action === "net_value_flow") {
pushUnique(result, "collect_incoming_movements");
pushUnique(result, "collect_outgoing_movements");
}
else {
pushUnique(result, "collect_scoped_movements");
}
pushUnique(result, input.aggregationNeed === "by_month" ? "aggregate_by_month" : "aggregate_checked_amounts");
pushUnique(result, "probe_coverage");
return result;
}
if (input.family === "movement_evidence") {
pushUnique(result, "resolve_entity_reference");
pushUnique(result, "fetch_scoped_movements");
pushUnique(result, "probe_coverage");
return result;
}
if (input.family === "document_evidence") {
pushUnique(result, "resolve_entity_reference");
pushUnique(result, "fetch_scoped_documents");
pushUnique(result, "probe_coverage");
return result;
}
if (input.family === "activity_lifecycle") {
pushUnique(result, "resolve_entity_reference");
pushUnique(result, "fetch_supporting_documents");
pushUnique(result, "probe_coverage");
pushUnique(result, "explain_evidence_basis");
}
return result;
}
function forbiddenOverclaimFlagsFor(family) {
const result = ["no_raw_model_claims"];
if (family === "schema_surface") {
pushUnique(result, "no_fake_schema_surface");
}
if (family === "entity_grounding") {
pushUnique(result, "no_unresolved_entity_claim");
}
if (family === "activity_lifecycle") {
pushUnique(result, "no_legal_age_claim_without_evidence");
}
if (family === "value_flow" || family === "movement_evidence" || family === "document_evidence") {
pushUnique(result, "no_unchecked_fact_totals");
}
return result;
}
function buildAssistantMcpDiscoveryDataNeedGraph(input) {
const semanticDataNeed = lower(input.semanticDataNeed);
const turnMeaning = input.turnMeaning ?? null;
const domain = lower(turnMeaning?.asked_domain_family);
const action = lower(turnMeaning?.asked_action_family);
const unsupported = lower(turnMeaning?.unsupported_but_understood_family);
const rawUtterance = lower(input.rawUtterance);
const aggregationAxis = lower(turnMeaning?.asked_aggregation_axis);
const seededRankingNeed = toNonEmptyString(turnMeaning?.seeded_ranking_need);
const explicitDateScope = toNonEmptyString(turnMeaning?.explicit_date_scope);
const explicitOrganizationScope = toNonEmptyString(turnMeaning?.explicit_organization_scope);
const subjectCandidates = (turnMeaning?.explicit_entity_candidates ?? [])
.map((item) => toNonEmptyString(item))
.filter((item) => Boolean(item));
const businessFactFamily = businessFactFamilyFor({
semanticDataNeed,
domain,
action,
unsupported
});
const aggregationNeed = aggregationNeedFor(aggregationAxis);
const comparisonNeed = comparisonNeedFor(action);
const rankingNeed = rankingNeedFromRawUtterance(rawUtterance) ?? seededRankingNeed;
const allTimeScopeHint = hasAllTimeScopeHint(rawUtterance);
const oneSidedOpenScopeTotalHint = hasOpenScopeOneSidedValueTotalHintUtf8Safe(rawUtterance, action);
const openScopeWithoutSubject = subjectCandidates.length === 0 &&
allowsOpenScopeWithoutSubject({
family: businessFactFamily,
action,
organizationScope: explicitOrganizationScope,
comparisonNeed,
rankingNeed,
oneSidedOpenScopeTotalHint
});
const clarificationGaps = [];
if (unsupported === "metadata_lane_choice_clarification" || action === "resolve_next_lane") {
pushUnique(clarificationGaps, "lane_family_choice");
}
if (subjectCandidates.length === 0 &&
businessFactFamily === "value_flow" &&
openScopeWithoutSubject &&
!explicitOrganizationScope) {
pushUnique(clarificationGaps, "organization");
}
else if (subjectCandidates.length === 0 && businessFactFamily !== "schema_surface" && !openScopeWithoutSubject) {
pushUnique(clarificationGaps, "subject");
}
const timeScopeNeed = timeScopeNeedFor({
family: businessFactFamily,
explicitDateScope,
allTimeScopeHint
});
if (timeScopeNeed === "period_required" && !explicitDateScope) {
pushUnique(clarificationGaps, "period");
}
const decompositionCandidates = decompositionCandidatesFor({
family: businessFactFamily,
action,
aggregationNeed,
comparisonNeed,
rankingNeed,
openScopeWithoutSubject
});
const reasonCodes = [];
pushReason(reasonCodes, "data_need_graph_built");
if (businessFactFamily) {
pushReason(reasonCodes, `data_need_graph_family_${businessFactFamily}`);
}
else {
pushReason(reasonCodes, "data_need_graph_family_unknown");
}
if (aggregationNeed) {
pushReason(reasonCodes, `data_need_graph_aggregation_${aggregationNeed}`);
}
if (rankingNeed) {
pushReason(reasonCodes, `data_need_graph_ranking_${rankingNeed}`);
}
if (comparisonNeed) {
pushReason(reasonCodes, `data_need_graph_comparison_${comparisonNeed}`);
}
if (openScopeWithoutSubject && !rankingNeed && !comparisonNeed) {
pushReason(reasonCodes, "data_need_graph_open_scope_total_without_subject");
}
if (allTimeScopeHint) {
pushReason(reasonCodes, "data_need_graph_all_time_scope_hint");
}
if (clarificationGaps.includes("organization")) {
pushReason(reasonCodes, "data_need_graph_open_scope_total_needs_organization");
}
if (clarificationGaps.length > 0) {
pushReason(reasonCodes, "data_need_graph_has_clarification_gaps");
}
return {
schema_version: exports.ASSISTANT_MCP_DISCOVERY_DATA_NEED_GRAPH_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: subjectCandidates,
business_fact_family: businessFactFamily,
action_family: toNonEmptyString(turnMeaning?.asked_action_family),
aggregation_need: aggregationNeed,
time_scope_need: timeScopeNeed,
comparison_need: comparisonNeed,
ranking_need: rankingNeed,
proof_expectation: proofExpectationFor({
family: businessFactFamily,
clarificationGaps
}),
clarification_gaps: clarificationGaps,
decomposition_candidates: decompositionCandidates,
forbidden_overclaim_flags: forbiddenOverclaimFlagsFor(businessFactFamily),
reason_codes: reasonCodes
};
}

View File

@ -133,6 +133,16 @@ function buildValueFlowFilters(planner) {
sort: "period_asc"
};
}
function organizationScopeForPlanner(planner) {
return toNonEmptyString(planner.discovery_plan.turn_meaning_ref?.explicit_organization_scope);
}
function rankingNeedForPlanner(planner) {
const rankingNeed = toNonEmptyString(planner.data_need_graph?.ranking_need)?.toLowerCase();
if (rankingNeed === "top_desc" || rankingNeed === "bottom_asc") {
return rankingNeed;
}
return null;
}
function normalizeEntityResolutionText(value) {
return String(value ?? "")
.toLowerCase()
@ -313,7 +323,9 @@ function isMovementEvidencePilotEligible(planner) {
combined.includes("list_movements")));
}
function isValueFlowPilotEligible(planner) {
if (planner.selected_chain_id === "value_flow") {
if (planner.selected_chain_id === "value_flow" ||
planner.selected_chain_id === "value_flow_ranking" ||
planner.selected_chain_id === "value_flow_comparison") {
return true;
}
const meaning = planner.discovery_plan.turn_meaning_ref;
@ -330,7 +342,8 @@ function isValueFlowPilotEligible(planner) {
}
function isMetadataPilotEligible(planner) {
if (planner.selected_chain_id === "metadata_inspection" ||
planner.selected_chain_id === "metadata_lane_clarification") {
planner.selected_chain_id === "metadata_lane_clarification" ||
planner.selected_chain_id === "catalog_drilldown") {
return true;
}
const meaning = planner.discovery_plan.turn_meaning_ref;
@ -368,6 +381,23 @@ function metadataScopeForPlanner(planner) {
if (entityCandidate) {
return entityCandidate;
}
if (planner.selected_chain_id === "catalog_drilldown") {
const surface = planner.metadata_surface_ref;
const scopeCandidate = [
...(surface?.selected_surface_objects ?? []),
surface?.selected_entity_set ?? ""
]
.map((value) => toNonEmptyString(value))
.filter((value) => Boolean(value))
.map((value) => {
const parts = value.split(".").map((item) => item.trim()).filter((item) => item.length > 0);
return parts.length > 0 ? parts[parts.length - 1] ?? value : value;
})
.find((value) => value.length > 0);
if (scopeCandidate) {
return scopeCandidate;
}
}
const meaning = planner.discovery_plan.turn_meaning_ref;
const combined = `${meaning?.asked_domain_family ?? ""} ${meaning?.asked_action_family ?? ""} ${meaning?.unsupported_but_understood_family ?? ""}`
.toLowerCase()
@ -384,6 +414,12 @@ function metadataScopeForPlanner(planner) {
return null;
}
function metadataTypesForPlanner(planner) {
if (planner.selected_chain_id === "catalog_drilldown") {
const selectedEntitySet = toNonEmptyString(planner.metadata_surface_ref?.selected_entity_set);
if (selectedEntitySet) {
return [selectedEntitySet];
}
}
const meaning = planner.discovery_plan.turn_meaning_ref;
const action = String(meaning?.asked_action_family ?? "").toLowerCase();
if (action === "inspect_registers") {
@ -397,6 +433,10 @@ function metadataTypesForPlanner(planner) {
}
return ["Документ", "РегистрНакопления", "РегистрСведений", "Справочник"];
}
function metadataScopeRankingAllowedForPlanner(planner) {
const action = String(planner.discovery_plan.turn_meaning_ref?.asked_action_family ?? "").toLowerCase().trim();
return action === "inspect_surface";
}
function valueFlowPilotProfile(planner) {
const meaning = planner.discovery_plan.turn_meaning_ref;
const action = String(meaning?.asked_action_family ?? "").toLowerCase();
@ -812,6 +852,29 @@ function metadataRouteFamilyForEntitySet(entitySet) {
}
return null;
}
function metadataRouteFamilyForEntitySetRelaxed(entitySet) {
const strict = metadataRouteFamilyForEntitySet(entitySet);
if (strict) {
return strict;
}
const raw = String(entitySet ?? "").trim();
if (!raw) {
return null;
}
if (raw.includes("Документ") || raw.includes("Документ")) {
return "document_evidence";
}
if (raw.includes("РегистрНакопления") ||
raw.includes("РегистрСведений") ||
raw.includes("РегистрНакопления") ||
raw.includes("РегистрСведений")) {
return "movement_evidence";
}
if (raw.includes("Справочник") || raw.includes("Справочник")) {
return "catalog_drilldown";
}
return null;
}
function metadataNextPrimitiveForRouteFamily(routeFamily) {
if (routeFamily === "document_evidence") {
return "query_documents";
@ -859,7 +922,140 @@ function metadataObjectsForEntitySet(entitySet, matchedObjects) {
}
return matchedObjects.filter((item) => item.startsWith(`${entitySet}.`) || item.includes(entitySet));
}
function deriveMetadataSurface(result, metadataScope, requestedMetaTypes) {
function emptyMetadataSurfaceFamilyScores() {
return {
document_evidence: 0,
movement_evidence: 0,
catalog_drilldown: 0
};
}
function metadataSurfaceFamilyScores(matchedObjects) {
const scores = emptyMetadataSurfaceFamilyScores();
for (const objectName of matchedObjects) {
const entitySet = inferMetadataEntitySetFromObjectName(objectName);
const routeFamily = entitySet ? metadataRouteFamilyForEntitySetRelaxed(entitySet) : null;
if (routeFamily) {
scores[routeFamily] += 1;
}
}
return scores;
}
function normalizeMetadataObjectRankingToken(value) {
return String(value ?? "")
.toLowerCase()
.replace(/[^\p{L}\p{N}]+/gu, "");
}
function metadataScopeRankingTokens(metadataScope) {
const scope = String(metadataScope ?? "").trim();
if (!scope) {
return [];
}
const condensed = normalizeMetadataObjectRankingToken(scope);
const result = [];
if (condensed.length >= 2) {
pushUnique(result, condensed);
}
for (const token of scope.toLowerCase().split(/[^\p{L}\p{N}]+/gu)) {
const normalized = normalizeMetadataObjectRankingToken(token);
if (normalized.length >= 2) {
pushUnique(result, normalized);
}
}
return result;
}
function metadataObjectRelevanceScore(metadataScope, objectName) {
const objectToken = normalizeMetadataObjectRankingToken(objectName);
if (!objectToken) {
return 1;
}
let score = 1;
for (const token of metadataScopeRankingTokens(metadataScope)) {
if (objectToken.includes(token)) {
score += token.length >= 6 ? 4 : 3;
}
}
return score;
}
function metadataWeightedSurfaceFamilyScores(matchedObjects, metadataScope) {
const scores = emptyMetadataSurfaceFamilyScores();
for (const objectName of matchedObjects) {
const entitySet = inferMetadataEntitySetFromObjectName(objectName);
const routeFamily = entitySet ? metadataRouteFamilyForEntitySetRelaxed(entitySet) : null;
if (routeFamily) {
scores[routeFamily] += metadataObjectRelevanceScore(metadataScope, objectName);
}
}
return scores;
}
function sortMetadataObjectsByRelevance(matchedObjects, metadataScope) {
return [...matchedObjects].sort((left, right) => {
const scoreDelta = metadataObjectRelevanceScore(metadataScope, right) - metadataObjectRelevanceScore(metadataScope, left);
if (scoreDelta !== 0) {
return scoreDelta;
}
return left.localeCompare(right, "ru");
});
}
function metadataObjectsForRouteFamily(routeFamily, matchedObjects, metadataScope) {
if (!routeFamily) {
return [];
}
const filtered = matchedObjects.filter((objectName) => {
const entitySet = inferMetadataEntitySetFromObjectName(objectName);
return entitySet ? metadataRouteFamilyForEntitySetRelaxed(entitySet) === routeFamily : false;
});
return sortMetadataObjectsByRelevance(filtered, metadataScope);
}
function selectDominantMetadataRouteFamilyFromScores(scores) {
const ranked = Object.entries(scores)
.filter(([, score]) => score > 0)
.sort((left, right) => right[1] - left[1]);
const top = ranked[0];
const second = ranked[1];
if (!top) {
return null;
}
if (!second) {
return top[0];
}
const absoluteMargin = top[1] - second[1];
const relativeRatio = second[1] > 0 ? top[1] / second[1] : Number.POSITIVE_INFINITY;
const clearlyDominant = absoluteMargin >= 2 || relativeRatio >= 1.5;
return clearlyDominant ? top[0] : null;
}
function selectMetadataRouteFamilyFromSurfaceScores(input) {
const countDominant = selectDominantMetadataRouteFamilyFromScores(input.countScores);
if (countDominant) {
return {
routeFamily: countDominant,
rankingApplied: false
};
}
if (!input.allowScopeRanking) {
return {
routeFamily: null,
rankingApplied: false
};
}
const rankedCounts = Object.entries(input.countScores)
.filter(([, score]) => score > 0)
.sort((left, right) => right[1] - left[1]);
const topCount = rankedCounts[0]?.[1] ?? 0;
const secondCount = rankedCounts[1]?.[1] ?? 0;
if (topCount <= 0 || topCount !== secondCount) {
return {
routeFamily: null,
rankingApplied: false
};
}
const weightedScores = metadataWeightedSurfaceFamilyScores(input.matchedObjects, input.metadataScope);
const weightedDominant = selectDominantMetadataRouteFamilyFromScores(weightedScores);
return {
routeFamily: weightedDominant,
rankingApplied: Boolean(weightedDominant)
};
}
function deriveMetadataSurface(result, metadataScope, requestedMetaTypes, allowScopeRanking) {
if (!result || result.error || result.rows.length <= 0) {
return null;
}
@ -876,13 +1072,36 @@ function deriveMetadataSurface(result, metadataScope, requestedMetaTypes) {
}
}
const grounding = selectMetadataEntityGrounding(availableEntitySets, requestedMetaTypes);
const downstreamRouteFamily = grounding.selectedEntitySet
? metadataRouteFamilyForEntitySet(grounding.selectedEntitySet)
const surfaceFamilyScores = metadataSurfaceFamilyScores(matchedObjects);
const selectedEntitySetRouteFamily = grounding.selectedEntitySet
? metadataRouteFamilyForEntitySetRelaxed(grounding.selectedEntitySet)
: null;
const scoredRouteSelection = selectedEntitySetRouteFamily === null
? selectMetadataRouteFamilyFromSurfaceScores({
matchedObjects,
metadataScope,
countScores: surfaceFamilyScores,
allowScopeRanking
})
: { routeFamily: null, rankingApplied: false };
const scoredRouteFamily = scoredRouteSelection.routeFamily;
const downstreamRouteFamily = selectedEntitySetRouteFamily ?? scoredRouteFamily;
const routeFamilySelectionBasis = selectedEntitySetRouteFamily
? "selected_entity_set"
: scoredRouteFamily
? "dominant_surface_objects"
: null;
const selectedSurfaceObjects = grounding.selectedEntitySet !== null
? sortMetadataObjectsByRelevance(metadataObjectsForEntitySet(grounding.selectedEntitySet, matchedObjects), metadataScope)
: metadataObjectsForRouteFamily(downstreamRouteFamily, matchedObjects, metadataScope);
const knownLimitations = [];
if (grounding.ambiguityDetected && grounding.ambiguityEntitySets.length > 0) {
const ambiguityRemainsUnresolved = grounding.ambiguityDetected && !downstreamRouteFamily;
if (ambiguityRemainsUnresolved && grounding.ambiguityEntitySets.length > 0) {
knownLimitations.push(`Exact downstream metadata surface remains ambiguous across: ${grounding.ambiguityEntitySets.join(", ")}`);
}
if (grounding.ambiguityDetected && downstreamRouteFamily && routeFamilySelectionBasis === "dominant_surface_objects") {
knownLimitations.push(`Metadata surface spans multiple object sets, but dominant confirmed objects point to ${downstreamRouteFamily}`);
}
return {
metadata_scope: metadataScope,
requested_meta_types: requestedMetaTypes,
@ -890,11 +1109,14 @@ function deriveMetadataSurface(result, metadataScope, requestedMetaTypes) {
available_entity_sets: availableEntitySets,
matched_objects: matchedObjects,
selected_entity_set: grounding.selectedEntitySet,
selected_surface_objects: metadataObjectsForEntitySet(grounding.selectedEntitySet, matchedObjects),
selected_surface_objects: selectedSurfaceObjects,
surface_family_scores: surfaceFamilyScores,
downstream_route_family: downstreamRouteFamily,
route_family_selection_basis: routeFamilySelectionBasis,
recommended_next_primitive: metadataNextPrimitiveForRouteFamily(downstreamRouteFamily),
ambiguity_detected: grounding.ambiguityDetected,
ambiguity_entity_sets: grounding.ambiguityEntitySets,
ambiguity_detected: ambiguityRemainsUnresolved,
ambiguity_entity_sets: ambiguityRemainsUnresolved ? grounding.ambiguityEntitySets : [],
surface_object_ranking_applied: scoredRouteSelection.rankingApplied,
available_fields: metadataAvailableFields(result.rows),
known_limitations: knownLimitations,
inference_basis: "confirmed_1c_metadata_surface_rows"
@ -916,13 +1138,18 @@ function buildMetadataConfirmedFacts(surface) {
if (surface.selected_surface_objects.length > 0) {
facts.push(`Selected metadata objects: ${surface.selected_surface_objects.slice(0, 8).join(", ")}`);
}
if (surface.surface_family_scores.document_evidence > 0 ||
surface.surface_family_scores.movement_evidence > 0 ||
surface.surface_family_scores.catalog_drilldown > 0) {
facts.push(`Metadata surface family scores: document=${surface.surface_family_scores.document_evidence}, movement=${surface.surface_family_scores.movement_evidence}, catalog=${surface.surface_family_scores.catalog_drilldown}`);
}
if (surface.available_fields.length > 0) {
facts.push(`Available metadata fields/sections: ${surface.available_fields.slice(0, 12).join(", ")}`);
}
return facts;
}
function buildMetadataInferredFacts(surface) {
if (!surface || !surface.selected_entity_set || !surface.downstream_route_family || !surface.recommended_next_primitive) {
if (!surface || !surface.downstream_route_family || !surface.recommended_next_primitive) {
return [];
}
return [
@ -1040,6 +1267,16 @@ function rowAmountValue(row) {
}
return null;
}
function rowCounterpartyValue(row) {
const candidates = [row["Контрагент"], row["Counterparty"], row["counterparty"], row["Наименование"], row["name"]];
for (const candidate of candidates) {
const text = toNonEmptyString(candidate);
if (text) {
return text;
}
}
return null;
}
function monthBucketFromIsoDate(isoDate) {
const match = isoDate?.match(/^(\d{4})-(\d{2})-\d{2}$/);
return match ? `${match[1]}-${match[2]}` : null;
@ -1213,6 +1450,62 @@ function deriveValueFlow(result, counterparty, periodScope, direction, aggregati
inference_basis: "sum_of_confirmed_1c_value_flow_rows"
};
}
function deriveRankedValueFlow(result, input) {
if (!result || result.error || result.matched_rows <= 0) {
return null;
}
const buckets = new Map();
let rowsWithAmount = 0;
for (const row of result.rows) {
const axisValue = rowCounterpartyValue(row);
const amount = rowAmountValue(row);
if (!axisValue || amount === null) {
continue;
}
rowsWithAmount += 1;
const current = buckets.get(axisValue) ?? { rows_with_amount: 0, total_amount: 0 };
current.rows_with_amount += 1;
current.total_amount += amount;
buckets.set(axisValue, current);
}
if (rowsWithAmount <= 0 || buckets.size <= 0) {
return null;
}
const rankedValues = Array.from(buckets.entries())
.map(([axisValue, bucket]) => ({
axis_value: axisValue,
rows_with_amount: bucket.rows_with_amount,
total_amount: bucket.total_amount,
total_amount_human_ru: formatAmountHumanRu(bucket.total_amount)
}))
.sort((left, right) => {
const amountDelta = right.total_amount - left.total_amount;
if (input.rankingNeed === "bottom_asc") {
if (amountDelta !== 0) {
return -amountDelta;
}
}
else if (amountDelta !== 0) {
return amountDelta;
}
return left.axis_value.localeCompare(right.axis_value, "ru");
})
.slice(0, 5);
return {
value_flow_direction: input.direction,
ranking_need: input.rankingNeed,
ranking_axis: "counterparty",
organization_scope: input.organizationScope,
period_scope: input.periodScope,
rows_matched: result.matched_rows,
rows_with_amount: rowsWithAmount,
ranked_values: rankedValues,
coverage_limited_by_probe_limit: result.coverage_limited_by_probe_limit,
coverage_recovered_by_period_chunking: result.coverage_recovered_by_period_chunking,
period_chunking_granularity: result.period_chunking_granularity,
inference_basis: "ranked_counterparty_totals_from_confirmed_1c_value_flow_rows"
};
}
function deriveValueFlowSideSummary(result) {
if (!result || result.error || result.matched_rows <= 0) {
return {
@ -1345,6 +1638,16 @@ function buildValueFlowConfirmedFacts(result, counterparty, direction) {
: "1C value-flow rows were found for the requested counterparty scope"
];
}
function buildRankedValueFlowConfirmedFacts(derived) {
if (!derived || derived.ranked_values.length <= 0) {
return [];
}
const leader = derived.ranked_values[0];
const directionLabel = derived.value_flow_direction === "outgoing_supplier_payout" ? "supplier-payout" : "incoming value-flow";
return [
`1C ${directionLabel} rows were ranked by counterparty for the checked scope; leader=${leader.axis_value}, rows_with_amount=${leader.rows_with_amount}`
];
}
function buildBidirectionalValueFlowConfirmedFacts(derived) {
if (!derived) {
return [];
@ -1411,6 +1714,16 @@ function buildValueFlowInferredFacts(derived) {
}
return facts;
}
function buildRankedValueFlowInferredFacts(derived) {
if (!derived) {
return [];
}
const facts = ["Counterparty ranking was calculated from confirmed 1C movement rows grouped by counterparty"];
if (derived.coverage_recovered_by_period_chunking && derived.period_chunking_granularity === "month") {
facts.push("Requested period coverage for counterparty ranking was recovered through monthly 1C probes after a broad probe hit the row limit");
}
return facts;
}
function buildBidirectionalValueFlowInferredFacts(derived) {
if (!derived) {
return [];
@ -1453,6 +1766,16 @@ function buildValueFlowUnknownFacts(periodScope, direction, derived) {
: "Full all-time turnover is not proven without an explicit checked period");
return unknownFacts;
}
function buildRankedValueFlowUnknownFacts(periodScope, derived) {
const unknownFacts = [];
if (derived?.coverage_limited_by_probe_limit) {
unknownFacts.push("Complete requested-period ranking coverage is not proven because the MCP discovery probe row limit was reached");
}
unknownFacts.push(periodScope
? "Full ranking outside the checked period is not proven by this MCP discovery pilot"
: "Full all-time counterparty ranking is not proven without an explicit checked period");
return unknownFacts;
}
function buildBidirectionalValueFlowUnknownFacts(periodScope, derived) {
const unknownFacts = [];
if (derived?.coverage_limited_by_probe_limit) {
@ -1474,11 +1797,14 @@ function buildEmptyEvidence(planner, dryRun, probeResults, reason) {
}
function pilotScopeForPlanner(planner) {
switch (planner.selected_chain_id) {
case "catalog_drilldown":
case "metadata_lane_clarification":
case "metadata_inspection":
return "metadata_inspection_v1";
case "movement_evidence":
return "counterparty_movement_evidence_query_movements_v1";
case "value_flow_comparison":
case "value_flow_ranking":
case "value_flow":
return valueFlowPilotProfile(planner).scope;
case "document_evidence":
@ -1595,11 +1921,16 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
}
const counterparty = firstEntityCandidate(planner);
const dateScope = toNonEmptyString(planner.discovery_plan.turn_meaning_ref?.explicit_date_scope);
const organizationScope = organizationScopeForPlanner(planner);
const aggregationAxis = aggregationAxisForPlanner(planner);
const rankingNeed = rankingNeedForPlanner(planner);
if (metadataPilotEligible) {
let metadataResult = null;
const metadataScope = metadataScopeForPlanner(planner);
const requestedMetaTypes = metadataTypesForPlanner(planner);
if (planner.selected_chain_id === "catalog_drilldown" && metadataScope) {
pushReason(reasonCodes, "pilot_catalog_drilldown_metadata_scope_seeded_from_surface_ref");
}
for (const step of dryRun.execution_steps) {
if (step.primitive_id !== "inspect_1c_metadata") {
skippedPrimitives.push(step.primitive_id);
@ -1622,9 +1953,15 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
}
}
const sourceRowsSummary = metadataResult ? summarizeMetadataRows(metadataResult) : null;
const derivedMetadataSurface = deriveMetadataSurface(metadataResult, metadataScope, requestedMetaTypes);
const derivedMetadataSurface = deriveMetadataSurface(metadataResult, metadataScope, requestedMetaTypes, metadataScopeRankingAllowedForPlanner(planner));
if (derivedMetadataSurface) {
pushReason(reasonCodes, "pilot_derived_metadata_surface_from_confirmed_rows");
if (derivedMetadataSurface.route_family_selection_basis === "dominant_surface_objects") {
pushReason(reasonCodes, "pilot_selected_metadata_route_family_from_dominant_surface_objects");
}
if (derivedMetadataSurface.surface_object_ranking_applied) {
pushReason(reasonCodes, "pilot_selected_metadata_route_family_from_surface_object_ranking");
}
}
const evidence = (0, assistantMcpDiscoveryPolicy_1.resolveAssistantMcpDiscoveryEvidence)({
plan: planner.discovery_plan,
@ -2151,6 +2488,48 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
}
}
const sourceRowsSummary = queryResult ? summarizeValueFlowRows(queryResult) : null;
if (planner.selected_chain_id === "value_flow_ranking" && rankingNeed) {
const derivedRankedValueFlow = deriveRankedValueFlow(queryResult, {
organizationScope,
periodScope: dateScope,
direction: valueFlowProfile.direction,
rankingNeed
});
if (derivedRankedValueFlow) {
pushReason(reasonCodes, "pilot_derived_ranked_value_flow_from_confirmed_rows");
}
const evidence = (0, assistantMcpDiscoveryPolicy_1.resolveAssistantMcpDiscoveryEvidence)({
plan: planner.discovery_plan,
probeResults,
confirmedFacts: buildRankedValueFlowConfirmedFacts(derivedRankedValueFlow),
inferredFacts: buildRankedValueFlowInferredFacts(derivedRankedValueFlow),
unknownFacts: buildRankedValueFlowUnknownFacts(dateScope, derivedRankedValueFlow),
sourceRowsSummary,
queryLimitations,
recommendedNextProbe: "explain_evidence_basis"
});
return {
schema_version: exports.ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryPilotExecutor",
pilot_status: "executed",
pilot_scope: valueFlowProfile.scope,
dry_run: dryRun,
mcp_execution_performed: executedPrimitives.length > 0,
executed_primitives: executedPrimitives,
skipped_primitives: skippedPrimitives,
probe_results: probeResults,
evidence,
source_rows_summary: sourceRowsSummary,
derived_metadata_surface: null,
derived_entity_resolution: null,
derived_activity_period: null,
derived_ranked_value_flow: derivedRankedValueFlow,
derived_value_flow: null,
derived_bidirectional_value_flow: null,
query_limitations: queryLimitations,
reason_codes: reasonCodes
};
}
const derivedValueFlow = deriveValueFlow(queryResult, counterparty, dateScope, valueFlowProfile.direction, aggregationAxis);
if (derivedValueFlow) {
pushReason(reasonCodes, "pilot_derived_value_flow_from_confirmed_rows");
@ -2183,6 +2562,7 @@ async function executeAssistantMcpDiscoveryPilot(planner, deps = DEFAULT_DEPS) {
derived_metadata_surface: null,
derived_entity_resolution: null,
derived_activity_period: null,
derived_ranked_value_flow: null,
derived_value_flow: derivedValueFlow,
derived_bidirectional_value_flow: null,
query_limitations: queryLimitations,

View File

@ -38,6 +38,12 @@ function pushUnique(target, value) {
function hasEntity(meaning) {
return (meaning?.explicit_entity_candidates?.length ?? 0) > 0;
}
function hasSubjectCandidates(graph) {
return (graph?.subject_candidates.length ?? 0) > 0;
}
function hasReasonCode(graph, reasonCode) {
return (graph?.reason_codes ?? []).includes(reasonCode);
}
function aggregationAxis(meaning) {
return toNonEmptyString(meaning?.asked_aggregation_axis)?.toLowerCase() ?? null;
}
@ -58,6 +64,150 @@ function includesAny(text, tokens) {
function isYearDateScope(meaning) {
return /^\d{4}$/.test(toNonEmptyString(meaning?.explicit_date_scope) ?? "");
}
function mergeCatalogPrimitivesWithFallback(catalogPrimitives, fallbackPrimitives) {
const result = [];
for (const primitive of fallbackPrimitives) {
if (catalogPrimitives.includes(primitive) && !result.includes(primitive)) {
result.push(primitive);
}
}
for (const primitive of catalogPrimitives) {
if (!result.includes(primitive)) {
result.push(primitive);
}
}
for (const primitive of fallbackPrimitives) {
if (!result.includes(primitive)) {
result.push(primitive);
}
}
return result;
}
function preferredPrimitiveFromMetadataSurface(surface) {
const recommendedPrimitive = surface?.recommended_next_primitive ?? null;
if (recommendedPrimitive) {
return recommendedPrimitive;
}
if (surface?.ambiguity_detected) {
return null;
}
if (surface?.downstream_route_family === "document_evidence") {
return "query_documents";
}
if (surface?.downstream_route_family === "movement_evidence") {
return "query_movements";
}
if (surface?.downstream_route_family === "catalog_drilldown") {
return "drilldown_related_objects";
}
return null;
}
function filterCatalogPrimitivesByMetadataSurface(input) {
const preferredPrimitive = preferredPrimitiveFromMetadataSurface(input.metadataSurface);
const reasonCodes = [];
if (!preferredPrimitive ||
input.metadataSurface?.ambiguity_detected ||
!input.fallbackPrimitives.includes(preferredPrimitive)) {
return {
primitives: input.catalogPrimitives,
reasonCodes
};
}
const laneSensitivePrimitives = new Set([
"query_documents",
"query_movements",
"drilldown_related_objects"
]);
const filteredPrimitives = input.catalogPrimitives.filter((primitive) => !laneSensitivePrimitives.has(primitive) || primitive === preferredPrimitive);
if (filteredPrimitives.length !== input.catalogPrimitives.length) {
reasonCodes.push("planner_filtered_catalog_primitives_by_confirmed_metadata_surface");
}
if ((filteredPrimitives.includes(preferredPrimitive) || input.fallbackPrimitives.includes(preferredPrimitive)) &&
input.metadataSurface?.selected_surface_objects.length) {
reasonCodes.push("planner_surface_aware_next_lane_from_confirmed_metadata_objects");
}
return {
primitives: filteredPrimitives,
reasonCodes
};
}
function selectPrimitivesFromGraphAndCatalog(input) {
const reasonCodes = [];
const decompositionCandidates = input.dataNeedGraph?.decomposition_candidates ?? [];
const decompositionPrimitives = decompositionCandidates.length > 0
? (0, assistantMcpCatalogIndex_1.searchAssistantMcpCatalogPrimitivesByDecompositionCandidates)({
decomposition_candidates: decompositionCandidates,
allow_aggregate_by_axis: input.allowAggregateByAxis
})
: [];
if (decompositionPrimitives.length > 0) {
reasonCodes.push("planner_selected_catalog_primitives_from_decomposition_candidates");
}
const metadataSurfacePrimitives = input.metadataSurface
? (0, assistantMcpCatalogIndex_1.searchAssistantMcpCatalogPrimitivesByMetadataSurface)({
downstream_route_family: input.metadataSurface.downstream_route_family,
selected_entity_set: input.metadataSurface.selected_entity_set,
selected_surface_objects: input.metadataSurface.selected_surface_objects,
recommended_next_primitive: input.metadataSurface.recommended_next_primitive,
required_axes: input.requiredAxes,
allow_aggregate_by_axis: input.allowAggregateByAxis
})
: [];
if (metadataSurfacePrimitives.length > 0) {
reasonCodes.push("planner_selected_catalog_primitives_from_metadata_surface_search");
}
const factAxisPrimitives = input.dataNeedGraph
? (0, assistantMcpCatalogIndex_1.searchAssistantMcpCatalogPrimitivesByFactAxis)({
business_fact_family: input.dataNeedGraph.business_fact_family,
action_family: input.actionFamily ?? input.dataNeedGraph.action_family,
required_axes: input.requiredAxes,
comparison_need: input.dataNeedGraph.comparison_need,
ranking_need: input.dataNeedGraph.ranking_need,
aggregation_need: input.dataNeedGraph.aggregation_need,
has_subject_candidates: hasSubjectCandidates(input.dataNeedGraph),
allow_aggregate_by_axis: input.allowAggregateByAxis
})
: [];
if (factAxisPrimitives.length > 0) {
reasonCodes.push("planner_selected_catalog_primitives_from_fact_axis_search");
}
const combinedCatalogPrimitives = [];
for (const primitive of decompositionPrimitives) {
if (!combinedCatalogPrimitives.includes(primitive)) {
combinedCatalogPrimitives.push(primitive);
}
}
for (const primitive of metadataSurfacePrimitives) {
if (!combinedCatalogPrimitives.includes(primitive)) {
combinedCatalogPrimitives.push(primitive);
}
}
for (const primitive of factAxisPrimitives) {
if (!combinedCatalogPrimitives.includes(primitive)) {
combinedCatalogPrimitives.push(primitive);
}
}
const filteredCatalogPrimitives = filterCatalogPrimitivesByMetadataSurface({
catalogPrimitives: combinedCatalogPrimitives,
fallbackPrimitives: input.fallbackPrimitives,
metadataSurface: input.metadataSurface
});
reasonCodes.push(...filteredCatalogPrimitives.reasonCodes);
if (filteredCatalogPrimitives.primitives.length <= 0) {
return {
primitives: input.fallbackPrimitives,
reasonCodes: ["planner_fell_back_to_recipe_primitives_after_empty_catalog_search"]
};
}
const mergedPrimitives = mergeCatalogPrimitivesWithFallback(filteredCatalogPrimitives.primitives, input.fallbackPrimitives);
if (input.fallbackPrimitives.some((primitive) => !filteredCatalogPrimitives.primitives.includes(primitive))) {
reasonCodes.push("planner_completed_catalog_searched_chain_with_recipe_primitives");
}
return {
primitives: mergedPrimitives,
reasonCodes
};
}
function budgetOverrideFor(input, recipe) {
const meaning = input.turnMeaning ?? null;
const requestedAggregationAxis = aggregationAxis(meaning);
@ -73,15 +223,330 @@ function budgetOverrideFor(input, recipe) {
}
return {};
}
function recipeFor(input) {
function routeFamilyFromThinMetadataSurfaceInput(input) {
const surface = input.metadataSurface ?? null;
if (!surface || surface.ambiguity_detected || !surface.downstream_route_family || !surface.recommended_next_primitive) {
return null;
}
const meaning = input.turnMeaning ?? null;
const dataNeedGraph = input.dataNeedGraph ?? null;
const graphFactFamily = lower(dataNeedGraph?.business_fact_family);
const domain = lower(meaning?.asked_domain_family);
const action = lower(meaning?.asked_action_family);
const unsupported = lower(meaning?.unsupported_but_understood_family);
const semanticNeed = lower(input.semanticDataNeed);
const combined = `${domain} ${action} ${unsupported} ${semanticNeed}`.trim();
const explicitlyOtherFamily = includesAny(combined, ["value_flow", "turnover", "revenue", "payment", "payout", "net", "lifecycle", "activity", "duration", "metadata lane clarification"]);
if (explicitlyOtherFamily) {
return null;
}
if (graphFactFamily === "document_evidence" || includesAny(combined, ["document", "documents", "list_documents"])) {
return surface.downstream_route_family === "document_evidence" ? "document_evidence" : null;
}
if (graphFactFamily === "movement_evidence" || includesAny(combined, ["movement", "movements", "list_movements", "bank_operations"])) {
return surface.downstream_route_family === "movement_evidence" ? "movement_evidence" : null;
}
if (graphFactFamily === "schema_surface" || includesAny(combined, ["catalog", "directory", "inspect_catalog"])) {
return surface.downstream_route_family === "catalog_drilldown" ? "catalog_drilldown" : null;
}
if (!graphFactFamily && !domain && !action) {
if (surface.downstream_route_family === "document_evidence" ||
surface.downstream_route_family === "movement_evidence" ||
surface.downstream_route_family === "catalog_drilldown") {
return surface.downstream_route_family;
}
}
return null;
}
function recipeFor(input) {
const meaning = input.turnMeaning ?? null;
const dataNeedGraph = input.dataNeedGraph ?? null;
const domain = lower(meaning?.asked_domain_family);
const action = lower(meaning?.asked_action_family);
const unsupported = lower(meaning?.unsupported_but_understood_family);
const graphFactFamily = lower(dataNeedGraph?.business_fact_family);
const graphAction = lower(dataNeedGraph?.action_family);
const graphAggregation = lower(dataNeedGraph?.aggregation_need);
const graphClarificationGaps = (dataNeedGraph?.clarification_gaps ?? []).map((item) => lower(item));
const organizationScope = toNonEmptyString(meaning?.explicit_organization_scope);
const openScopeTotalWithoutSubject = graphFactFamily === "value_flow" &&
!hasSubjectCandidates(dataNeedGraph) &&
hasReasonCode(dataNeedGraph, "data_need_graph_open_scope_total_without_subject");
const combined = `${domain} ${action} ${unsupported}`.trim();
const axes = [];
const requestedAggregationAxis = aggregationAxis(meaning);
addScopeAxes(axes, meaning);
if (graphClarificationGaps.includes("lane_family_choice")) {
pushUnique(axes, "lane_family_choice");
return {
semanticDataNeed: "metadata lane clarification",
chainId: "metadata_lane_clarification",
chainSummary: "Preserve the ambiguous metadata surface and ask the user to choose the next data lane before running MCP probes.",
primitives: [],
axes,
reason: "planner_selected_metadata_lane_clarification_from_data_need_graph"
};
}
const thinSurfaceRouteFamily = routeFamilyFromThinMetadataSurfaceInput(input);
if (thinSurfaceRouteFamily === "document_evidence") {
pushUnique(axes, "coverage_target");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph,
fallbackPrimitives: ["resolve_entity_reference", "query_documents", "probe_coverage"],
requiredAxes: axes,
metadataSurface: input.metadataSurface,
actionFamily: action
});
return {
semanticDataNeed: "document evidence",
chainId: "document_evidence",
chainSummary: "Ground the next checked document lane from the confirmed metadata surface, then fetch scoped document rows and probe coverage before answering.",
primitives: primitiveSelection.primitives,
axes,
reason: "planner_selected_document_from_confirmed_metadata_surface_ref",
extraReasons: primitiveSelection.reasonCodes
};
}
if (thinSurfaceRouteFamily === "movement_evidence") {
pushUnique(axes, "coverage_target");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph,
fallbackPrimitives: ["resolve_entity_reference", "query_movements", "probe_coverage"],
requiredAxes: axes,
metadataSurface: input.metadataSurface,
actionFamily: action
});
return {
semanticDataNeed: "movement evidence",
chainId: "movement_evidence",
chainSummary: "Ground the next checked movement lane from the confirmed metadata surface, then fetch scoped movement rows and probe coverage before answering.",
primitives: primitiveSelection.primitives,
axes,
reason: "planner_selected_movement_from_confirmed_metadata_surface_ref",
extraReasons: primitiveSelection.reasonCodes
};
}
if (thinSurfaceRouteFamily === "catalog_drilldown") {
pushUnique(axes, "metadata_scope");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph,
fallbackPrimitives: ["inspect_1c_metadata"],
requiredAxes: axes,
metadataSurface: input.metadataSurface,
actionFamily: action
});
return {
semanticDataNeed: "catalog drilldown metadata evidence",
chainId: "catalog_drilldown",
chainSummary: "Drill deeper into the confirmed catalog-oriented metadata surface, inspect related metadata objects, and keep the next safe lane grounded in checked schema evidence.",
primitives: primitiveSelection.primitives,
axes,
reason: "planner_selected_catalog_drilldown_from_confirmed_metadata_surface_ref",
extraReasons: primitiveSelection.reasonCodes
};
}
if (graphFactFamily === "value_flow") {
if (dataNeedGraph?.comparison_need === "incoming_vs_outgoing" && !hasSubjectCandidates(dataNeedGraph)) {
pushUnique(axes, "amount");
pushUnique(axes, "coverage_target");
if (requestedAggregationAxis === "month" || graphAggregation === "by_month") {
pushUnique(axes, "calendar_month");
}
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph,
fallbackPrimitives: ["query_movements", "probe_coverage"],
requiredAxes: axes,
metadataSurface: input.metadataSurface,
actionFamily: action,
allowAggregateByAxis: false
});
return {
semanticDataNeed: "bidirectional value-flow comparison evidence",
chainId: "value_flow_comparison",
chainSummary: "Query incoming and outgoing movements for the checked period and organization, compare the checked sides, and probe coverage before answering a bounded comparison.",
primitives: primitiveSelection.primitives,
axes,
reason: "planner_selected_bidirectional_value_flow_comparison_from_data_need_graph",
extraReasons: primitiveSelection.reasonCodes
};
}
if (dataNeedGraph?.ranking_need && !hasSubjectCandidates(dataNeedGraph)) {
pushUnique(axes, "aggregate_axis");
pushUnique(axes, "amount");
pushUnique(axes, "coverage_target");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph,
fallbackPrimitives: ["query_movements", "aggregate_by_axis", "probe_coverage"],
requiredAxes: axes,
metadataSurface: input.metadataSurface,
actionFamily: action,
allowAggregateByAxis: true
});
return {
semanticDataNeed: "ranked value-flow evidence",
chainId: "value_flow_ranking",
chainSummary: "Query scoped movements for the checked period and organization, aggregate checked amounts by counterparty, then probe coverage before answering a bounded ranking.",
primitives: primitiveSelection.primitives,
axes,
reason: dataNeedGraph.ranking_need === "bottom_asc"
? "planner_selected_bottom_ranked_value_flow_from_data_need_graph"
: "planner_selected_top_ranked_value_flow_from_data_need_graph",
extraReasons: primitiveSelection.reasonCodes
};
}
if (openScopeTotalWithoutSubject) {
pushUnique(axes, "organization");
pushUnique(axes, "aggregate_axis");
pushUnique(axes, "amount");
pushUnique(axes, "coverage_target");
if (requestedAggregationAxis === "month" || graphAggregation === "by_month") {
pushUnique(axes, "calendar_month");
}
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph,
fallbackPrimitives: ["query_movements", "aggregate_by_axis", "probe_coverage"],
requiredAxes: axes,
actionFamily: action,
allowAggregateByAxis: true
});
return {
semanticDataNeed: "organization-scoped value-flow evidence",
chainId: "value_flow",
chainSummary: "Query scoped movements for the checked period and organization without a preselected counterparty, aggregate checked amounts, then probe coverage before answering a bounded total.",
primitives: primitiveSelection.primitives,
axes,
reason: requestedAggregationAxis === "month" || graphAggregation === "by_month"
? "planner_selected_monthly_open_scope_value_flow_total_from_data_need_graph"
: "planner_selected_open_scope_value_flow_total_from_data_need_graph",
extraReasons: primitiveSelection.reasonCodes
};
}
pushUnique(axes, "aggregate_axis");
pushUnique(axes, "amount");
pushUnique(axes, "coverage_target");
if (requestedAggregationAxis === "month" || graphAggregation === "by_month") {
pushUnique(axes, "calendar_month");
}
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph,
fallbackPrimitives: ["resolve_entity_reference", "query_movements", "aggregate_by_axis", "probe_coverage"],
requiredAxes: axes,
metadataSurface: input.metadataSurface,
actionFamily: action,
allowAggregateByAxis: true
});
return {
semanticDataNeed: "counterparty value-flow evidence",
chainId: "value_flow",
chainSummary: "Resolve the business entity, query scoped movements, aggregate checked amounts, then probe coverage before answering.",
primitives: primitiveSelection.primitives,
axes,
reason: requestedAggregationAxis === "month" || graphAggregation === "by_month"
? "planner_selected_monthly_value_flow_from_data_need_graph"
: "planner_selected_value_flow_from_data_need_graph",
extraReasons: primitiveSelection.reasonCodes
};
}
if (graphFactFamily === "activity_lifecycle") {
pushUnique(axes, "document_date");
pushUnique(axes, "coverage_target");
pushUnique(axes, "evidence_basis");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph,
fallbackPrimitives: ["resolve_entity_reference", "query_documents", "probe_coverage", "explain_evidence_basis"],
requiredAxes: axes,
metadataSurface: input.metadataSurface,
actionFamily: action
});
return {
semanticDataNeed: "counterparty lifecycle evidence",
chainId: "lifecycle",
chainSummary: "Resolve the business entity, query supporting documents, probe coverage, then explain the evidence basis for the inferred activity window.",
primitives: primitiveSelection.primitives,
axes,
reason: "planner_selected_lifecycle_from_data_need_graph",
extraReasons: primitiveSelection.reasonCodes
};
}
if (graphFactFamily === "schema_surface") {
pushUnique(axes, "metadata_scope");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph,
fallbackPrimitives: ["inspect_1c_metadata"],
requiredAxes: axes,
metadataSurface: input.metadataSurface,
actionFamily: action
});
return {
semanticDataNeed: "1C metadata evidence",
chainId: "metadata_inspection",
chainSummary: "Inspect the 1C metadata surface first, then ground the next safe lane from confirmed schema evidence.",
primitives: primitiveSelection.primitives,
axes,
reason: "planner_selected_metadata_from_data_need_graph",
extraReasons: primitiveSelection.reasonCodes
};
}
if (graphFactFamily === "movement_evidence") {
pushUnique(axes, "coverage_target");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph,
fallbackPrimitives: ["resolve_entity_reference", "query_movements", "probe_coverage"],
requiredAxes: axes,
metadataSurface: input.metadataSurface,
actionFamily: action
});
return {
semanticDataNeed: "movement evidence",
chainId: "movement_evidence",
chainSummary: "Resolve the business entity, fetch scoped movement rows, and probe coverage without pretending to have a full movement universe.",
primitives: primitiveSelection.primitives,
axes,
reason: "planner_selected_movement_from_data_need_graph",
extraReasons: primitiveSelection.reasonCodes
};
}
if (graphFactFamily === "document_evidence") {
pushUnique(axes, "coverage_target");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph,
fallbackPrimitives: ["resolve_entity_reference", "query_documents", "probe_coverage"],
requiredAxes: axes,
metadataSurface: input.metadataSurface,
actionFamily: action
});
return {
semanticDataNeed: "document evidence",
chainId: "document_evidence",
chainSummary: "Resolve the business entity, fetch scoped document rows, and probe coverage before stating the checked document evidence.",
primitives: primitiveSelection.primitives,
axes,
reason: "planner_selected_document_from_data_need_graph",
extraReasons: primitiveSelection.reasonCodes
};
}
if (graphFactFamily === "entity_grounding" || (!graphFactFamily && (dataNeedGraph?.subject_candidates.length ?? 0) > 0)) {
pushUnique(axes, "business_entity");
pushUnique(axes, "coverage_target");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph,
fallbackPrimitives: ["search_business_entity", "resolve_entity_reference", "probe_coverage"],
requiredAxes: axes,
metadataSurface: input.metadataSurface,
actionFamily: action
});
return {
semanticDataNeed: "entity discovery evidence",
chainId: "entity_resolution",
chainSummary: "Search candidate business entities, resolve the most relevant 1C reference, and prove whether the entity grounding is stable enough for the next probe.",
primitives: primitiveSelection.primitives,
axes,
reason: graphAction === "search_business_entity"
? "planner_selected_entity_resolution_from_data_need_graph"
: "planner_selected_entity_resolution_recipe",
extraReasons: primitiveSelection.reasonCodes
};
}
if (includesAny(combined, ["metadata_lane_choice_clarification", "resolve_next_lane"])) {
pushUnique(axes, "lane_family_choice");
return {
@ -191,8 +656,19 @@ function planAssistantMcpDiscovery(input) {
const recipe = recipeFor(input);
const budgetOverride = budgetOverrideFor(input, recipe);
const semanticDataNeed = toNonEmptyString(input.semanticDataNeed) ?? recipe.semanticDataNeed;
const dataNeedGraph = input.dataNeedGraph ?? null;
const metadataSurface = input.metadataSurface ?? null;
const reasonCodes = [];
pushReason(reasonCodes, recipe.reason);
for (const reason of recipe.extraReasons ?? []) {
pushReason(reasonCodes, reason);
}
if (dataNeedGraph) {
pushReason(reasonCodes, "planner_consumed_data_need_graph_v1");
}
if (metadataSurface) {
pushReason(reasonCodes, "planner_consumed_metadata_surface_ref_v1");
}
if (budgetOverride.maxProbeCount) {
pushReason(reasonCodes, "planner_enabled_chunked_coverage_probe_budget");
}
@ -204,7 +680,27 @@ function planAssistantMcpDiscovery(input) {
maxProbeCount: budgetOverride.maxProbeCount
});
const review = (0, assistantMcpCatalogIndex_1.reviewAssistantMcpDiscoveryPlanAgainstCatalog)(plan);
const plannerStatus = statusFrom(plan, review);
const organizationClarificationRequired = (dataNeedGraph?.clarification_gaps ?? []).includes("organization") &&
!toNonEmptyString(input.turnMeaning?.explicit_organization_scope);
const adjustedReview = organizationClarificationRequired && recipe.primitives.includes("query_movements")
? {
...review,
review_status: "needs_more_axes",
missing_axes_by_primitive: {
...review.missing_axes_by_primitive,
query_movements: review.missing_axes_by_primitive.query_movements?.length
? review.missing_axes_by_primitive.query_movements
: [["organization"]]
},
reason_codes: review.reason_codes.includes("catalog_requires_organization_scope_from_data_need_graph")
? review.reason_codes
: [...review.reason_codes, "catalog_requires_organization_scope_from_data_need_graph"]
}
: review;
const plannerStatus = organizationClarificationRequired ? "needs_clarification" : statusFrom(plan, adjustedReview);
if (organizationClarificationRequired) {
pushReason(reasonCodes, "planner_requires_organization_scope_from_data_need_graph");
}
if (plannerStatus === "ready_for_execution") {
pushReason(reasonCodes, "planner_ready_for_guarded_mcp_execution");
}
@ -219,12 +715,14 @@ function planAssistantMcpDiscovery(input) {
policy_owner: "assistantMcpDiscoveryPlanner",
planner_status: plannerStatus,
semantic_data_need: semanticDataNeed,
data_need_graph: dataNeedGraph,
metadata_surface_ref: metadataSurface,
selected_chain_id: recipe.chainId,
selected_chain_summary: recipe.chainSummary,
proposed_primitives: recipe.primitives,
required_axes: recipe.axes,
discovery_plan: plan,
catalog_review: review,
catalog_review: adjustedReview,
reason_codes: reasonCodes
};
}

View File

@ -75,6 +75,7 @@ function normalizeTurnMeaning(value) {
const domain = toNonEmptyString(value.asked_domain_family);
const action = toNonEmptyString(value.asked_action_family);
const aggregationAxis = toNonEmptyString(value.asked_aggregation_axis);
const seededRankingNeed = toNonEmptyString(value.seeded_ranking_need);
const organization = toNonEmptyString(value.explicit_organization_scope);
const dateScope = toNonEmptyString(value.explicit_date_scope);
const unsupported = toNonEmptyString(value.unsupported_but_understood_family);
@ -89,6 +90,9 @@ function normalizeTurnMeaning(value) {
if (aggregationAxis) {
result.asked_aggregation_axis = aggregationAxis;
}
if (seededRankingNeed) {
result.seeded_ranking_need = seededRankingNeed;
}
if (entities.length > 0) {
result.explicit_entity_candidates = entities;
}

View File

@ -61,6 +61,28 @@ function userFacingLines(values) {
return uniqueStrings(values).filter((line) => !hasInternalMechanics(line));
}
function localizeLine(value) {
if (/^1C activity rows were found for the requested counterparty scope$/i.test(value)) {
return "В 1С найдены строки активности в запрошенном срезе.";
}
if (/^1C value-flow rows were found for the requested counterparty scope$/i.test(value)) {
return "В 1С найдены строки входящих денежных поступлений в запрошенном срезе.";
}
if (/^1C supplier-payout rows were found for the requested counterparty scope$/i.test(value)) {
return "В 1С найдены строки исходящих платежей и списаний в запрошенном срезе.";
}
const openScopeBidirectionalMatch = value.match(/^1C bidirectional value-flow rows were checked for the requested counterparty scope: incoming=(found|not_found), outgoing=(found|not_found)$/i);
if (openScopeBidirectionalMatch) {
const incoming = openScopeBidirectionalMatch[1] === "found"
? "входящие строки найдены"
: "входящие строки не найдены";
const outgoing = openScopeBidirectionalMatch[2] === "found"
? "исходящие строки найдены"
: "исходящие строки не найдены";
return `В 1С проверены входящие и исходящие денежные строки в запрошенном срезе: ${incoming}, ${outgoing}.`;
}
if (/^Requested period hit the MCP row limit, but the approved monthly recovery probe budget is smaller than the required subperiod count$/i.test(value)) {
return "Запрошенный период уперся в лимит строк MCP; доступного бюджета помесячных дозапросов не хватило, чтобы покрыть все подпериоды.";
}
const counterpartyMatch = value.match(/^1C activity rows were found for counterparty\s+(.+)$/i);
if (counterpartyMatch) {
return `В 1С найдены строки активности по контрагенту ${counterpartyMatch[1]}.`;

View File

@ -138,6 +138,34 @@ function readDiscoveryTurnMeaning(entryPoint) {
const turnInput = toRecordObject(entryPoint?.turn_input);
return toRecordObject(turnInput?.turn_meaning_ref);
}
function readDiscoveryDataNeedGraph(entryPoint) {
const turnInput = toRecordObject(entryPoint?.turn_input);
return toRecordObject(turnInput?.data_need_graph);
}
function isOpenScopeValueFlowWithoutSubject(entryPoint) {
const graph = readDiscoveryDataNeedGraph(entryPoint);
const businessFactFamily = toNonEmptyString(graph?.business_fact_family);
const subjectCandidates = Array.isArray(graph?.subject_candidates) ? graph.subject_candidates : [];
const reasonCodes = Array.isArray(graph?.reason_codes) ? graph.reason_codes : [];
return (businessFactFamily === "value_flow" &&
subjectCandidates.length === 0 &&
reasonCodes.some((reason) => toNonEmptyString(reason) === "data_need_graph_open_scope_total_without_subject"));
}
function needsOpenScopeValueFlowOrganizationClarification(entryPoint) {
const graph = readDiscoveryDataNeedGraph(entryPoint);
const businessFactFamily = toNonEmptyString(graph?.business_fact_family);
const subjectCandidates = Array.isArray(graph?.subject_candidates) ? graph.subject_candidates : [];
const clarificationGaps = Array.isArray(graph?.clarification_gaps) ? graph.clarification_gaps : [];
return (businessFactFamily === "value_flow" &&
subjectCandidates.length === 0 &&
clarificationGaps.some((gap) => toNonEmptyString(gap) === "organization"));
}
function isOpenScopeValueFlowRanking(entryPoint) {
const graph = readDiscoveryDataNeedGraph(entryPoint);
const businessFactFamily = toNonEmptyString(graph?.business_fact_family);
const subjectCandidates = Array.isArray(graph?.subject_candidates) ? graph.subject_candidates : [];
return businessFactFamily === "value_flow" && subjectCandidates.length === 0 && Boolean(toNonEmptyString(graph?.ranking_need));
}
function readTruthAnswerShape(input) {
const directShape = toRecordObject(input.addressRuntimeMeta?.answer_shape_contract);
if (directShape) {
@ -197,6 +225,9 @@ function hasAlignedFactualAddressReply(input, entryPoint) {
if (!hasEffectivelyFactualAddressReply(input)) {
return false;
}
if (hasSemanticConflictWithDiscoveryTurnMeaning(input, entryPoint)) {
return false;
}
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
return isDetectedIntentAlignedWithTurnMeaning(detectedIntent, readDiscoveryTurnMeaning(entryPoint));
}
@ -218,6 +249,16 @@ function hasSemanticConflictWithDiscoveryTurnMeaning(input, entryPoint) {
if (!detectedIntent || (!askedDomain && !askedAction && !unsupportedFamily)) {
return false;
}
if (isOpenScopeValueFlowRanking(entryPoint)) {
return true;
}
if (needsOpenScopeValueFlowOrganizationClarification(entryPoint)) {
return true;
}
if (detectedIntent === "customer_revenue_and_payments" &&
isOpenScopeValueFlowWithoutSubject(entryPoint)) {
return true;
}
return !isDetectedIntentAlignedWithTurnMeaning(detectedIntent, turnMeaning);
}
function hasMatchedFactualAddressContinuationTarget(input, entryPoint) {

View File

@ -51,6 +51,8 @@ function businessFactAnswerAllowed(draft) {
async function runAssistantMcpDiscoveryRuntimeBridge(input) {
const planner = (0, assistantMcpDiscoveryPlanner_1.planAssistantMcpDiscovery)({
semanticDataNeed: input.semanticDataNeed,
dataNeedGraph: input.dataNeedGraph,
metadataSurface: input.metadataSurface,
turnMeaning: input.turnMeaning
});
const pilot = await (0, assistantMcpDiscoveryPilotExecutor_1.executeAssistantMcpDiscoveryPilot)(planner, input.deps);

View File

@ -62,6 +62,8 @@ async function runAssistantMcpDiscoveryRuntimeEntryPoint(input) {
}
const bridge = await (0, assistantMcpDiscoveryRuntimeBridge_1.runAssistantMcpDiscoveryRuntimeBridge)({
semanticDataNeed: turnInput.semantic_data_need,
dataNeedGraph: turnInput.data_need_graph,
metadataSurface: turnInput.metadata_surface_ref,
turnMeaning: turnInput.turn_meaning_ref,
deps: input.deps
});

View File

@ -2,6 +2,7 @@
Object.defineProperty(exports, "__esModule", { value: true });
exports.ASSISTANT_MCP_DISCOVERY_TURN_INPUT_SCHEMA_VERSION = void 0;
exports.buildAssistantMcpDiscoveryTurnInput = buildAssistantMcpDiscoveryTurnInput;
const assistantMcpDiscoveryDataNeedGraph_1 = require("./assistantMcpDiscoveryDataNeedGraph");
exports.ASSISTANT_MCP_DISCOVERY_TURN_INPUT_SCHEMA_VERSION = "assistant_mcp_discovery_turn_input_v1";
function toRecordObject(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
@ -71,6 +72,9 @@ function compactLower(value) {
.replace(/\s+/g, " ")
.trim();
}
function sameScopedName(left, right) {
return Boolean(left && right && compactLower(left) === compactLower(right));
}
function candidateValue(value) {
const direct = toNonEmptyString(value);
if (direct && direct !== "[object Object]") {
@ -141,6 +145,24 @@ function collectDateScopeFromFilters(filters) {
}
return periodFrom ?? periodTo ?? null;
}
function normalizeMetadataRouteFamily(value) {
const text = toNonEmptyString(value);
if (text === "document_evidence" || text === "movement_evidence" || text === "catalog_drilldown") {
return text;
}
return null;
}
function normalizeMetadataRecommendedPrimitive(value) {
const text = toNonEmptyString(value);
if (text === "query_documents" || text === "query_movements" || text === "drilldown_related_objects") {
return text;
}
return null;
}
function normalizeMetadataRouteFamilySelectionBasis(value) {
const text = toNonEmptyString(value);
return text === "selected_entity_set" || text === "dominant_surface_objects" ? text : null;
}
function mapPilotScopeToFollowupMeaning(pilotScope) {
if (pilotScope === "counterparty_lifecycle_query_documents_v1") {
return {
@ -266,14 +288,42 @@ function collectFollowupDiscoverySeed(followupContext) {
discoveryEntity: ambiguityBlocksImplicitGrounding ? null : discoveryEntities[0] ?? null,
entityResolutionStatus,
entityResolutionAmbiguityCandidates,
rankingNeed: toNonEmptyString(followupContext?.previous_discovery_ranking_need),
organization,
dateScope,
metadataRouteFamily: toNonEmptyString(followupContext?.previous_discovery_metadata_route_family),
metadataRouteFamily: normalizeMetadataRouteFamily(followupContext?.previous_discovery_metadata_route_family),
metadataRouteFamilySelectionBasis: normalizeMetadataRouteFamilySelectionBasis(followupContext?.previous_discovery_metadata_route_family_selection_basis),
metadataSelectedEntitySet: toNonEmptyString(followupContext?.previous_discovery_metadata_selected_entity_set),
metadataSelectedSurfaceObjects: collectEntityCandidates(followupContext?.previous_discovery_metadata_selected_surface_objects),
metadataRecommendedNextPrimitive: normalizeMetadataRecommendedPrimitive(followupContext?.previous_discovery_metadata_recommended_next_primitive),
metadataAmbiguityDetected: followupContext?.previous_discovery_metadata_ambiguity_detected === true,
metadataAmbiguityEntitySets: collectEntityCandidates(followupContext?.previous_discovery_metadata_ambiguity_entity_sets)
};
}
function buildMetadataSurfaceRef(followupSeed) {
if (followupSeed.pilotScope !== "metadata_inspection_v1") {
return null;
}
const hasPayload = Boolean(followupSeed.metadataRouteFamily ||
followupSeed.metadataSelectedEntitySet ||
followupSeed.metadataRecommendedNextPrimitive ||
followupSeed.metadataRouteFamilySelectionBasis) ||
followupSeed.metadataSelectedSurfaceObjects.length > 0 ||
followupSeed.metadataAmbiguityDetected ||
followupSeed.metadataAmbiguityEntitySets.length > 0;
if (!hasPayload) {
return null;
}
return {
selected_entity_set: followupSeed.metadataSelectedEntitySet,
selected_surface_objects: followupSeed.metadataSelectedSurfaceObjects,
downstream_route_family: followupSeed.metadataRouteFamily,
route_family_selection_basis: followupSeed.metadataRouteFamilySelectionBasis,
recommended_next_primitive: followupSeed.metadataRecommendedNextPrimitive,
ambiguity_detected: followupSeed.metadataAmbiguityDetected,
ambiguity_entity_sets: followupSeed.metadataAmbiguityEntitySets
};
}
function metadataEntitySetsSuggestDocumentLane(values) {
return values.some((value) => /(?:документ|document|invoice|waybill|накладн|счет[- ]?фактур|акт)/iu.test(value));
}
@ -290,7 +340,7 @@ function hasLifecycleSignal(text) {
return /(?:сколько\s+лет|как\s+давно|давно\s+ли|возраст|перв(?:ая|ый)\s+актив|когда\s+начал|когда\s+появ|lifecycle|activity\s+duration|business\s+age|how\s+long)/iu.test(text);
}
function hasValueFlowSignal(text) {
return /(?:оборот|выручк|оплат|плат[её]ж|заплат|перечисл|списан|расход|исходящ|входящ|получ(?:ил|ено|ен)|поступил|поступлен|денежн[а-яёa-z0-9_-]*\s+поток|supplier|value[-\s]?flow|turnover|revenue|payment|payout|outflow|cash\s+flow)/iu.test(text);
return /(?:оборот|выручк|оплат|плат[её]ж|заплат|перечисл|списан|расход|исходящ|входящ|получ(?:ил|ено|ен)|поступил|поступлен|денежн[а-яёa-z0-9_-]*\s+поток|(?<!\p{L})заработ(?:ал|али|ало|аем|ает|ать|ано|ок)(?!\p{L})|supplier|value[-\s]?flow|turnover|revenue|payment|payout|outflow|cash\s+flow|\bearn(?:ed|ing|ings)?\b)/iu.test(text);
}
function hasPayoutSignal(text) {
return /(?:\bмы\s+(?:за)?плат|(?:за)?платил|оплатил|перечисл|списан|расход|поставщик|исходящ|supplier|payout|outflow|paid\s+to|payment\s+to)/iu.test(text);
@ -298,9 +348,34 @@ function hasPayoutSignal(text) {
function hasBidirectionalValueFlowSignal(text) {
return /(?:нетто|сальдо|баланс\s+(?:плат|денег|денеж)|взаиморасч[её]т|получил[иа]?.*(?:за)?платил|(?:за)?платил[иа]?.*получил|входящ.*исходящ|исходящ.*входящ|дебет.*кредит|кредит.*дебет|net\s+(?:flow|cash|payment)|cash\s+net|incoming\s+and\s+outgoing|received\s+and\s+paid|paid\s+and\s+received)/iu.test(text);
}
function hasValueRankingSignal(text) {
return /(?:кто\s+больше\s+всего.*ден[её]г|больше\s+всего.*ден[её]г|прин[её]с.*ден[её]г|сам(?:ый|ая|ое|ые).*(?:доходн|прибыльн)|most.*money|highest\s+(?:revenue|payment))/iu.test(text);
}
function hasOrganizationScopeSignal(text) {
return /(?:\bооо\b|\bип\b|\bао\b|\bпао\b|\bзао\b|\bllc\b|\binc\b|\bcorp\b|\bcompany\b|\borganization\b|\borganisation\b|организац|компан)/iu.test(text);
}
function hasOrganizationScopeSignalUtf8(text) {
return (/(?<!\p{L})(?:\u043e\u043e\u043e|\u0438\u043f|\u0430\u043e|\u043f\u0430\u043e|\u0437\u0430\u043e)(?!\p{L})/iu.test(text) ||
/\b(?:llc|inc|corp|company|organization|organisation)\b/iu.test(text) ||
/(?:\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446|\u043a\u043e\u043c\u043f\u0430\u043d)/iu.test(text));
}
function extractOrganizationScopeFromRawText(value) {
const text = toNonEmptyString(value);
if (!text) {
return null;
}
const match = text.match(/(?:^|[\s,;:])(?:\u043f\u043e|for|in|within)?\s*((?:\u041e\u041e\u041e|\u0418\u041f|\u0410\u041e|\u041f\u0410\u041e|\u0417\u0410\u041e|LLC|Inc|LTD|Corp)\s+[^\n,.;:!?]+)/u);
if (!match?.[1]) {
return null;
}
return toNonEmptyString(match[1]);
}
function hasMonthlyAggregationSignal(text) {
return /(?:\u043f\u043e\s+\u043c\u0435\u0441\u044f\u0446\u0430\u043c|\u043f\u043e\u043c\u0435\u0441\u044f\u0447\u043d\u043e|\u0435\u0436\u0435\u043c\u0435\u0441\u044f\u0447\u043d\u043e|month\s+by\s+month|by\s+month|monthly)/iu.test(text);
}
function hasAllTimeScopeHint(text) {
return /(?:\u0437\u0430\s+\u0432\u0441[\u0435\u0451]\s+\u0432\u0440\u0435\u043c\u044f|\u0437\u0430\s+\u0432\u0435\u0441\u044c\s+\u043f\u0435\u0440\u0438\u043e\u0434|\u0437\u0430\s+\u0432\u0441\u044e\s+\u0438\u0441\u0442\u043e\u0440\u0438(?:\u044e|\u0438)|\u0437\u0430\s+\u043b\u044e\u0431\u043e\u0439\s+\u043f\u0435\u0440\u0438\u043e\u0434|for\s+all\s+time|all\s+time|entire\s+period|full\s+history|any\s+period)/iu.test(text);
}
function hasMetadataSignal(text) {
if (/(?:\u043c\u0435\u0442\u0430\u0434\u0430\u043d|schema|catalog|metadata\s+surface|\u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440[\u0430\u044b]\s+1\u0441|\u0441\u0445\u0435\u043c[\u0430\u044b]\s+1\u0441)/iu.test(text)) {
return true;
@ -543,6 +618,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
const followupContext = toRecordObject(input.followupContext);
const predecomposeEntities = collectPredecomposeEntities(predecomposeContract);
const followupSeed = collectFollowupDiscoverySeed(followupContext);
const metadataSurfaceRef = buildMetadataSurfaceRef(followupSeed);
const reasonCodes = [];
const rawUserText = toNonEmptyString(input.userMessage);
const rawEffectiveText = toNonEmptyString(input.effectiveMessage);
@ -551,11 +627,13 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
const rawText = compactLower(rawSignalSourceText);
const rawLifecycleSignal = hasLifecycleSignal(rawText);
const rawBidirectionalValueFlowSignal = !rawLifecycleSignal && hasBidirectionalValueFlowSignal(rawText);
const rawValueFlowSignal = !rawLifecycleSignal && (hasValueFlowSignal(rawText) || rawBidirectionalValueFlowSignal);
const rawValueFlowSignal = !rawLifecycleSignal &&
(hasValueFlowSignal(rawText) || hasValueRankingSignal(rawText) || rawBidirectionalValueFlowSignal);
const rawMetadataSignal = !rawLifecycleSignal && !rawValueFlowSignal && hasMetadataSignal(rawText);
const rawEntityResolutionSignal = !rawLifecycleSignal && !rawValueFlowSignal && !rawMetadataSignal && hasEntityResolutionSignal(rawText);
const rawPayoutSignal = rawValueFlowSignal && !rawBidirectionalValueFlowSignal && hasPayoutSignal(rawText);
const monthlyAggregationSignal = hasMonthlyAggregationSignal(rawText);
const rawAllTimeScopeSignal = hasAllTimeScopeHint(rawText);
const explicitDateScopeLiteralDetected = hasExplicitDateScopeLiteral(rawText);
const rawDateScope = collectDateScopeFromRawText(rawText);
const rawMetadataScopeHint = rawMetadataSignal ? metadataScopeHintFromRawText(rawText) : null;
@ -574,11 +652,38 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
const explicitIntentCandidate = toNonEmptyString(assistantTurnMeaning?.explicit_intent_candidate);
const assistantTurnMeaningDateScope = toNonEmptyString(assistantTurnMeaning?.explicit_date_scope);
const assistantTurnMeaningOrganizationScope = toNonEmptyString(assistantTurnMeaning?.explicit_organization_scope);
const rawOrganizationMentionSignal = hasOrganizationScopeSignalUtf8(rawText);
const rawOrganizationScope = extractOrganizationScopeFromRawText(rawUserText ?? rawEffectiveText ?? rawSignalSourceText);
const currentTurnOrganizationScope = rawOrganizationScope ?? predecomposeEntities.organization ?? assistantTurnMeaningOrganizationScope;
const explicitOrganizationScopeSignal = Boolean(rawOrganizationMentionSignal && currentTurnOrganizationScope);
const organizationClarificationFollowupApplicable = Boolean(followupSeed.domain === "counterparty_value" &&
!followupSeed.counterparty &&
currentTurnOrganizationScope &&
!rawLifecycleSignal &&
!rawValueFlowSignal &&
!rawMetadataSignal);
const rawOpenScopeValueFlowOrganizationSignal = Boolean(rawValueFlowSignal &&
!rawBidirectionalValueFlowSignal &&
explicitOrganizationScopeSignal);
const predecomposeOrganizationMirrorsCounterparty = sameScopedName(predecomposeEntities.counterparty, predecomposeEntities.organization);
const organizationMirrorsPredecomposeCounterparty = Boolean((rawBidirectionalValueFlowSignal ||
hasValueRankingSignal(rawText) ||
rawOpenScopeValueFlowOrganizationSignal ||
explicitOrganizationScopeSignal) &&
(sameScopedName(predecomposeEntities.counterparty, assistantTurnMeaningOrganizationScope) ||
predecomposeOrganizationMirrorsCounterparty));
const normalizedPredecomposeCounterparty = organizationMirrorsPredecomposeCounterparty
? null
: predecomposeEntities.counterparty;
const predecomposeDateScope = collectDateScope(predecomposeContract);
const followupDiscoverySeedApplicable = Boolean(followupSeed.domain &&
!rawLifecycleSignal &&
!rawValueFlowSignal &&
(monthlyAggregationSignal || explicitDateScopeLiteralDetected || predecomposeDateScope));
(monthlyAggregationSignal ||
explicitDateScopeLiteralDetected ||
predecomposeDateScope ||
explicitOrganizationScopeSignal ||
organizationClarificationFollowupApplicable));
const metadataFollowupSeedApplicable = Boolean(followupSeed.domain === "metadata" &&
!rawLifecycleSignal &&
!rawValueFlowSignal &&
@ -683,6 +788,15 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
!metadataDocumentHintSignal &&
!metadataMovementHintSignal &&
hasMetadataDownstreamContinuationSignal(rawText));
const metadataGroundedCatalogLaneContinuationApplicable = Boolean(followupSeed.pilotScope === "metadata_inspection_v1" &&
followupSeed.metadataRouteFamily === "catalog_drilldown" &&
!followupSeed.metadataAmbiguityDetected &&
!rawLifecycleSignal &&
!rawValueFlowSignal &&
!rawMetadataSignal &&
!metadataDocumentHintSignal &&
!metadataMovementHintSignal &&
hasMetadataDownstreamContinuationSignal(rawText));
const metadataAmbiguityCollapsedDocumentLaneContinuationApplicable = Boolean(followupSeed.pilotScope === "metadata_inspection_v1" &&
followupSeed.metadataAmbiguityDetected &&
metadataAmbiguityCollapsesToDocumentLane(followupSeed.metadataAmbiguityEntitySets) &&
@ -733,34 +847,41 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
const effectiveMetadataFollowupSeedApplicable = metadataFollowupSeedApplicable &&
!metadataAmbiguityLaneClarificationApplicable &&
!metadataGroundedDocumentLaneApplicable &&
!metadataGroundedMovementLaneApplicable;
!metadataGroundedMovementLaneApplicable &&
!metadataGroundedCatalogLaneContinuationApplicable;
const seededDomain = metadataAmbiguityLaneClarificationApplicable
? "metadata"
: metadataGroundedDocumentLaneApplicable
? "documents"
: metadataGroundedMovementLaneApplicable
? "movements"
: followupDiscoverySeedApplicable || effectiveMetadataFollowupSeedApplicable
? followupSeed.domain
: null;
: metadataGroundedCatalogLaneContinuationApplicable
? "metadata"
: followupDiscoverySeedApplicable || effectiveMetadataFollowupSeedApplicable
? followupSeed.domain
: null;
const seededAction = metadataAmbiguityLaneClarificationApplicable
? "resolve_next_lane"
: metadataGroundedDocumentLaneApplicable
? "list_documents"
: metadataGroundedMovementLaneApplicable
? "list_movements"
: followupDiscoverySeedApplicable || effectiveMetadataFollowupSeedApplicable
? followupSeed.action
: null;
: metadataGroundedCatalogLaneContinuationApplicable
? "inspect_catalog"
: followupDiscoverySeedApplicable || effectiveMetadataFollowupSeedApplicable
? followupSeed.action
: null;
const seededUnsupported = metadataAmbiguityLaneClarificationApplicable
? "metadata_lane_choice_clarification"
: metadataGroundedDocumentLaneApplicable
? "document_evidence"
: metadataGroundedMovementLaneApplicable
? "movement_evidence"
: followupDiscoverySeedApplicable || effectiveMetadataFollowupSeedApplicable
? followupSeed.unsupported
: null;
: metadataGroundedCatalogLaneContinuationApplicable
? "schema_surface"
: followupDiscoverySeedApplicable || effectiveMetadataFollowupSeedApplicable
? followupSeed.unsupported
: null;
const lifecycleSignal = rawLifecycleSignal || seededDomain === "counterparty_lifecycle";
const bidirectionalValueFlowSignal = !lifecycleSignal &&
(rawBidirectionalValueFlowSignal || seededAction === "net_value_flow");
@ -791,7 +912,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
for (const candidate of collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates)) {
pushNormalizedEntityResolutionCandidate(entityCandidates, candidate);
}
pushNormalizedEntityResolutionCandidate(entityCandidates, predecomposeEntities.counterparty);
pushNormalizedEntityResolutionCandidate(entityCandidates, normalizedPredecomposeCounterparty);
pushNormalizedEntityResolutionCandidate(entityCandidates, followupSeed.counterparty);
}
else {
@ -801,7 +922,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
for (const candidate of collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates)) {
pushScopedEntityCandidate(entityCandidates, candidate, groundedFollowupEntity);
}
pushScopedEntityCandidate(entityCandidates, predecomposeEntities.counterparty, groundedFollowupEntity);
pushScopedEntityCandidate(entityCandidates, normalizedPredecomposeCounterparty, groundedFollowupEntity);
if (!groundedFollowupEntity) {
pushScopedEntityCandidate(entityCandidates, followupSeed.counterparty, null);
pushScopedEntityCandidate(entityCandidates, followupSeed.discoveryEntity, null);
@ -812,14 +933,36 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
pushUnique(entityCandidates, followupSeed.discoveryEntity);
pushUnique(entityCandidates, rawMetadataScopeHint);
}
if (valueFlowSignal && !predecomposeEntities.counterparty && !followupSeed.counterparty) {
const openScopeValueFlowWithoutCounterparty = valueFlowSignal && !normalizedPredecomposeCounterparty && !followupSeed.counterparty;
const valueFlowOrganizationStaysScope = openScopeValueFlowWithoutCounterparty &&
Boolean(bidirectionalValueFlowSignal ||
hasValueRankingSignal(rawText) ||
rawOpenScopeValueFlowOrganizationSignal ||
explicitOrganizationScopeSignal ||
organizationClarificationFollowupApplicable ||
followupSeed.organization);
if (openScopeValueFlowWithoutCounterparty && !valueFlowOrganizationStaysScope) {
pushUnique(entityCandidates, predecomposeEntities.organization);
pushUnique(entityCandidates, followupSeed.organization);
}
const explicitOrganizationScope = valueFlowSignal && !predecomposeEntities.counterparty && !followupSeed.counterparty
const explicitOrganizationScope = valueFlowOrganizationStaysScope || !openScopeValueFlowWithoutCounterparty
? currentTurnOrganizationScope ?? followupSeed.organization
: null;
if (valueFlowOrganizationStaysScope && explicitOrganizationScope) {
for (let index = entityCandidates.length - 1; index >= 0; index -= 1) {
if (entityCandidates[index] === explicitOrganizationScope) {
entityCandidates.splice(index, 1);
}
}
}
const explicitDateScope = rawAllTimeScopeSignal
? null
: predecomposeEntities.organization ?? assistantTurnMeaningOrganizationScope ?? followupSeed.organization;
const explicitDateScope = assistantTurnMeaningDateScope ?? predecomposeDateScope ?? rawDateScope ?? followupSeed.dateScope;
: assistantTurnMeaningDateScope ?? predecomposeDateScope ?? rawDateScope ?? followupSeed.dateScope;
const followupDateScopeApplied = Boolean(!rawAllTimeScopeSignal &&
!assistantTurnMeaningDateScope &&
!predecomposeDateScope &&
!rawDateScope &&
followupSeed.dateScope);
const turnMeaning = {
asked_domain_family: lifecycleSignal
? "counterparty_lifecycle"
@ -852,6 +995,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
? metadataActionFromRawText(rawText) ?? seededAction
: rawAction ?? seededAction,
asked_aggregation_axis: monthlyAggregationSignal ? "month" : rawAggregationAxis,
seeded_ranking_need: valueFlowSignal && followupSeed.rankingNeed ? followupSeed.rankingNeed : undefined,
explicit_entity_candidates: entityCandidates,
metadata_ambiguity_entity_sets: metadataAmbiguityLaneClarificationApplicable && followupSeed.metadataAmbiguityEntitySets.length > 0
? followupSeed.metadataAmbiguityEntitySets
@ -902,6 +1046,9 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
if (toNonEmptyString(turnMeaning.asked_aggregation_axis)) {
cleanTurnMeaning.asked_aggregation_axis = turnMeaning.asked_aggregation_axis;
}
if (toNonEmptyString(turnMeaning.seeded_ranking_need)) {
cleanTurnMeaning.seeded_ranking_need = turnMeaning.seeded_ranking_need;
}
if ((turnMeaning.explicit_entity_candidates?.length ?? 0) > 0) {
cleanTurnMeaning.explicit_entity_candidates = turnMeaning.explicit_entity_candidates;
}
@ -936,6 +1083,7 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
metadataGroundedDocumentLaneApplicable ||
groundedValueFlowFollowupApplicable,
forceDiscoveryOverExplicitIntent: Boolean(entityResolutionClarificationCandidate) ||
organizationClarificationFollowupApplicable ||
metadataAmbiguityLaneClarificationApplicable ||
metadataGroundedMovementLaneApplicable ||
metadataGroundedDocumentLaneApplicable ||
@ -985,6 +1133,9 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
if (entityResolutionClarificationCandidate) {
pushReason(reasonCodes, "mcp_discovery_entity_resolution_clarification_candidate_selected");
}
if (organizationClarificationFollowupApplicable) {
pushReason(reasonCodes, "mcp_discovery_organization_clarification_followup_from_followup_context");
}
if (payoutSignal) {
pushReason(reasonCodes, "mcp_discovery_payout_signal_detected");
}
@ -994,6 +1145,9 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
if (monthlyAggregationSignal) {
pushReason(reasonCodes, "mcp_discovery_monthly_aggregation_signal_detected");
}
if (rawAllTimeScopeSignal) {
pushReason(reasonCodes, "mcp_discovery_all_time_scope_signal_detected");
}
if (followupDiscoverySeedApplicable) {
pushReason(reasonCodes, "mcp_discovery_seeded_from_followup_context");
}
@ -1042,6 +1196,9 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
if (metadataGroundedLaneContinuationApplicable) {
pushReason(reasonCodes, "mcp_discovery_metadata_grounded_lane_continuation");
}
if (metadataGroundedCatalogLaneContinuationApplicable) {
pushReason(reasonCodes, "mcp_discovery_metadata_grounded_catalog_continuation");
}
if (metadataAmbiguityCollapsedDocumentLaneContinuationApplicable) {
pushReason(reasonCodes, "mcp_discovery_metadata_ambiguity_collapsed_to_document_lane");
}
@ -1054,15 +1211,22 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
if (unsupported) {
pushReason(reasonCodes, "mcp_discovery_unsupported_but_understood_turn");
}
if (predecomposeEntities.counterparty) {
if (!(valueFlowOrganizationStaysScope && normalizedPredecomposeCounterparty === explicitOrganizationScope) &&
normalizedPredecomposeCounterparty) {
pushReason(reasonCodes, "mcp_discovery_counterparty_from_predecompose");
}
if (followupSeed.counterparty) {
pushReason(reasonCodes, "mcp_discovery_counterparty_from_followup_context");
}
if (followupSeed.dateScope) {
if (followupDateScopeApplied) {
pushReason(reasonCodes, "mcp_discovery_date_scope_from_followup_context");
}
if (metadataSurfaceRef) {
pushReason(reasonCodes, "mcp_discovery_metadata_surface_ref_from_followup_context");
}
if (metadataSurfaceRef?.recommended_next_primitive) {
pushReason(reasonCodes, "mcp_discovery_metadata_next_primitive_from_followup_context");
}
if (entityCandidates.length > 0) {
pushReason(reasonCodes, "mcp_discovery_entity_scope_available");
}
@ -1072,12 +1236,24 @@ function buildAssistantMcpDiscoveryTurnInput(input) {
if (runDiscovery && !hasTurnMeaning) {
pushReason(reasonCodes, "mcp_discovery_turn_meaning_missing");
}
const dataNeedGraph = runDiscovery && hasTurnMeaning
? (0, assistantMcpDiscoveryDataNeedGraph_1.buildAssistantMcpDiscoveryDataNeedGraph)({
semanticDataNeed,
rawUtterance: rawSignalSourceText,
turnMeaning: cleanTurnMeaning
})
: null;
if (dataNeedGraph) {
pushReason(reasonCodes, "mcp_discovery_data_need_graph_built");
}
return {
schema_version: exports.ASSISTANT_MCP_DISCOVERY_TURN_INPUT_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryTurnInputAdapter",
adapter_status: !runDiscovery ? "not_applicable" : hasTurnMeaning ? "ready" : "needs_more_context",
should_run_discovery: runDiscovery,
semantic_data_need: runDiscovery ? semanticDataNeed : null,
data_need_graph: dataNeedGraph,
metadata_surface_ref: runDiscovery ? metadataSurfaceRef : null,
turn_meaning_ref: runDiscovery && hasTurnMeaning ? cleanTurnMeaning : null,
source_signal: sourceSignal,
reason_codes: reasonCodes

View File

@ -500,11 +500,15 @@ function createAssistantTransitionPolicy(deps) {
const sourceIntent = (0, assistantContinuityPolicy_1.readAddressDebugIntent)(carryoverSourceDebug, deps.toNonEmptyString);
const sourceDiscoveryPilotScope = sourceDiscoveryPilotScopeHint;
const sourceDiscoveryMetadataRouteFamily = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryMetadataRouteFamily)(carryoverSourceDebug, deps.toNonEmptyString);
const sourceDiscoveryMetadataRouteFamilySelectionBasis = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryMetadataRouteFamilySelectionBasis)(carryoverSourceDebug, deps.toNonEmptyString);
const sourceDiscoveryMetadataSelectedEntitySet = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryMetadataSelectedEntitySet)(carryoverSourceDebug, deps.toNonEmptyString);
const sourceDiscoveryMetadataSelectedSurfaceObjects = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryMetadataSelectedSurfaceObjects)(carryoverSourceDebug, deps.toNonEmptyString);
const sourceDiscoveryMetadataRecommendedNextPrimitive = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryMetadataRecommendedNextPrimitive)(carryoverSourceDebug, deps.toNonEmptyString);
const sourceDiscoveryMetadataAmbiguityDetected = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryMetadataAmbiguityDetected)(carryoverSourceDebug);
const sourceDiscoveryMetadataAmbiguityEntitySets = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryMetadataAmbiguityEntitySets)(carryoverSourceDebug, deps.toNonEmptyString);
const sourceDiscoveryEntityResolutionStatus = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryEntityResolutionStatus)(carryoverSourceDebug, deps.toNonEmptyString);
const sourceDiscoveryEntityCandidates = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryEntityCandidates)(carryoverSourceDebug, deps.toNonEmptyString);
const sourceDiscoveryRankingNeed = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryRankingNeed)(carryoverSourceDebug, deps.toNonEmptyString);
const sourceDiscoveryEntityAmbiguityCandidates = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryEntityAmbiguityCandidates)(carryoverSourceDebug, deps.toNonEmptyString);
const llmExplicitIntent = deps.toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent);
const llmSelectedObjectScopeDetected = llmPreDecomposeMeta?.predecomposeContract?.semantics?.selected_object_scope_detected === true;
@ -742,11 +746,17 @@ function createAssistantTransitionPolicy(deps) {
previous_discovery_pilot_scope: sourceDiscoveryPilotScope ?? undefined,
previous_discovery_entity_resolution_status: sourceDiscoveryEntityResolutionStatus ?? undefined,
previous_discovery_entity_candidates: sourceDiscoveryEntityCandidates.length > 0 ? sourceDiscoveryEntityCandidates : undefined,
previous_discovery_ranking_need: sourceDiscoveryRankingNeed ?? undefined,
previous_discovery_entity_ambiguity_candidates: sourceDiscoveryEntityAmbiguityCandidates.length > 0
? sourceDiscoveryEntityAmbiguityCandidates
: undefined,
previous_discovery_metadata_route_family: sourceDiscoveryMetadataRouteFamily ?? undefined,
previous_discovery_metadata_route_family_selection_basis: sourceDiscoveryMetadataRouteFamilySelectionBasis ?? undefined,
previous_discovery_metadata_selected_entity_set: sourceDiscoveryMetadataSelectedEntitySet ?? undefined,
previous_discovery_metadata_selected_surface_objects: sourceDiscoveryMetadataSelectedSurfaceObjects.length > 0
? sourceDiscoveryMetadataSelectedSurfaceObjects
: undefined,
previous_discovery_metadata_recommended_next_primitive: sourceDiscoveryMetadataRecommendedNextPrimitive ?? undefined,
previous_discovery_metadata_ambiguity_detected: sourceDiscoveryMetadataAmbiguityDetected || undefined,
previous_discovery_metadata_ambiguity_entity_sets: sourceDiscoveryMetadataAmbiguityEntitySets.length > 0 ? sourceDiscoveryMetadataAmbiguityEntitySets : undefined,
resolved_counterparty_from_display: resolvedCounterpartyFromDisplay || undefined,

View File

@ -142,6 +142,14 @@ function readAssistantMcpDiscoveryTurnMeaning(
return toRecordObject(turnInput?.turn_meaning_ref);
}
function readAssistantMcpDiscoveryDataNeedGraph(
debug: Record<string, unknown> | null
): Record<string, unknown> | null {
const entry = readAssistantMcpDiscoveryEntry(debug);
const turnInput = toRecordObject(entry?.turn_input);
return toRecordObject(turnInput?.data_need_graph);
}
function readAssistantMcpDiscoveryTurnMeaningMetadataAmbiguityEntitySets(
debug: Record<string, unknown> | null,
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
@ -248,6 +256,13 @@ export function readAssistantMcpDiscoveryPilotScope(
return toNonEmptyString(pilot?.pilot_scope);
}
export function readAssistantMcpDiscoveryRankingNeed(
debug: Record<string, unknown> | null,
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
): string | null {
return toNonEmptyString(readAssistantMcpDiscoveryDataNeedGraph(debug)?.ranking_need);
}
export function readAssistantMcpDiscoveryMetadataRouteFamily(
debug: Record<string, unknown> | null,
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
@ -255,6 +270,13 @@ export function readAssistantMcpDiscoveryMetadataRouteFamily(
return toNonEmptyString(readAssistantMcpDiscoveryDerivedMetadataSurface(debug)?.downstream_route_family);
}
export function readAssistantMcpDiscoveryMetadataRouteFamilySelectionBasis(
debug: Record<string, unknown> | null,
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
): string | null {
return toNonEmptyString(readAssistantMcpDiscoveryDerivedMetadataSurface(debug)?.route_family_selection_basis);
}
export function readAssistantMcpDiscoveryMetadataSelectedEntitySet(
debug: Record<string, unknown> | null,
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
@ -262,6 +284,24 @@ export function readAssistantMcpDiscoveryMetadataSelectedEntitySet(
return toNonEmptyString(readAssistantMcpDiscoveryDerivedMetadataSurface(debug)?.selected_entity_set);
}
export function readAssistantMcpDiscoveryMetadataSelectedSurfaceObjects(
debug: Record<string, unknown> | null,
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
): string[] {
const values = readAssistantMcpDiscoveryDerivedMetadataSurface(debug)?.selected_surface_objects;
if (!Array.isArray(values)) {
return [];
}
return values.map((item) => toNonEmptyString(item)).filter((item): item is string => Boolean(item));
}
export function readAssistantMcpDiscoveryMetadataRecommendedNextPrimitive(
debug: Record<string, unknown> | null,
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
): string | null {
return toNonEmptyString(readAssistantMcpDiscoveryDerivedMetadataSurface(debug)?.recommended_next_primitive);
}
export function readAssistantMcpDiscoveryMetadataAmbiguityDetected(
debug: Record<string, unknown> | null
): boolean {

View File

@ -13,6 +13,10 @@ export type AssistantMcpCatalogPlanReviewStatus = "catalog_compatible" | "needs_
export interface AssistantMcpCatalogPrimitiveContract {
primitive_id: AssistantMcpDiscoveryPrimitive;
purpose: string;
decomposition_hints: string[];
supported_fact_families: string[];
supported_action_families: string[];
planning_tags: string[];
required_axes_any_of: string[][];
optional_axes: string[];
output_fact_kinds: string[];
@ -43,6 +47,10 @@ const PRIMITIVE_CONTRACTS: AssistantMcpCatalogPrimitiveContract[] = [
{
primitive_id: "search_business_entity",
purpose: "Find candidate 1C business entities by user wording before a fact query is executed.",
decomposition_hints: ["search_business_entity"],
supported_fact_families: ["entity_grounding"],
supported_action_families: ["search_business_entity"],
planning_tags: ["subject_resolution"],
required_axes_any_of: [["business_entity"], ["counterparty"], ["organization"], ["contract"], ["item"]],
optional_axes: ["period", "document", "account"],
output_fact_kinds: ["entity_candidates", "entity_ambiguity"],
@ -53,6 +61,10 @@ const PRIMITIVE_CONTRACTS: AssistantMcpCatalogPrimitiveContract[] = [
{
primitive_id: "inspect_1c_metadata",
purpose: "Inspect available 1C schema/catalog/document/register surface before selecting a query lane.",
decomposition_hints: ["inspect_metadata_surface"],
supported_fact_families: ["schema_surface"],
supported_action_families: ["inspect_catalog", "inspect_documents", "inspect_registers", "inspect_fields", "inspect_surface"],
planning_tags: ["metadata", "surface_inspection"],
required_axes_any_of: [["metadata_scope"], ["domain_family"], ["document"], ["register"]],
optional_axes: ["business_entity", "account", "counterparty"],
output_fact_kinds: ["available_fields", "available_entity_sets", "known_limitations"],
@ -63,6 +75,10 @@ const PRIMITIVE_CONTRACTS: AssistantMcpCatalogPrimitiveContract[] = [
{
primitive_id: "resolve_entity_reference",
purpose: "Resolve a user-visible entity name to a concrete 1C reference candidate.",
decomposition_hints: ["resolve_entity_reference"],
supported_fact_families: ["entity_grounding", "value_flow", "document_evidence", "movement_evidence", "activity_lifecycle"],
supported_action_families: ["search_business_entity", "turnover", "payout", "net_value_flow", "list_documents", "list_movements", "activity_duration"],
planning_tags: ["subject_resolution"],
required_axes_any_of: [["business_entity"], ["counterparty"], ["organization"], ["contract"], ["item"]],
optional_axes: ["period", "inn", "document"],
output_fact_kinds: ["resolved_entity_ref", "entity_conflict"],
@ -73,6 +89,15 @@ const PRIMITIVE_CONTRACTS: AssistantMcpCatalogPrimitiveContract[] = [
{
primitive_id: "query_movements",
purpose: "Fetch or aggregate accounting/register movements for a scoped business question.",
decomposition_hints: [
"collect_scoped_movements",
"collect_incoming_movements",
"collect_outgoing_movements",
"fetch_scoped_movements"
],
supported_fact_families: ["value_flow", "movement_evidence"],
supported_action_families: ["turnover", "payout", "net_value_flow", "list_movements"],
planning_tags: ["movement", "comparison", "ranking", "aggregation", "monthly_aggregation"],
required_axes_any_of: [["period", "account"], ["period", "counterparty"], ["period", "organization"]],
optional_axes: ["contract", "document", "amount", "item", "warehouse"],
output_fact_kinds: ["movement_rows", "turnover", "balance_delta"],
@ -83,6 +108,10 @@ const PRIMITIVE_CONTRACTS: AssistantMcpCatalogPrimitiveContract[] = [
{
primitive_id: "query_documents",
purpose: "Fetch documents related to a scoped entity, period, contract, or movement explanation.",
decomposition_hints: ["fetch_scoped_documents", "fetch_supporting_documents"],
supported_fact_families: ["document_evidence", "activity_lifecycle"],
supported_action_families: ["list_documents", "activity_duration"],
planning_tags: ["document"],
required_axes_any_of: [["document"], ["counterparty"], ["contract"], ["period", "organization"]],
optional_axes: ["account", "amount", "item", "warehouse"],
output_fact_kinds: ["document_rows", "document_dates", "document_amounts"],
@ -93,6 +122,10 @@ const PRIMITIVE_CONTRACTS: AssistantMcpCatalogPrimitiveContract[] = [
{
primitive_id: "aggregate_by_axis",
purpose: "Aggregate already-scoped 1C evidence by a business axis such as counterparty, contract, or period.",
decomposition_hints: ["aggregate_checked_amounts", "aggregate_ranked_axis_values", "aggregate_by_month"],
supported_fact_families: ["value_flow"],
supported_action_families: ["turnover", "payout", "net_value_flow"],
planning_tags: ["aggregation", "ranking", "monthly_aggregation"],
required_axes_any_of: [["aggregate_axis", "period"], ["aggregate_axis", "counterparty"], ["aggregate_axis", "account"]],
optional_axes: ["organization", "contract", "document", "amount"],
output_fact_kinds: ["aggregate_totals", "ranked_axis_values"],
@ -103,6 +136,10 @@ const PRIMITIVE_CONTRACTS: AssistantMcpCatalogPrimitiveContract[] = [
{
primitive_id: "drilldown_related_objects",
purpose: "Drill from a known entity or document into related contracts, documents, movements, or payments.",
decomposition_hints: ["drilldown_related_objects"],
supported_fact_families: [],
supported_action_families: [],
planning_tags: ["drilldown"],
required_axes_any_of: [["business_entity"], ["document"], ["contract"], ["counterparty"]],
optional_axes: ["period", "account", "amount"],
output_fact_kinds: ["related_objects", "relationship_edges"],
@ -113,6 +150,30 @@ const PRIMITIVE_CONTRACTS: AssistantMcpCatalogPrimitiveContract[] = [
{
primitive_id: "probe_coverage",
purpose: "Check whether the selected MCP/schema route can prove the requested fact or only support a bounded inference.",
decomposition_hints: ["probe_coverage"],
supported_fact_families: [
"schema_surface",
"entity_grounding",
"value_flow",
"document_evidence",
"movement_evidence",
"activity_lifecycle"
],
supported_action_families: [
"inspect_catalog",
"inspect_documents",
"inspect_registers",
"inspect_fields",
"inspect_surface",
"search_business_entity",
"turnover",
"payout",
"net_value_flow",
"list_documents",
"list_movements",
"activity_duration"
],
planning_tags: ["coverage"],
required_axes_any_of: [["coverage_target"], ["domain_family"], ["primitive_id"]],
optional_axes: ["period", "organization", "counterparty", "document", "account"],
output_fact_kinds: ["coverage_status", "known_gaps"],
@ -123,6 +184,10 @@ const PRIMITIVE_CONTRACTS: AssistantMcpCatalogPrimitiveContract[] = [
{
primitive_id: "explain_evidence_basis",
purpose: "Produce a machine-readable explanation of which checked MCP evidence supports, limits, or fails the answer.",
decomposition_hints: ["explain_evidence_basis"],
supported_fact_families: ["activity_lifecycle"],
supported_action_families: ["activity_duration"],
planning_tags: ["explanation"],
required_axes_any_of: [["evidence_basis"], ["primitive_id"], ["source_rows_summary"]],
optional_axes: ["coverage_target", "domain_family"],
output_fact_kinds: ["confirmed_facts", "inferred_facts", "unknown_facts"],
@ -164,6 +229,376 @@ function missingAxisGroups(axisSet: Set<string>, groups: string[][]): string[][]
return groups.filter((group) => !group.every((axis) => axisSet.has(axis)));
}
function normalizeDecompositionStep(value: string): string {
return value.trim().toLowerCase();
}
function normalizePlanningToken(value: string): string {
return value.trim().toLowerCase();
}
function countAxisOverlap(axisSet: Set<string>, groups: string[][]): number {
let best = 0;
for (const group of groups) {
const overlap = group.filter((axis) => axisSet.has(axis)).length;
if (overlap > best) {
best = overlap;
}
}
return best;
}
function tagSetFromFactAxisInput(input: AssistantMcpCatalogFactAxisSearchInput): Set<string> {
const tags = new Set<string>();
const requiredAxes = input.required_axes ?? [];
if (input.business_fact_family === "schema_surface") {
tags.add("metadata");
tags.add("surface_inspection");
}
if (
input.business_fact_family === "entity_grounding" ||
input.has_subject_candidates ||
requiredAxes.some((axis) => ["business_entity", "counterparty", "contract", "item"].includes(axis))
) {
tags.add("subject_resolution");
}
if (input.business_fact_family === "document_evidence") {
tags.add("document");
}
if (input.business_fact_family === "movement_evidence") {
tags.add("movement");
}
if (input.business_fact_family === "value_flow") {
tags.add("movement");
}
if (input.comparison_need) {
tags.add("comparison");
}
if (input.ranking_need) {
tags.add("ranking");
tags.add("aggregation");
}
if ((input.aggregation_need ?? "") === "by_month" || requiredAxes.includes("calendar_month")) {
tags.add("monthly_aggregation");
tags.add("aggregation");
}
if (requiredAxes.includes("aggregate_axis")) {
tags.add("aggregation");
}
if (requiredAxes.includes("coverage_target")) {
tags.add("coverage");
}
if (input.business_fact_family === "activity_lifecycle" || requiredAxes.includes("evidence_basis")) {
tags.add("explanation");
}
return tags;
}
function matchesPlanningToken(value: string | null | undefined, candidates: string[]): boolean {
const normalizedValue = normalizePlanningToken(value ?? "");
return normalizedValue.length > 0 && candidates.some((candidate) => normalizePlanningToken(candidate) === normalizedValue);
}
export interface AssistantMcpCatalogFactAxisSearchInput {
business_fact_family?: string | null;
action_family?: string | null;
required_axes?: string[];
comparison_need?: string | null;
ranking_need?: string | null;
aggregation_need?: string | null;
has_subject_candidates?: boolean;
allow_aggregate_by_axis?: boolean;
}
export interface AssistantMcpCatalogPrimitiveSearchInput {
decomposition_candidates: string[];
allow_aggregate_by_axis?: boolean;
}
export interface AssistantMcpCatalogMetadataSurfaceSearchInput {
downstream_route_family?: string | null;
selected_entity_set?: string | null;
selected_surface_objects?: string[];
recommended_next_primitive?: string | null;
required_axes?: string[];
allow_aggregate_by_axis?: boolean;
}
function normalizeMetadataSurfaceToken(value: string): string {
return value.trim().toLowerCase().replace(/[\s_.-]+/g, "");
}
function metadataSurfaceSuggestsDocument(value: string): boolean {
const token = normalizeMetadataSurfaceToken(value);
return (
token.includes("документ") ||
token.includes("document") ||
token.includes("invoice") ||
token.includes("waybill") ||
token.includes("накладн") ||
token.includes("счетфактур") ||
token.includes("счётфактур") ||
token.includes("акт")
);
}
function metadataSurfaceSuggestsMovement(value: string): boolean {
const token = normalizeMetadataSurfaceToken(value);
return (
token.includes("регистр") ||
token.includes("register") ||
token.includes("movement") ||
token.includes("движени") ||
token.includes("операц") ||
token.includes("проводк") ||
token.includes("bank")
);
}
function metadataSurfaceSuggestsCatalog(value: string): boolean {
const token = normalizeMetadataSurfaceToken(value);
return (
token.includes("справочник") ||
token.includes("catalog") ||
token.includes("directory")
);
}
function tagSetFromMetadataSurfaceInput(input: AssistantMcpCatalogMetadataSurfaceSearchInput): Set<string> {
const tags = new Set<string>();
const routeFamily = normalizePlanningToken(input.downstream_route_family ?? "");
const recommendedPrimitive = normalizePlanningToken(input.recommended_next_primitive ?? "");
const surfaceValues = [
input.selected_entity_set ?? "",
...(input.selected_surface_objects ?? [])
];
if (routeFamily === "document_evidence" || recommendedPrimitive === "query_documents") {
tags.add("document");
}
if (routeFamily === "movement_evidence" || recommendedPrimitive === "query_movements") {
tags.add("movement");
}
if (routeFamily === "catalog_drilldown" || recommendedPrimitive === "drilldown_related_objects") {
tags.add("drilldown");
tags.add("metadata");
tags.add("surface_inspection");
}
for (const value of surfaceValues) {
if (metadataSurfaceSuggestsDocument(value)) {
tags.add("document");
}
if (metadataSurfaceSuggestsMovement(value)) {
tags.add("movement");
}
if (metadataSurfaceSuggestsCatalog(value)) {
tags.add("drilldown");
tags.add("metadata");
tags.add("surface_inspection");
}
}
const requiredAxisSet = toStringSet(input.required_axes ?? []);
if (requiredAxisSet.has("coverage_target")) {
tags.add("coverage");
}
if (
requiredAxisSet.has("counterparty") ||
requiredAxisSet.has("business_entity") ||
requiredAxisSet.has("contract")
) {
tags.add("subject_resolution");
}
return tags;
}
function factFamiliesFromMetadataSurfaceInput(input: AssistantMcpCatalogMetadataSurfaceSearchInput): Set<string> {
const families = new Set<string>();
const routeFamily = normalizePlanningToken(input.downstream_route_family ?? "");
if (routeFamily === "document_evidence") {
families.add("document_evidence");
}
if (routeFamily === "movement_evidence") {
families.add("movement_evidence");
}
if (routeFamily === "catalog_drilldown") {
families.add("schema_surface");
}
for (const value of [input.selected_entity_set ?? "", ...(input.selected_surface_objects ?? [])]) {
if (metadataSurfaceSuggestsCatalog(value)) {
families.add("schema_surface");
break;
}
}
return families;
}
export function searchAssistantMcpCatalogPrimitivesByDecompositionCandidates(
input: AssistantMcpCatalogPrimitiveSearchInput
): AssistantMcpDiscoveryPrimitive[] {
const allowAggregateByAxis = input.allow_aggregate_by_axis !== false;
const result: AssistantMcpDiscoveryPrimitive[] = [];
for (const candidate of input.decomposition_candidates) {
const normalizedCandidate = normalizeDecompositionStep(candidate);
if (!normalizedCandidate) {
continue;
}
for (const contract of PRIMITIVE_CONTRACTS) {
if (
contract.primitive_id === "aggregate_by_axis" &&
normalizedCandidate === "aggregate_by_month" &&
!allowAggregateByAxis
) {
continue;
}
if (!contract.decomposition_hints.includes(normalizedCandidate)) {
continue;
}
if (!result.includes(contract.primitive_id)) {
result.push(contract.primitive_id);
}
}
}
return result;
}
export function searchAssistantMcpCatalogPrimitivesByFactAxis(
input: AssistantMcpCatalogFactAxisSearchInput
): AssistantMcpDiscoveryPrimitive[] {
const allowAggregateByAxis = input.allow_aggregate_by_axis !== false;
const requiredAxisSet = toStringSet(input.required_axes ?? []);
const desiredTags = tagSetFromFactAxisInput(input);
const scored: Array<{ primitive: AssistantMcpDiscoveryPrimitive; score: number }> = [];
for (const contract of PRIMITIVE_CONTRACTS) {
if (contract.primitive_id === "aggregate_by_axis" && !allowAggregateByAxis) {
continue;
}
if (contract.primitive_id === "search_business_entity" && input.business_fact_family !== "entity_grounding") {
continue;
}
if (input.business_fact_family === "schema_surface" && contract.primitive_id !== "inspect_1c_metadata") {
continue;
}
const factMatch = matchesPlanningToken(input.business_fact_family, contract.supported_fact_families);
const actionMatch = matchesPlanningToken(input.action_family, contract.supported_action_families);
const tagMatches = contract.planning_tags.filter((tag) => desiredTags.has(normalizePlanningToken(tag)));
if (contract.primitive_id === "search_business_entity" && !desiredTags.has("subject_resolution")) {
continue;
}
if (contract.primitive_id === "resolve_entity_reference" && !desiredTags.has("subject_resolution")) {
continue;
}
if (contract.primitive_id === "aggregate_by_axis" && !desiredTags.has("aggregation")) {
continue;
}
if (contract.primitive_id === "explain_evidence_basis" && !desiredTags.has("explanation")) {
continue;
}
if (!factMatch && !actionMatch && tagMatches.length <= 0) {
continue;
}
const hasCompatibleAxisGroup = requiredAxisSet.size > 0 && hasAnyAxisGroup(requiredAxisSet, contract.required_axes_any_of);
const axisOverlap = requiredAxisSet.size > 0 ? countAxisOverlap(requiredAxisSet, contract.required_axes_any_of) : 0;
let score = 0;
if (factMatch) {
score += 5;
}
if (actionMatch) {
score += 3;
}
score += tagMatches.length * 2;
if (hasCompatibleAxisGroup) {
score += 2;
} else if (axisOverlap > 0) {
score += 1;
}
if (score <= 0) {
continue;
}
scored.push({
primitive: contract.primitive_id,
score
});
}
return scored
.sort((left, right) => right.score - left.score)
.map((item) => item.primitive);
}
export function searchAssistantMcpCatalogPrimitivesByMetadataSurface(
input: AssistantMcpCatalogMetadataSurfaceSearchInput
): AssistantMcpDiscoveryPrimitive[] {
const allowAggregateByAxis = input.allow_aggregate_by_axis !== false;
const requiredAxisSet = toStringSet(input.required_axes ?? []);
const desiredTags = tagSetFromMetadataSurfaceInput(input);
const desiredFactFamilies = factFamiliesFromMetadataSurfaceInput(input);
const recommendedPrimitive = normalizePlanningToken(input.recommended_next_primitive ?? "");
const scored: Array<{ primitive: AssistantMcpDiscoveryPrimitive; score: number }> = [];
for (const contract of PRIMITIVE_CONTRACTS) {
if (contract.primitive_id === "aggregate_by_axis" && !allowAggregateByAxis) {
continue;
}
if (contract.primitive_id === "search_business_entity") {
continue;
}
const tagMatches = contract.planning_tags.filter((tag) => desiredTags.has(normalizePlanningToken(tag)));
const factMatch = contract.supported_fact_families.some((family) =>
desiredFactFamilies.has(normalizePlanningToken(family))
);
const primitiveRecommended = normalizePlanningToken(contract.primitive_id) === recommendedPrimitive;
if (contract.primitive_id === "resolve_entity_reference" && !desiredTags.has("subject_resolution")) {
continue;
}
if (contract.primitive_id === "aggregate_by_axis" && !desiredTags.has("aggregation")) {
continue;
}
if (!primitiveRecommended && !factMatch && tagMatches.length <= 0) {
continue;
}
const hasCompatibleAxisGroup = requiredAxisSet.size > 0 && hasAnyAxisGroup(requiredAxisSet, contract.required_axes_any_of);
const axisOverlap = requiredAxisSet.size > 0 ? countAxisOverlap(requiredAxisSet, contract.required_axes_any_of) : 0;
if (contract.primitive_id === "probe_coverage" && !desiredTags.has("coverage")) {
continue;
}
if (contract.primitive_id === "drilldown_related_objects" && !hasCompatibleAxisGroup && axisOverlap <= 0) {
continue;
}
let score = 0;
if (primitiveRecommended) {
score += 6;
}
if (factMatch) {
score += 5;
}
score += tagMatches.length * 2;
if (hasCompatibleAxisGroup) {
score += 2;
} else if (axisOverlap > 0) {
score += 1;
}
if (score <= 0) {
continue;
}
scored.push({
primitive: contract.primitive_id,
score
});
}
return scored
.sort((left, right) => right.score - left.score)
.map((item) => item.primitive);
}
export function buildAssistantMcpCatalogIndex(): AssistantMcpCatalogIndexContract {
const reasonCodes: string[] = [];
const missingContracts = ASSISTANT_MCP_DISCOVERY_PRIMITIVES.filter((primitive) => !PRIMITIVE_CONTRACT_MAP.has(primitive));

View File

@ -123,6 +123,15 @@ function isMetadataPilot(pilot: AssistantMcpDiscoveryPilotExecutionContract): bo
return pilot.pilot_scope === "metadata_inspection_v1";
}
function isCatalogDrilldownPilot(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
return (
isMetadataPilot(pilot) &&
(pilot.reason_codes.includes("planner_selected_catalog_drilldown_from_confirmed_metadata_surface_ref") ||
pilot.dry_run.reason_codes.includes("planner_selected_catalog_drilldown_from_confirmed_metadata_surface_ref") ||
pilot.reason_codes.includes("pilot_catalog_drilldown_metadata_scope_seeded_from_surface_ref"))
);
}
function isEntityResolutionPilot(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
return pilot.pilot_scope === "entity_resolution_search_v1";
}
@ -130,7 +139,9 @@ function isEntityResolutionPilot(pilot: AssistantMcpDiscoveryPilotExecutionContr
function isMetadataLaneChoiceClarification(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
return (
pilot.reason_codes.includes("planner_selected_metadata_lane_clarification_recipe") ||
pilot.dry_run.reason_codes.includes("planner_selected_metadata_lane_clarification_recipe")
pilot.reason_codes.includes("planner_selected_metadata_lane_clarification_from_data_need_graph") ||
pilot.dry_run.reason_codes.includes("planner_selected_metadata_lane_clarification_recipe") ||
pilot.dry_run.reason_codes.includes("planner_selected_metadata_lane_clarification_from_data_need_graph")
);
}
@ -174,6 +185,15 @@ function explicitDateScope(pilot: AssistantMcpDiscoveryPilotExecutionContract):
return normalized.length > 0 ? normalized : null;
}
function explicitOrganizationScope(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
const value = pilot.evidence.query_plan.turn_meaning_ref?.explicit_organization_scope;
if (typeof value !== "string") {
return null;
}
const normalized = value.trim();
return normalized.length > 0 ? normalized : null;
}
function documentOrMovementScopeRu(pilot: AssistantMcpDiscoveryPilotExecutionContract): string {
const entity = firstEntityCandidate(pilot);
const period = explicitDateScope(pilot);
@ -192,6 +212,33 @@ function isMovementLaneClarification(pilot: AssistantMcpDiscoveryPilotExecutionC
);
}
function isRankedValueFlowClarification(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
return (
pilot.reason_codes.includes("planner_selected_top_ranked_value_flow_from_data_need_graph") ||
pilot.reason_codes.includes("planner_selected_bottom_ranked_value_flow_from_data_need_graph") ||
pilot.dry_run.reason_codes.includes("planner_selected_top_ranked_value_flow_from_data_need_graph") ||
pilot.dry_run.reason_codes.includes("planner_selected_bottom_ranked_value_flow_from_data_need_graph")
);
}
function isBidirectionalValueFlowComparisonClarification(
pilot: AssistantMcpDiscoveryPilotExecutionContract
): boolean {
return (
pilot.reason_codes.includes("planner_selected_bidirectional_value_flow_comparison_from_data_need_graph") ||
pilot.dry_run.reason_codes.includes("planner_selected_bidirectional_value_flow_comparison_from_data_need_graph")
);
}
function isOpenScopeValueFlowClarification(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
return (
pilot.reason_codes.includes("planner_selected_open_scope_value_flow_total_from_data_need_graph") ||
pilot.reason_codes.includes("planner_selected_monthly_open_scope_value_flow_total_from_data_need_graph") ||
pilot.dry_run.reason_codes.includes("planner_selected_open_scope_value_flow_total_from_data_need_graph") ||
pilot.dry_run.reason_codes.includes("planner_selected_monthly_open_scope_value_flow_total_from_data_need_graph")
);
}
function isDocumentLaneClarification(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
return (
isDocumentPilot(pilot) ||
@ -207,7 +254,14 @@ function laneScopeSuffix(pilot: AssistantMcpDiscoveryPilotExecutionContract): st
return entity ? ` по "${entity}"` : "";
}
function dryRunHasAxis(pilot: AssistantMcpDiscoveryPilotExecutionContract, axis: string): boolean {
return pilot.dry_run.execution_steps.some((step) => step.provided_axes.includes(axis));
}
function dryRunMissingAxis(pilot: AssistantMcpDiscoveryPilotExecutionContract, axis: string): boolean {
if (dryRunHasAxis(pilot, axis)) {
return false;
}
return pilot.dry_run.execution_steps.some((step) =>
step.missing_axis_options.some((option) => option.includes(axis))
);
@ -216,8 +270,21 @@ function dryRunMissingAxis(pilot: AssistantMcpDiscoveryPilotExecutionContract, a
function clarificationNeedRu(
pilot: AssistantMcpDiscoveryPilotExecutionContract
): { subject: string; verb: string } {
const organizationScopedOpenTotal =
pilot.reason_codes.includes("data_need_graph_open_scope_total_needs_organization") ||
pilot.dry_run.reason_codes.includes("data_need_graph_open_scope_total_needs_organization") ||
pilot.reason_codes.includes("planner_requires_organization_scope_from_data_need_graph") ||
pilot.dry_run.reason_codes.includes("planner_requires_organization_scope_from_data_need_graph");
if (organizationScopedOpenTotal) {
return {
subject: "\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\u0438\u044e",
verb: "\u043d\u0443\u0436\u043d\u043e"
};
}
const hasCounterparty = dryRunHasAxis(pilot, "counterparty");
const hasAccount = dryRunHasAxis(pilot, "account");
const needsPeriod = dryRunMissingAxis(pilot, "period");
const needsOrganization = dryRunMissingAxis(pilot, "organization");
const needsOrganization = !hasCounterparty && !hasAccount && dryRunMissingAxis(pilot, "organization");
if (needsPeriod && needsOrganization) {
return { subject: "проверяемый период и организацию", verb: "нужно" };
}
@ -234,9 +301,17 @@ function clarificationNextStepLine(
pilot: AssistantMcpDiscoveryPilotExecutionContract,
laneLabel: string
): string {
const organizationScopedOpenTotal =
pilot.reason_codes.includes("data_need_graph_open_scope_total_needs_organization") ||
pilot.dry_run.reason_codes.includes("data_need_graph_open_scope_total_needs_organization") ||
pilot.reason_codes.includes("planner_requires_organization_scope_from_data_need_graph") ||
pilot.dry_run.reason_codes.includes("planner_requires_organization_scope_from_data_need_graph");
const needsPeriod = dryRunMissingAxis(pilot, "period");
const needsOrganization = dryRunMissingAxis(pilot, "organization");
const scopeSuffix = laneScopeSuffix(pilot);
if (organizationScopedOpenTotal && !needsPeriod) {
return `Уточните организацию, и я продолжу поиск по ${laneLabel}${scopeSuffix} в 1С.`;
}
if (needsPeriod && needsOrganization) {
return `Уточните период и организацию, и я продолжу поиск по ${laneLabel}${scopeSuffix} в 1С.`;
}
@ -281,9 +356,15 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
) {
return "По текущему каталожному поиску 1С точный контрагент пока не подтвержден.";
}
if (pilot.derived_ranked_value_flow && mode === "confirmed_with_bounded_inference") {
return "По данным 1С можно построить ограниченный рейтинг по контрагентам на подтвержденных строках денежных движений.";
}
if (isMovementPilot(pilot) && mode === "confirmed_with_bounded_inference") {
return `По движениям${documentOrMovementScopeRu(pilot)} в 1С найдены подтвержденные строки; ответ ограничен проверенным окном и найденными строками.`;
}
if (isCatalogDrilldownPilot(pilot) && mode === "confirmed_with_bounded_inference") {
return "По метаданным 1С удалось углубиться в контур справочников и связанных объектов; это уже не общий обзор схемы, а следующий безопасный catalog drilldown.";
}
if (pilot.derived_metadata_surface && mode === "confirmed_with_bounded_inference") {
if (pilot.derived_metadata_surface.ambiguity_detected) {
return "По метаданным 1С найдены конкурирующие schema-поверхности; перед следующим шагом нужно удержать неоднозначность явно.";
@ -340,6 +421,18 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
const need = clarificationNeedRu(pilot);
return `Могу идти дальше по документам${laneScopeSuffix(pilot)}, но для запуска поиска в 1С ${need.verb} ${need.subject}.`;
}
if (mode === "needs_clarification" && isBidirectionalValueFlowComparisonClarification(pilot)) {
const need = clarificationNeedRu(pilot);
return `Могу сравнить входящий и исходящий денежный поток, но для проверяемого поиска в 1С ${need.verb} ${need.subject}.`;
}
if (mode === "needs_clarification" && isRankedValueFlowClarification(pilot)) {
const need = clarificationNeedRu(pilot);
return `Могу посчитать рейтинг по денежному потоку между контрагентами, но для проверяемого поиска в 1С ${need.verb} ${need.subject}.`;
}
if (mode === "needs_clarification" && isOpenScopeValueFlowClarification(pilot)) {
const need = clarificationNeedRu(pilot);
return `Могу посчитать общий денежный поток в проверяемом окне, но для проверяемого поиска в 1С ${need.verb} ${need.subject}.`;
}
if (mode === "needs_clarification") {
return "Нужно уточнить контекст перед поиском в 1С.";
}
@ -378,6 +471,15 @@ function nextStepFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
if (mode === "needs_clarification" && isDocumentLaneClarification(pilot)) {
return clarificationNextStepLine(pilot, "документам");
}
if (mode === "needs_clarification" && isBidirectionalValueFlowComparisonClarification(pilot)) {
return clarificationNextStepLine(pilot, "сравнению входящих и исходящих денежных потоков");
}
if (mode === "needs_clarification" && isRankedValueFlowClarification(pilot)) {
return clarificationNextStepLine(pilot, "рейтингу контрагентов по денежному потоку");
}
if (mode === "needs_clarification" && isOpenScopeValueFlowClarification(pilot)) {
return clarificationNextStepLine(pilot, "денежному потоку");
}
if (mode === "needs_clarification") {
return "Уточните контрагента, период или организацию, и я смогу выполнить проверку по 1С.";
}
@ -413,6 +515,10 @@ function buildMustNotClaim(pilot: AssistantMcpDiscoveryPilotExecutionContract):
claims.push("Do not claim full all-time turnover unless the checked period and coverage prove it.");
claims.push("Do not present a derived sum as a legal/accounting final total outside the checked 1C rows.");
}
if (pilot.derived_ranked_value_flow) {
claims.push("Do not present a bounded ranking as a complete all-time ranking outside the checked period and organization.");
claims.push("Do not imply the top-ranked counterparty is globally final when probe-limit or scope boundaries still exist.");
}
if (isDocumentPilot(pilot)) {
claims.push("Do not claim full document history outside the checked period.");
claims.push("Do not present the confirmed document rows as a complete document universe.");
@ -556,11 +662,50 @@ function derivedEntityResolutionInferenceLine(pilot: AssistantMcpDiscoveryPilotE
return null;
}
function derivedRankedValueFlowInferenceLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
const ranking = pilot.derived_ranked_value_flow;
if (!ranking) {
return null;
}
const organization = ranking.organization_scope ? ` по организации ${ranking.organization_scope}` : "";
const period = ranking.period_scope ? ` за период ${ranking.period_scope}` : " в проверенном окне";
return `Рейтинг по контрагентам${organization}${period} рассчитан только по подтвержденным строкам 1С и не доказывает полный исторический срез вне проверенного окна.`;
}
function derivedRankedValueFlowConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
const ranking = pilot.derived_ranked_value_flow;
if (!ranking || ranking.ranked_values.length <= 0) {
return null;
}
const leader = ranking.ranked_values[0];
const organization = ranking.organization_scope ? ` по организации ${ranking.organization_scope}` : "";
const period = ranking.period_scope ? ` за период ${ranking.period_scope}` : " в проверенном окне";
const directionLead =
ranking.ranking_need === "bottom_asc"
? ranking.value_flow_direction === "outgoing_supplier_payout"
? "Меньше всего заплатили контрагенту"
: "Меньше всего денег принёс контрагент"
: ranking.value_flow_direction === "outgoing_supplier_payout"
? "Больше всего заплатили контрагенту"
: "Больше всего денег принёс контрагент";
const tail = ranking.ranked_values
.slice(1, 3)
.map((bucket) => `${bucket.axis_value}${bucket.total_amount_human_ru}`)
.join("; ");
const trail = tail ? ` Следом: ${tail}.` : "";
const limitCaveat = ranking.coverage_limited_by_probe_limit
? " Лимит строк проверки достигнут; рейтинг может быть неполным."
: "";
return `${directionLead} ${leader.axis_value}${organization}${period}: ${leader.total_amount_human_ru} по ${leader.rows_with_amount} строкам с суммой.${trail}${limitCaveat}`;
}
function derivedValueFlowConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
const flow = pilot.derived_value_flow;
if (!flow) {
return null;
}
const organizationScope = explicitOrganizationScope(pilot);
const organization = organizationScope ? ` по организации ${organizationScope}` : "";
const counterparty = flow.counterparty ? ` по контрагенту ${flow.counterparty}` : "";
const period = flow.period_scope ? ` за период ${flow.period_scope}` : " в проверенном окне";
const movementLabel =
@ -662,13 +807,17 @@ export function buildAssistantMcpDiscoveryAnswerDraft(
const derivedInferenceLine =
derivedActivityInferenceLine(pilot) ??
derivedMetadataInferenceLine(pilot) ??
derivedRankedValueFlowInferenceLine(pilot) ??
derivedEntityResolutionInferenceLine(pilot);
const inferenceLines = derivedInferenceLine
? [derivedInferenceLine]
: pilot.evidence.inferred_facts;
const derivedMetadataLine = derivedMetadataConfirmedLine(pilot);
const derivedEntityResolutionLine = derivedEntityResolutionConfirmedLine(pilot);
const derivedValueLine = derivedBidirectionalValueFlowConfirmedLine(pilot) ?? derivedValueFlowConfirmedLine(pilot);
const derivedValueLine =
derivedBidirectionalValueFlowConfirmedLine(pilot) ??
derivedRankedValueFlowConfirmedLine(pilot) ??
derivedValueFlowConfirmedLine(pilot);
const monthlyConfirmedLines =
derivedBidirectionalValueFlowMonthlyLines(pilot).length > 0
? derivedBidirectionalValueFlowMonthlyLines(pilot)

View File

@ -0,0 +1,450 @@
import type { AssistantMcpDiscoveryTurnMeaningRef } from "./assistantMcpDiscoveryPolicy";
export const ASSISTANT_MCP_DISCOVERY_DATA_NEED_GRAPH_SCHEMA_VERSION =
"assistant_data_need_graph_v1" as const;
export type AssistantMcpDiscoveryDataNeedProofExpectation =
| "schema_surface"
| "entity_grounding"
| "coverage_checked_fact"
| "bounded_inference"
| "clarification_required";
export interface AssistantMcpDiscoveryDataNeedGraphContract {
schema_version: typeof ASSISTANT_MCP_DISCOVERY_DATA_NEED_GRAPH_SCHEMA_VERSION;
policy_owner: "assistantMcpDiscoveryDataNeedGraph";
subject_candidates: string[];
business_fact_family: string | null;
action_family: string | null;
aggregation_need: string | null;
time_scope_need: string | null;
comparison_need: string | null;
ranking_need: string | null;
proof_expectation: AssistantMcpDiscoveryDataNeedProofExpectation;
clarification_gaps: string[];
decomposition_candidates: string[];
forbidden_overclaim_flags: string[];
reason_codes: string[];
}
export interface BuildAssistantMcpDiscoveryDataNeedGraphInput {
semanticDataNeed?: string | null;
rawUtterance?: string | null;
turnMeaning?: AssistantMcpDiscoveryTurnMeaningRef | null;
}
function toNonEmptyString(value: unknown): string | null {
if (value === null || value === undefined) {
return null;
}
const text = String(value).trim();
return text.length > 0 ? text : null;
}
function lower(value: unknown): string {
return String(value ?? "").trim().toLowerCase();
}
function normalizeReasonCode(value: string): string | null {
const normalized = value
.trim()
.replace(/[^\p{L}\p{N}_.:-]+/gu, "_")
.replace(/^_+|_+$/g, "")
.toLowerCase();
return normalized.length > 0 ? normalized.slice(0, 120) : null;
}
function pushReason(target: string[], value: string): void {
const normalized = normalizeReasonCode(value);
if (normalized && !target.includes(normalized)) {
target.push(normalized);
}
}
function pushUnique(target: string[], value: string | null | undefined): void {
const text = toNonEmptyString(value);
if (text && !target.includes(text)) {
target.push(text);
}
}
function businessFactFamilyFor(input: {
semanticDataNeed: string;
domain: string;
action: string;
unsupported: string;
}): string | null {
const combined = `${input.semanticDataNeed} ${input.domain} ${input.action} ${input.unsupported}`.trim();
if (combined.includes("metadata lane clarification")) {
return "schema_surface";
}
if (combined.includes("metadata")) {
return "schema_surface";
}
if (combined.includes("entity discovery") || combined.includes("entity_resolution")) {
return "entity_grounding";
}
if (combined.includes("lifecycle") || combined.includes("activity")) {
return "activity_lifecycle";
}
if (combined.includes("movement")) {
return "movement_evidence";
}
if (combined.includes("document")) {
return "document_evidence";
}
if (combined.includes("value-flow") || combined.includes("turnover") || combined.includes("payout") || combined.includes("net")) {
return "value_flow";
}
return null;
}
function aggregationNeedFor(axis: string): string | null {
if (!axis) {
return null;
}
if (axis === "month") {
return "by_month";
}
return `by_${axis}`;
}
function hasAllTimeScopeHint(rawUtterance: string): boolean {
if (!rawUtterance) {
return false;
}
return /(?:\u0437\u0430\s+\u0432\u0441[\u0435\u0451]\s+\u0432\u0440\u0435\u043c\u044f|\u0437\u0430\s+\u0432\u0435\u0441\u044c\s+\u043f\u0435\u0440\u0438\u043e\u0434|\u0437\u0430\s+\u0432\u0441\u044e\s+\u0438\u0441\u0442\u043e\u0440\u0438(?:\u044e|\u0438)|\u0437\u0430\s+\u043b\u044e\u0431\u043e\u0439\s+\u043f\u0435\u0440\u0438\u043e\u0434|for\s+all\s+time|all\s+time|entire\s+period|full\s+history|any\s+period)/iu.test(
rawUtterance
);
}
function timeScopeNeedFor(input: {
family: string | null;
explicitDateScope: string | null;
allTimeScopeHint: boolean;
}): string | null {
if (input.explicitDateScope) {
return "explicit_period";
}
if (
input.allTimeScopeHint &&
(input.family === "value_flow" || input.family === "movement_evidence" || input.family === "document_evidence")
) {
return "all_time_scope";
}
if (input.family === "value_flow" || input.family === "movement_evidence" || input.family === "document_evidence") {
return "period_required";
}
if (input.family === "activity_lifecycle") {
return "open_activity_window";
}
return null;
}
function comparisonNeedFor(action: string): string | null {
if (action === "net_value_flow") {
return "incoming_vs_outgoing";
}
return null;
}
function hasOpenScopeOneSidedValueTotalHint(rawUtterance: string, action: string): boolean {
if (!rawUtterance) {
return false;
}
if (action === "turnover") {
return /(?:\bсколько\s+(?:мы\s+)?(?:получили|получено|входящих(?:\s+денег)?|поступлений|денег\s+пришло)\b|(?:сумма|объем)\s+(?:входящих|поступлений)|поступлений\s+за\b)/iu.test(
rawUtterance
);
}
if (action === "payout") {
return /(?:\bсколько\s+(?:мы\s+)?(?:заплатили|выплатили|потратили|исходящих(?:\s+денег)?|платежей|списаний)\b|(?:сумма|объем)\s+(?:исходящих|платежей|списаний)|(?:платежей|списаний)\s+за\b)/iu.test(
rawUtterance
);
}
return false;
}
function hasOpenScopeOneSidedValueTotalHintUtf8Safe(rawUtterance: string, action: string): boolean {
if (!rawUtterance) {
return false;
}
if (action === "turnover") {
return /(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e\s+(?:\u043c\u044b\s+)?(?:\u043f\u043e\u043b\u0443\u0447\u0438\u043b\u0438|\u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043e|\u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0445(?:\s+\u0434\u0435\u043d\u0435\u0433)?|\u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d\u0438\u0439|\u0434\u0435\u043d\u0435\u0433\s+\u043f\u0440\u0438\u0448\u043b\u043e)|(?:\u0441\u0443\u043c\u043c\u0430|\u043e\u0431\u044a\u0435\u043c)\s+(?:\u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0445|\u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d\u0438\u0439)|\u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d\u0438\u0439\s+\u0437\u0430)/u.test(
rawUtterance
);
}
if (action === "payout") {
return /(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e\s+(?:\u043c\u044b\s+)?(?:\u0437\u0430\u043f\u043b\u0430\u0442\u0438\u043b\u0438|\u0432\u044b\u043f\u043b\u0430\u0442\u0438\u043b\u0438|\u043f\u043e\u0442\u0440\u0430\u0442\u0438\u043b\u0438|\u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0445(?:\s+\u0434\u0435\u043d\u0435\u0433)?|\u043f\u043b\u0430\u0442\u0435\u0436\u0435\u0439|\u0441\u043f\u0438\u0441\u0430\u043d\u0438\u0439)|(?:\u0441\u0443\u043c\u043c\u0430|\u043e\u0431\u044a\u0435\u043c)\s+(?:\u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0445|\u043f\u043b\u0430\u0442\u0435\u0436\u0435\u0439|\u0441\u043f\u0438\u0441\u0430\u043d\u0438\u0439)|(?:\u043f\u043b\u0430\u0442\u0435\u0436\u0435\u0439|\u0441\u043f\u0438\u0441\u0430\u043d\u0438\u0439)\s+\u0437\u0430)/u.test(
rawUtterance
);
}
return false;
}
function supportsOrganizationScopedOpenTotal(action: string): boolean {
return action === "turnover" || action === "payout";
}
function allowsOpenScopeWithoutSubject(input: {
family: string | null;
action: string;
organizationScope: string | null;
comparisonNeed: string | null;
rankingNeed: string | null;
oneSidedOpenScopeTotalHint: boolean;
}): boolean {
if (input.family !== "value_flow") {
return false;
}
if (input.rankingNeed || input.comparisonNeed === "incoming_vs_outgoing") {
return true;
}
return Boolean(
supportsOrganizationScopedOpenTotal(input.action) && (input.organizationScope || input.oneSidedOpenScopeTotalHint)
);
}
function rankingNeedFromRawUtterance(value: string): string | null {
const text = lower(value);
if (!text) {
return null;
}
if (
/(?:\btop[-\s]?\d+\b|\btop\b|топ[-\s]?\d+|топ\b|сам(?:ый|ая|ое|ые)\b|больше\s+всего|наибол[её]е|highest|largest|most)/iu.test(
text
)
) {
return "top_desc";
}
if (/(?:меньше\s+всего|наимен[ьш]е|lowest|smallest|least)/iu.test(text)) {
return "bottom_asc";
}
return null;
}
function proofExpectationFor(input: {
family: string | null;
clarificationGaps: string[];
}): AssistantMcpDiscoveryDataNeedProofExpectation {
if (input.clarificationGaps.length > 0) {
return "clarification_required";
}
if (input.family === "schema_surface") {
return "schema_surface";
}
if (input.family === "entity_grounding") {
return "entity_grounding";
}
if (input.family === "activity_lifecycle") {
return "bounded_inference";
}
return "coverage_checked_fact";
}
function decompositionCandidatesFor(input: {
family: string | null;
action: string;
aggregationNeed: string | null;
comparisonNeed: string | null;
rankingNeed: string | null;
openScopeWithoutSubject: boolean;
}): string[] {
const result: string[] = [];
if (input.family === "schema_surface") {
pushUnique(result, "inspect_metadata_surface");
return result;
}
if (input.family === "entity_grounding") {
pushUnique(result, "search_business_entity");
pushUnique(result, "resolve_entity_reference");
pushUnique(result, "probe_coverage");
return result;
}
if (input.family === "value_flow") {
if (input.rankingNeed && input.openScopeWithoutSubject) {
pushUnique(result, "collect_scoped_movements");
pushUnique(result, "aggregate_ranked_axis_values");
pushUnique(result, "probe_coverage");
return result;
}
if (input.comparisonNeed === "incoming_vs_outgoing" && input.openScopeWithoutSubject) {
pushUnique(result, "collect_incoming_movements");
pushUnique(result, "collect_outgoing_movements");
if (input.aggregationNeed === "by_month") {
pushUnique(result, "aggregate_by_month");
}
pushUnique(result, "probe_coverage");
return result;
}
if (input.openScopeWithoutSubject) {
pushUnique(result, "collect_scoped_movements");
pushUnique(result, input.aggregationNeed === "by_month" ? "aggregate_by_month" : "aggregate_checked_amounts");
pushUnique(result, "probe_coverage");
return result;
}
pushUnique(result, "resolve_entity_reference");
if (input.action === "net_value_flow") {
pushUnique(result, "collect_incoming_movements");
pushUnique(result, "collect_outgoing_movements");
} else {
pushUnique(result, "collect_scoped_movements");
}
pushUnique(result, input.aggregationNeed === "by_month" ? "aggregate_by_month" : "aggregate_checked_amounts");
pushUnique(result, "probe_coverage");
return result;
}
if (input.family === "movement_evidence") {
pushUnique(result, "resolve_entity_reference");
pushUnique(result, "fetch_scoped_movements");
pushUnique(result, "probe_coverage");
return result;
}
if (input.family === "document_evidence") {
pushUnique(result, "resolve_entity_reference");
pushUnique(result, "fetch_scoped_documents");
pushUnique(result, "probe_coverage");
return result;
}
if (input.family === "activity_lifecycle") {
pushUnique(result, "resolve_entity_reference");
pushUnique(result, "fetch_supporting_documents");
pushUnique(result, "probe_coverage");
pushUnique(result, "explain_evidence_basis");
}
return result;
}
function forbiddenOverclaimFlagsFor(family: string | null): string[] {
const result: string[] = ["no_raw_model_claims"];
if (family === "schema_surface") {
pushUnique(result, "no_fake_schema_surface");
}
if (family === "entity_grounding") {
pushUnique(result, "no_unresolved_entity_claim");
}
if (family === "activity_lifecycle") {
pushUnique(result, "no_legal_age_claim_without_evidence");
}
if (family === "value_flow" || family === "movement_evidence" || family === "document_evidence") {
pushUnique(result, "no_unchecked_fact_totals");
}
return result;
}
export function buildAssistantMcpDiscoveryDataNeedGraph(
input: BuildAssistantMcpDiscoveryDataNeedGraphInput
): AssistantMcpDiscoveryDataNeedGraphContract {
const semanticDataNeed = lower(input.semanticDataNeed);
const turnMeaning = input.turnMeaning ?? null;
const domain = lower(turnMeaning?.asked_domain_family);
const action = lower(turnMeaning?.asked_action_family);
const unsupported = lower(turnMeaning?.unsupported_but_understood_family);
const rawUtterance = lower(input.rawUtterance);
const aggregationAxis = lower(turnMeaning?.asked_aggregation_axis);
const seededRankingNeed = toNonEmptyString(turnMeaning?.seeded_ranking_need);
const explicitDateScope = toNonEmptyString(turnMeaning?.explicit_date_scope);
const explicitOrganizationScope = toNonEmptyString(turnMeaning?.explicit_organization_scope);
const subjectCandidates = (turnMeaning?.explicit_entity_candidates ?? [])
.map((item) => toNonEmptyString(item))
.filter((item): item is string => Boolean(item));
const businessFactFamily = businessFactFamilyFor({
semanticDataNeed,
domain,
action,
unsupported
});
const aggregationNeed = aggregationNeedFor(aggregationAxis);
const comparisonNeed = comparisonNeedFor(action);
const rankingNeed = rankingNeedFromRawUtterance(rawUtterance) ?? seededRankingNeed;
const allTimeScopeHint = hasAllTimeScopeHint(rawUtterance);
const oneSidedOpenScopeTotalHint = hasOpenScopeOneSidedValueTotalHintUtf8Safe(rawUtterance, action);
const openScopeWithoutSubject =
subjectCandidates.length === 0 &&
allowsOpenScopeWithoutSubject({
family: businessFactFamily,
action,
organizationScope: explicitOrganizationScope,
comparisonNeed,
rankingNeed,
oneSidedOpenScopeTotalHint
});
const clarificationGaps: string[] = [];
if (unsupported === "metadata_lane_choice_clarification" || action === "resolve_next_lane") {
pushUnique(clarificationGaps, "lane_family_choice");
}
if (
subjectCandidates.length === 0 &&
businessFactFamily === "value_flow" &&
openScopeWithoutSubject &&
!explicitOrganizationScope
) {
pushUnique(clarificationGaps, "organization");
} else if (subjectCandidates.length === 0 && businessFactFamily !== "schema_surface" && !openScopeWithoutSubject) {
pushUnique(clarificationGaps, "subject");
}
const timeScopeNeed = timeScopeNeedFor({
family: businessFactFamily,
explicitDateScope,
allTimeScopeHint
});
if (timeScopeNeed === "period_required" && !explicitDateScope) {
pushUnique(clarificationGaps, "period");
}
const decompositionCandidates = decompositionCandidatesFor({
family: businessFactFamily,
action,
aggregationNeed,
comparisonNeed,
rankingNeed,
openScopeWithoutSubject
});
const reasonCodes: string[] = [];
pushReason(reasonCodes, "data_need_graph_built");
if (businessFactFamily) {
pushReason(reasonCodes, `data_need_graph_family_${businessFactFamily}`);
} else {
pushReason(reasonCodes, "data_need_graph_family_unknown");
}
if (aggregationNeed) {
pushReason(reasonCodes, `data_need_graph_aggregation_${aggregationNeed}`);
}
if (rankingNeed) {
pushReason(reasonCodes, `data_need_graph_ranking_${rankingNeed}`);
}
if (comparisonNeed) {
pushReason(reasonCodes, `data_need_graph_comparison_${comparisonNeed}`);
}
if (openScopeWithoutSubject && !rankingNeed && !comparisonNeed) {
pushReason(reasonCodes, "data_need_graph_open_scope_total_without_subject");
}
if (allTimeScopeHint) {
pushReason(reasonCodes, "data_need_graph_all_time_scope_hint");
}
if (clarificationGaps.includes("organization")) {
pushReason(reasonCodes, "data_need_graph_open_scope_total_needs_organization");
}
if (clarificationGaps.length > 0) {
pushReason(reasonCodes, "data_need_graph_has_clarification_gaps");
}
return {
schema_version: ASSISTANT_MCP_DISCOVERY_DATA_NEED_GRAPH_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: subjectCandidates,
business_fact_family: businessFactFamily,
action_family: toNonEmptyString(turnMeaning?.asked_action_family),
aggregation_need: aggregationNeed,
time_scope_need: timeScopeNeed,
comparison_need: comparisonNeed,
ranking_need: rankingNeed,
proof_expectation: proofExpectationFor({
family: businessFactFamily,
clarificationGaps
}),
clarification_gaps: clarificationGaps,
decomposition_candidates: decompositionCandidates,
forbidden_overclaim_flags: forbiddenOverclaimFlagsFor(businessFactFamily),
reason_codes: reasonCodes
};
}

View File

@ -78,6 +78,28 @@ export interface AssistantMcpDiscoveryDerivedValueFlow {
inference_basis: "sum_of_confirmed_1c_value_flow_rows";
}
export interface AssistantMcpDiscoveryRankedValueFlowBucket {
axis_value: string;
rows_with_amount: number;
total_amount: number;
total_amount_human_ru: string;
}
export interface AssistantMcpDiscoveryDerivedRankedValueFlow {
value_flow_direction: "incoming_customer_revenue" | "outgoing_supplier_payout";
ranking_need: "top_desc" | "bottom_asc";
ranking_axis: "counterparty";
organization_scope: string | null;
period_scope: string | null;
rows_matched: number;
rows_with_amount: number;
ranked_values: AssistantMcpDiscoveryRankedValueFlowBucket[];
coverage_limited_by_probe_limit: boolean;
coverage_recovered_by_period_chunking: boolean;
period_chunking_granularity: AssistantMcpDiscoveryAggregationAxis | null;
inference_basis: "ranked_counterparty_totals_from_confirmed_1c_value_flow_rows";
}
export interface AssistantMcpDiscoveryValueFlowSideSummary {
rows_matched: number;
rows_with_amount: number;
@ -127,10 +149,17 @@ export interface AssistantMcpDiscoveryDerivedMetadataSurface {
matched_objects: string[];
selected_entity_set: string | null;
selected_surface_objects: string[];
surface_family_scores: {
document_evidence: number;
movement_evidence: number;
catalog_drilldown: number;
};
downstream_route_family: "document_evidence" | "movement_evidence" | "catalog_drilldown" | null;
route_family_selection_basis: "selected_entity_set" | "dominant_surface_objects" | null;
recommended_next_primitive: "query_documents" | "query_movements" | "drilldown_related_objects" | null;
ambiguity_detected: boolean;
ambiguity_entity_sets: string[];
surface_object_ranking_applied?: boolean;
available_fields: string[];
known_limitations: string[];
inference_basis: "confirmed_1c_metadata_surface_rows";
@ -187,6 +216,7 @@ export interface AssistantMcpDiscoveryPilotExecutionContract {
derived_metadata_surface: AssistantMcpDiscoveryDerivedMetadataSurface | null;
derived_entity_resolution: AssistantMcpDiscoveryDerivedEntityResolution | null;
derived_activity_period: AssistantMcpDiscoveryDerivedActivityPeriod | null;
derived_ranked_value_flow?: AssistantMcpDiscoveryDerivedRankedValueFlow | null;
derived_value_flow: AssistantMcpDiscoveryDerivedValueFlow | null;
derived_bidirectional_value_flow: AssistantMcpDiscoveryDerivedBidirectionalValueFlow | null;
query_limitations: string[];
@ -334,6 +364,20 @@ function buildValueFlowFilters(planner: AssistantMcpDiscoveryPlannerContract): A
};
}
function organizationScopeForPlanner(planner: AssistantMcpDiscoveryPlannerContract): string | null {
return toNonEmptyString(planner.discovery_plan.turn_meaning_ref?.explicit_organization_scope);
}
function rankingNeedForPlanner(
planner: AssistantMcpDiscoveryPlannerContract
): AssistantMcpDiscoveryDerivedRankedValueFlow["ranking_need"] | null {
const rankingNeed = toNonEmptyString(planner.data_need_graph?.ranking_need)?.toLowerCase();
if (rankingNeed === "top_desc" || rankingNeed === "bottom_asc") {
return rankingNeed;
}
return null;
}
function normalizeEntityResolutionText(value: string | null): string {
return String(value ?? "")
.toLowerCase()
@ -544,7 +588,11 @@ function isMovementEvidencePilotEligible(planner: AssistantMcpDiscoveryPlannerCo
}
function isValueFlowPilotEligible(planner: AssistantMcpDiscoveryPlannerContract): boolean {
if (planner.selected_chain_id === "value_flow") {
if (
planner.selected_chain_id === "value_flow" ||
planner.selected_chain_id === "value_flow_ranking" ||
planner.selected_chain_id === "value_flow_comparison"
) {
return true;
}
const meaning = planner.discovery_plan.turn_meaning_ref;
@ -565,7 +613,8 @@ function isValueFlowPilotEligible(planner: AssistantMcpDiscoveryPlannerContract)
function isMetadataPilotEligible(planner: AssistantMcpDiscoveryPlannerContract): boolean {
if (
planner.selected_chain_id === "metadata_inspection" ||
planner.selected_chain_id === "metadata_lane_clarification"
planner.selected_chain_id === "metadata_lane_clarification" ||
planner.selected_chain_id === "catalog_drilldown"
) {
return true;
}
@ -610,6 +659,23 @@ function metadataScopeForPlanner(planner: AssistantMcpDiscoveryPlannerContract):
if (entityCandidate) {
return entityCandidate;
}
if (planner.selected_chain_id === "catalog_drilldown") {
const surface = planner.metadata_surface_ref;
const scopeCandidate = [
...(surface?.selected_surface_objects ?? []),
surface?.selected_entity_set ?? ""
]
.map((value) => toNonEmptyString(value))
.filter((value): value is string => Boolean(value))
.map((value) => {
const parts = value.split(".").map((item) => item.trim()).filter((item) => item.length > 0);
return parts.length > 0 ? parts[parts.length - 1] ?? value : value;
})
.find((value) => value.length > 0);
if (scopeCandidate) {
return scopeCandidate;
}
}
const meaning = planner.discovery_plan.turn_meaning_ref;
const combined = `${meaning?.asked_domain_family ?? ""} ${meaning?.asked_action_family ?? ""} ${meaning?.unsupported_but_understood_family ?? ""}`
.toLowerCase()
@ -627,6 +693,12 @@ function metadataScopeForPlanner(planner: AssistantMcpDiscoveryPlannerContract):
}
function metadataTypesForPlanner(planner: AssistantMcpDiscoveryPlannerContract): string[] {
if (planner.selected_chain_id === "catalog_drilldown") {
const selectedEntitySet = toNonEmptyString(planner.metadata_surface_ref?.selected_entity_set);
if (selectedEntitySet) {
return [selectedEntitySet];
}
}
const meaning = planner.discovery_plan.turn_meaning_ref;
const action = String(meaning?.asked_action_family ?? "").toLowerCase();
if (action === "inspect_registers") {
@ -641,6 +713,11 @@ function metadataTypesForPlanner(planner: AssistantMcpDiscoveryPlannerContract):
return ["Документ", "РегистрНакопления", "РегистрСведений", "Справочник"];
}
function metadataScopeRankingAllowedForPlanner(planner: AssistantMcpDiscoveryPlannerContract): boolean {
const action = String(planner.discovery_plan.turn_meaning_ref?.asked_action_family ?? "").toLowerCase().trim();
return action === "inspect_surface";
}
interface ValueFlowPilotProfile {
scope: Extract<
AssistantMcpDiscoveryPilotScope,
@ -1152,6 +1229,34 @@ function metadataRouteFamilyForEntitySet(
return null;
}
function metadataRouteFamilyForEntitySetRelaxed(
entitySet: string
): "document_evidence" | "movement_evidence" | "catalog_drilldown" | null {
const strict = metadataRouteFamilyForEntitySet(entitySet);
if (strict) {
return strict;
}
const raw = String(entitySet ?? "").trim();
if (!raw) {
return null;
}
if (raw.includes("Документ") || raw.includes("Документ")) {
return "document_evidence";
}
if (
raw.includes("РегистрНакопления") ||
raw.includes("РегистрСведений") ||
raw.includes("РегистрНакопления") ||
raw.includes("РегистрСведений")
) {
return "movement_evidence";
}
if (raw.includes("Справочник") || raw.includes("Справочник")) {
return "catalog_drilldown";
}
return null;
}
function metadataNextPrimitiveForRouteFamily(
routeFamily: "document_evidence" | "movement_evidence" | "catalog_drilldown" | null
): "query_documents" | "query_movements" | "drilldown_related_objects" | null {
@ -1213,10 +1318,177 @@ function metadataObjectsForEntitySet(entitySet: string | null, matchedObjects: s
return matchedObjects.filter((item) => item.startsWith(`${entitySet}.`) || item.includes(entitySet));
}
function emptyMetadataSurfaceFamilyScores(): AssistantMcpDiscoveryDerivedMetadataSurface["surface_family_scores"] {
return {
document_evidence: 0,
movement_evidence: 0,
catalog_drilldown: 0
};
}
function metadataSurfaceFamilyScores(
matchedObjects: string[]
): AssistantMcpDiscoveryDerivedMetadataSurface["surface_family_scores"] {
const scores = emptyMetadataSurfaceFamilyScores();
for (const objectName of matchedObjects) {
const entitySet = inferMetadataEntitySetFromObjectName(objectName);
const routeFamily = entitySet ? metadataRouteFamilyForEntitySetRelaxed(entitySet) : null;
if (routeFamily) {
scores[routeFamily] += 1;
}
}
return scores;
}
function normalizeMetadataObjectRankingToken(value: string): string {
return String(value ?? "")
.toLowerCase()
.replace(/[^\p{L}\p{N}]+/gu, "");
}
function metadataScopeRankingTokens(metadataScope: string | null): string[] {
const scope = String(metadataScope ?? "").trim();
if (!scope) {
return [];
}
const condensed = normalizeMetadataObjectRankingToken(scope);
const result: string[] = [];
if (condensed.length >= 2) {
pushUnique(result, condensed);
}
for (const token of scope.toLowerCase().split(/[^\p{L}\p{N}]+/gu)) {
const normalized = normalizeMetadataObjectRankingToken(token);
if (normalized.length >= 2) {
pushUnique(result, normalized);
}
}
return result;
}
function metadataObjectRelevanceScore(metadataScope: string | null, objectName: string): number {
const objectToken = normalizeMetadataObjectRankingToken(objectName);
if (!objectToken) {
return 1;
}
let score = 1;
for (const token of metadataScopeRankingTokens(metadataScope)) {
if (objectToken.includes(token)) {
score += token.length >= 6 ? 4 : 3;
}
}
return score;
}
function metadataWeightedSurfaceFamilyScores(
matchedObjects: string[],
metadataScope: string | null
): AssistantMcpDiscoveryDerivedMetadataSurface["surface_family_scores"] {
const scores = emptyMetadataSurfaceFamilyScores();
for (const objectName of matchedObjects) {
const entitySet = inferMetadataEntitySetFromObjectName(objectName);
const routeFamily = entitySet ? metadataRouteFamilyForEntitySetRelaxed(entitySet) : null;
if (routeFamily) {
scores[routeFamily] += metadataObjectRelevanceScore(metadataScope, objectName);
}
}
return scores;
}
function sortMetadataObjectsByRelevance(matchedObjects: string[], metadataScope: string | null): string[] {
return [...matchedObjects].sort((left, right) => {
const scoreDelta = metadataObjectRelevanceScore(metadataScope, right) - metadataObjectRelevanceScore(metadataScope, left);
if (scoreDelta !== 0) {
return scoreDelta;
}
return left.localeCompare(right, "ru");
});
}
function metadataObjectsForRouteFamily(
routeFamily: "document_evidence" | "movement_evidence" | "catalog_drilldown" | null,
matchedObjects: string[],
metadataScope: string | null
): string[] {
if (!routeFamily) {
return [];
}
const filtered = matchedObjects.filter((objectName) => {
const entitySet = inferMetadataEntitySetFromObjectName(objectName);
return entitySet ? metadataRouteFamilyForEntitySetRelaxed(entitySet) === routeFamily : false;
});
return sortMetadataObjectsByRelevance(filtered, metadataScope);
}
function selectDominantMetadataRouteFamilyFromScores(
scores: AssistantMcpDiscoveryDerivedMetadataSurface["surface_family_scores"]
): "document_evidence" | "movement_evidence" | "catalog_drilldown" | null {
const ranked = (Object.entries(scores) as Array<
["document_evidence" | "movement_evidence" | "catalog_drilldown", number]
>)
.filter(([, score]) => score > 0)
.sort((left, right) => right[1] - left[1]);
const top = ranked[0];
const second = ranked[1];
if (!top) {
return null;
}
if (!second) {
return top[0];
}
const absoluteMargin = top[1] - second[1];
const relativeRatio = second[1] > 0 ? top[1] / second[1] : Number.POSITIVE_INFINITY;
const clearlyDominant = absoluteMargin >= 2 || relativeRatio >= 1.5;
return clearlyDominant ? top[0] : null;
}
function selectMetadataRouteFamilyFromSurfaceScores(input: {
matchedObjects: string[];
metadataScope: string | null;
countScores: AssistantMcpDiscoveryDerivedMetadataSurface["surface_family_scores"];
allowScopeRanking: boolean;
}): {
routeFamily: "document_evidence" | "movement_evidence" | "catalog_drilldown" | null;
rankingApplied: boolean;
} {
const countDominant = selectDominantMetadataRouteFamilyFromScores(input.countScores);
if (countDominant) {
return {
routeFamily: countDominant,
rankingApplied: false
};
}
if (!input.allowScopeRanking) {
return {
routeFamily: null,
rankingApplied: false
};
}
const rankedCounts = (Object.entries(input.countScores) as Array<
["document_evidence" | "movement_evidence" | "catalog_drilldown", number]
>)
.filter(([, score]) => score > 0)
.sort((left, right) => right[1] - left[1]);
const topCount = rankedCounts[0]?.[1] ?? 0;
const secondCount = rankedCounts[1]?.[1] ?? 0;
if (topCount <= 0 || topCount !== secondCount) {
return {
routeFamily: null,
rankingApplied: false
};
}
const weightedScores = metadataWeightedSurfaceFamilyScores(input.matchedObjects, input.metadataScope);
const weightedDominant = selectDominantMetadataRouteFamilyFromScores(weightedScores);
return {
routeFamily: weightedDominant,
rankingApplied: Boolean(weightedDominant)
};
}
function deriveMetadataSurface(
result: AddressMcpMetadataRowsResult | null,
metadataScope: string | null,
requestedMetaTypes: string[]
requestedMetaTypes: string[],
allowScopeRanking: boolean
): AssistantMcpDiscoveryDerivedMetadataSurface | null {
if (!result || result.error || result.rows.length <= 0) {
return null;
@ -1234,15 +1506,42 @@ function deriveMetadataSurface(
}
}
const grounding = selectMetadataEntityGrounding(availableEntitySets, requestedMetaTypes);
const downstreamRouteFamily = grounding.selectedEntitySet
? metadataRouteFamilyForEntitySet(grounding.selectedEntitySet)
const surfaceFamilyScores = metadataSurfaceFamilyScores(matchedObjects);
const selectedEntitySetRouteFamily = grounding.selectedEntitySet
? metadataRouteFamilyForEntitySetRelaxed(grounding.selectedEntitySet)
: null;
const scoredRouteSelection =
selectedEntitySetRouteFamily === null
? selectMetadataRouteFamilyFromSurfaceScores({
matchedObjects,
metadataScope,
countScores: surfaceFamilyScores,
allowScopeRanking
})
: { routeFamily: null, rankingApplied: false };
const scoredRouteFamily = scoredRouteSelection.routeFamily;
const downstreamRouteFamily = selectedEntitySetRouteFamily ?? scoredRouteFamily;
const routeFamilySelectionBasis = selectedEntitySetRouteFamily
? "selected_entity_set"
: scoredRouteFamily
? "dominant_surface_objects"
: null;
const selectedSurfaceObjects =
grounding.selectedEntitySet !== null
? sortMetadataObjectsByRelevance(metadataObjectsForEntitySet(grounding.selectedEntitySet, matchedObjects), metadataScope)
: metadataObjectsForRouteFamily(downstreamRouteFamily, matchedObjects, metadataScope);
const knownLimitations: string[] = [];
if (grounding.ambiguityDetected && grounding.ambiguityEntitySets.length > 0) {
const ambiguityRemainsUnresolved = grounding.ambiguityDetected && !downstreamRouteFamily;
if (ambiguityRemainsUnresolved && grounding.ambiguityEntitySets.length > 0) {
knownLimitations.push(
`Exact downstream metadata surface remains ambiguous across: ${grounding.ambiguityEntitySets.join(", ")}`
);
}
if (grounding.ambiguityDetected && downstreamRouteFamily && routeFamilySelectionBasis === "dominant_surface_objects") {
knownLimitations.push(
`Metadata surface spans multiple object sets, but dominant confirmed objects point to ${downstreamRouteFamily}`
);
}
return {
metadata_scope: metadataScope,
requested_meta_types: requestedMetaTypes,
@ -1250,11 +1549,14 @@ function deriveMetadataSurface(
available_entity_sets: availableEntitySets,
matched_objects: matchedObjects,
selected_entity_set: grounding.selectedEntitySet,
selected_surface_objects: metadataObjectsForEntitySet(grounding.selectedEntitySet, matchedObjects),
selected_surface_objects: selectedSurfaceObjects,
surface_family_scores: surfaceFamilyScores,
downstream_route_family: downstreamRouteFamily,
route_family_selection_basis: routeFamilySelectionBasis,
recommended_next_primitive: metadataNextPrimitiveForRouteFamily(downstreamRouteFamily),
ambiguity_detected: grounding.ambiguityDetected,
ambiguity_entity_sets: grounding.ambiguityEntitySets,
ambiguity_detected: ambiguityRemainsUnresolved,
ambiguity_entity_sets: ambiguityRemainsUnresolved ? grounding.ambiguityEntitySets : [],
surface_object_ranking_applied: scoredRouteSelection.rankingApplied,
available_fields: metadataAvailableFields(result.rows),
known_limitations: knownLimitations,
inference_basis: "confirmed_1c_metadata_surface_rows"
@ -1281,6 +1583,15 @@ function buildMetadataConfirmedFacts(
if (surface.selected_surface_objects.length > 0) {
facts.push(`Selected metadata objects: ${surface.selected_surface_objects.slice(0, 8).join(", ")}`);
}
if (
surface.surface_family_scores.document_evidence > 0 ||
surface.surface_family_scores.movement_evidence > 0 ||
surface.surface_family_scores.catalog_drilldown > 0
) {
facts.push(
`Metadata surface family scores: document=${surface.surface_family_scores.document_evidence}, movement=${surface.surface_family_scores.movement_evidence}, catalog=${surface.surface_family_scores.catalog_drilldown}`
);
}
if (surface.available_fields.length > 0) {
facts.push(`Available metadata fields/sections: ${surface.available_fields.slice(0, 12).join(", ")}`);
}
@ -1290,7 +1601,7 @@ function buildMetadataConfirmedFacts(
function buildMetadataInferredFacts(
surface: AssistantMcpDiscoveryDerivedMetadataSurface | null
): string[] {
if (!surface || !surface.selected_entity_set || !surface.downstream_route_family || !surface.recommended_next_primitive) {
if (!surface || !surface.downstream_route_family || !surface.recommended_next_primitive) {
return [];
}
return [
@ -1429,6 +1740,17 @@ function rowAmountValue(row: Record<string, unknown>): number | null {
return null;
}
function rowCounterpartyValue(row: Record<string, unknown>): string | null {
const candidates = [row["Контрагент"], row["Counterparty"], row["counterparty"], row["Наименование"], row["name"]];
for (const candidate of candidates) {
const text = toNonEmptyString(candidate);
if (text) {
return text;
}
}
return null;
}
function monthBucketFromIsoDate(isoDate: string | null): string | null {
const match = isoDate?.match(/^(\d{4})-(\d{2})-\d{2}$/);
return match ? `${match[1]}-${match[2]}` : null;
@ -1629,6 +1951,74 @@ function deriveValueFlow(
};
}
function deriveRankedValueFlow(
result: AssistantMcpDiscoveryCoverageAwareQueryResult | null,
input: {
organizationScope: string | null;
periodScope: string | null;
direction: AssistantMcpDiscoveryDerivedRankedValueFlow["value_flow_direction"];
rankingNeed: AssistantMcpDiscoveryDerivedRankedValueFlow["ranking_need"];
}
): AssistantMcpDiscoveryDerivedRankedValueFlow | null {
if (!result || result.error || result.matched_rows <= 0) {
return null;
}
const buckets = new Map<string, { rows_with_amount: number; total_amount: number }>();
let rowsWithAmount = 0;
for (const row of result.rows) {
const axisValue = rowCounterpartyValue(row);
const amount = rowAmountValue(row);
if (!axisValue || amount === null) {
continue;
}
rowsWithAmount += 1;
const current = buckets.get(axisValue) ?? { rows_with_amount: 0, total_amount: 0 };
current.rows_with_amount += 1;
current.total_amount += amount;
buckets.set(axisValue, current);
}
if (rowsWithAmount <= 0 || buckets.size <= 0) {
return null;
}
const rankedValues = Array.from(buckets.entries())
.map(([axisValue, bucket]) => ({
axis_value: axisValue,
rows_with_amount: bucket.rows_with_amount,
total_amount: bucket.total_amount,
total_amount_human_ru: formatAmountHumanRu(bucket.total_amount)
}))
.sort((left, right) => {
const amountDelta = right.total_amount - left.total_amount;
if (input.rankingNeed === "bottom_asc") {
if (amountDelta !== 0) {
return -amountDelta;
}
} else if (amountDelta !== 0) {
return amountDelta;
}
return left.axis_value.localeCompare(right.axis_value, "ru");
})
.slice(0, 5);
return {
value_flow_direction: input.direction,
ranking_need: input.rankingNeed,
ranking_axis: "counterparty",
organization_scope: input.organizationScope,
period_scope: input.periodScope,
rows_matched: result.matched_rows,
rows_with_amount: rowsWithAmount,
ranked_values: rankedValues,
coverage_limited_by_probe_limit: result.coverage_limited_by_probe_limit,
coverage_recovered_by_period_chunking: result.coverage_recovered_by_period_chunking,
period_chunking_granularity: result.period_chunking_granularity,
inference_basis: "ranked_counterparty_totals_from_confirmed_1c_value_flow_rows"
};
}
function deriveValueFlowSideSummary(
result: AssistantMcpDiscoveryCoverageAwareQueryResult | null
): AssistantMcpDiscoveryValueFlowSideSummary {
@ -1798,6 +2188,18 @@ function buildValueFlowConfirmedFacts(
];
}
function buildRankedValueFlowConfirmedFacts(derived: AssistantMcpDiscoveryDerivedRankedValueFlow | null): string[] {
if (!derived || derived.ranked_values.length <= 0) {
return [];
}
const leader = derived.ranked_values[0];
const directionLabel =
derived.value_flow_direction === "outgoing_supplier_payout" ? "supplier-payout" : "incoming value-flow";
return [
`1C ${directionLabel} rows were ranked by counterparty for the checked scope; leader=${leader.axis_value}, rows_with_amount=${leader.rows_with_amount}`
];
}
function buildBidirectionalValueFlowConfirmedFacts(
derived: AssistantMcpDiscoveryDerivedBidirectionalValueFlow | null
): string[] {
@ -1880,6 +2282,19 @@ function buildValueFlowInferredFacts(derived: AssistantMcpDiscoveryDerivedValueF
return facts;
}
function buildRankedValueFlowInferredFacts(derived: AssistantMcpDiscoveryDerivedRankedValueFlow | null): string[] {
if (!derived) {
return [];
}
const facts = ["Counterparty ranking was calculated from confirmed 1C movement rows grouped by counterparty"];
if (derived.coverage_recovered_by_period_chunking && derived.period_chunking_granularity === "month") {
facts.push(
"Requested period coverage for counterparty ranking was recovered through monthly 1C probes after a broad probe hit the row limit"
);
}
return facts;
}
function buildBidirectionalValueFlowInferredFacts(
derived: AssistantMcpDiscoveryDerivedBidirectionalValueFlow | null
): string[] {
@ -1939,6 +2354,22 @@ function buildValueFlowUnknownFacts(
return unknownFacts;
}
function buildRankedValueFlowUnknownFacts(
periodScope: string | null,
derived: AssistantMcpDiscoveryDerivedRankedValueFlow | null
): string[] {
const unknownFacts: string[] = [];
if (derived?.coverage_limited_by_probe_limit) {
unknownFacts.push("Complete requested-period ranking coverage is not proven because the MCP discovery probe row limit was reached");
}
unknownFacts.push(
periodScope
? "Full ranking outside the checked period is not proven by this MCP discovery pilot"
: "Full all-time counterparty ranking is not proven without an explicit checked period"
);
return unknownFacts;
}
function buildBidirectionalValueFlowUnknownFacts(
periodScope: string | null,
derived: AssistantMcpDiscoveryDerivedBidirectionalValueFlow | null
@ -1974,11 +2405,14 @@ function buildEmptyEvidence(
function pilotScopeForPlanner(planner: AssistantMcpDiscoveryPlannerContract): AssistantMcpDiscoveryPilotScope {
switch (planner.selected_chain_id) {
case "catalog_drilldown":
case "metadata_lane_clarification":
case "metadata_inspection":
return "metadata_inspection_v1";
case "movement_evidence":
return "counterparty_movement_evidence_query_movements_v1";
case "value_flow_comparison":
case "value_flow_ranking":
case "value_flow":
return valueFlowPilotProfile(planner).scope;
case "document_evidence":
@ -2107,12 +2541,17 @@ export async function executeAssistantMcpDiscoveryPilot(
const counterparty = firstEntityCandidate(planner);
const dateScope = toNonEmptyString(planner.discovery_plan.turn_meaning_ref?.explicit_date_scope);
const organizationScope = organizationScopeForPlanner(planner);
const aggregationAxis = aggregationAxisForPlanner(planner);
const rankingNeed = rankingNeedForPlanner(planner);
if (metadataPilotEligible) {
let metadataResult: AddressMcpMetadataRowsResult | null = null;
const metadataScope = metadataScopeForPlanner(planner);
const requestedMetaTypes = metadataTypesForPlanner(planner);
if (planner.selected_chain_id === "catalog_drilldown" && metadataScope) {
pushReason(reasonCodes, "pilot_catalog_drilldown_metadata_scope_seeded_from_surface_ref");
}
for (const step of dryRun.execution_steps) {
if (step.primitive_id !== "inspect_1c_metadata") {
@ -2136,9 +2575,20 @@ export async function executeAssistantMcpDiscoveryPilot(
}
const sourceRowsSummary = metadataResult ? summarizeMetadataRows(metadataResult) : null;
const derivedMetadataSurface = deriveMetadataSurface(metadataResult, metadataScope, requestedMetaTypes);
const derivedMetadataSurface = deriveMetadataSurface(
metadataResult,
metadataScope,
requestedMetaTypes,
metadataScopeRankingAllowedForPlanner(planner)
);
if (derivedMetadataSurface) {
pushReason(reasonCodes, "pilot_derived_metadata_surface_from_confirmed_rows");
if (derivedMetadataSurface.route_family_selection_basis === "dominant_surface_objects") {
pushReason(reasonCodes, "pilot_selected_metadata_route_family_from_dominant_surface_objects");
}
if (derivedMetadataSurface.surface_object_ranking_applied) {
pushReason(reasonCodes, "pilot_selected_metadata_route_family_from_surface_object_ranking");
}
}
const evidence = resolveAssistantMcpDiscoveryEvidence({
plan: planner.discovery_plan,
@ -2694,6 +3144,50 @@ export async function executeAssistantMcpDiscoveryPilot(
}
const sourceRowsSummary = queryResult ? summarizeValueFlowRows(queryResult) : null;
if (planner.selected_chain_id === "value_flow_ranking" && rankingNeed) {
const derivedRankedValueFlow = deriveRankedValueFlow(queryResult, {
organizationScope,
periodScope: dateScope,
direction: valueFlowProfile.direction,
rankingNeed
});
if (derivedRankedValueFlow) {
pushReason(reasonCodes, "pilot_derived_ranked_value_flow_from_confirmed_rows");
}
const evidence = resolveAssistantMcpDiscoveryEvidence({
plan: planner.discovery_plan,
probeResults,
confirmedFacts: buildRankedValueFlowConfirmedFacts(derivedRankedValueFlow),
inferredFacts: buildRankedValueFlowInferredFacts(derivedRankedValueFlow),
unknownFacts: buildRankedValueFlowUnknownFacts(dateScope, derivedRankedValueFlow),
sourceRowsSummary,
queryLimitations,
recommendedNextProbe: "explain_evidence_basis"
});
return {
schema_version: ASSISTANT_MCP_DISCOVERY_PILOT_EXECUTOR_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryPilotExecutor",
pilot_status: "executed",
pilot_scope: valueFlowProfile.scope,
dry_run: dryRun,
mcp_execution_performed: executedPrimitives.length > 0,
executed_primitives: executedPrimitives,
skipped_primitives: skippedPrimitives,
probe_results: probeResults,
evidence,
source_rows_summary: sourceRowsSummary,
derived_metadata_surface: null,
derived_entity_resolution: null,
derived_activity_period: null,
derived_ranked_value_flow: derivedRankedValueFlow,
derived_value_flow: null,
derived_bidirectional_value_flow: null,
query_limitations: queryLimitations,
reason_codes: reasonCodes
};
}
const derivedValueFlow = deriveValueFlow(
queryResult,
counterparty,
@ -2733,6 +3227,7 @@ export async function executeAssistantMcpDiscoveryPilot(
derived_metadata_surface: null,
derived_entity_resolution: null,
derived_activity_period: null,
derived_ranked_value_flow: null,
derived_value_flow: derivedValueFlow,
derived_bidirectional_value_flow: null,
query_limitations: queryLimitations,

View File

@ -5,18 +5,43 @@ import {
type AssistantMcpDiscoveryTurnMeaningRef
} from "./assistantMcpDiscoveryPolicy";
import {
searchAssistantMcpCatalogPrimitivesByDecompositionCandidates,
searchAssistantMcpCatalogPrimitivesByFactAxis,
searchAssistantMcpCatalogPrimitivesByMetadataSurface,
reviewAssistantMcpDiscoveryPlanAgainstCatalog,
type AssistantMcpCatalogPlanReview
} from "./assistantMcpCatalogIndex";
import type { AssistantMcpDiscoveryDataNeedGraphContract } from "./assistantMcpDiscoveryDataNeedGraph";
export const ASSISTANT_MCP_DISCOVERY_PLANNER_SCHEMA_VERSION = "assistant_mcp_discovery_planner_v1" as const;
export type AssistantMcpDiscoveryPlannerStatus = "ready_for_execution" | "needs_clarification" | "blocked";
export type AssistantMcpDiscoveryMetadataRouteFamily =
| "document_evidence"
| "movement_evidence"
| "catalog_drilldown";
export type AssistantMcpDiscoveryMetadataRecommendedPrimitive =
| "query_documents"
| "query_movements"
| "drilldown_related_objects";
export interface AssistantMcpDiscoveryMetadataSurfaceRef {
selected_entity_set: string | null;
selected_surface_objects: string[];
downstream_route_family: AssistantMcpDiscoveryMetadataRouteFamily | null;
route_family_selection_basis: "selected_entity_set" | "dominant_surface_objects" | null;
recommended_next_primitive: AssistantMcpDiscoveryMetadataRecommendedPrimitive | null;
ambiguity_detected: boolean;
ambiguity_entity_sets: string[];
}
export type AssistantMcpDiscoveryChainId =
| "metadata_inspection"
| "catalog_drilldown"
| "metadata_lane_clarification"
| "value_flow"
| "value_flow_comparison"
| "value_flow_ranking"
| "lifecycle"
| "movement_evidence"
| "document_evidence"
@ -24,6 +49,8 @@ export type AssistantMcpDiscoveryChainId =
export interface AssistantMcpDiscoveryPlannerInput {
semanticDataNeed?: string | null;
dataNeedGraph?: AssistantMcpDiscoveryDataNeedGraphContract | null;
metadataSurface?: AssistantMcpDiscoveryMetadataSurfaceRef | null;
turnMeaning?: AssistantMcpDiscoveryTurnMeaningRef | null;
}
@ -32,6 +59,8 @@ export interface AssistantMcpDiscoveryPlannerContract {
policy_owner: "assistantMcpDiscoveryPlanner";
planner_status: AssistantMcpDiscoveryPlannerStatus;
semantic_data_need: string | null;
data_need_graph: AssistantMcpDiscoveryDataNeedGraphContract | null;
metadata_surface_ref: AssistantMcpDiscoveryMetadataSurfaceRef | null;
selected_chain_id: AssistantMcpDiscoveryChainId;
selected_chain_summary: string;
proposed_primitives: AssistantMcpDiscoveryPrimitive[];
@ -48,6 +77,7 @@ interface PlannerRecipe {
primitives: AssistantMcpDiscoveryPrimitive[];
axes: string[];
reason: string;
extraReasons?: string[];
}
interface PlannerBudgetOverride {
@ -93,6 +123,17 @@ function hasEntity(meaning: AssistantMcpDiscoveryTurnMeaningRef | null | undefin
return (meaning?.explicit_entity_candidates?.length ?? 0) > 0;
}
function hasSubjectCandidates(graph: AssistantMcpDiscoveryDataNeedGraphContract | null | undefined): boolean {
return (graph?.subject_candidates.length ?? 0) > 0;
}
function hasReasonCode(
graph: AssistantMcpDiscoveryDataNeedGraphContract | null | undefined,
reasonCode: string
): boolean {
return (graph?.reason_codes ?? []).includes(reasonCode);
}
function aggregationAxis(meaning: AssistantMcpDiscoveryTurnMeaningRef | null | undefined): string | null {
return toNonEmptyString(meaning?.asked_aggregation_axis)?.toLowerCase() ?? null;
}
@ -117,6 +158,188 @@ function isYearDateScope(meaning: AssistantMcpDiscoveryTurnMeaningRef | null | u
return /^\d{4}$/.test(toNonEmptyString(meaning?.explicit_date_scope) ?? "");
}
function mergeCatalogPrimitivesWithFallback(
catalogPrimitives: AssistantMcpDiscoveryPrimitive[],
fallbackPrimitives: AssistantMcpDiscoveryPrimitive[]
): AssistantMcpDiscoveryPrimitive[] {
const result: AssistantMcpDiscoveryPrimitive[] = [];
for (const primitive of fallbackPrimitives) {
if (catalogPrimitives.includes(primitive) && !result.includes(primitive)) {
result.push(primitive);
}
}
for (const primitive of catalogPrimitives) {
if (!result.includes(primitive)) {
result.push(primitive);
}
}
for (const primitive of fallbackPrimitives) {
if (!result.includes(primitive)) {
result.push(primitive);
}
}
return result;
}
function preferredPrimitiveFromMetadataSurface(
surface: AssistantMcpDiscoveryMetadataSurfaceRef | null | undefined
): AssistantMcpDiscoveryPrimitive | null {
const recommendedPrimitive = surface?.recommended_next_primitive ?? null;
if (recommendedPrimitive) {
return recommendedPrimitive;
}
if (surface?.ambiguity_detected) {
return null;
}
if (surface?.downstream_route_family === "document_evidence") {
return "query_documents";
}
if (surface?.downstream_route_family === "movement_evidence") {
return "query_movements";
}
if (surface?.downstream_route_family === "catalog_drilldown") {
return "drilldown_related_objects";
}
return null;
}
function filterCatalogPrimitivesByMetadataSurface(input: {
catalogPrimitives: AssistantMcpDiscoveryPrimitive[];
fallbackPrimitives: AssistantMcpDiscoveryPrimitive[];
metadataSurface?: AssistantMcpDiscoveryMetadataSurfaceRef | null;
}): { primitives: AssistantMcpDiscoveryPrimitive[]; reasonCodes: string[] } {
const preferredPrimitive = preferredPrimitiveFromMetadataSurface(input.metadataSurface);
const reasonCodes: string[] = [];
if (
!preferredPrimitive ||
input.metadataSurface?.ambiguity_detected ||
!input.fallbackPrimitives.includes(preferredPrimitive)
) {
return {
primitives: input.catalogPrimitives,
reasonCodes
};
}
const laneSensitivePrimitives = new Set<AssistantMcpDiscoveryPrimitive>([
"query_documents",
"query_movements",
"drilldown_related_objects"
]);
const filteredPrimitives = input.catalogPrimitives.filter(
(primitive) => !laneSensitivePrimitives.has(primitive) || primitive === preferredPrimitive
);
if (filteredPrimitives.length !== input.catalogPrimitives.length) {
reasonCodes.push("planner_filtered_catalog_primitives_by_confirmed_metadata_surface");
}
if (
(filteredPrimitives.includes(preferredPrimitive) || input.fallbackPrimitives.includes(preferredPrimitive)) &&
input.metadataSurface?.selected_surface_objects.length
) {
reasonCodes.push("planner_surface_aware_next_lane_from_confirmed_metadata_objects");
}
return {
primitives: filteredPrimitives,
reasonCodes
};
}
function selectPrimitivesFromGraphAndCatalog(input: {
dataNeedGraph: AssistantMcpDiscoveryDataNeedGraphContract | null;
fallbackPrimitives: AssistantMcpDiscoveryPrimitive[];
requiredAxes: string[];
metadataSurface?: AssistantMcpDiscoveryMetadataSurfaceRef | null;
actionFamily?: string | null;
allowAggregateByAxis?: boolean;
}): { primitives: AssistantMcpDiscoveryPrimitive[]; reasonCodes: string[] } {
const reasonCodes: string[] = [];
const decompositionCandidates = input.dataNeedGraph?.decomposition_candidates ?? [];
const decompositionPrimitives =
decompositionCandidates.length > 0
? searchAssistantMcpCatalogPrimitivesByDecompositionCandidates({
decomposition_candidates: decompositionCandidates,
allow_aggregate_by_axis: input.allowAggregateByAxis
})
: [];
if (decompositionPrimitives.length > 0) {
reasonCodes.push("planner_selected_catalog_primitives_from_decomposition_candidates");
}
const metadataSurfacePrimitives = input.metadataSurface
? searchAssistantMcpCatalogPrimitivesByMetadataSurface({
downstream_route_family: input.metadataSurface.downstream_route_family,
selected_entity_set: input.metadataSurface.selected_entity_set,
selected_surface_objects: input.metadataSurface.selected_surface_objects,
recommended_next_primitive: input.metadataSurface.recommended_next_primitive,
required_axes: input.requiredAxes,
allow_aggregate_by_axis: input.allowAggregateByAxis
})
: [];
if (metadataSurfacePrimitives.length > 0) {
reasonCodes.push("planner_selected_catalog_primitives_from_metadata_surface_search");
}
const factAxisPrimitives = input.dataNeedGraph
? searchAssistantMcpCatalogPrimitivesByFactAxis({
business_fact_family: input.dataNeedGraph.business_fact_family,
action_family: input.actionFamily ?? input.dataNeedGraph.action_family,
required_axes: input.requiredAxes,
comparison_need: input.dataNeedGraph.comparison_need,
ranking_need: input.dataNeedGraph.ranking_need,
aggregation_need: input.dataNeedGraph.aggregation_need,
has_subject_candidates: hasSubjectCandidates(input.dataNeedGraph),
allow_aggregate_by_axis: input.allowAggregateByAxis
})
: [];
if (factAxisPrimitives.length > 0) {
reasonCodes.push("planner_selected_catalog_primitives_from_fact_axis_search");
}
const combinedCatalogPrimitives: AssistantMcpDiscoveryPrimitive[] = [];
for (const primitive of decompositionPrimitives) {
if (!combinedCatalogPrimitives.includes(primitive)) {
combinedCatalogPrimitives.push(primitive);
}
}
for (const primitive of metadataSurfacePrimitives) {
if (!combinedCatalogPrimitives.includes(primitive)) {
combinedCatalogPrimitives.push(primitive);
}
}
for (const primitive of factAxisPrimitives) {
if (!combinedCatalogPrimitives.includes(primitive)) {
combinedCatalogPrimitives.push(primitive);
}
}
const filteredCatalogPrimitives = filterCatalogPrimitivesByMetadataSurface({
catalogPrimitives: combinedCatalogPrimitives,
fallbackPrimitives: input.fallbackPrimitives,
metadataSurface: input.metadataSurface
});
reasonCodes.push(...filteredCatalogPrimitives.reasonCodes);
if (filteredCatalogPrimitives.primitives.length <= 0) {
return {
primitives: input.fallbackPrimitives,
reasonCodes: ["planner_fell_back_to_recipe_primitives_after_empty_catalog_search"]
};
}
const mergedPrimitives = mergeCatalogPrimitivesWithFallback(
filteredCatalogPrimitives.primitives,
input.fallbackPrimitives
);
if (input.fallbackPrimitives.some((primitive) => !filteredCatalogPrimitives.primitives.includes(primitive))) {
reasonCodes.push("planner_completed_catalog_searched_chain_with_recipe_primitives");
}
return {
primitives: mergedPrimitives,
reasonCodes
};
}
function budgetOverrideFor(input: AssistantMcpDiscoveryPlannerInput, recipe: PlannerRecipe): PlannerBudgetOverride {
const meaning = input.turnMeaning ?? null;
const requestedAggregationAxis = aggregationAxis(meaning);
@ -134,16 +357,356 @@ function budgetOverrideFor(input: AssistantMcpDiscoveryPlannerInput, recipe: Pla
return {};
}
function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
function routeFamilyFromThinMetadataSurfaceInput(
input: AssistantMcpDiscoveryPlannerInput
): AssistantMcpDiscoveryMetadataRouteFamily | null {
const surface = input.metadataSurface ?? null;
if (!surface || surface.ambiguity_detected || !surface.downstream_route_family || !surface.recommended_next_primitive) {
return null;
}
const meaning = input.turnMeaning ?? null;
const dataNeedGraph = input.dataNeedGraph ?? null;
const graphFactFamily = lower(dataNeedGraph?.business_fact_family);
const domain = lower(meaning?.asked_domain_family);
const action = lower(meaning?.asked_action_family);
const unsupported = lower(meaning?.unsupported_but_understood_family);
const semanticNeed = lower(input.semanticDataNeed);
const combined = `${domain} ${action} ${unsupported} ${semanticNeed}`.trim();
const explicitlyOtherFamily =
includesAny(combined, ["value_flow", "turnover", "revenue", "payment", "payout", "net", "lifecycle", "activity", "duration", "metadata lane clarification"]);
if (explicitlyOtherFamily) {
return null;
}
if (graphFactFamily === "document_evidence" || includesAny(combined, ["document", "documents", "list_documents"])) {
return surface.downstream_route_family === "document_evidence" ? "document_evidence" : null;
}
if (graphFactFamily === "movement_evidence" || includesAny(combined, ["movement", "movements", "list_movements", "bank_operations"])) {
return surface.downstream_route_family === "movement_evidence" ? "movement_evidence" : null;
}
if (graphFactFamily === "schema_surface" || includesAny(combined, ["catalog", "directory", "inspect_catalog"])) {
return surface.downstream_route_family === "catalog_drilldown" ? "catalog_drilldown" : null;
}
if (!graphFactFamily && !domain && !action) {
if (
surface.downstream_route_family === "document_evidence" ||
surface.downstream_route_family === "movement_evidence" ||
surface.downstream_route_family === "catalog_drilldown"
) {
return surface.downstream_route_family;
}
}
return null;
}
function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
const meaning = input.turnMeaning ?? null;
const dataNeedGraph = input.dataNeedGraph ?? null;
const domain = lower(meaning?.asked_domain_family);
const action = lower(meaning?.asked_action_family);
const unsupported = lower(meaning?.unsupported_but_understood_family);
const graphFactFamily = lower(dataNeedGraph?.business_fact_family);
const graphAction = lower(dataNeedGraph?.action_family);
const graphAggregation = lower(dataNeedGraph?.aggregation_need);
const graphClarificationGaps = (dataNeedGraph?.clarification_gaps ?? []).map((item) => lower(item));
const organizationScope = toNonEmptyString(meaning?.explicit_organization_scope);
const openScopeTotalWithoutSubject =
graphFactFamily === "value_flow" &&
!hasSubjectCandidates(dataNeedGraph) &&
hasReasonCode(dataNeedGraph, "data_need_graph_open_scope_total_without_subject");
const combined = `${domain} ${action} ${unsupported}`.trim();
const axes: string[] = [];
const requestedAggregationAxis = aggregationAxis(meaning);
addScopeAxes(axes, meaning);
if (graphClarificationGaps.includes("lane_family_choice")) {
pushUnique(axes, "lane_family_choice");
return {
semanticDataNeed: "metadata lane clarification",
chainId: "metadata_lane_clarification",
chainSummary: "Preserve the ambiguous metadata surface and ask the user to choose the next data lane before running MCP probes.",
primitives: [],
axes,
reason: "planner_selected_metadata_lane_clarification_from_data_need_graph"
};
}
const thinSurfaceRouteFamily = routeFamilyFromThinMetadataSurfaceInput(input);
if (thinSurfaceRouteFamily === "document_evidence") {
pushUnique(axes, "coverage_target");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph,
fallbackPrimitives: ["resolve_entity_reference", "query_documents", "probe_coverage"],
requiredAxes: axes,
metadataSurface: input.metadataSurface,
actionFamily: action
});
return {
semanticDataNeed: "document evidence",
chainId: "document_evidence",
chainSummary:
"Ground the next checked document lane from the confirmed metadata surface, then fetch scoped document rows and probe coverage before answering.",
primitives: primitiveSelection.primitives,
axes,
reason: "planner_selected_document_from_confirmed_metadata_surface_ref",
extraReasons: primitiveSelection.reasonCodes
};
}
if (thinSurfaceRouteFamily === "movement_evidence") {
pushUnique(axes, "coverage_target");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph,
fallbackPrimitives: ["resolve_entity_reference", "query_movements", "probe_coverage"],
requiredAxes: axes,
metadataSurface: input.metadataSurface,
actionFamily: action
});
return {
semanticDataNeed: "movement evidence",
chainId: "movement_evidence",
chainSummary:
"Ground the next checked movement lane from the confirmed metadata surface, then fetch scoped movement rows and probe coverage before answering.",
primitives: primitiveSelection.primitives,
axes,
reason: "planner_selected_movement_from_confirmed_metadata_surface_ref",
extraReasons: primitiveSelection.reasonCodes
};
}
if (thinSurfaceRouteFamily === "catalog_drilldown") {
pushUnique(axes, "metadata_scope");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph,
fallbackPrimitives: ["inspect_1c_metadata"],
requiredAxes: axes,
metadataSurface: input.metadataSurface,
actionFamily: action
});
return {
semanticDataNeed: "catalog drilldown metadata evidence",
chainId: "catalog_drilldown",
chainSummary:
"Drill deeper into the confirmed catalog-oriented metadata surface, inspect related metadata objects, and keep the next safe lane grounded in checked schema evidence.",
primitives: primitiveSelection.primitives,
axes,
reason: "planner_selected_catalog_drilldown_from_confirmed_metadata_surface_ref",
extraReasons: primitiveSelection.reasonCodes
};
}
if (graphFactFamily === "value_flow") {
if (dataNeedGraph?.comparison_need === "incoming_vs_outgoing" && !hasSubjectCandidates(dataNeedGraph)) {
pushUnique(axes, "amount");
pushUnique(axes, "coverage_target");
if (requestedAggregationAxis === "month" || graphAggregation === "by_month") {
pushUnique(axes, "calendar_month");
}
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph,
fallbackPrimitives: ["query_movements", "probe_coverage"],
requiredAxes: axes,
metadataSurface: input.metadataSurface,
actionFamily: action,
allowAggregateByAxis: false
});
return {
semanticDataNeed: "bidirectional value-flow comparison evidence",
chainId: "value_flow_comparison",
chainSummary:
"Query incoming and outgoing movements for the checked period and organization, compare the checked sides, and probe coverage before answering a bounded comparison.",
primitives: primitiveSelection.primitives,
axes,
reason: "planner_selected_bidirectional_value_flow_comparison_from_data_need_graph",
extraReasons: primitiveSelection.reasonCodes
};
}
if (dataNeedGraph?.ranking_need && !hasSubjectCandidates(dataNeedGraph)) {
pushUnique(axes, "aggregate_axis");
pushUnique(axes, "amount");
pushUnique(axes, "coverage_target");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph,
fallbackPrimitives: ["query_movements", "aggregate_by_axis", "probe_coverage"],
requiredAxes: axes,
metadataSurface: input.metadataSurface,
actionFamily: action,
allowAggregateByAxis: true
});
return {
semanticDataNeed: "ranked value-flow evidence",
chainId: "value_flow_ranking",
chainSummary:
"Query scoped movements for the checked period and organization, aggregate checked amounts by counterparty, then probe coverage before answering a bounded ranking.",
primitives: primitiveSelection.primitives,
axes,
reason:
dataNeedGraph.ranking_need === "bottom_asc"
? "planner_selected_bottom_ranked_value_flow_from_data_need_graph"
: "planner_selected_top_ranked_value_flow_from_data_need_graph",
extraReasons: primitiveSelection.reasonCodes
};
}
if (openScopeTotalWithoutSubject) {
pushUnique(axes, "organization");
pushUnique(axes, "aggregate_axis");
pushUnique(axes, "amount");
pushUnique(axes, "coverage_target");
if (requestedAggregationAxis === "month" || graphAggregation === "by_month") {
pushUnique(axes, "calendar_month");
}
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph,
fallbackPrimitives: ["query_movements", "aggregate_by_axis", "probe_coverage"],
requiredAxes: axes,
actionFamily: action,
allowAggregateByAxis: true
});
return {
semanticDataNeed: "organization-scoped value-flow evidence",
chainId: "value_flow",
chainSummary:
"Query scoped movements for the checked period and organization without a preselected counterparty, aggregate checked amounts, then probe coverage before answering a bounded total.",
primitives: primitiveSelection.primitives,
axes,
reason:
requestedAggregationAxis === "month" || graphAggregation === "by_month"
? "planner_selected_monthly_open_scope_value_flow_total_from_data_need_graph"
: "planner_selected_open_scope_value_flow_total_from_data_need_graph",
extraReasons: primitiveSelection.reasonCodes
};
}
pushUnique(axes, "aggregate_axis");
pushUnique(axes, "amount");
pushUnique(axes, "coverage_target");
if (requestedAggregationAxis === "month" || graphAggregation === "by_month") {
pushUnique(axes, "calendar_month");
}
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph,
fallbackPrimitives: ["resolve_entity_reference", "query_movements", "aggregate_by_axis", "probe_coverage"],
requiredAxes: axes,
metadataSurface: input.metadataSurface,
actionFamily: action,
allowAggregateByAxis: true
});
return {
semanticDataNeed: "counterparty value-flow evidence",
chainId: "value_flow",
chainSummary: "Resolve the business entity, query scoped movements, aggregate checked amounts, then probe coverage before answering.",
primitives: primitiveSelection.primitives,
axes,
reason:
requestedAggregationAxis === "month" || graphAggregation === "by_month"
? "planner_selected_monthly_value_flow_from_data_need_graph"
: "planner_selected_value_flow_from_data_need_graph",
extraReasons: primitiveSelection.reasonCodes
};
}
if (graphFactFamily === "activity_lifecycle") {
pushUnique(axes, "document_date");
pushUnique(axes, "coverage_target");
pushUnique(axes, "evidence_basis");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph,
fallbackPrimitives: ["resolve_entity_reference", "query_documents", "probe_coverage", "explain_evidence_basis"],
requiredAxes: axes,
metadataSurface: input.metadataSurface,
actionFamily: action
});
return {
semanticDataNeed: "counterparty lifecycle evidence",
chainId: "lifecycle",
chainSummary: "Resolve the business entity, query supporting documents, probe coverage, then explain the evidence basis for the inferred activity window.",
primitives: primitiveSelection.primitives,
axes,
reason: "planner_selected_lifecycle_from_data_need_graph",
extraReasons: primitiveSelection.reasonCodes
};
}
if (graphFactFamily === "schema_surface") {
pushUnique(axes, "metadata_scope");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph,
fallbackPrimitives: ["inspect_1c_metadata"],
requiredAxes: axes,
metadataSurface: input.metadataSurface,
actionFamily: action
});
return {
semanticDataNeed: "1C metadata evidence",
chainId: "metadata_inspection",
chainSummary: "Inspect the 1C metadata surface first, then ground the next safe lane from confirmed schema evidence.",
primitives: primitiveSelection.primitives,
axes,
reason: "planner_selected_metadata_from_data_need_graph",
extraReasons: primitiveSelection.reasonCodes
};
}
if (graphFactFamily === "movement_evidence") {
pushUnique(axes, "coverage_target");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph,
fallbackPrimitives: ["resolve_entity_reference", "query_movements", "probe_coverage"],
requiredAxes: axes,
metadataSurface: input.metadataSurface,
actionFamily: action
});
return {
semanticDataNeed: "movement evidence",
chainId: "movement_evidence",
chainSummary: "Resolve the business entity, fetch scoped movement rows, and probe coverage without pretending to have a full movement universe.",
primitives: primitiveSelection.primitives,
axes,
reason: "planner_selected_movement_from_data_need_graph",
extraReasons: primitiveSelection.reasonCodes
};
}
if (graphFactFamily === "document_evidence") {
pushUnique(axes, "coverage_target");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph,
fallbackPrimitives: ["resolve_entity_reference", "query_documents", "probe_coverage"],
requiredAxes: axes,
metadataSurface: input.metadataSurface,
actionFamily: action
});
return {
semanticDataNeed: "document evidence",
chainId: "document_evidence",
chainSummary: "Resolve the business entity, fetch scoped document rows, and probe coverage before stating the checked document evidence.",
primitives: primitiveSelection.primitives,
axes,
reason: "planner_selected_document_from_data_need_graph",
extraReasons: primitiveSelection.reasonCodes
};
}
if (graphFactFamily === "entity_grounding" || (!graphFactFamily && (dataNeedGraph?.subject_candidates.length ?? 0) > 0)) {
pushUnique(axes, "business_entity");
pushUnique(axes, "coverage_target");
const primitiveSelection = selectPrimitivesFromGraphAndCatalog({
dataNeedGraph,
fallbackPrimitives: ["search_business_entity", "resolve_entity_reference", "probe_coverage"],
requiredAxes: axes,
metadataSurface: input.metadataSurface,
actionFamily: action
});
return {
semanticDataNeed: "entity discovery evidence",
chainId: "entity_resolution",
chainSummary: "Search candidate business entities, resolve the most relevant 1C reference, and prove whether the entity grounding is stable enough for the next probe.",
primitives: primitiveSelection.primitives,
axes,
reason:
graphAction === "search_business_entity"
? "planner_selected_entity_resolution_from_data_need_graph"
: "planner_selected_entity_resolution_recipe",
extraReasons: primitiveSelection.reasonCodes
};
}
if (includesAny(combined, ["metadata_lane_choice_clarification", "resolve_next_lane"])) {
pushUnique(axes, "lane_family_choice");
return {
@ -267,8 +830,19 @@ export function planAssistantMcpDiscovery(
const recipe = recipeFor(input);
const budgetOverride = budgetOverrideFor(input, recipe);
const semanticDataNeed = toNonEmptyString(input.semanticDataNeed) ?? recipe.semanticDataNeed;
const dataNeedGraph = input.dataNeedGraph ?? null;
const metadataSurface = input.metadataSurface ?? null;
const reasonCodes: string[] = [];
pushReason(reasonCodes, recipe.reason);
for (const reason of recipe.extraReasons ?? []) {
pushReason(reasonCodes, reason);
}
if (dataNeedGraph) {
pushReason(reasonCodes, "planner_consumed_data_need_graph_v1");
}
if (metadataSurface) {
pushReason(reasonCodes, "planner_consumed_metadata_surface_ref_v1");
}
if (budgetOverride.maxProbeCount) {
pushReason(reasonCodes, "planner_enabled_chunked_coverage_probe_budget");
}
@ -281,7 +855,30 @@ export function planAssistantMcpDiscovery(
maxProbeCount: budgetOverride.maxProbeCount
});
const review = reviewAssistantMcpDiscoveryPlanAgainstCatalog(plan);
const plannerStatus = statusFrom(plan, review);
const organizationClarificationRequired =
(dataNeedGraph?.clarification_gaps ?? []).includes("organization") &&
!toNonEmptyString(input.turnMeaning?.explicit_organization_scope);
const adjustedReview =
organizationClarificationRequired && recipe.primitives.includes("query_movements")
? {
...review,
review_status: "needs_more_axes" as const,
missing_axes_by_primitive: {
...review.missing_axes_by_primitive,
query_movements: review.missing_axes_by_primitive.query_movements?.length
? review.missing_axes_by_primitive.query_movements
: [["organization"]]
},
reason_codes: review.reason_codes.includes("catalog_requires_organization_scope_from_data_need_graph")
? review.reason_codes
: [...review.reason_codes, "catalog_requires_organization_scope_from_data_need_graph"]
}
: review;
const plannerStatus = organizationClarificationRequired ? "needs_clarification" : statusFrom(plan, adjustedReview);
if (organizationClarificationRequired) {
pushReason(reasonCodes, "planner_requires_organization_scope_from_data_need_graph");
}
if (plannerStatus === "ready_for_execution") {
pushReason(reasonCodes, "planner_ready_for_guarded_mcp_execution");
@ -296,12 +893,14 @@ export function planAssistantMcpDiscovery(
policy_owner: "assistantMcpDiscoveryPlanner",
planner_status: plannerStatus,
semantic_data_need: semanticDataNeed,
data_need_graph: dataNeedGraph,
metadata_surface_ref: metadataSurface,
selected_chain_id: recipe.chainId,
selected_chain_summary: recipe.chainSummary,
proposed_primitives: recipe.primitives,
required_axes: recipe.axes,
discovery_plan: plan,
catalog_review: review,
catalog_review: adjustedReview,
reason_codes: reasonCodes
};
}

View File

@ -24,6 +24,7 @@ export interface AssistantMcpDiscoveryTurnMeaningRef {
asked_domain_family?: string | null;
asked_action_family?: string | null;
asked_aggregation_axis?: string | null;
seeded_ranking_need?: string | null;
explicit_entity_candidates?: string[];
metadata_ambiguity_entity_sets?: string[];
explicit_organization_scope?: string | null;
@ -167,6 +168,7 @@ function normalizeTurnMeaning(
const domain = toNonEmptyString(value.asked_domain_family);
const action = toNonEmptyString(value.asked_action_family);
const aggregationAxis = toNonEmptyString(value.asked_aggregation_axis);
const seededRankingNeed = toNonEmptyString(value.seeded_ranking_need);
const organization = toNonEmptyString(value.explicit_organization_scope);
const dateScope = toNonEmptyString(value.explicit_date_scope);
const unsupported = toNonEmptyString(value.unsupported_but_understood_family);
@ -181,6 +183,9 @@ function normalizeTurnMeaning(
if (aggregationAxis) {
result.asked_aggregation_axis = aggregationAxis;
}
if (seededRankingNeed) {
result.seeded_ranking_need = seededRankingNeed;
}
if (entities.length > 0) {
result.explicit_entity_candidates = entities;
}

View File

@ -91,6 +91,36 @@ function userFacingLines(values: string[]): string[] {
}
function localizeLine(value: string): string {
if (/^1C activity rows were found for the requested counterparty scope$/i.test(value)) {
return "В 1С найдены строки активности в запрошенном срезе.";
}
if (/^1C value-flow rows were found for the requested counterparty scope$/i.test(value)) {
return "В 1С найдены строки входящих денежных поступлений в запрошенном срезе.";
}
if (/^1C supplier-payout rows were found for the requested counterparty scope$/i.test(value)) {
return "В 1С найдены строки исходящих платежей и списаний в запрошенном срезе.";
}
const openScopeBidirectionalMatch = value.match(
/^1C bidirectional value-flow rows were checked for the requested counterparty scope: incoming=(found|not_found), outgoing=(found|not_found)$/i
);
if (openScopeBidirectionalMatch) {
const incoming =
openScopeBidirectionalMatch[1] === "found"
? "входящие строки найдены"
: "входящие строки не найдены";
const outgoing =
openScopeBidirectionalMatch[2] === "found"
? "исходящие строки найдены"
: "исходящие строки не найдены";
return `В 1С проверены входящие и исходящие денежные строки в запрошенном срезе: ${incoming}, ${outgoing}.`;
}
if (
/^Requested period hit the MCP row limit, but the approved monthly recovery probe budget is smaller than the required subperiod count$/i.test(
value
)
) {
return "Запрошенный период уперся в лимит строк MCP; доступного бюджета помесячных дозапросов не хватило, чтобы покрыть все подпериоды.";
}
const counterpartyMatch = value.match(/^1C activity rows were found for counterparty\s+(.+)$/i);
if (counterpartyMatch) {
return `В 1С найдены строки активности по контрагенту ${counterpartyMatch[1]}.`;

View File

@ -214,6 +214,50 @@ function readDiscoveryTurnMeaning(
return toRecordObject(turnInput?.turn_meaning_ref);
}
function readDiscoveryDataNeedGraph(
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
): Record<string, unknown> | null {
const turnInput = toRecordObject(entryPoint?.turn_input);
return toRecordObject(turnInput?.data_need_graph);
}
function isOpenScopeValueFlowWithoutSubject(
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
): boolean {
const graph = readDiscoveryDataNeedGraph(entryPoint);
const businessFactFamily = toNonEmptyString(graph?.business_fact_family);
const subjectCandidates = Array.isArray(graph?.subject_candidates) ? graph.subject_candidates : [];
const reasonCodes = Array.isArray(graph?.reason_codes) ? graph.reason_codes : [];
return (
businessFactFamily === "value_flow" &&
subjectCandidates.length === 0 &&
reasonCodes.some((reason) => toNonEmptyString(reason) === "data_need_graph_open_scope_total_without_subject")
);
}
function needsOpenScopeValueFlowOrganizationClarification(
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
): boolean {
const graph = readDiscoveryDataNeedGraph(entryPoint);
const businessFactFamily = toNonEmptyString(graph?.business_fact_family);
const subjectCandidates = Array.isArray(graph?.subject_candidates) ? graph.subject_candidates : [];
const clarificationGaps = Array.isArray(graph?.clarification_gaps) ? graph.clarification_gaps : [];
return (
businessFactFamily === "value_flow" &&
subjectCandidates.length === 0 &&
clarificationGaps.some((gap) => toNonEmptyString(gap) === "organization")
);
}
function isOpenScopeValueFlowRanking(
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
): boolean {
const graph = readDiscoveryDataNeedGraph(entryPoint);
const businessFactFamily = toNonEmptyString(graph?.business_fact_family);
const subjectCandidates = Array.isArray(graph?.subject_candidates) ? graph.subject_candidates : [];
return businessFactFamily === "value_flow" && subjectCandidates.length === 0 && Boolean(toNonEmptyString(graph?.ranking_need));
}
function readTruthAnswerShape(input: ApplyAssistantMcpDiscoveryResponsePolicyInput): Record<string, unknown> | null {
const directShape = toRecordObject(input.addressRuntimeMeta?.answer_shape_contract);
if (directShape) {
@ -286,6 +330,9 @@ function hasAlignedFactualAddressReply(
if (!hasEffectivelyFactualAddressReply(input)) {
return false;
}
if (hasSemanticConflictWithDiscoveryTurnMeaning(input, entryPoint)) {
return false;
}
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
return isDetectedIntentAlignedWithTurnMeaning(detectedIntent, readDiscoveryTurnMeaning(entryPoint));
}
@ -311,6 +358,18 @@ function hasSemanticConflictWithDiscoveryTurnMeaning(
if (!detectedIntent || (!askedDomain && !askedAction && !unsupportedFamily)) {
return false;
}
if (isOpenScopeValueFlowRanking(entryPoint)) {
return true;
}
if (needsOpenScopeValueFlowOrganizationClarification(entryPoint)) {
return true;
}
if (
detectedIntent === "customer_revenue_and_payments" &&
isOpenScopeValueFlowWithoutSubject(entryPoint)
) {
return true;
}
return !isDetectedIntentAlignedWithTurnMeaning(detectedIntent, turnMeaning);
}

View File

@ -9,8 +9,10 @@ import {
} from "./assistantMcpDiscoveryPilotExecutor";
import {
planAssistantMcpDiscovery,
type AssistantMcpDiscoveryMetadataSurfaceRef,
type AssistantMcpDiscoveryPlannerContract
} from "./assistantMcpDiscoveryPlanner";
import type { AssistantMcpDiscoveryDataNeedGraphContract } from "./assistantMcpDiscoveryDataNeedGraph";
import type { AssistantMcpDiscoveryTurnMeaningRef } from "./assistantMcpDiscoveryPolicy";
export const ASSISTANT_MCP_DISCOVERY_RUNTIME_BRIDGE_SCHEMA_VERSION =
@ -25,6 +27,8 @@ export type AssistantMcpDiscoveryRuntimeBridgeStatus =
export interface AssistantMcpDiscoveryRuntimeBridgeInput {
semanticDataNeed?: string | null;
dataNeedGraph?: AssistantMcpDiscoveryDataNeedGraphContract | null;
metadataSurface?: AssistantMcpDiscoveryMetadataSurfaceRef | null;
turnMeaning?: AssistantMcpDiscoveryTurnMeaningRef | null;
deps?: AssistantMcpDiscoveryPilotExecutorDeps;
}
@ -98,6 +102,8 @@ export async function runAssistantMcpDiscoveryRuntimeBridge(
): Promise<AssistantMcpDiscoveryRuntimeBridgeContract> {
const planner = planAssistantMcpDiscovery({
semanticDataNeed: input.semanticDataNeed,
dataNeedGraph: input.dataNeedGraph,
metadataSurface: input.metadataSurface,
turnMeaning: input.turnMeaning
});
const pilot = await executeAssistantMcpDiscoveryPilot(planner, input.deps);

View File

@ -101,6 +101,8 @@ export async function runAssistantMcpDiscoveryRuntimeEntryPoint(
const bridge = await runAssistantMcpDiscoveryRuntimeBridge({
semanticDataNeed: turnInput.semantic_data_need,
dataNeedGraph: turnInput.data_need_graph,
metadataSurface: turnInput.metadata_surface_ref,
turnMeaning: turnInput.turn_meaning_ref,
deps: input.deps
});

View File

@ -1,4 +1,13 @@
import type { AssistantMcpDiscoveryTurnMeaningRef } from "./assistantMcpDiscoveryPolicy";
import {
buildAssistantMcpDiscoveryDataNeedGraph,
type AssistantMcpDiscoveryDataNeedGraphContract
} from "./assistantMcpDiscoveryDataNeedGraph";
import type {
AssistantMcpDiscoveryMetadataRecommendedPrimitive,
AssistantMcpDiscoveryMetadataRouteFamily,
AssistantMcpDiscoveryMetadataSurfaceRef
} from "./assistantMcpDiscoveryPlanner";
export const ASSISTANT_MCP_DISCOVERY_TURN_INPUT_SCHEMA_VERSION =
"assistant_mcp_discovery_turn_input_v1" as const;
@ -25,6 +34,8 @@ export interface AssistantMcpDiscoveryTurnInputContract {
adapter_status: AssistantMcpDiscoveryTurnInputStatus;
should_run_discovery: boolean;
semantic_data_need: string | null;
data_need_graph: AssistantMcpDiscoveryDataNeedGraphContract | null;
metadata_surface_ref: AssistantMcpDiscoveryMetadataSurfaceRef | null;
turn_meaning_ref: AssistantMcpDiscoveryTurnMeaningRef | null;
source_signal: AssistantMcpDiscoveryTurnInputSource;
reason_codes: string[];
@ -114,6 +125,10 @@ function compactLower(value: unknown): string {
.trim();
}
function sameScopedName(left: string | null, right: string | null): boolean {
return Boolean(left && right && compactLower(left) === compactLower(right));
}
function candidateValue(value: unknown): string | null {
const direct = toNonEmptyString(value);
if (direct && direct !== "[object Object]") {
@ -194,6 +209,31 @@ function collectDateScopeFromFilters(filters: Record<string, unknown> | null): s
return periodFrom ?? periodTo ?? null;
}
function normalizeMetadataRouteFamily(value: unknown): AssistantMcpDiscoveryMetadataRouteFamily | null {
const text = toNonEmptyString(value);
if (text === "document_evidence" || text === "movement_evidence" || text === "catalog_drilldown") {
return text;
}
return null;
}
function normalizeMetadataRecommendedPrimitive(
value: unknown
): AssistantMcpDiscoveryMetadataRecommendedPrimitive | null {
const text = toNonEmptyString(value);
if (text === "query_documents" || text === "query_movements" || text === "drilldown_related_objects") {
return text;
}
return null;
}
function normalizeMetadataRouteFamilySelectionBasis(
value: unknown
): AssistantMcpDiscoveryMetadataSurfaceRef["route_family_selection_basis"] {
const text = toNonEmptyString(value);
return text === "selected_entity_set" || text === "dominant_surface_objects" ? text : null;
}
function mapPilotScopeToFollowupMeaning(
pilotScope: string | null
): {
@ -308,10 +348,14 @@ function collectFollowupDiscoverySeed(followupContext: Record<string, unknown> |
discoveryEntity: string | null;
entityResolutionStatus: string | null;
entityResolutionAmbiguityCandidates: string[];
rankingNeed: string | null;
organization: string | null;
dateScope: string | null;
metadataRouteFamily: string | null;
metadataRouteFamily: AssistantMcpDiscoveryMetadataRouteFamily | null;
metadataRouteFamilySelectionBasis: AssistantMcpDiscoveryMetadataSurfaceRef["route_family_selection_basis"];
metadataSelectedEntitySet: string | null;
metadataSelectedSurfaceObjects: string[];
metadataRecommendedNextPrimitive: AssistantMcpDiscoveryMetadataRecommendedPrimitive | null;
metadataAmbiguityDetected: boolean;
metadataAmbiguityEntitySets: string[];
} {
@ -356,15 +400,55 @@ function collectFollowupDiscoverySeed(followupContext: Record<string, unknown> |
discoveryEntity: ambiguityBlocksImplicitGrounding ? null : discoveryEntities[0] ?? null,
entityResolutionStatus,
entityResolutionAmbiguityCandidates,
rankingNeed: toNonEmptyString(followupContext?.previous_discovery_ranking_need),
organization,
dateScope,
metadataRouteFamily: toNonEmptyString(followupContext?.previous_discovery_metadata_route_family),
metadataRouteFamily: normalizeMetadataRouteFamily(followupContext?.previous_discovery_metadata_route_family),
metadataRouteFamilySelectionBasis: normalizeMetadataRouteFamilySelectionBasis(
followupContext?.previous_discovery_metadata_route_family_selection_basis
),
metadataSelectedEntitySet: toNonEmptyString(followupContext?.previous_discovery_metadata_selected_entity_set),
metadataSelectedSurfaceObjects: collectEntityCandidates(
followupContext?.previous_discovery_metadata_selected_surface_objects
),
metadataRecommendedNextPrimitive: normalizeMetadataRecommendedPrimitive(
followupContext?.previous_discovery_metadata_recommended_next_primitive
),
metadataAmbiguityDetected: followupContext?.previous_discovery_metadata_ambiguity_detected === true,
metadataAmbiguityEntitySets: collectEntityCandidates(followupContext?.previous_discovery_metadata_ambiguity_entity_sets)
};
}
function buildMetadataSurfaceRef(
followupSeed: ReturnType<typeof collectFollowupDiscoverySeed>
): AssistantMcpDiscoveryMetadataSurfaceRef | null {
if (followupSeed.pilotScope !== "metadata_inspection_v1") {
return null;
}
const hasPayload =
Boolean(
followupSeed.metadataRouteFamily ||
followupSeed.metadataSelectedEntitySet ||
followupSeed.metadataRecommendedNextPrimitive ||
followupSeed.metadataRouteFamilySelectionBasis
) ||
followupSeed.metadataSelectedSurfaceObjects.length > 0 ||
followupSeed.metadataAmbiguityDetected ||
followupSeed.metadataAmbiguityEntitySets.length > 0;
if (!hasPayload) {
return null;
}
return {
selected_entity_set: followupSeed.metadataSelectedEntitySet,
selected_surface_objects: followupSeed.metadataSelectedSurfaceObjects,
downstream_route_family: followupSeed.metadataRouteFamily,
route_family_selection_basis: followupSeed.metadataRouteFamilySelectionBasis,
recommended_next_primitive: followupSeed.metadataRecommendedNextPrimitive,
ambiguity_detected: followupSeed.metadataAmbiguityDetected,
ambiguity_entity_sets: followupSeed.metadataAmbiguityEntitySets
};
}
function metadataEntitySetsSuggestDocumentLane(values: string[]): boolean {
return values.some((value) => /(?:документ|document|invoice|waybill|накладн|счет[- ]?фактур|акт)/iu.test(value));
}
@ -390,7 +474,7 @@ function hasLifecycleSignal(text: string): boolean {
}
function hasValueFlowSignal(text: string): boolean {
return /(?:оборот|выручк|оплат|плат[её]ж|заплат|перечисл|списан|расход|исходящ|входящ|получ(?:ил|ено|ен)|поступил|поступлен|денежн[а-яёa-z0-9_-]*\s+поток|supplier|value[-\s]?flow|turnover|revenue|payment|payout|outflow|cash\s+flow)/iu.test(
return /(?:оборот|выручк|оплат|плат[её]ж|заплат|перечисл|списан|расход|исходящ|входящ|получ(?:ил|ено|ен)|поступил|поступлен|денежн[а-яёa-z0-9_-]*\s+поток|(?<!\p{L})заработ(?:ал|али|ало|аем|ает|ать|ано|ок)(?!\p{L})|supplier|value[-\s]?flow|turnover|revenue|payment|payout|outflow|cash\s+flow|\bearn(?:ed|ing|ings)?\b)/iu.test(
text
);
}
@ -407,12 +491,52 @@ function hasBidirectionalValueFlowSignal(text: string): boolean {
);
}
function hasValueRankingSignal(text: string): boolean {
return /(?:кто\s+больше\s+всего.*ден[её]г|больше\s+всего.*ден[её]г|прин[её]с.*ден[её]г|сам(?:ый|ая|ое|ые).*(?:доходн|прибыльн)|most.*money|highest\s+(?:revenue|payment))/iu.test(
text
);
}
function hasOrganizationScopeSignal(text: string): boolean {
return /(?:\bРѕРѕРѕ\b|\bРёРї\b|\bао\b|\bпао\b|\bзао\b|\bllc\b|\binc\b|\bcorp\b|\bcompany\b|\borganization\b|\borganisation\b|организаС|компан)/iu.test(
text
);
}
function hasOrganizationScopeSignalUtf8(text: string): boolean {
return (
/(?<!\p{L})(?:\u043e\u043e\u043e|\u0438\u043f|\u0430\u043e|\u043f\u0430\u043e|\u0437\u0430\u043e)(?!\p{L})/iu.test(text) ||
/\b(?:llc|inc|corp|company|organization|organisation)\b/iu.test(text) ||
/(?:\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446|\u043a\u043e\u043c\u043f\u0430\u043d)/iu.test(text)
);
}
function extractOrganizationScopeFromRawText(value: unknown): string | null {
const text = toNonEmptyString(value);
if (!text) {
return null;
}
const match = text.match(
/(?:^|[\s,;:])(?:\u043f\u043e|for|in|within)?\s*((?:\u041e\u041e\u041e|\u0418\u041f|\u0410\u041e|\u041f\u0410\u041e|\u0417\u0410\u041e|LLC|Inc|LTD|Corp)\s+[^\n,.;:!?]+)/u
);
if (!match?.[1]) {
return null;
}
return toNonEmptyString(match[1]);
}
function hasMonthlyAggregationSignal(text: string): boolean {
return /(?:\u043f\u043e\s+\u043c\u0435\u0441\u044f\u0446\u0430\u043c|\u043f\u043e\u043c\u0435\u0441\u044f\u0447\u043d\u043e|\u0435\u0436\u0435\u043c\u0435\u0441\u044f\u0447\u043d\u043e|month\s+by\s+month|by\s+month|monthly)/iu.test(
text
);
}
function hasAllTimeScopeHint(text: string): boolean {
return /(?:\u0437\u0430\s+\u0432\u0441[\u0435\u0451]\s+\u0432\u0440\u0435\u043c\u044f|\u0437\u0430\s+\u0432\u0435\u0441\u044c\s+\u043f\u0435\u0440\u0438\u043e\u0434|\u0437\u0430\s+\u0432\u0441\u044e\s+\u0438\u0441\u0442\u043e\u0440\u0438(?:\u044e|\u0438)|\u0437\u0430\s+\u043b\u044e\u0431\u043e\u0439\s+\u043f\u0435\u0440\u0438\u043e\u0434|for\s+all\s+time|all\s+time|entire\s+period|full\s+history|any\s+period)/iu.test(
text
);
}
function hasMetadataSignal(text: string): boolean {
if (
/(?:\u043c\u0435\u0442\u0430\u0434\u0430\u043d|schema|catalog|metadata\s+surface|\u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440[\u0430\u044b]\s+1\u0441|\u0441\u0445\u0435\u043c[\u0430\u044b]\s+1\u0441)/iu.test(
@ -732,6 +856,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
const followupContext = toRecordObject(input.followupContext);
const predecomposeEntities = collectPredecomposeEntities(predecomposeContract);
const followupSeed = collectFollowupDiscoverySeed(followupContext);
const metadataSurfaceRef = buildMetadataSurfaceRef(followupSeed);
const reasonCodes: string[] = [];
const rawUserText = toNonEmptyString(input.userMessage);
const rawEffectiveText = toNonEmptyString(input.effectiveMessage);
@ -741,12 +866,14 @@ export function buildAssistantMcpDiscoveryTurnInput(
const rawLifecycleSignal = hasLifecycleSignal(rawText);
const rawBidirectionalValueFlowSignal = !rawLifecycleSignal && hasBidirectionalValueFlowSignal(rawText);
const rawValueFlowSignal =
!rawLifecycleSignal && (hasValueFlowSignal(rawText) || rawBidirectionalValueFlowSignal);
!rawLifecycleSignal &&
(hasValueFlowSignal(rawText) || hasValueRankingSignal(rawText) || rawBidirectionalValueFlowSignal);
const rawMetadataSignal = !rawLifecycleSignal && !rawValueFlowSignal && hasMetadataSignal(rawText);
const rawEntityResolutionSignal =
!rawLifecycleSignal && !rawValueFlowSignal && !rawMetadataSignal && hasEntityResolutionSignal(rawText);
const rawPayoutSignal = rawValueFlowSignal && !rawBidirectionalValueFlowSignal && hasPayoutSignal(rawText);
const monthlyAggregationSignal = hasMonthlyAggregationSignal(rawText);
const rawAllTimeScopeSignal = hasAllTimeScopeHint(rawText);
const explicitDateScopeLiteralDetected = hasExplicitDateScopeLiteral(rawText);
const rawDateScope = collectDateScopeFromRawText(rawText);
const rawMetadataScopeHint = rawMetadataSignal ? metadataScopeHintFromRawText(rawText) : null;
@ -773,12 +900,49 @@ export function buildAssistantMcpDiscoveryTurnInput(
const explicitIntentCandidate = toNonEmptyString(assistantTurnMeaning?.explicit_intent_candidate);
const assistantTurnMeaningDateScope = toNonEmptyString(assistantTurnMeaning?.explicit_date_scope);
const assistantTurnMeaningOrganizationScope = toNonEmptyString(assistantTurnMeaning?.explicit_organization_scope);
const rawOrganizationMentionSignal = hasOrganizationScopeSignalUtf8(rawText);
const rawOrganizationScope = extractOrganizationScopeFromRawText(rawUserText ?? rawEffectiveText ?? rawSignalSourceText);
const currentTurnOrganizationScope =
rawOrganizationScope ?? predecomposeEntities.organization ?? assistantTurnMeaningOrganizationScope;
const explicitOrganizationScopeSignal = Boolean(rawOrganizationMentionSignal && currentTurnOrganizationScope);
const organizationClarificationFollowupApplicable = Boolean(
followupSeed.domain === "counterparty_value" &&
!followupSeed.counterparty &&
currentTurnOrganizationScope &&
!rawLifecycleSignal &&
!rawValueFlowSignal &&
!rawMetadataSignal
);
const rawOpenScopeValueFlowOrganizationSignal = Boolean(
rawValueFlowSignal &&
!rawBidirectionalValueFlowSignal &&
explicitOrganizationScopeSignal
);
const predecomposeOrganizationMirrorsCounterparty = sameScopedName(
predecomposeEntities.counterparty,
predecomposeEntities.organization
);
const organizationMirrorsPredecomposeCounterparty = Boolean(
(rawBidirectionalValueFlowSignal ||
hasValueRankingSignal(rawText) ||
rawOpenScopeValueFlowOrganizationSignal ||
explicitOrganizationScopeSignal) &&
(sameScopedName(predecomposeEntities.counterparty, assistantTurnMeaningOrganizationScope) ||
predecomposeOrganizationMirrorsCounterparty)
);
const normalizedPredecomposeCounterparty = organizationMirrorsPredecomposeCounterparty
? null
: predecomposeEntities.counterparty;
const predecomposeDateScope = collectDateScope(predecomposeContract);
const followupDiscoverySeedApplicable = Boolean(
followupSeed.domain &&
!rawLifecycleSignal &&
!rawValueFlowSignal &&
(monthlyAggregationSignal || explicitDateScopeLiteralDetected || predecomposeDateScope)
(monthlyAggregationSignal ||
explicitDateScopeLiteralDetected ||
predecomposeDateScope ||
explicitOrganizationScopeSignal ||
organizationClarificationFollowupApplicable)
);
const metadataFollowupSeedApplicable = Boolean(
followupSeed.domain === "metadata" &&
@ -916,6 +1080,17 @@ export function buildAssistantMcpDiscoveryTurnInput(
!metadataMovementHintSignal &&
hasMetadataDownstreamContinuationSignal(rawText)
);
const metadataGroundedCatalogLaneContinuationApplicable = Boolean(
followupSeed.pilotScope === "metadata_inspection_v1" &&
followupSeed.metadataRouteFamily === "catalog_drilldown" &&
!followupSeed.metadataAmbiguityDetected &&
!rawLifecycleSignal &&
!rawValueFlowSignal &&
!rawMetadataSignal &&
!metadataDocumentHintSignal &&
!metadataMovementHintSignal &&
hasMetadataDownstreamContinuationSignal(rawText)
);
const metadataAmbiguityCollapsedDocumentLaneContinuationApplicable = Boolean(
followupSeed.pilotScope === "metadata_inspection_v1" &&
followupSeed.metadataAmbiguityDetected &&
@ -975,13 +1150,16 @@ export function buildAssistantMcpDiscoveryTurnInput(
metadataFollowupSeedApplicable &&
!metadataAmbiguityLaneClarificationApplicable &&
!metadataGroundedDocumentLaneApplicable &&
!metadataGroundedMovementLaneApplicable;
!metadataGroundedMovementLaneApplicable &&
!metadataGroundedCatalogLaneContinuationApplicable;
const seededDomain = metadataAmbiguityLaneClarificationApplicable
? "metadata"
: metadataGroundedDocumentLaneApplicable
? "documents"
: metadataGroundedMovementLaneApplicable
? "movements"
: metadataGroundedCatalogLaneContinuationApplicable
? "metadata"
: followupDiscoverySeedApplicable || effectiveMetadataFollowupSeedApplicable
? followupSeed.domain
: null;
@ -991,6 +1169,8 @@ export function buildAssistantMcpDiscoveryTurnInput(
? "list_documents"
: metadataGroundedMovementLaneApplicable
? "list_movements"
: metadataGroundedCatalogLaneContinuationApplicable
? "inspect_catalog"
: followupDiscoverySeedApplicable || effectiveMetadataFollowupSeedApplicable
? followupSeed.action
: null;
@ -1000,6 +1180,8 @@ export function buildAssistantMcpDiscoveryTurnInput(
? "document_evidence"
: metadataGroundedMovementLaneApplicable
? "movement_evidence"
: metadataGroundedCatalogLaneContinuationApplicable
? "schema_surface"
: followupDiscoverySeedApplicable || effectiveMetadataFollowupSeedApplicable
? followupSeed.unsupported
: null;
@ -1038,7 +1220,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
for (const candidate of collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates)) {
pushNormalizedEntityResolutionCandidate(entityCandidates, candidate);
}
pushNormalizedEntityResolutionCandidate(entityCandidates, predecomposeEntities.counterparty);
pushNormalizedEntityResolutionCandidate(entityCandidates, normalizedPredecomposeCounterparty);
pushNormalizedEntityResolutionCandidate(entityCandidates, followupSeed.counterparty);
} else {
if (groundedFollowupEntity) {
@ -1047,7 +1229,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
for (const candidate of collectEntityCandidates(assistantTurnMeaning?.explicit_entity_candidates)) {
pushScopedEntityCandidate(entityCandidates, candidate, groundedFollowupEntity);
}
pushScopedEntityCandidate(entityCandidates, predecomposeEntities.counterparty, groundedFollowupEntity);
pushScopedEntityCandidate(entityCandidates, normalizedPredecomposeCounterparty, groundedFollowupEntity);
if (!groundedFollowupEntity) {
pushScopedEntityCandidate(entityCandidates, followupSeed.counterparty, null);
pushScopedEntityCandidate(entityCandidates, followupSeed.discoveryEntity, null);
@ -1058,15 +1240,44 @@ export function buildAssistantMcpDiscoveryTurnInput(
pushUnique(entityCandidates, followupSeed.discoveryEntity);
pushUnique(entityCandidates, rawMetadataScopeHint);
}
if (valueFlowSignal && !predecomposeEntities.counterparty && !followupSeed.counterparty) {
const openScopeValueFlowWithoutCounterparty =
valueFlowSignal && !normalizedPredecomposeCounterparty && !followupSeed.counterparty;
const valueFlowOrganizationStaysScope =
openScopeValueFlowWithoutCounterparty &&
Boolean(
bidirectionalValueFlowSignal ||
hasValueRankingSignal(rawText) ||
rawOpenScopeValueFlowOrganizationSignal ||
explicitOrganizationScopeSignal ||
organizationClarificationFollowupApplicable ||
followupSeed.organization
);
if (openScopeValueFlowWithoutCounterparty && !valueFlowOrganizationStaysScope) {
pushUnique(entityCandidates, predecomposeEntities.organization);
pushUnique(entityCandidates, followupSeed.organization);
}
const explicitOrganizationScope =
valueFlowSignal && !predecomposeEntities.counterparty && !followupSeed.counterparty
valueFlowOrganizationStaysScope || !openScopeValueFlowWithoutCounterparty
? currentTurnOrganizationScope ?? followupSeed.organization
: null;
if (valueFlowOrganizationStaysScope && explicitOrganizationScope) {
for (let index = entityCandidates.length - 1; index >= 0; index -= 1) {
if (entityCandidates[index] === explicitOrganizationScope) {
entityCandidates.splice(index, 1);
}
}
}
const explicitDateScope =
rawAllTimeScopeSignal
? null
: predecomposeEntities.organization ?? assistantTurnMeaningOrganizationScope ?? followupSeed.organization;
const explicitDateScope = assistantTurnMeaningDateScope ?? predecomposeDateScope ?? rawDateScope ?? followupSeed.dateScope;
: assistantTurnMeaningDateScope ?? predecomposeDateScope ?? rawDateScope ?? followupSeed.dateScope;
const followupDateScopeApplied = Boolean(
!rawAllTimeScopeSignal &&
!assistantTurnMeaningDateScope &&
!predecomposeDateScope &&
!rawDateScope &&
followupSeed.dateScope
);
const turnMeaning: AssistantMcpDiscoveryTurnMeaningRef = {
asked_domain_family:
@ -1101,6 +1312,8 @@ export function buildAssistantMcpDiscoveryTurnInput(
? metadataActionFromRawText(rawText) ?? seededAction
: rawAction ?? seededAction,
asked_aggregation_axis: monthlyAggregationSignal ? "month" : rawAggregationAxis,
seeded_ranking_need:
valueFlowSignal && followupSeed.rankingNeed ? followupSeed.rankingNeed : undefined,
explicit_entity_candidates: entityCandidates,
metadata_ambiguity_entity_sets:
metadataAmbiguityLaneClarificationApplicable && followupSeed.metadataAmbiguityEntitySets.length > 0
@ -1156,6 +1369,9 @@ export function buildAssistantMcpDiscoveryTurnInput(
if (toNonEmptyString(turnMeaning.asked_aggregation_axis)) {
cleanTurnMeaning.asked_aggregation_axis = turnMeaning.asked_aggregation_axis;
}
if (toNonEmptyString(turnMeaning.seeded_ranking_need)) {
cleanTurnMeaning.seeded_ranking_need = turnMeaning.seeded_ranking_need;
}
if ((turnMeaning.explicit_entity_candidates?.length ?? 0) > 0) {
cleanTurnMeaning.explicit_entity_candidates = turnMeaning.explicit_entity_candidates;
}
@ -1193,6 +1409,7 @@ export function buildAssistantMcpDiscoveryTurnInput(
groundedValueFlowFollowupApplicable,
forceDiscoveryOverExplicitIntent:
Boolean(entityResolutionClarificationCandidate) ||
organizationClarificationFollowupApplicable ||
metadataAmbiguityLaneClarificationApplicable ||
metadataGroundedMovementLaneApplicable ||
metadataGroundedDocumentLaneApplicable ||
@ -1243,6 +1460,9 @@ export function buildAssistantMcpDiscoveryTurnInput(
if (entityResolutionClarificationCandidate) {
pushReason(reasonCodes, "mcp_discovery_entity_resolution_clarification_candidate_selected");
}
if (organizationClarificationFollowupApplicable) {
pushReason(reasonCodes, "mcp_discovery_organization_clarification_followup_from_followup_context");
}
if (payoutSignal) {
pushReason(reasonCodes, "mcp_discovery_payout_signal_detected");
}
@ -1252,6 +1472,9 @@ export function buildAssistantMcpDiscoveryTurnInput(
if (monthlyAggregationSignal) {
pushReason(reasonCodes, "mcp_discovery_monthly_aggregation_signal_detected");
}
if (rawAllTimeScopeSignal) {
pushReason(reasonCodes, "mcp_discovery_all_time_scope_signal_detected");
}
if (followupDiscoverySeedApplicable) {
pushReason(reasonCodes, "mcp_discovery_seeded_from_followup_context");
}
@ -1300,6 +1523,9 @@ export function buildAssistantMcpDiscoveryTurnInput(
if (metadataGroundedLaneContinuationApplicable) {
pushReason(reasonCodes, "mcp_discovery_metadata_grounded_lane_continuation");
}
if (metadataGroundedCatalogLaneContinuationApplicable) {
pushReason(reasonCodes, "mcp_discovery_metadata_grounded_catalog_continuation");
}
if (metadataAmbiguityCollapsedDocumentLaneContinuationApplicable) {
pushReason(reasonCodes, "mcp_discovery_metadata_ambiguity_collapsed_to_document_lane");
}
@ -1312,15 +1538,24 @@ export function buildAssistantMcpDiscoveryTurnInput(
if (unsupported) {
pushReason(reasonCodes, "mcp_discovery_unsupported_but_understood_turn");
}
if (predecomposeEntities.counterparty) {
if (
!(valueFlowOrganizationStaysScope && normalizedPredecomposeCounterparty === explicitOrganizationScope) &&
normalizedPredecomposeCounterparty
) {
pushReason(reasonCodes, "mcp_discovery_counterparty_from_predecompose");
}
if (followupSeed.counterparty) {
pushReason(reasonCodes, "mcp_discovery_counterparty_from_followup_context");
}
if (followupSeed.dateScope) {
if (followupDateScopeApplied) {
pushReason(reasonCodes, "mcp_discovery_date_scope_from_followup_context");
}
if (metadataSurfaceRef) {
pushReason(reasonCodes, "mcp_discovery_metadata_surface_ref_from_followup_context");
}
if (metadataSurfaceRef?.recommended_next_primitive) {
pushReason(reasonCodes, "mcp_discovery_metadata_next_primitive_from_followup_context");
}
if (entityCandidates.length > 0) {
pushReason(reasonCodes, "mcp_discovery_entity_scope_available");
}
@ -1330,6 +1565,17 @@ export function buildAssistantMcpDiscoveryTurnInput(
if (runDiscovery && !hasTurnMeaning) {
pushReason(reasonCodes, "mcp_discovery_turn_meaning_missing");
}
const dataNeedGraph =
runDiscovery && hasTurnMeaning
? buildAssistantMcpDiscoveryDataNeedGraph({
semanticDataNeed,
rawUtterance: rawSignalSourceText,
turnMeaning: cleanTurnMeaning
})
: null;
if (dataNeedGraph) {
pushReason(reasonCodes, "mcp_discovery_data_need_graph_built");
}
return {
schema_version: ASSISTANT_MCP_DISCOVERY_TURN_INPUT_SCHEMA_VERSION,
@ -1337,6 +1583,8 @@ export function buildAssistantMcpDiscoveryTurnInput(
adapter_status: !runDiscovery ? "not_applicable" : hasTurnMeaning ? "ready" : "needs_more_context",
should_run_discovery: runDiscovery,
semantic_data_need: runDiscovery ? semanticDataNeed : null,
data_need_graph: dataNeedGraph,
metadata_surface_ref: runDiscovery ? metadataSurfaceRef : null,
turn_meaning_ref: runDiscovery && hasTurnMeaning ? cleanTurnMeaning : null,
source_signal: sourceSignal,
reason_codes: reasonCodes

View File

@ -16,7 +16,11 @@ import {
readAssistantMcpDiscoveryEntityAmbiguityCandidates,
readAssistantMcpDiscoveryEntityResolutionStatus,
readAssistantMcpDiscoveryMetadataRouteFamily,
readAssistantMcpDiscoveryMetadataRouteFamilySelectionBasis,
readAssistantMcpDiscoveryMetadataSelectedEntitySet,
readAssistantMcpDiscoveryMetadataSelectedSurfaceObjects,
readAssistantMcpDiscoveryMetadataRecommendedNextPrimitive,
readAssistantMcpDiscoveryRankingNeed,
readAddressDebugTemporalScope,
readAssistantMcpDiscoveryPilotScope,
resolveOrganizationClarificationContinuation,
@ -664,10 +668,18 @@ export function createAssistantTransitionPolicy(deps) {
carryoverSourceDebug,
deps.toNonEmptyString
);
const sourceDiscoveryMetadataRouteFamilySelectionBasis =
readAssistantMcpDiscoveryMetadataRouteFamilySelectionBasis(carryoverSourceDebug, deps.toNonEmptyString);
const sourceDiscoveryMetadataSelectedEntitySet = readAssistantMcpDiscoveryMetadataSelectedEntitySet(
carryoverSourceDebug,
deps.toNonEmptyString
);
const sourceDiscoveryMetadataSelectedSurfaceObjects = readAssistantMcpDiscoveryMetadataSelectedSurfaceObjects(
carryoverSourceDebug,
deps.toNonEmptyString
);
const sourceDiscoveryMetadataRecommendedNextPrimitive =
readAssistantMcpDiscoveryMetadataRecommendedNextPrimitive(carryoverSourceDebug, deps.toNonEmptyString);
const sourceDiscoveryMetadataAmbiguityDetected = readAssistantMcpDiscoveryMetadataAmbiguityDetected(
carryoverSourceDebug
);
@ -683,6 +695,10 @@ export function createAssistantTransitionPolicy(deps) {
carryoverSourceDebug,
deps.toNonEmptyString
);
const sourceDiscoveryRankingNeed = readAssistantMcpDiscoveryRankingNeed(
carryoverSourceDebug,
deps.toNonEmptyString
);
const sourceDiscoveryEntityAmbiguityCandidates = readAssistantMcpDiscoveryEntityAmbiguityCandidates(
carryoverSourceDebug,
deps.toNonEmptyString
@ -1026,12 +1042,21 @@ export function createAssistantTransitionPolicy(deps) {
previous_discovery_entity_resolution_status: sourceDiscoveryEntityResolutionStatus ?? undefined,
previous_discovery_entity_candidates:
sourceDiscoveryEntityCandidates.length > 0 ? sourceDiscoveryEntityCandidates : undefined,
previous_discovery_ranking_need: sourceDiscoveryRankingNeed ?? undefined,
previous_discovery_entity_ambiguity_candidates:
sourceDiscoveryEntityAmbiguityCandidates.length > 0
? sourceDiscoveryEntityAmbiguityCandidates
: undefined,
previous_discovery_metadata_route_family: sourceDiscoveryMetadataRouteFamily ?? undefined,
previous_discovery_metadata_route_family_selection_basis:
sourceDiscoveryMetadataRouteFamilySelectionBasis ?? undefined,
previous_discovery_metadata_selected_entity_set: sourceDiscoveryMetadataSelectedEntitySet ?? undefined,
previous_discovery_metadata_selected_surface_objects:
sourceDiscoveryMetadataSelectedSurfaceObjects.length > 0
? sourceDiscoveryMetadataSelectedSurfaceObjects
: undefined,
previous_discovery_metadata_recommended_next_primitive:
sourceDiscoveryMetadataRecommendedNextPrimitive ?? undefined,
previous_discovery_metadata_ambiguity_detected: sourceDiscoveryMetadataAmbiguityDetected || undefined,
previous_discovery_metadata_ambiguity_entity_sets:
sourceDiscoveryMetadataAmbiguityEntitySets.length > 0 ? sourceDiscoveryMetadataAmbiguityEntitySets : undefined,

View File

@ -3,7 +3,10 @@ import { ASSISTANT_MCP_DISCOVERY_PRIMITIVES, buildAssistantMcpDiscoveryPlan } fr
import {
buildAssistantMcpCatalogIndex,
getAssistantMcpCatalogPrimitive,
reviewAssistantMcpDiscoveryPlanAgainstCatalog
reviewAssistantMcpDiscoveryPlanAgainstCatalog,
searchAssistantMcpCatalogPrimitivesByDecompositionCandidates,
searchAssistantMcpCatalogPrimitivesByFactAxis,
searchAssistantMcpCatalogPrimitivesByMetadataSurface
} from "../src/services/assistantMcpCatalogIndex";
describe("assistant MCP catalog index", () => {
@ -16,11 +19,94 @@ describe("assistant MCP catalog index", () => {
for (const entry of index.primitives) {
expect(entry.safe_for_model_planning).toBe(true);
expect(entry.runtime_must_execute).toBe(true);
expect(entry.decomposition_hints.length).toBeGreaterThan(0);
expect(Array.isArray(entry.supported_fact_families)).toBe(true);
expect(Array.isArray(entry.supported_action_families)).toBe(true);
expect(Array.isArray(entry.planning_tags)).toBe(true);
expect(entry.required_axes_any_of.length).toBeGreaterThan(0);
expect(entry.output_fact_kinds.length).toBeGreaterThan(0);
}
});
it("can search reviewed primitives from data-need decomposition candidates", () => {
const primitives = searchAssistantMcpCatalogPrimitivesByDecompositionCandidates({
decomposition_candidates: [
"resolve_entity_reference",
"collect_scoped_movements",
"aggregate_checked_amounts",
"probe_coverage"
]
});
expect(primitives).toEqual([
"resolve_entity_reference",
"query_movements",
"aggregate_by_axis",
"probe_coverage"
]);
});
it("can suppress aggregate_by_axis for decomposition shapes that derive comparison without an aggregate primitive", () => {
const primitives = searchAssistantMcpCatalogPrimitivesByDecompositionCandidates({
decomposition_candidates: [
"collect_incoming_movements",
"collect_outgoing_movements",
"aggregate_by_month",
"probe_coverage"
],
allow_aggregate_by_axis: false
});
expect(primitives).toEqual(["query_movements", "probe_coverage"]);
});
it("can search reviewed primitives directly from fact family and required axes", () => {
const primitives = searchAssistantMcpCatalogPrimitivesByFactAxis({
business_fact_family: "document_evidence",
action_family: "list_documents",
has_subject_candidates: true,
required_axes: ["counterparty", "period", "coverage_target"]
});
expect(primitives).toEqual(["resolve_entity_reference", "query_documents", "probe_coverage"]);
});
it("can search reviewed primitives directly from a confirmed document metadata surface", () => {
const primitives = searchAssistantMcpCatalogPrimitivesByMetadataSurface({
downstream_route_family: "document_evidence",
selected_entity_set: "Document",
selected_surface_objects: ["Document.InvoiceIssued"],
recommended_next_primitive: "query_documents",
required_axes: ["counterparty", "period", "coverage_target"]
});
expect(primitives).toEqual(["query_documents", "resolve_entity_reference", "probe_coverage"]);
});
it("can search reviewed primitives directly from a confirmed movement metadata surface", () => {
const primitives = searchAssistantMcpCatalogPrimitivesByMetadataSurface({
downstream_route_family: "movement_evidence",
selected_entity_set: "AccumulationRegister",
selected_surface_objects: ["Register.BankOperations"],
recommended_next_primitive: "query_movements",
required_axes: ["counterparty", "period", "coverage_target"]
});
expect(primitives).toEqual(["query_movements", "resolve_entity_reference", "probe_coverage"]);
});
it("can search reviewed primitives directly from a confirmed catalog metadata surface without inventing an unsupported drilldown primitive", () => {
const primitives = searchAssistantMcpCatalogPrimitivesByMetadataSurface({
downstream_route_family: "catalog_drilldown",
selected_entity_set: "Catalog",
selected_surface_objects: ["Catalog.Counterparties"],
recommended_next_primitive: "drilldown_related_objects",
required_axes: ["metadata_scope"]
});
expect(primitives).toEqual(["inspect_1c_metadata"]);
});
it("marks a counterparty turnover discovery plan as catalog-compatible when required axes exist", () => {
const plan = buildAssistantMcpDiscoveryPlan({
semanticDataNeed: "counterparty turnover evidence",

View File

@ -222,6 +222,74 @@ describe("assistant MCP discovery answer adapter", () => {
expect(draft.must_not_claim).toContain("Do not claim rows were checked when mcp_execution_performed=false.");
});
it("asks for organization rather than counterparty when a ranked value-flow ask already has the period", async () => {
const planner = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
business_fact_family: "value_flow",
action_family: "turnover",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: null,
ranking_need: "top_desc",
proof_expectation: "coverage_checked_fact",
clarification_gaps: [],
decomposition_candidates: ["collect_scoped_movements", "aggregate_ranked_axis_values", "probe_coverage"],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
reason_codes: ["data_need_graph_built", "data_need_graph_ranking_top_desc"]
},
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_date_scope: "2020"
}
});
const pilot = await executeAssistantMcpDiscoveryPilot(planner, buildDeps([]));
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
expect(draft.answer_mode).toBe("needs_clarification");
expect(draft.headline).toContain("рейтинг");
expect(draft.next_step_line).toContain("организацию");
expect(draft.next_step_line).not.toContain("Уточните контрагента");
});
it("asks for organization rather than counterparty on open bidirectional comparison when only the period is known", async () => {
const planner = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
business_fact_family: "value_flow",
action_family: "net_value_flow",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: "incoming_vs_outgoing",
ranking_need: null,
proof_expectation: "coverage_checked_fact",
clarification_gaps: [],
decomposition_candidates: ["collect_incoming_movements", "collect_outgoing_movements", "probe_coverage"],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
reason_codes: ["data_need_graph_built", "data_need_graph_comparison_incoming_vs_outgoing"]
},
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "net_value_flow",
explicit_date_scope: "2020"
}
});
const pilot = await executeAssistantMcpDiscoveryPilot(planner, buildDeps([]));
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
expect(draft.answer_mode).toBe("needs_clarification");
expect(draft.headline).toContain("входящий и исходящий");
expect(draft.next_step_line).toContain("организацию");
expect(draft.next_step_line).not.toContain("Уточните контрагента");
});
it("asks for an explicit lane choice when mixed metadata ambiguity cannot continue on a neutral follow-up", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
@ -243,6 +311,42 @@ describe("assistant MCP discovery answer adapter", () => {
expect(draft.must_not_claim).toContain("Do not claim rows were checked when mcp_execution_performed=false.");
});
it("keeps metadata lane-choice clarification human-facing when planner selects it from data-need graph", async () => {
const planner = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: ["НДС"],
business_fact_family: "metadata_surface",
action_family: "resolve_next_lane",
aggregation_need: null,
time_scope_need: null,
comparison_need: null,
ranking_need: null,
proof_expectation: "supporting_evidence",
clarification_gaps: ["lane_family_choice"],
decomposition_candidates: [],
forbidden_overclaim_flags: ["no_raw_model_claims"],
reason_codes: ["data_need_graph_built", "data_need_graph_requires_lane_family_choice"]
},
turnMeaning: {
asked_domain_family: "metadata",
asked_action_family: "resolve_next_lane",
explicit_entity_candidates: ["НДС"],
unsupported_but_understood_family: "metadata_lane_choice_clarification"
}
});
const pilot = await executeAssistantMcpDiscoveryPilot(planner, buildDeps([]));
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
expect(draft.answer_mode).toBe("needs_clarification");
expect(draft.headline).toContain("data-lane");
expect(draft.next_step_line).toContain("по документам");
expect(draft.next_step_line).toContain("по движениям/регистрам");
expect(draft.next_step_line).not.toContain("Уточните контрагента");
});
it("keeps movement clarification anchored to the chosen lane after metadata ambiguity was resolved", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
@ -382,6 +486,42 @@ describe("assistant MCP discovery answer adapter", () => {
expect(draft.must_not_claim).toContain("Do not present the inferred next checked lane as already executed data retrieval.");
});
it("uses a distinct human headline for catalog drilldown instead of repeating a generic metadata overview", async () => {
const planner = planAssistantMcpDiscovery({
metadataSurface: {
selected_entity_set: "Catalog",
selected_surface_objects: ["Catalog.Counterparties"],
downstream_route_family: "catalog_drilldown",
route_family_selection_basis: "selected_entity_set",
recommended_next_primitive: "drilldown_related_objects",
ambiguity_detected: false,
ambiguity_entity_sets: []
},
turnMeaning: {
asked_domain_family: "metadata",
asked_action_family: "inspect_catalog",
unsupported_but_understood_family: "schema_surface"
}
});
const pilot = await executeAssistantMcpDiscoveryPilot(
planner,
buildMetadataDeps([
{
FullName: "Catalog.Counterparties",
MetaType: "Catalog",
attributes: [{ Name: "Description" }]
}
])
);
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
expect(draft.answer_mode).toBe("confirmed_with_bounded_inference");
expect(draft.headline).toContain("углубиться");
expect(draft.headline).toContain("справочников");
expect(draft.headline).toContain("catalog drilldown");
});
it("keeps metadata answer honest when schema surface stays ambiguous", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {

View File

@ -0,0 +1,209 @@
import { describe, expect, it } from "vitest";
import { buildAssistantMcpDiscoveryDataNeedGraph } from "../src/services/assistantMcpDiscoveryDataNeedGraph";
describe("assistant MCP discovery data need graph", () => {
it("builds a monthly bidirectional value-flow graph from grounded turn meaning", () => {
const result = buildAssistantMcpDiscoveryDataNeedGraph({
semanticDataNeed: "counterparty value-flow evidence",
rawUtterance: "какое нетто по деньгам с SVK за 2020 год по месяцам",
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "net_value_flow",
asked_aggregation_axis: "month",
explicit_entity_candidates: ["SVK"],
explicit_date_scope: "2020"
}
});
expect(result.business_fact_family).toBe("value_flow");
expect(result.action_family).toBe("net_value_flow");
expect(result.aggregation_need).toBe("by_month");
expect(result.time_scope_need).toBe("explicit_period");
expect(result.comparison_need).toBe("incoming_vs_outgoing");
expect(result.proof_expectation).toBe("coverage_checked_fact");
expect(result.clarification_gaps).toEqual([]);
expect(result.decomposition_candidates).toEqual([
"resolve_entity_reference",
"collect_incoming_movements",
"collect_outgoing_movements",
"aggregate_by_month",
"probe_coverage"
]);
expect(result.forbidden_overclaim_flags).toContain("no_unchecked_fact_totals");
});
it("marks metadata lane choice as a clarification-required graph", () => {
const result = buildAssistantMcpDiscoveryDataNeedGraph({
semanticDataNeed: "metadata lane clarification",
rawUtterance: "давай дальше",
turnMeaning: {
asked_domain_family: "metadata",
asked_action_family: "resolve_next_lane",
explicit_entity_candidates: ["SVK"],
unsupported_but_understood_family: "metadata_lane_choice_clarification"
}
});
expect(result.business_fact_family).toBe("schema_surface");
expect(result.clarification_gaps).toEqual(["lane_family_choice"]);
expect(result.proof_expectation).toBe("clarification_required");
});
it("keeps entity search as an entity-grounding graph", () => {
const result = buildAssistantMcpDiscoveryDataNeedGraph({
semanticDataNeed: "entity discovery evidence",
rawUtterance: "найди в 1С контрагента Группа СВК",
turnMeaning: {
asked_domain_family: "entity_resolution",
asked_action_family: "search_business_entity",
explicit_entity_candidates: ["Группа СВК"]
}
});
expect(result.business_fact_family).toBe("entity_grounding");
expect(result.subject_candidates).toEqual(["Группа СВК"]);
expect(result.proof_expectation).toBe("entity_grounding");
expect(result.decomposition_candidates).toEqual([
"search_business_entity",
"resolve_entity_reference",
"probe_coverage"
]);
expect(result.forbidden_overclaim_flags).toContain("no_unresolved_entity_claim");
});
it("treats top-value wording as a ranking ask rather than a missing-subject fact ask", () => {
const result = buildAssistantMcpDiscoveryDataNeedGraph({
semanticDataNeed: "counterparty value-flow evidence",
rawUtterance: "кто больше всего принес денег в 2020",
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_date_scope: "2020"
}
});
expect(result.business_fact_family).toBe("value_flow");
expect(result.ranking_need).toBe("top_desc");
expect(result.clarification_gaps).toEqual(["organization"]);
expect(result.proof_expectation).toBe("clarification_required");
expect(result.decomposition_candidates).toEqual([
"collect_scoped_movements",
"aggregate_ranked_axis_values",
"probe_coverage"
]);
expect(result.reason_codes).toContain("data_need_graph_ranking_top_desc");
expect(result.reason_codes).toContain("data_need_graph_open_scope_total_needs_organization");
});
it("keeps organization-scoped ranking executable when the ranking axis comes from follow-up context", () => {
const result = buildAssistantMcpDiscoveryDataNeedGraph({
semanticDataNeed: "counterparty value-flow evidence",
rawUtterance: "по ООО Альтернатива Плюс",
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020",
seeded_ranking_need: "top_desc"
}
});
expect(result.business_fact_family).toBe("value_flow");
expect(result.ranking_need).toBe("top_desc");
expect(result.clarification_gaps).toEqual([]);
expect(result.proof_expectation).toBe("coverage_checked_fact");
});
it("treats incoming-vs-outgoing comparison as an open-scope value need rather than a missing-subject fact ask", () => {
const result = buildAssistantMcpDiscoveryDataNeedGraph({
semanticDataNeed: "counterparty value-flow evidence",
rawUtterance: "что больше: входящие или исходящие деньги за 2020 год?",
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "net_value_flow",
explicit_date_scope: "2020"
}
});
expect(result.business_fact_family).toBe("value_flow");
expect(result.comparison_need).toBe("incoming_vs_outgoing");
expect(result.clarification_gaps).toEqual(["organization"]);
expect(result.decomposition_candidates).toEqual([
"collect_incoming_movements",
"collect_outgoing_movements",
"probe_coverage"
]);
expect(result.reason_codes).toContain("data_need_graph_comparison_incoming_vs_outgoing");
});
it("treats organization-scoped incoming totals as an open-scope value need rather than a missing-subject fact ask", () => {
const result = buildAssistantMcpDiscoveryDataNeedGraph({
semanticDataNeed: "counterparty value-flow evidence",
rawUtterance: "сколько входящих денег за 2020 год по ООО Альтернатива Плюс?",
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_date_scope: "2020",
explicit_organization_scope: "ООО Альтернатива Плюс"
}
});
expect(result.business_fact_family).toBe("value_flow");
expect(result.comparison_need).toBeNull();
expect(result.ranking_need).toBeNull();
expect(result.clarification_gaps).toEqual([]);
expect(result.decomposition_candidates).toEqual([
"collect_scoped_movements",
"aggregate_checked_amounts",
"probe_coverage"
]);
expect(result.reason_codes).toContain("data_need_graph_open_scope_total_without_subject");
});
it("treats a generic incoming total as an understood open-scope ask that still needs organization", () => {
const result = buildAssistantMcpDiscoveryDataNeedGraph({
semanticDataNeed: "counterparty value-flow evidence",
rawUtterance: "сколько входящих денег за 2020 год?",
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_date_scope: "2020"
}
});
expect(result.business_fact_family).toBe("value_flow");
expect(result.subject_candidates).toEqual([]);
expect(result.clarification_gaps).toEqual(["organization"]);
expect(result.proof_expectation).toBe("clarification_required");
expect(result.decomposition_candidates).toEqual([
"collect_scoped_movements",
"aggregate_checked_amounts",
"probe_coverage"
]);
expect(result.reason_codes).toContain("data_need_graph_open_scope_total_without_subject");
expect(result.reason_codes).toContain("data_need_graph_open_scope_total_needs_organization");
});
it("treats all-time open-scope totals as an open-ended period rather than a missing period", () => {
const result = buildAssistantMcpDiscoveryDataNeedGraph({
semanticDataNeed: "counterparty value-flow evidence",
rawUtterance: "сколько вообще денег мы заработали за все время?",
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_organization_scope: "ООО Альтернатива Плюс"
}
});
expect(result.business_fact_family).toBe("value_flow");
expect(result.subject_candidates).toEqual([]);
expect(result.time_scope_need).toBe("all_time_scope");
expect(result.clarification_gaps).toEqual([]);
expect(result.proof_expectation).toBe("coverage_checked_fact");
expect(result.decomposition_candidates).toEqual([
"collect_scoped_movements",
"aggregate_checked_amounts",
"probe_coverage"
]);
expect(result.reason_codes).toContain("data_need_graph_open_scope_total_without_subject");
expect(result.reason_codes).toContain("data_need_graph_all_time_scope_hint");
});
});

View File

@ -249,6 +249,57 @@ describe("assistant MCP discovery pilot executor", () => {
});
});
it("executes catalog drilldown through a narrowed metadata probe seeded from the confirmed surface object", async () => {
const planner = planAssistantMcpDiscovery({
metadataSurface: {
selected_entity_set: "Catalog",
selected_surface_objects: ["Catalog.Counterparties"],
downstream_route_family: "catalog_drilldown",
route_family_selection_basis: "selected_entity_set",
recommended_next_primitive: "drilldown_related_objects",
ambiguity_detected: false,
ambiguity_entity_sets: []
},
turnMeaning: {
asked_domain_family: "metadata",
asked_action_family: "inspect_catalog",
unsupported_but_understood_family: "schema_surface"
}
});
const deps = buildMetadataDeps([
{
FullName: "Catalog.Counterparties",
MetaType: "Catalog",
attributes: [{ Name: "Description" }]
},
{
FullName: "Catalog.Contracts",
MetaType: "Catalog",
attributes: [{ Name: "Owner" }]
}
]);
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
expect(result.pilot_status).toBe("executed");
expect(result.pilot_scope).toBe("metadata_inspection_v1");
expect(result.executed_primitives).toEqual(["inspect_1c_metadata"]);
expect(result.derived_metadata_surface).toMatchObject({
metadata_scope: "Counterparties",
requested_meta_types: ["Catalog"],
available_entity_sets: ["Catalog"],
selected_entity_set: "Catalog",
downstream_route_family: "catalog_drilldown",
recommended_next_primitive: "drilldown_related_objects"
});
expect(result.reason_codes).toContain("pilot_catalog_drilldown_metadata_scope_seeded_from_surface_ref");
expect(deps.executeAddressMcpMetadata).toHaveBeenCalledTimes(1);
expect(deps.executeAddressMcpMetadata.mock.calls[0]?.[0]).toMatchObject({
meta_type: ["Catalog"],
name_mask: "Counterparties"
});
});
it("executes the full entity-resolution chain through the checked counterparty catalog slice", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
@ -378,6 +429,149 @@ describe("assistant MCP discovery pilot executor", () => {
);
});
it("selects a downstream lane from dominant metadata surface objects when one family clearly prevails", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "metadata",
asked_action_family: "inspect_surface",
explicit_entity_candidates: ["НДС"]
}
});
const deps = buildMetadataDeps([
{
FullName: "Документ.СчетФактураВыданный",
MetaType: "Документ",
attributes: [{ Name: "Дата" }]
},
{
FullName: "Документ.СчетФактураПолученный",
MetaType: "Документ",
attributes: [{ Name: "Контрагент" }]
},
{
FullName: "РегистрНакопления.НДСПокупок",
MetaType: "РегистрНакопления",
resources: [{ Name: "СуммаНДС" }]
}
]);
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
expect(result.pilot_status).toBe("executed");
expect(result.derived_metadata_surface).toMatchObject({
metadata_scope: "НДС",
available_entity_sets: ["Документ", "РегистрНакопления"],
selected_entity_set: null,
selected_surface_objects: [
"Документ.СчетФактураВыданный",
"Документ.СчетФактураПолученный"
],
surface_family_scores: {
document_evidence: 2,
movement_evidence: 1,
catalog_drilldown: 0
},
downstream_route_family: "document_evidence",
route_family_selection_basis: "dominant_surface_objects",
recommended_next_primitive: "query_documents",
ambiguity_detected: false,
ambiguity_entity_sets: []
});
expect(result.reason_codes).toContain("pilot_selected_metadata_route_family_from_dominant_surface_objects");
expect(result.evidence.inferred_facts).toContain(
"A likely next checked lane may be inferred as document_evidence from the confirmed metadata surface"
);
expect(result.evidence.unknown_facts).not.toContain(
"Exact downstream metadata surface remains ambiguous across: Документ, РегистрНакопления"
);
});
it("can break a weak metadata family tie by ranking surface objects against the requested scope", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "metadata",
asked_action_family: "inspect_surface",
explicit_entity_candidates: ["НДС"]
}
});
const deps = buildMetadataDeps([
{
FullName: "Document.НДССчетФактура",
MetaType: "Document",
attributes: [{ Name: "Дата" }]
},
{
FullName: "AccumulationRegister.BankOperations",
MetaType: "AccumulationRegister",
resources: [{ Name: "Amount" }]
}
]);
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
expect(result.pilot_status).toBe("executed");
expect(result.derived_metadata_surface).toMatchObject({
metadata_scope: "НДС",
available_entity_sets: ["Document", "AccumulationRegister"],
selected_entity_set: null,
selected_surface_objects: ["Document.НДССчетФактура"],
surface_family_scores: {
document_evidence: 1,
movement_evidence: 1,
catalog_drilldown: 0
},
downstream_route_family: "document_evidence",
route_family_selection_basis: "dominant_surface_objects",
recommended_next_primitive: "query_documents",
ambiguity_detected: false,
ambiguity_entity_sets: [],
surface_object_ranking_applied: true
});
expect(result.reason_codes).toContain("pilot_selected_metadata_route_family_from_surface_object_ranking");
});
it("keeps metadata ambiguity unresolved when surface-family scores are nearly tied", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "metadata",
asked_action_family: "inspect_surface",
explicit_entity_candidates: ["НДС"]
}
});
const deps = buildMetadataDeps([
{ FullName: "Документ.A", MetaType: "Документ" },
{ FullName: "Документ.B", MetaType: "Документ" },
{ FullName: "Документ.C", MetaType: "Документ" },
{ FullName: "Документ.D", MetaType: "Документ" },
{ FullName: "РегистрНакопления.A", MetaType: "РегистрНакопления" },
{ FullName: "РегистрНакопления.B", MetaType: "РегистрНакопления" },
{ FullName: "РегистрНакопления.C", MetaType: "РегистрНакопления" }
]);
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
expect(result.pilot_status).toBe("executed");
expect(result.derived_metadata_surface).toMatchObject({
metadata_scope: "НДС",
selected_entity_set: null,
surface_family_scores: {
document_evidence: 4,
movement_evidence: 3,
catalog_drilldown: 0
},
downstream_route_family: null,
route_family_selection_basis: null,
recommended_next_primitive: null,
ambiguity_detected: true
});
expect(result.derived_metadata_surface?.ambiguity_entity_sets).toContain("Документ");
expect(result.derived_metadata_surface?.ambiguity_entity_sets).toContain("РегистрНакопления");
expect(result.reason_codes).not.toContain("pilot_selected_metadata_route_family_from_dominant_surface_objects");
expect(result.evidence.unknown_facts).toContain(
"Exact downstream metadata surface remains ambiguous across: Документ, РегистрНакопления"
);
});
it("infers metadata entity-set families from object names when meta type columns are absent", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {

View File

@ -4,6 +4,27 @@ import { planAssistantMcpDiscovery } from "../src/services/assistantMcpDiscovery
describe("assistant MCP discovery planner", () => {
it("builds a catalog-compatible value-flow discovery plan from current turn meaning", () => {
const result = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: ["SVK"],
business_fact_family: "value_flow",
action_family: "turnover",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: null,
ranking_need: null,
proof_expectation: "coverage_checked_fact",
clarification_gaps: [],
decomposition_candidates: [
"resolve_entity_reference",
"collect_scoped_movements",
"aggregate_checked_amounts",
"probe_coverage"
],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
reason_codes: ["data_need_graph_built"]
},
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
@ -25,8 +46,11 @@ describe("assistant MCP discovery planner", () => {
expect(result.required_axes).toEqual(["counterparty", "period", "aggregate_axis", "amount", "coverage_target"]);
expect(result.catalog_review.review_status).toBe("catalog_compatible");
expect(result.discovery_plan.answer_may_use_raw_model_claims).toBe(false);
expect(result.data_need_graph?.business_fact_family).toBe("value_flow");
expect(result.discovery_plan.execution_budget.max_probe_count).toBe(30);
expect(result.reason_codes).toContain("planner_enabled_chunked_coverage_probe_budget");
expect(result.reason_codes).toContain("planner_consumed_data_need_graph_v1");
expect(result.reason_codes).toContain("planner_selected_catalog_primitives_from_decomposition_candidates");
});
it("keeps a value-flow plan in clarification state when period axis is missing", () => {
@ -89,8 +113,128 @@ describe("assistant MCP discovery planner", () => {
expect(result.required_axes).toEqual(["counterparty", "coverage_target"]);
});
it("expands a document evidence chain from catalog fact-axis search when decomposition hints are absent", () => {
const result = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: ["SVK"],
business_fact_family: "document_evidence",
action_family: "list_documents",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: null,
ranking_need: null,
proof_expectation: "coverage_checked_fact",
clarification_gaps: [],
decomposition_candidates: [],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
reason_codes: ["data_need_graph_built"]
},
turnMeaning: {
asked_domain_family: "documents",
asked_action_family: "list_documents",
explicit_entity_candidates: ["SVK"],
explicit_date_scope: "2020",
unsupported_but_understood_family: "document_evidence"
}
});
expect(result.planner_status).toBe("ready_for_execution");
expect(result.selected_chain_id).toBe("document_evidence");
expect(result.proposed_primitives).toEqual(["resolve_entity_reference", "query_documents", "probe_coverage"]);
expect(result.reason_codes).toContain("planner_selected_catalog_primitives_from_fact_axis_search");
expect(result.reason_codes).not.toContain("planner_fell_back_to_recipe_primitives_after_empty_catalog_search");
});
it("filters conflicting document-vs-movement primitives when confirmed metadata surface recommends documents", () => {
const result = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: ["SVK"],
business_fact_family: "document_evidence",
action_family: "list_documents",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: null,
ranking_need: null,
proof_expectation: "coverage_checked_fact",
clarification_gaps: [],
decomposition_candidates: ["fetch_scoped_documents", "fetch_scoped_movements", "probe_coverage"],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
reason_codes: ["data_need_graph_built"]
},
metadataSurface: {
selected_entity_set: "Документ",
selected_surface_objects: ["Документ.СчетФактураВыданный"],
downstream_route_family: "document_evidence",
route_family_selection_basis: "selected_entity_set",
recommended_next_primitive: "query_documents",
ambiguity_detected: false,
ambiguity_entity_sets: []
},
turnMeaning: {
asked_domain_family: "documents",
asked_action_family: "list_documents",
explicit_entity_candidates: ["SVK"],
explicit_date_scope: "2020",
unsupported_but_understood_family: "document_evidence"
}
});
expect(result.planner_status).toBe("ready_for_execution");
expect(result.metadata_surface_ref?.recommended_next_primitive).toBe("query_documents");
expect(result.proposed_primitives).toEqual(["resolve_entity_reference", "query_documents", "probe_coverage"]);
expect(result.proposed_primitives).not.toContain("query_movements");
expect(result.reason_codes).toContain("planner_consumed_metadata_surface_ref_v1");
expect(result.reason_codes).toContain("planner_filtered_catalog_primitives_by_confirmed_metadata_surface");
expect(result.reason_codes).toContain("planner_surface_aware_next_lane_from_confirmed_metadata_objects");
});
it("can select document evidence directly from a confirmed metadata surface when the follow-up itself is thin", () => {
const result = planAssistantMcpDiscovery({
metadataSurface: {
selected_entity_set: "Document",
selected_surface_objects: ["Document.InvoiceIssued"],
downstream_route_family: "document_evidence",
route_family_selection_basis: "selected_entity_set",
recommended_next_primitive: "query_documents",
ambiguity_detected: false,
ambiguity_entity_sets: []
},
turnMeaning: {
explicit_entity_candidates: ["SVK"],
explicit_date_scope: "2020"
}
});
expect(result.planner_status).toBe("ready_for_execution");
expect(result.selected_chain_id).toBe("document_evidence");
expect(result.proposed_primitives).toEqual(["resolve_entity_reference", "query_documents", "probe_coverage"]);
expect(result.required_axes).toEqual(["counterparty", "period", "coverage_target"]);
expect(result.reason_codes).toContain("planner_selected_document_from_confirmed_metadata_surface_ref");
expect(result.reason_codes).toContain("planner_selected_catalog_primitives_from_metadata_surface_search");
});
it("builds a movement discovery plan without aggregating value-flow totals", () => {
const result = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: ["SVK"],
business_fact_family: "movement_evidence",
action_family: "list_movements",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: null,
ranking_need: null,
proof_expectation: "coverage_checked_fact",
clarification_gaps: [],
decomposition_candidates: ["resolve_entity_reference", "fetch_scoped_movements", "probe_coverage"],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
reason_codes: ["data_need_graph_built"]
},
turnMeaning: {
asked_domain_family: "movements",
asked_action_family: "list_movements",
@ -107,7 +251,354 @@ describe("assistant MCP discovery planner", () => {
expect(result.proposed_primitives).toEqual(["resolve_entity_reference", "query_movements", "probe_coverage"]);
expect(result.proposed_primitives).not.toContain("aggregate_by_axis");
expect(result.required_axes).toEqual(["counterparty", "period", "coverage_target"]);
expect(result.reason_codes).toContain("planner_selected_movement_recipe");
expect(result.reason_codes).toContain("planner_selected_movement_from_data_need_graph");
expect(result.reason_codes).toContain("planner_selected_catalog_primitives_from_decomposition_candidates");
});
it("filters conflicting document-vs-movement primitives when confirmed metadata surface recommends movements", () => {
const result = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: ["SVK"],
business_fact_family: "movement_evidence",
action_family: "list_movements",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: null,
ranking_need: null,
proof_expectation: "coverage_checked_fact",
clarification_gaps: [],
decomposition_candidates: ["fetch_scoped_documents", "fetch_scoped_movements", "probe_coverage"],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
reason_codes: ["data_need_graph_built"]
},
metadataSurface: {
selected_entity_set: "РегистрНакопления",
selected_surface_objects: ["РегистрНакопления.ДвиженияДенежныхСредств"],
downstream_route_family: "movement_evidence",
route_family_selection_basis: "dominant_surface_objects",
recommended_next_primitive: "query_movements",
ambiguity_detected: false,
ambiguity_entity_sets: []
},
turnMeaning: {
asked_domain_family: "movements",
asked_action_family: "list_movements",
explicit_entity_candidates: ["SVK"],
explicit_date_scope: "2020",
unsupported_but_understood_family: "movement_evidence"
}
});
expect(result.planner_status).toBe("ready_for_execution");
expect(result.metadata_surface_ref?.recommended_next_primitive).toBe("query_movements");
expect(result.proposed_primitives).toEqual(["resolve_entity_reference", "query_movements", "probe_coverage"]);
expect(result.proposed_primitives).not.toContain("query_documents");
expect(result.reason_codes).toContain("planner_filtered_catalog_primitives_by_confirmed_metadata_surface");
});
it("can select movement evidence directly from a confirmed metadata surface when the follow-up itself is thin", () => {
const result = planAssistantMcpDiscovery({
metadataSurface: {
selected_entity_set: "AccumulationRegister",
selected_surface_objects: ["Register.BankOperations"],
downstream_route_family: "movement_evidence",
route_family_selection_basis: "dominant_surface_objects",
recommended_next_primitive: "query_movements",
ambiguity_detected: false,
ambiguity_entity_sets: []
},
turnMeaning: {
explicit_entity_candidates: ["SVK"],
explicit_date_scope: "2020"
}
});
expect(result.planner_status).toBe("ready_for_execution");
expect(result.selected_chain_id).toBe("movement_evidence");
expect(result.proposed_primitives).toEqual(["resolve_entity_reference", "query_movements", "probe_coverage"]);
expect(result.required_axes).toEqual(["counterparty", "period", "coverage_target"]);
expect(result.reason_codes).toContain("planner_selected_movement_from_confirmed_metadata_surface_ref");
expect(result.reason_codes).toContain("planner_selected_catalog_primitives_from_metadata_surface_search");
});
it("can select catalog drilldown directly from a confirmed catalog metadata surface when the follow-up itself is thin", () => {
const result = planAssistantMcpDiscovery({
metadataSurface: {
selected_entity_set: "Catalog",
selected_surface_objects: ["Catalog.Counterparties"],
downstream_route_family: "catalog_drilldown",
route_family_selection_basis: "selected_entity_set",
recommended_next_primitive: "drilldown_related_objects",
ambiguity_detected: false,
ambiguity_entity_sets: []
},
turnMeaning: {
asked_domain_family: "metadata",
asked_action_family: "inspect_catalog",
unsupported_but_understood_family: "schema_surface"
}
});
expect(result.planner_status).toBe("ready_for_execution");
expect(result.selected_chain_id).toBe("catalog_drilldown");
expect(result.proposed_primitives).toEqual(["inspect_1c_metadata"]);
expect(result.required_axes).toEqual(["metadata_scope"]);
expect(result.reason_codes).toContain("planner_selected_catalog_drilldown_from_confirmed_metadata_surface_ref");
expect(result.reason_codes).toContain("planner_selected_catalog_primitives_from_metadata_surface_search");
});
it("does not force a lane from ambiguous metadata surface even when decomposition hints mention both documents and movements", () => {
const result = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: ["SVK"],
business_fact_family: "document_evidence",
action_family: "list_documents",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: null,
ranking_need: null,
proof_expectation: "coverage_checked_fact",
clarification_gaps: [],
decomposition_candidates: ["fetch_scoped_documents", "fetch_scoped_movements", "probe_coverage"],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
reason_codes: ["data_need_graph_built"]
},
metadataSurface: {
selected_entity_set: null,
selected_surface_objects: ["Документ.СчетФактураВыданный", "РегистрНакопления.ДвиженияДенежныхСредств"],
downstream_route_family: null,
route_family_selection_basis: null,
recommended_next_primitive: null,
ambiguity_detected: true,
ambiguity_entity_sets: ["Документ", "РегистрНакопления"]
},
turnMeaning: {
asked_domain_family: "documents",
asked_action_family: "list_documents",
explicit_entity_candidates: ["SVK"],
explicit_date_scope: "2020",
unsupported_but_understood_family: "document_evidence"
}
});
expect(result.planner_status).toBe("ready_for_execution");
expect(result.reason_codes).toContain("planner_consumed_metadata_surface_ref_v1");
expect(result.reason_codes).not.toContain("planner_filtered_catalog_primitives_by_confirmed_metadata_surface");
expect(result.proposed_primitives).toContain("query_documents");
expect(result.proposed_primitives).toContain("query_movements");
});
it("does not force a thin follow-up into a lane when the carried metadata surface is still ambiguous", () => {
const result = planAssistantMcpDiscovery({
metadataSurface: {
selected_entity_set: null,
selected_surface_objects: ["Document.InvoiceIssued", "Register.BankOperations"],
downstream_route_family: null,
route_family_selection_basis: null,
recommended_next_primitive: null,
ambiguity_detected: true,
ambiguity_entity_sets: ["Document", "AccumulationRegister"]
},
turnMeaning: {
explicit_entity_candidates: ["SVK"],
explicit_date_scope: "2020"
}
});
expect(result.planner_status).toBe("ready_for_execution");
expect(result.selected_chain_id).toBe("entity_resolution");
expect(result.reason_codes).toContain("planner_selected_entity_resolution_recipe");
expect(result.reason_codes).not.toContain("planner_selected_document_from_confirmed_metadata_surface_ref");
expect(result.reason_codes).not.toContain("planner_selected_movement_from_confirmed_metadata_surface_ref");
});
it("can select value-flow chain from data need graph even when turn meaning family is still under-specified", () => {
const result = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: ["SVK"],
business_fact_family: "value_flow",
action_family: "net_value_flow",
aggregation_need: "by_month",
time_scope_need: "explicit_period",
comparison_need: "incoming_vs_outgoing",
ranking_need: null,
proof_expectation: "coverage_checked_fact",
clarification_gaps: [],
decomposition_candidates: [
"resolve_entity_reference",
"collect_incoming_movements",
"collect_outgoing_movements",
"aggregate_by_month",
"probe_coverage"
],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
reason_codes: ["data_need_graph_built"]
},
turnMeaning: {
explicit_entity_candidates: ["SVK"],
explicit_date_scope: "2020",
asked_aggregation_axis: "month"
}
});
expect(result.planner_status).toBe("ready_for_execution");
expect(result.selected_chain_id).toBe("value_flow");
expect(result.proposed_primitives).toEqual([
"resolve_entity_reference",
"query_movements",
"aggregate_by_axis",
"probe_coverage"
]);
expect(result.required_axes).toEqual([
"counterparty",
"period",
"aggregate_axis",
"amount",
"coverage_target",
"calendar_month"
]);
expect(result.reason_codes).toContain("planner_selected_monthly_value_flow_from_data_need_graph");
});
it("does not collapse a ranking-shaped value graph into entity-resolution just because no subject is preselected", () => {
const result = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
business_fact_family: "value_flow",
action_family: "turnover",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: null,
ranking_need: "top_desc",
proof_expectation: "coverage_checked_fact",
clarification_gaps: [],
decomposition_candidates: ["collect_scoped_movements", "aggregate_ranked_axis_values", "probe_coverage"],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
reason_codes: ["data_need_graph_built", "data_need_graph_ranking_top_desc"]
},
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_date_scope: "2020"
}
});
expect(result.planner_status).toBe("needs_clarification");
expect(result.selected_chain_id).toBe("value_flow_ranking");
expect(result.proposed_primitives).toEqual(["query_movements", "aggregate_by_axis", "probe_coverage"]);
expect(result.required_axes).toEqual(["period", "aggregate_axis", "amount", "coverage_target"]);
expect(result.catalog_review.review_status).toBe("needs_more_axes");
expect(result.reason_codes).toContain("planner_selected_top_ranked_value_flow_from_data_need_graph");
expect(result.selected_chain_id).not.toBe("entity_resolution");
});
it("keeps ranked value-flow ready for execution once checked period and organization are known", () => {
const result = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
business_fact_family: "value_flow",
action_family: "turnover",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: null,
ranking_need: "top_desc",
proof_expectation: "coverage_checked_fact",
clarification_gaps: [],
decomposition_candidates: ["collect_scoped_movements", "aggregate_ranked_axis_values", "probe_coverage"],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
reason_codes: ["data_need_graph_built", "data_need_graph_ranking_top_desc"]
},
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_date_scope: "2020",
explicit_organization_scope: "ООО Альтернатива Плюс"
}
});
expect(result.planner_status).toBe("ready_for_execution");
expect(result.selected_chain_id).toBe("value_flow_ranking");
expect(result.proposed_primitives).toEqual(["query_movements", "aggregate_by_axis", "probe_coverage"]);
expect(result.required_axes).toEqual(["organization", "period", "aggregate_axis", "amount", "coverage_target"]);
expect(result.catalog_review.review_status).toBe("catalog_compatible");
expect(result.reason_codes).toContain("planner_selected_top_ranked_value_flow_from_data_need_graph");
});
it("does not collapse incoming-vs-outgoing comparison into entity-resolution when no counterparty is preselected", () => {
const result = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
business_fact_family: "value_flow",
action_family: "net_value_flow",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: "incoming_vs_outgoing",
ranking_need: null,
proof_expectation: "coverage_checked_fact",
clarification_gaps: [],
decomposition_candidates: ["collect_incoming_movements", "collect_outgoing_movements", "probe_coverage"],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
reason_codes: ["data_need_graph_built", "data_need_graph_comparison_incoming_vs_outgoing"]
},
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "net_value_flow",
explicit_date_scope: "2020"
}
});
expect(result.planner_status).toBe("needs_clarification");
expect(result.selected_chain_id).toBe("value_flow_comparison");
expect(result.proposed_primitives).toEqual(["query_movements", "probe_coverage"]);
expect(result.required_axes).toEqual(["period", "amount", "coverage_target"]);
expect(result.reason_codes).toContain("planner_selected_bidirectional_value_flow_comparison_from_data_need_graph");
expect(result.selected_chain_id).not.toBe("entity_resolution");
});
it("keeps bidirectional comparison ready for execution once checked period and organization are known", () => {
const result = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
business_fact_family: "value_flow",
action_family: "net_value_flow",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: "incoming_vs_outgoing",
ranking_need: null,
proof_expectation: "coverage_checked_fact",
clarification_gaps: [],
decomposition_candidates: ["collect_incoming_movements", "collect_outgoing_movements", "probe_coverage"],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
reason_codes: ["data_need_graph_built", "data_need_graph_comparison_incoming_vs_outgoing"]
},
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "net_value_flow",
explicit_date_scope: "2020",
explicit_organization_scope: "ООО Альтернатива Плюс"
}
});
expect(result.planner_status).toBe("ready_for_execution");
expect(result.selected_chain_id).toBe("value_flow_comparison");
expect(result.proposed_primitives).toEqual(["query_movements", "probe_coverage"]);
expect(result.required_axes).toEqual(["organization", "period", "amount", "coverage_target"]);
expect(result.catalog_review.review_status).toBe("catalog_compatible");
expect(result.reason_codes).toContain("planner_selected_bidirectional_value_flow_comparison_from_data_need_graph");
});
it("builds an inference-safe lifecycle plan with evidence explanation", () => {
@ -216,4 +707,76 @@ describe("assistant MCP discovery planner", () => {
expect(result.selected_chain_summary).toContain("resolve the most relevant 1C reference");
expect(result.proposed_primitives).toEqual(["search_business_entity", "resolve_entity_reference", "probe_coverage"]);
});
it("keeps organization-scoped one-sided value-flow totals executable without forcing a counterparty", () => {
const result = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
business_fact_family: "value_flow",
action_family: "turnover",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: null,
ranking_need: null,
proof_expectation: "coverage_checked_fact",
clarification_gaps: [],
decomposition_candidates: ["collect_scoped_movements", "aggregate_checked_amounts", "probe_coverage"],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
reason_codes: ["data_need_graph_built", "data_need_graph_open_scope_total_without_subject"]
},
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_date_scope: "2020",
explicit_organization_scope: "ООО Альтернатива Плюс"
}
});
expect(result.planner_status).toBe("ready_for_execution");
expect(result.semantic_data_need).toBe("organization-scoped value-flow evidence");
expect(result.selected_chain_id).toBe("value_flow");
expect(result.proposed_primitives).toEqual(["query_movements", "aggregate_by_axis", "probe_coverage"]);
expect(result.required_axes).toEqual(["organization", "period", "aggregate_axis", "amount", "coverage_target"]);
expect(result.catalog_review.review_status).toBe("catalog_compatible");
expect(result.reason_codes).toContain("planner_selected_open_scope_value_flow_total_from_data_need_graph");
});
it("keeps generic one-sided open totals in organization clarification instead of forcing entity resolution", () => {
const result = planAssistantMcpDiscovery({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
business_fact_family: "value_flow",
action_family: "turnover",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: null,
ranking_need: null,
proof_expectation: "clarification_required",
clarification_gaps: ["organization"],
decomposition_candidates: ["collect_scoped_movements", "aggregate_checked_amounts", "probe_coverage"],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
reason_codes: [
"data_need_graph_built",
"data_need_graph_open_scope_total_without_subject",
"data_need_graph_open_scope_total_needs_organization"
]
},
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_date_scope: "2020"
}
});
expect(result.planner_status).toBe("needs_clarification");
expect(result.selected_chain_id).toBe("value_flow");
expect(result.proposed_primitives).toEqual(["query_movements", "aggregate_by_axis", "probe_coverage"]);
expect(result.required_axes).toEqual(["period", "organization", "aggregate_axis", "amount", "coverage_target"]);
expect(result.catalog_review.review_status).toBe("needs_more_axes");
expect(result.catalog_review.missing_axes_by_primitive.query_movements).toContainEqual(["organization"]);
expect(result.reason_codes).toContain("planner_selected_open_scope_value_flow_total_from_data_need_graph");
expect(result.selected_chain_id).not.toBe("entity_resolution");
});
});

View File

@ -397,4 +397,41 @@ describe("assistant MCP discovery response candidate", () => {
expect(candidate.reply_text).toBeNull();
expect(candidate.eligible_for_future_hot_runtime).toBe(false);
});
it("localizes open-scope bidirectional comparison scope and probe-limit wording without contour garbage", () => {
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
entryPoint({
bridge: {
bridge_status: "answer_draft_ready",
user_facing_response_allowed: true,
business_fact_answer_allowed: true,
requires_user_clarification: false,
answer_draft: {
answer_mode: "confirmed_with_bounded_inference",
headline:
"\u041f\u043e \u0434\u0430\u043d\u043d\u044b\u043c 1\u0421 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0441\u0442\u0440\u043e\u043a\u0438 \u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0445 \u0438 \u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0445 \u0434\u0435\u043d\u0435\u0436\u043d\u044b\u0445 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0439; \u043d\u0435\u0442\u0442\u043e \u043c\u043e\u0436\u043d\u043e \u043d\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043a\u0430\u043a \u0440\u0430\u0441\u0447\u0435\u0442 \u043f\u043e \u043d\u0430\u0439\u0434\u0435\u043d\u043d\u044b\u043c \u0441\u0442\u0440\u043e\u043a\u0430\u043c \u0438 \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u043d\u043d\u043e\u043c\u0443 \u043f\u0435\u0440\u0438\u043e\u0434\u0443.",
confirmed_lines: [
"1C bidirectional value-flow rows were checked for the requested counterparty scope: incoming=found, outgoing=found"
],
inference_lines: [],
unknown_lines: [],
limitation_lines: [
"Requested period hit the MCP row limit, but the approved monthly recovery probe budget is smaller than the required subperiod count"
],
next_step_line: null
}
}
})
);
expect(candidate.reply_text).toContain(
"\u0412 1\u0421 \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u043d\u044b \u0432\u0445\u043e\u0434\u044f\u0449\u0438\u0435 \u0438 \u0438\u0441\u0445\u043e\u0434\u044f\u0449\u0438\u0435 \u0434\u0435\u043d\u0435\u0436\u043d\u044b\u0435 \u0441\u0442\u0440\u043e\u043a\u0438 \u0432 \u0437\u0430\u043f\u0440\u043e\u0448\u0435\u043d\u043d\u043e\u043c \u0441\u0440\u0435\u0437\u0435"
);
expect(candidate.reply_text).toContain(
"\u0417\u0430\u043f\u0440\u043e\u0448\u0435\u043d\u043d\u044b\u0439 \u043f\u0435\u0440\u0438\u043e\u0434 \u0443\u043f\u0435\u0440\u0441\u044f \u0432 \u043b\u0438\u043c\u0438\u0442 \u0441\u0442\u0440\u043e\u043a MCP"
);
expect(candidate.reply_text).not.toContain(
"\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0441\u043a\u043e\u043c\u0443 \u043a\u043e\u043d\u0442\u0443\u0440\u0443"
);
expect(candidate.reply_text).not.toContain("Requested period hit the MCP row limit");
});
});

View File

@ -161,6 +161,52 @@ describe("assistant MCP discovery response policy", () => {
expect(result.reason_codes).toContain("mcp_discovery_response_policy_keep_aligned_factual_address_reply");
});
it("does not treat an open-scope value-flow total as aligned with exact top-counterparty carryover", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: "\u0421\u0430\u043c\u044b\u0439 \u0434\u043e\u0445\u043e\u0434\u043d\u044b\u0439 \u043a\u043b\u0438\u0435\u043d\u0442: \u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a.",
currentReplySource: "address_query_runtime_v1",
currentReplyType: "factual",
addressRuntimeMeta: {
detected_intent: "customer_revenue_and_payments",
dialogContinuationContract: {
target_intent: "customer_revenue_and_payments"
},
truth_gate_contract_status: "full_confirmed",
assistant_truth_answer_policy_v1: {
truth_gate: {
coverage_status: "full",
grounding_status: "grounded",
source_truth_gate_status: "full_confirmed"
}
},
assistant_mcp_discovery_entry_point_v1: entryPoint({
turn_input: {
adapter_status: "ready",
should_run_discovery: true,
data_need_graph: {
business_fact_family: "value_flow",
subject_candidates: [],
reason_codes: ["data_need_graph_open_scope_total_without_subject"]
},
turn_meaning_ref: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_organization_scope: "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441",
explicit_date_scope: "2020"
}
}
})
}
});
expect(result.applied).toBe(true);
expect(result.decision).toBe("apply_candidate");
expect(result.reason_codes).toContain("mcp_discovery_response_policy_semantic_conflict_allows_candidate_override");
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_aligned_factual_address_reply");
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_factual_address_continuation_target");
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_full_confirmed_factual_address_reply");
});
it("keeps factual address follow-up replies when they already match the continuation target intent", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: "ИП Калинин Н.М. | сумма: 216600 | операций: 2",
@ -189,6 +235,121 @@ describe("assistant MCP discovery response policy", () => {
expect(result.reason_codes).toContain("mcp_discovery_response_policy_keep_factual_address_continuation_target");
});
it("overrides an exact ranking-shaped address reply when open-scope ranking still needs organization", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply:
"Самый доходный год по подтвержденным поступлениям: 2020 (15 744 052,48 ₽ по 20 операциям).",
currentReplySource: "address_query_runtime_v1",
currentReplyType: "factual",
addressRuntimeMeta: {
detected_intent: "customer_revenue_and_payments",
assistant_mcp_discovery_entry_point_v1: entryPoint({
turn_input: {
adapter_status: "ready",
should_run_discovery: true,
data_need_graph: {
business_fact_family: "value_flow",
subject_candidates: [],
ranking_need: "top_desc",
clarification_gaps: ["organization"],
reason_codes: ["data_need_graph_built", "data_need_graph_ranking_top_desc"]
},
turn_meaning_ref: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_date_scope: "2020",
seeded_ranking_need: "top_desc"
}
},
bridge: {
bridge_status: "answer_draft_ready",
user_facing_response_allowed: true,
business_fact_answer_allowed: false,
requires_user_clarification: true,
answer_draft: {
answer_mode: "needs_clarification",
headline: "Нужно уточнить организацию.",
confirmed_lines: [],
inference_lines: [],
unknown_lines: ["Без организации поиск по контрагентам не запустить."],
limitation_lines: [],
next_step_line: "Уточните организацию, и я продолжу поиск по контрагентам."
}
}
})
}
});
expect(result.applied).toBe(true);
expect(result.decision).toBe("apply_candidate");
expect(result.reason_codes).toContain("mcp_discovery_response_policy_semantic_conflict_allows_candidate_override");
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_aligned_factual_address_reply");
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_factual_address_continuation_target");
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_full_confirmed_factual_address_reply");
});
it("overrides an exact ranking-shaped address reply when bounded open-scope ranking already has organization and period", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply:
"Самый доходный клиент за доступное время по подтвержденным поступлениям: Группа СВК (12 224 925,00 ₽ по 16 операциям).",
currentReplySource: "address_query_runtime_v1",
currentReplyType: "factual",
addressRuntimeMeta: {
detected_intent: "customer_revenue_and_payments",
dialogContinuationContract: {
target_intent: "customer_revenue_and_payments"
},
assistant_mcp_discovery_entry_point_v1: entryPoint({
turn_input: {
adapter_status: "ready",
should_run_discovery: true,
data_need_graph: {
business_fact_family: "value_flow",
subject_candidates: [],
ranking_need: "top_desc",
clarification_gaps: [],
reason_codes: ["data_need_graph_built", "data_need_graph_ranking_top_desc"]
},
turn_meaning_ref: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020",
seeded_ranking_need: "top_desc"
}
},
bridge: {
bridge_status: "answer_draft_ready",
user_facing_response_allowed: true,
business_fact_answer_allowed: true,
requires_user_clarification: false,
answer_draft: {
answer_mode: "confirmed_with_bounded_inference",
headline: "Рейтинг по контрагентам построен по подтвержденным строкам 1С.",
confirmed_lines: [
"Больше всего денег принёс контрагент СБЕРБАНК, ПАО по организации ООО Альтернатива Плюс за период 2020: 12 792 194,31 руб. по 9 строкам с суммой."
],
inference_lines: [
"Рейтинг по контрагентам по организации ООО Альтернатива Плюс за период 2020 рассчитан только по подтвержденным строкам 1С."
],
unknown_lines: ["Полный исторический рейтинг вне проверенного окна не доказан."],
limitation_lines: [],
next_step_line: null
}
}
})
}
});
expect(result.applied).toBe(true);
expect(result.decision).toBe("apply_candidate");
expect(result.reply_text).toContain("ООО Альтернатива Плюс");
expect(result.reply_text).toContain("2020");
expect(result.reason_codes).toContain("mcp_discovery_response_policy_semantic_conflict_allows_candidate_override");
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_aligned_factual_address_reply");
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_keep_factual_address_continuation_target");
});
it("keeps full-confirmed factual address replies even when discovery has a guarded candidate", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: "ООО Ромашка | сумма: 128000 | операций: 3",

View File

@ -13,6 +13,30 @@ function buildDeps(rows: Array<Record<string, unknown>>, error: string | null =
};
}
function buildBidirectionalDeps(
incomingRows: Array<Record<string, unknown>>,
outgoingRows: Array<Record<string, unknown>>
) {
return {
executeAddressMcpQuery: vi
.fn()
.mockResolvedValueOnce({
fetched_rows: incomingRows.length,
matched_rows: incomingRows.length,
raw_rows: incomingRows,
rows: incomingRows,
error: null
})
.mockResolvedValueOnce({
fetched_rows: outgoingRows.length,
matched_rows: outgoingRows.length,
raw_rows: outgoingRows,
rows: outgoingRows,
error: null
})
};
}
describe("assistant MCP discovery runtime bridge", () => {
it("composes planner, pilot executor, and answer draft without wiring the hot runtime", async () => {
const result = await runAssistantMcpDiscoveryRuntimeBridge({
@ -51,6 +75,166 @@ describe("assistant MCP discovery runtime bridge", () => {
expect(result.answer_draft.next_step_line).toContain("Уточните контрагента");
});
it("keeps ranked value-flow in clarification without asking for a counterparty when only the period is known", async () => {
const result = await runAssistantMcpDiscoveryRuntimeBridge({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
business_fact_family: "value_flow",
action_family: "turnover",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: null,
ranking_need: "top_desc",
proof_expectation: "coverage_checked_fact",
clarification_gaps: [],
decomposition_candidates: ["collect_scoped_movements", "aggregate_ranked_axis_values", "probe_coverage"],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
reason_codes: ["data_need_graph_built", "data_need_graph_ranking_top_desc"]
},
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_date_scope: "2020"
},
deps: buildDeps([])
});
expect(result.bridge_status).toBe("needs_clarification");
expect(result.requires_user_clarification).toBe(true);
expect(result.pilot.mcp_execution_performed).toBe(false);
expect(result.planner.selected_chain_id).toBe("value_flow_ranking");
expect(result.answer_draft.headline).toContain("рейтинг");
expect(result.answer_draft.next_step_line).toContain("организацию");
expect(result.answer_draft.next_step_line).not.toContain("Уточните контрагента");
});
it("produces a bounded ranked value-flow answer when period and organization are known", async () => {
const result = await runAssistantMcpDiscoveryRuntimeBridge({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
business_fact_family: "value_flow",
action_family: "turnover",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: null,
ranking_need: "top_desc",
proof_expectation: "coverage_checked_fact",
clarification_gaps: [],
decomposition_candidates: ["collect_scoped_movements", "aggregate_ranked_axis_values", "probe_coverage"],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
reason_codes: ["data_need_graph_built", "data_need_graph_ranking_top_desc"]
},
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_date_scope: "2020",
explicit_organization_scope: "ООО Альтернатива Плюс"
},
deps: buildDeps([
{ Period: "2020-01-10T00:00:00", Amount: 1200, Counterparty: "СВК-А" },
{ Period: "2020-03-11T00:00:00", Amount: 800, Counterparty: "СВК-Б" },
{ Period: "2020-05-12T00:00:00", Amount: 900, Counterparty: "СВК-А" }
])
});
expect(result.bridge_status).toBe("answer_draft_ready");
expect(result.business_fact_answer_allowed).toBe(true);
expect(result.planner.selected_chain_id).toBe("value_flow_ranking");
expect(result.pilot.derived_ranked_value_flow?.ranked_values[0]).toMatchObject({
axis_value: "СВК-А",
total_amount: 2100
});
expect(result.answer_draft.confirmed_lines.join("\n")).toContain("СВК-А");
});
it("keeps open bidirectional comparison in clarification without asking for a counterparty when only the period is known", async () => {
const result = await runAssistantMcpDiscoveryRuntimeBridge({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
business_fact_family: "value_flow",
action_family: "net_value_flow",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: "incoming_vs_outgoing",
ranking_need: null,
proof_expectation: "coverage_checked_fact",
clarification_gaps: [],
decomposition_candidates: ["collect_incoming_movements", "collect_outgoing_movements", "probe_coverage"],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
reason_codes: ["data_need_graph_built", "data_need_graph_comparison_incoming_vs_outgoing"]
},
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "net_value_flow",
explicit_date_scope: "2020"
},
deps: buildDeps([])
});
expect(result.bridge_status).toBe("needs_clarification");
expect(result.requires_user_clarification).toBe(true);
expect(result.pilot.mcp_execution_performed).toBe(false);
expect(result.planner.selected_chain_id).toBe("value_flow_comparison");
expect(result.answer_draft.headline).toContain("входящий и исходящий");
expect(result.answer_draft.next_step_line).toContain("организацию");
expect(result.answer_draft.next_step_line).not.toContain("Уточните контрагента");
});
it("produces a bounded bidirectional comparison answer when period and organization are known", async () => {
const result = await runAssistantMcpDiscoveryRuntimeBridge({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
business_fact_family: "value_flow",
action_family: "net_value_flow",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: "incoming_vs_outgoing",
ranking_need: null,
proof_expectation: "coverage_checked_fact",
clarification_gaps: [],
decomposition_candidates: ["collect_incoming_movements", "collect_outgoing_movements", "probe_coverage"],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
reason_codes: ["data_need_graph_built", "data_need_graph_comparison_incoming_vs_outgoing"]
},
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "net_value_flow",
explicit_date_scope: "2020",
explicit_organization_scope: "ООО Альтернатива Плюс"
},
deps: buildBidirectionalDeps(
[
{ Period: "2020-01-10T00:00:00", Amount: 3200, Counterparty: "СВК-А" },
{ Period: "2020-04-11T00:00:00", Amount: 1800, Counterparty: "СВК-Б" }
],
[{ Period: "2020-02-12T00:00:00", Amount: 1400, Counterparty: "СВК-А" }]
)
});
expect(result.bridge_status).toBe("answer_draft_ready");
expect(result.business_fact_answer_allowed).toBe(true);
expect(result.planner.selected_chain_id).toBe("value_flow_comparison");
expect(result.pilot.derived_bidirectional_value_flow).toMatchObject({
period_scope: "2020",
incoming_customer_revenue: {
total_amount: 5000
},
outgoing_supplier_payout: {
total_amount: 1400
}
});
expect(result.answer_draft.confirmed_lines.join("\n")).toContain("получили");
expect(result.answer_draft.confirmed_lines.join("\n")).toContain("заплатили");
});
it("keeps document-ready plans bounded when the pilot finds no confirmed rows", async () => {
const result = await runAssistantMcpDiscoveryRuntimeBridge({
turnMeaning: {
@ -69,6 +253,41 @@ describe("assistant MCP discovery runtime bridge", () => {
expect(result.reason_codes).toContain("runtime_bridge_status_checked_sources_only");
});
it("keeps document evidence executable when the planner expands primitives from fact-axis search", async () => {
const result = await runAssistantMcpDiscoveryRuntimeBridge({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: ["SVK"],
business_fact_family: "document_evidence",
action_family: "list_documents",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: null,
ranking_need: null,
proof_expectation: "coverage_checked_fact",
clarification_gaps: [],
decomposition_candidates: [],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
reason_codes: ["data_need_graph_built"]
},
turnMeaning: {
asked_domain_family: "documents",
asked_action_family: "list_documents",
explicit_entity_candidates: ["SVK"],
explicit_date_scope: "2020",
unsupported_but_understood_family: "document_evidence"
},
deps: buildDeps([{ Period: "2020-01-15T00:00:00", Registrator: "DOC-1", Counterparty: "SVK" }])
});
expect(result.bridge_status).toBe("answer_draft_ready");
expect(result.planner.selected_chain_id).toBe("document_evidence");
expect(result.planner.proposed_primitives).toEqual(["resolve_entity_reference", "query_documents", "probe_coverage"]);
expect(result.planner.reason_codes).toContain("planner_selected_catalog_primitives_from_fact_axis_search");
expect(result.business_fact_answer_allowed).toBe(true);
});
it("preserves the answer adapter boundary against internal mechanics leakage", async () => {
const result = await runAssistantMcpDiscoveryRuntimeBridge({
turnMeaning: {
@ -91,4 +310,83 @@ describe("assistant MCP discovery runtime bridge", () => {
expect(userFacing).not.toContain("runtime_bridge");
expect(userFacing).not.toContain("primitive");
});
it("produces a bounded one-sided value-flow answer for an organization-scoped total without inventing a counterparty", async () => {
const result = await runAssistantMcpDiscoveryRuntimeBridge({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
business_fact_family: "value_flow",
action_family: "turnover",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: null,
ranking_need: null,
proof_expectation: "coverage_checked_fact",
clarification_gaps: [],
decomposition_candidates: ["collect_scoped_movements", "aggregate_checked_amounts", "probe_coverage"],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
reason_codes: ["data_need_graph_built", "data_need_graph_open_scope_total_without_subject"]
},
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_date_scope: "2020",
explicit_organization_scope: "ООО Альтернатива Плюс"
},
deps: buildDeps([
{ Period: "2020-01-10T00:00:00", Amount: 3200, Counterparty: "Клиент-А" },
{ Period: "2020-05-22T00:00:00", Amount: 1800, Counterparty: "Клиент-Б" }
])
});
expect(result.bridge_status).toBe("answer_draft_ready");
expect(result.business_fact_answer_allowed).toBe(true);
expect(result.planner.selected_chain_id).toBe("value_flow");
expect(result.pilot.derived_value_flow).toMatchObject({
counterparty: null,
period_scope: "2020",
total_amount: 5000
});
expect(result.answer_draft.confirmed_lines.join("\n")).toContain("5 000");
expect(result.answer_draft.confirmed_lines.join("\n")).not.toContain("контрагенту");
});
it("keeps generic one-sided open totals in organization clarification without asking for a counterparty", async () => {
const result = await runAssistantMcpDiscoveryRuntimeBridge({
dataNeedGraph: {
schema_version: "assistant_data_need_graph_v1",
policy_owner: "assistantMcpDiscoveryDataNeedGraph",
subject_candidates: [],
business_fact_family: "value_flow",
action_family: "turnover",
aggregation_need: null,
time_scope_need: "explicit_period",
comparison_need: null,
ranking_need: null,
proof_expectation: "clarification_required",
clarification_gaps: ["organization"],
decomposition_candidates: ["collect_scoped_movements", "aggregate_checked_amounts", "probe_coverage"],
forbidden_overclaim_flags: ["no_raw_model_claims", "no_unchecked_fact_totals"],
reason_codes: [
"data_need_graph_built",
"data_need_graph_open_scope_total_without_subject",
"data_need_graph_open_scope_total_needs_organization"
]
},
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_date_scope: "2020"
},
deps: buildDeps([])
});
expect(result.bridge_status).toBe("needs_clarification");
expect(result.requires_user_clarification).toBe(true);
expect(result.planner.selected_chain_id).toBe("value_flow");
expect(result.answer_draft.next_step_line).toContain("\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\u0438\u044e");
expect(result.answer_draft.next_step_line).not.toContain(
"\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0430"
);
});
});

View File

@ -13,6 +13,30 @@ function buildDeps(rows: Array<Record<string, unknown>>, error: string | null =
};
}
function buildBidirectionalDeps(
incomingRows: Array<Record<string, unknown>>,
outgoingRows: Array<Record<string, unknown>>
) {
return {
executeAddressMcpQuery: vi
.fn()
.mockResolvedValueOnce({
fetched_rows: incomingRows.length,
matched_rows: incomingRows.length,
raw_rows: incomingRows,
rows: incomingRows,
error: null
})
.mockResolvedValueOnce({
fetched_rows: outgoingRows.length,
matched_rows: outgoingRows.length,
raw_rows: outgoingRows,
rows: outgoingRows,
error: null
})
};
}
function buildMetadataDeps(rows: Array<Record<string, unknown>>, error: string | null = null) {
return {
executeAddressMcpMetadata: vi.fn(async () => ({
@ -240,4 +264,248 @@ describe("assistant MCP discovery runtime entry point", () => {
expect(result.bridge?.pilot.pilot_scope).toBe("counterparty_document_evidence_query_documents_v1");
expect(result.bridge?.answer_draft.answer_mode).toBe("confirmed_with_bounded_inference");
});
it("runs raw incoming-vs-outgoing comparison as an open-scope value-flow chain without inventing a counterparty", async () => {
const result = await runAssistantMcpDiscoveryRuntimeEntryPoint({
userMessage: "что больше: входящие или исходящие деньги за 2020 год по ООО Альтернатива Плюс?",
predecomposeContract: {
entities: { organization: "ООО Альтернатива Плюс" },
period: { period_from: "2020-01-01", period_to: "2020-12-31" }
},
deps: buildBidirectionalDeps(
[
{ Period: "2020-01-15T00:00:00", Amount: 2500, Counterparty: "Клиент-А" },
{ Period: "2020-06-20T00:00:00", Amount: 1000, Counterparty: "Клиент-Б" }
],
[{ Period: "2020-02-18T00:00:00", Amount: 900, Counterparty: "Поставщик-А" }]
)
});
expect(result.entry_status).toBe("bridge_executed");
expect(result.discovery_attempted).toBe(true);
expect(result.turn_input.data_need_graph?.comparison_need).toBe("incoming_vs_outgoing");
expect(result.turn_input.turn_meaning_ref).toMatchObject({
asked_domain_family: "counterparty_value",
asked_action_family: "net_value_flow",
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020"
});
expect(result.turn_input.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.bridge?.planner.selected_chain_id).toBe("value_flow_comparison");
expect(result.bridge?.pilot.pilot_scope).toBe("counterparty_bidirectional_value_flow_query_movements_v1");
expect(result.bridge?.answer_draft.confirmed_lines.join("\n")).toContain("получили");
expect(result.bridge?.answer_draft.confirmed_lines.join("\n")).toContain("заплатили");
});
it.skip("keeps mirrored predecompose organization and counterparty out of the subject lane for open comparison", async () => {
const result = await runAssistantMcpDiscoveryRuntimeEntryPoint({
userMessage: "что больше: входящие или исходящие деньги за 2020 год по ООО Альтернатива Плюс?",
predecomposeContract: {
entities: {
counterparty: "ООО Альтернатива Плюс",
organization: "ООО Альтернатива Плюс"
},
period: { period_from: "2020-01-01", period_to: "2020-12-31" }
},
deps: buildBidirectionalDeps(
[{ Period: "2020-01-15T00:00:00", Amount: 2500, Counterparty: "Клиент-А" }],
[{ Period: "2020-02-18T00:00:00", Amount: 900, Counterparty: "Поставщик-А" }]
)
});
expect(result.entry_status).toBe("bridge_executed");
expect(result.turn_input.turn_meaning_ref).toMatchObject({
asked_domain_family: "counterparty_value",
asked_action_family: "net_value_flow",
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020"
});
expect(result.turn_input.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.turn_input.data_need_graph?.subject_candidates).toEqual([]);
expect(result.bridge?.planner.selected_chain_id).toBe("value_flow_comparison");
});
it.skip("keeps mirrored predecompose organization and counterparty out of the subject lane for open comparison (utf8-safe)", async () => {
const orgName = "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
const result = await runAssistantMcpDiscoveryRuntimeEntryPoint({
userMessage: "что больше: входящие или исходящие деньги за 2020 год по ООО Альтернатива Плюс?",
predecomposeContract: {
entities: {
counterparty: orgName,
organization: orgName
},
period: { period_from: "2020-01-01", period_to: "2020-12-31" }
},
deps: buildBidirectionalDeps(
[{ Period: "2020-01-15T00:00:00", Amount: 2500, Counterparty: "\u041a\u043b\u0438\u0435\u043d\u0442-\u0410" }],
[{ Period: "2020-02-18T00:00:00", Amount: 900, Counterparty: "\u041f\u043e\u0441\u0442\u0430\u0432\u0449\u0438\u043a-\u0410" }]
)
});
expect(result.entry_status).toBe("bridge_executed");
expect(result.turn_input.turn_meaning_ref).toMatchObject({
asked_domain_family: "counterparty_value",
asked_action_family: "net_value_flow",
explicit_organization_scope: orgName,
explicit_date_scope: "2020"
});
expect(result.turn_input.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.turn_input.data_need_graph?.subject_candidates).toEqual([]);
expect(result.bridge?.planner.selected_chain_id).toBe("value_flow_comparison");
});
it("runs raw organization-scoped incoming totals as an open value-flow chain without inventing a counterparty", async () => {
const result = await runAssistantMcpDiscoveryRuntimeEntryPoint({
userMessage: "сколько входящих денег за 2020 год по ООО Альтернатива Плюс?",
predecomposeContract: {
entities: { organization: "ООО Альтернатива Плюс" },
period: { period_from: "2020-01-01", period_to: "2020-12-31" }
},
deps: buildDeps([
{ Period: "2020-01-15T00:00:00", Amount: 2500, Counterparty: "Клиент-А" },
{ Period: "2020-06-20T00:00:00", Amount: 1000, Counterparty: "Клиент-Б" }
])
});
expect(result.entry_status).toBe("bridge_executed");
expect(result.discovery_attempted).toBe(true);
expect(result.turn_input.turn_meaning_ref).toMatchObject({
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020"
});
expect(result.turn_input.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.turn_input.data_need_graph?.subject_candidates).toEqual([]);
expect(result.bridge?.planner.selected_chain_id).toBe("value_flow");
expect(result.bridge?.pilot.pilot_scope).toBe("counterparty_value_flow_query_movements_v1");
expect(result.bridge?.answer_draft.confirmed_lines.join("\n")).toContain("входящ");
});
it("runs raw organization-scoped outgoing totals as an open payout chain without inventing a counterparty", async () => {
const orgName = "ООО Альтернатива Плюс";
const result = await runAssistantMcpDiscoveryRuntimeEntryPoint({
userMessage: "сколько исходящих денег за 2020 год по ООО Альтернатива Плюс?",
predecomposeContract: {
entities: { counterparty: orgName, organization: orgName },
period: { period_from: "2020-01-01", period_to: "2020-12-31" }
},
deps: buildDeps([
{ Period: "2020-02-18T00:00:00", Amount: 900, Counterparty: "Поставщик-А" },
{ Period: "2020-08-07T00:00:00", Amount: 300, Counterparty: "Поставщик-Б" }
])
});
expect(result.entry_status).toBe("bridge_executed");
expect(result.discovery_attempted).toBe(true);
expect(result.turn_input.turn_meaning_ref).toMatchObject({
asked_domain_family: "counterparty_value",
asked_action_family: "payout",
explicit_organization_scope: orgName,
explicit_date_scope: "2020"
});
expect(result.turn_input.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.bridge?.planner.selected_chain_id).toBe("value_flow");
expect(result.bridge?.pilot.pilot_scope).toBe("counterparty_supplier_payout_query_movements_v1");
expect(result.bridge?.answer_draft.confirmed_lines.join("\n")).toContain("исход");
});
it("keeps a generic incoming total in organization clarification instead of asking for counterparty", async () => {
const result = await runAssistantMcpDiscoveryRuntimeEntryPoint({
userMessage: "сколько входящих денег за 2020 год?",
deps: buildDeps([])
});
expect(result.entry_status).toBe("bridge_executed");
expect(result.discovery_attempted).toBe(true);
expect(result.turn_input.turn_meaning_ref).toMatchObject({
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_date_scope: "2020"
});
expect(result.turn_input.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.turn_input.data_need_graph?.clarification_gaps).toEqual(["organization"]);
expect(result.bridge?.planner.selected_chain_id).toBe("value_flow");
expect(result.bridge?.bridge_status).toBe("needs_clarification");
expect(result.bridge?.answer_draft.next_step_line).toContain("\u043e\u0440\u0433\u0430\u043d\u0438\u0437\u0430\u0446\u0438\u044e");
expect(result.bridge?.answer_draft.next_step_line).not.toContain(
"\u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0430"
);
});
it("resumes a generic incoming total after organization clarification without reintroducing a counterparty", async () => {
const orgName = "ООО Альтернатива Плюс";
const result = await runAssistantMcpDiscoveryRuntimeEntryPoint({
userMessage: "по ООО Альтернатива Плюс",
predecomposeContract: {
entities: { organization: orgName }
},
followupContext: {
previous_discovery_pilot_scope: "counterparty_value_flow_query_movements_v1",
previous_filters: {
period_from: "2020-01-01",
period_to: "2020-12-31"
}
},
deps: buildDeps([
{ Period: "2020-01-15T00:00:00", Amount: 2500, Counterparty: "Клиент-А" },
{ Period: "2020-06-20T00:00:00", Amount: 1000, Counterparty: "Клиент-Б" }
])
});
expect(result.entry_status).toBe("bridge_executed");
expect(result.discovery_attempted).toBe(true);
expect(result.turn_input.source_signal).toBe("followup_context");
expect(result.turn_input.turn_meaning_ref).toMatchObject({
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_organization_scope: orgName,
explicit_date_scope: "2020"
});
expect(result.turn_input.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.bridge?.planner.selected_chain_id).toBe("value_flow");
expect(result.bridge?.pilot.pilot_scope).toBe("counterparty_value_flow_query_movements_v1");
expect(result.bridge?.pilot.derived_value_flow).toMatchObject({
counterparty: null,
period_scope: "2020",
total_amount: 3500
});
});
it("overrides a supported exact intent when organization-only follow-up resolves an open-scope total", async () => {
const orgName = "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
const result = await runAssistantMcpDiscoveryRuntimeEntryPoint({
userMessage: "\u043f\u043e \u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441",
assistantTurnMeaning: {
asked_domain_family: "counterparty",
asked_action_family: "turnover",
explicit_intent_candidate: "customer_revenue_and_payments"
},
followupContext: {
previous_discovery_pilot_scope: "counterparty_value_flow_query_movements_v1",
previous_filters: {
period_from: "2020-01-01",
period_to: "2020-12-31"
}
},
deps: buildDeps([
{ Period: "2020-01-15T00:00:00", Amount: 2500, Counterparty: "\u041a\u043b\u0438\u0435\u043d\u0442-\u0410" },
{ Period: "2020-06-20T00:00:00", Amount: 1000, Counterparty: "\u041a\u043b\u0438\u0435\u043d\u0442-\u0411" }
])
});
expect(result.entry_status).toBe("bridge_executed");
expect(result.discovery_attempted).toBe(true);
expect(result.turn_input.turn_meaning_ref).toMatchObject({
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_organization_scope: orgName,
explicit_date_scope: "2020"
});
expect(result.turn_input.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.bridge?.planner.selected_chain_id).toBe("value_flow");
expect(result.bridge?.pilot.pilot_scope).toBe("counterparty_value_flow_query_movements_v1");
expect(result.bridge?.pilot.derived_value_flow).toMatchObject({
counterparty: null,
period_scope: "2020",
total_amount: 3500
});
});
});

View File

@ -21,6 +21,8 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.semantic_data_need).toBe("counterparty value-flow evidence");
expect(result.data_need_graph?.business_fact_family).toBe("value_flow");
expect(result.data_need_graph?.time_scope_need).toBe("explicit_period");
expect(result.turn_meaning_ref?.explicit_entity_candidates).toEqual(["SVK", "Группа СВК"]);
expect(result.turn_meaning_ref?.explicit_organization_scope).toBe("Альтернатива");
expect(result.turn_meaning_ref?.explicit_date_scope).toBe("2020");
@ -160,6 +162,8 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.should_run_discovery).toBe(true);
expect(result.source_signal).toBe("raw_text");
expect(result.semantic_data_need).toBe("1C metadata evidence");
expect(result.data_need_graph?.business_fact_family).toBe("schema_surface");
expect(result.data_need_graph?.decomposition_candidates).toEqual(["inspect_metadata_surface"]);
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "metadata",
asked_action_family: "inspect_fields",
@ -198,6 +202,8 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.should_run_discovery).toBe(true);
expect(result.source_signal).toBe("raw_text");
expect(result.semantic_data_need).toBe("entity discovery evidence");
expect(result.data_need_graph?.business_fact_family).toBe("entity_grounding");
expect(result.data_need_graph?.subject_candidates).toEqual(["Группа СВК"]);
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "entity_resolution",
asked_action_family: "search_business_entity",
@ -795,7 +801,10 @@ describe("assistant MCP discovery turn input adapter", () => {
followupContext: {
previous_discovery_pilot_scope: "metadata_inspection_v1",
previous_discovery_metadata_route_family: "document_evidence",
previous_discovery_metadata_route_family_selection_basis: "selected_entity_set",
previous_discovery_metadata_selected_entity_set: "Документ",
previous_discovery_metadata_selected_surface_objects: ["Документ.СчетФактураВыданный"],
previous_discovery_metadata_recommended_next_primitive: "query_documents",
previous_filters: {
counterparty: "SVK",
period_from: "2020-01-01",
@ -818,7 +827,18 @@ describe("assistant MCP discovery turn input adapter", () => {
unsupported_but_understood_family: "document_evidence",
stale_replay_forbidden: true
});
expect(result.metadata_surface_ref).toMatchObject({
selected_entity_set: "Документ",
selected_surface_objects: ["Документ.СчетФактураВыданный"],
downstream_route_family: "document_evidence",
route_family_selection_basis: "selected_entity_set",
recommended_next_primitive: "query_documents",
ambiguity_detected: false,
ambiguity_entity_sets: []
});
expect(result.reason_codes).toContain("mcp_discovery_metadata_grounded_document_followup");
expect(result.reason_codes).toContain("mcp_discovery_metadata_surface_ref_from_followup_context");
expect(result.reason_codes).toContain("mcp_discovery_metadata_next_primitive_from_followup_context");
expect(result.reason_codes).toContain("mcp_discovery_counterparty_from_followup_context");
});
@ -1128,6 +1148,38 @@ describe("assistant MCP discovery turn input adapter", () => {
expect(result.reason_codes).toContain("mcp_discovery_metadata_ambiguity_collapsed_to_movement_lane");
});
it("continues from a confirmed catalog metadata surface into inspect_catalog on a generic downstream follow-up", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "continue with data",
followupContext: {
previous_discovery_pilot_scope: "metadata_inspection_v1",
previous_discovery_metadata_route_family: "catalog_drilldown",
previous_discovery_metadata_selected_surface_objects: ["Catalog.Counterparties"],
previous_discovery_metadata_recommended_next_primitive: "drilldown_related_objects",
previous_discovery_metadata_ambiguity_detected: false
}
});
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.semantic_data_need).toBe("1C metadata evidence");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "metadata",
asked_action_family: "inspect_catalog"
});
expect(result.metadata_surface_ref).toMatchObject({
selected_entity_set: null,
selected_surface_objects: ["Catalog.Counterparties"],
downstream_route_family: "catalog_drilldown",
route_family_selection_basis: null,
recommended_next_primitive: "drilldown_related_objects",
ambiguity_detected: false,
ambiguity_entity_sets: []
});
expect(result.reason_codes).toContain("mcp_discovery_metadata_grounded_catalog_continuation");
expect(result.reason_codes).toContain("mcp_discovery_metadata_surface_ref_from_followup_context");
});
it("requires an explicit lane choice on a generic downstream follow-up when metadata ambiguity stays mixed", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "давай дальше",
@ -1158,6 +1210,15 @@ describe("assistant MCP discovery turn input adapter", () => {
unsupported_but_understood_family: "metadata_lane_choice_clarification",
stale_replay_forbidden: true
});
expect(result.metadata_surface_ref).toMatchObject({
selected_entity_set: null,
selected_surface_objects: [],
downstream_route_family: null,
route_family_selection_basis: null,
recommended_next_primitive: null,
ambiguity_detected: true,
ambiguity_entity_sets: ["Документ", "РегистрНакопления"]
});
expect(result.reason_codes).toContain("mcp_discovery_metadata_ambiguity_requires_lane_choice");
});
@ -1256,4 +1317,351 @@ describe("assistant MCP discovery turn input adapter", () => {
"\u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a \u043d\u0430\u0439\u0442\u0438 \u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0430 \u0441 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435\u043c '\u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a'"
);
});
it("marks top-value wording as a ranking data need without inventing a missing subject gap", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "кто больше всего принес денег в 2020"
});
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.semantic_data_need).toBe("counterparty value-flow evidence");
expect(result.data_need_graph?.business_fact_family).toBe("value_flow");
expect(result.data_need_graph?.ranking_need).toBe("top_desc");
expect(result.data_need_graph?.clarification_gaps).toEqual(["organization"]);
expect(result.data_need_graph?.decomposition_candidates).toEqual([
"collect_scoped_movements",
"aggregate_ranked_axis_values",
"probe_coverage"
]);
});
it("keeps organization as scope for open bidirectional comparison wording instead of inventing a subject candidate", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "что больше: входящие или исходящие деньги за 2020 год по ООО Альтернатива Плюс?",
predecomposeContract: {
entities: { organization: "ООО Альтернатива Плюс" },
period: { period_from: "2020-01-01", period_to: "2020-12-31" }
}
});
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "counterparty_value",
asked_action_family: "net_value_flow",
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020"
});
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.data_need_graph?.comparison_need).toBe("incoming_vs_outgoing");
expect(result.data_need_graph?.clarification_gaps).toEqual([]);
});
it("drops organization-shaped assistant-turn entity pollution when open comparison already has explicit organization scope", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "что больше: входящие или исходящие деньги за 2020 год по ООО Альтернатива Плюс?",
assistantTurnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "net_value_flow",
explicit_entity_candidates: [{ value: "ООО Альтернатива Плюс" }],
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020",
unsupported_but_understood_family: "counterparty_bidirectional_value_flow_or_netting"
},
predecomposeContract: {
entities: { counterparty: "ООО Альтернатива Плюс", organization: "ООО Альтернатива Плюс" },
period: { period_from: "2020-01-01", period_to: "2020-12-31" }
}
});
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "counterparty_value",
asked_action_family: "net_value_flow",
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020"
});
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.data_need_graph?.comparison_need).toBe("incoming_vs_outgoing");
});
it.skip("treats mirrored predecompose organization and counterparty as organization scope for open comparison", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "что больше: входящие или исходящие деньги за 2020 год по ООО Альтернатива Плюс?",
predecomposeContract: {
entities: {
counterparty: "ООО Альтернатива Плюс",
organization: "ООО Альтернатива Плюс"
},
period: { period_from: "2020-01-01", period_to: "2020-12-31" }
}
});
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "counterparty_value",
asked_action_family: "net_value_flow",
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020"
});
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.data_need_graph?.subject_candidates).toEqual([]);
expect(result.data_need_graph?.comparison_need).toBe("incoming_vs_outgoing");
expect(result.reason_codes).not.toContain("mcp_discovery_counterparty_from_predecompose");
});
it.skip("treats mirrored predecompose organization and counterparty as organization scope for open comparison (utf8-safe)", () => {
const orgName = "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "что больше: входящие или исходящие деньги за 2020 год по ООО Альтернатива Плюс?",
predecomposeContract: {
entities: {
counterparty: orgName,
organization: orgName
},
period: { period_from: "2020-01-01", period_to: "2020-12-31" }
}
});
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "counterparty_value",
asked_action_family: "net_value_flow",
explicit_organization_scope: orgName,
explicit_date_scope: "2020"
});
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.data_need_graph?.subject_candidates).toEqual([]);
expect(result.data_need_graph?.comparison_need).toBe("incoming_vs_outgoing");
expect(result.reason_codes).not.toContain("mcp_discovery_counterparty_from_predecompose");
});
it("keeps organization-scoped incoming totals in an open value-flow lane without inventing a counterparty", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "сколько входящих денег за 2020 год по ООО Альтернатива Плюс?",
predecomposeContract: {
entities: { organization: "ООО Альтернатива Плюс" },
period: { period_from: "2020-01-01", period_to: "2020-12-31" }
}
});
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.semantic_data_need).toBe("counterparty value-flow evidence");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020",
unsupported_but_understood_family: "counterparty_value_or_turnover",
stale_replay_forbidden: true
});
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.data_need_graph?.subject_candidates).toEqual([]);
expect(result.data_need_graph?.reason_codes).toContain("data_need_graph_open_scope_total_without_subject");
});
it("does not treat mirrored organization/counterparty predecompose as a real subject for organization-scoped payouts", () => {
const orgName = "ООО Альтернатива Плюс";
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "сколько исходящих денег за 2020 год по ООО Альтернатива Плюс?",
predecomposeContract: {
entities: { counterparty: orgName, organization: orgName },
period: { period_from: "2020-01-01", period_to: "2020-12-31" }
}
});
expect(result.adapter_status).toBe("ready");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "counterparty_value",
asked_action_family: "payout",
explicit_organization_scope: orgName,
explicit_date_scope: "2020"
});
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.data_need_graph?.subject_candidates).toEqual([]);
});
it("treats a generic incoming total as an open-scope value ask that needs organization rather than a counterparty", () => {
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "сколько входящих денег за 2020 год?"
});
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.semantic_data_need).toBe("counterparty value-flow evidence");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_date_scope: "2020",
unsupported_but_understood_family: "counterparty_value_or_turnover",
stale_replay_forbidden: true
});
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.data_need_graph?.subject_candidates).toEqual([]);
expect(result.data_need_graph?.clarification_gaps).toEqual(["organization"]);
expect(result.data_need_graph?.reason_codes).toContain("data_need_graph_open_scope_total_without_subject");
expect(result.data_need_graph?.reason_codes).toContain("data_need_graph_open_scope_total_needs_organization");
});
it("resumes an open-scope incoming total from follow-up context when the user clarifies only the organization", () => {
const orgName = "ООО Альтернатива Плюс";
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "по ООО Альтернатива Плюс",
predecomposeContract: {
entities: { organization: orgName }
},
followupContext: {
previous_discovery_pilot_scope: "counterparty_value_flow_query_movements_v1",
previous_filters: {
period_from: "2020-01-01",
period_to: "2020-12-31"
}
}
});
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.source_signal).toBe("followup_context");
expect(result.semantic_data_need).toBe("counterparty value-flow evidence");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_organization_scope: orgName,
explicit_date_scope: "2020",
unsupported_but_understood_family: "counterparty_value_or_turnover",
stale_replay_forbidden: true
});
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.reason_codes).toContain("mcp_discovery_seeded_from_followup_context");
expect(result.data_need_graph?.clarification_gaps).toEqual([]);
});
it("does not keep a stale follow-up date when the user switches an open-scope total to all-time wording", () => {
const orgName = "ООО Альтернатива Плюс";
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "сколько вообще денег мы заработали за все время?",
followupContext: {
previous_discovery_pilot_scope: "counterparty_value_flow_query_movements_v1",
previous_filters: {
organization: orgName,
as_of_date: "2026-04-23"
}
}
});
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.semantic_data_need).toBe("counterparty value-flow evidence");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_organization_scope: orgName,
unsupported_but_understood_family: "counterparty_value_or_turnover",
stale_replay_forbidden: true
});
expect(result.turn_meaning_ref?.explicit_date_scope).toBeUndefined();
expect(result.reason_codes).toContain("mcp_discovery_all_time_scope_signal_detected");
expect(result.reason_codes).not.toContain("mcp_discovery_date_scope_from_followup_context");
expect(result.data_need_graph?.clarification_gaps).toEqual([]);
expect(result.data_need_graph?.time_scope_need).toBe("all_time_scope");
expect(result.data_need_graph?.reason_codes).toContain("data_need_graph_all_time_scope_hint");
});
it("resumes an open-scope ranking from follow-up context when the user clarifies only the organization", () => {
const orgName = "ООО Альтернатива Плюс";
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "по ООО Альтернатива Плюс",
predecomposeContract: {
entities: { organization: orgName }
},
followupContext: {
previous_discovery_pilot_scope: "counterparty_value_flow_query_movements_v1",
previous_discovery_ranking_need: "top_desc",
previous_filters: {
period_from: "2020-01-01",
period_to: "2020-12-31"
}
}
});
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.source_signal).toBe("followup_context");
expect(result.semantic_data_need).toBe("counterparty value-flow evidence");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
seeded_ranking_need: "top_desc",
explicit_organization_scope: orgName,
explicit_date_scope: "2020",
unsupported_but_understood_family: "counterparty_value_or_turnover",
stale_replay_forbidden: true
});
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.data_need_graph?.ranking_need).toBe("top_desc");
expect(result.data_need_graph?.clarification_gaps).toEqual([]);
});
it("keeps seeded ranking through a year-switch follow-up after organization clarification", () => {
const orgName = "ООО Альтернатива Плюс";
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "Р° Р·Р° 2021?",
followupContext: {
previous_discovery_pilot_scope: "counterparty_value_flow_query_movements_v1",
previous_discovery_ranking_need: "top_desc",
previous_filters: {
organization: orgName,
period_from: "2020-01-01",
period_to: "2020-12-31"
}
}
});
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.source_signal).toBe("followup_context");
expect(result.semantic_data_need).toBe("counterparty value-flow evidence");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
seeded_ranking_need: "top_desc",
explicit_organization_scope: orgName,
explicit_date_scope: "2021",
unsupported_but_understood_family: "counterparty_value_or_turnover",
stale_replay_forbidden: true
});
expect(result.data_need_graph?.ranking_need).toBe("top_desc");
expect(result.data_need_graph?.clarification_gaps).toEqual([]);
});
it("forces discovery over a supported exact intent when organization-only follow-up resolves an open-scope total", () => {
const orgName = "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
const result = buildAssistantMcpDiscoveryTurnInput({
userMessage: "\u043f\u043e \u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441",
assistantTurnMeaning: {
asked_domain_family: "counterparty",
asked_action_family: "turnover",
explicit_intent_candidate: "customer_revenue_and_payments"
},
followupContext: {
previous_discovery_pilot_scope: "counterparty_value_flow_query_movements_v1",
previous_filters: {
period_from: "2020-01-01",
period_to: "2020-12-31"
}
}
});
expect(result.adapter_status).toBe("ready");
expect(result.should_run_discovery).toBe(true);
expect(result.semantic_data_need).toBe("counterparty value-flow evidence");
expect(result.turn_meaning_ref).toMatchObject({
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_organization_scope: orgName,
explicit_date_scope: "2020",
unsupported_but_understood_family: "counterparty_value_or_turnover",
stale_replay_forbidden: true
});
expect(result.turn_meaning_ref?.explicit_entity_candidates).toBeUndefined();
expect(result.reason_codes).toContain(
"mcp_discovery_organization_clarification_followup_from_followup_context"
);
expect(result.reason_codes).not.toContain("mcp_discovery_not_applicable_for_supported_exact_turn");
});
});

View File

@ -1274,6 +1274,64 @@ describe("assistantTransitionPolicy", () => {
});
});
it("carries ranking need from grounded discovery into followup context", () => {
const policy = buildPolicy({
findLastAddressAssistantItem: () => null,
hasAddressFollowupContextSignal: () => true
});
const carryover = policy.resolveAddressFollowupCarryoverContext(
"по ООО Альтернатива Плюс",
[
{
role: "assistant",
text: "Нужно уточнить организацию, чтобы продолжить поиск по контрагентам.",
debug: {
execution_lane: "living_chat",
mcp_discovery_response_applied: true,
assistant_mcp_discovery_entry_point_v1: {
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
entry_status: "bridge_executed",
turn_input: {
data_need_graph: {
business_fact_family: "value_flow",
ranking_need: "top_desc",
subject_candidates: [],
clarification_gaps: ["organization"]
},
turn_meaning_ref: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_date_scope: "2020",
seeded_ranking_need: "top_desc"
}
},
bridge: {
bridge_status: "answer_draft_ready",
business_fact_answer_allowed: false,
pilot: {
pilot_scope: "counterparty_value_flow_query_movements_v1"
},
answer_draft: {
answer_mode: "needs_clarification"
}
}
}
}
}
],
null,
null,
null
);
expect(carryover?.followupContext?.previous_discovery_pilot_scope).toBe(
"counterparty_value_flow_query_movements_v1"
);
expect(carryover?.followupContext?.previous_discovery_ranking_need).toBe("top_desc");
expect(carryover?.followupContext?.target_intent).toBe("customer_revenue_and_payments");
});
it("carries grounded metadata downstream route hints into followup context", () => {
const policy = buildPolicy({
findLastAddressAssistantItem: () => null,
@ -1307,7 +1365,10 @@ describe("assistantTransitionPolicy", () => {
pilot_scope: "metadata_inspection_v1",
derived_metadata_surface: {
selected_entity_set: "Документ",
selected_surface_objects: ["Документ.СчетФактураВыданный"],
downstream_route_family: "document_evidence",
route_family_selection_basis: "selected_entity_set",
recommended_next_primitive: "query_documents",
ambiguity_detected: false
}
},
@ -1326,7 +1387,12 @@ describe("assistantTransitionPolicy", () => {
expect(carryover?.followupContext?.previous_discovery_pilot_scope).toBe("metadata_inspection_v1");
expect(carryover?.followupContext?.previous_discovery_metadata_route_family).toBe("document_evidence");
expect(carryover?.followupContext?.previous_discovery_metadata_route_family_selection_basis).toBe("selected_entity_set");
expect(carryover?.followupContext?.previous_discovery_metadata_selected_entity_set).toBe("Документ");
expect(carryover?.followupContext?.previous_discovery_metadata_selected_surface_objects).toEqual([
"Документ.СчетФактураВыданный"
]);
expect(carryover?.followupContext?.previous_discovery_metadata_recommended_next_primitive).toBe("query_documents");
expect(carryover?.followupContext?.previous_discovery_metadata_ambiguity_detected).toBeUndefined();
});
it("carries metadata ambiguity entity sets into follow-up context for downstream lane arbitration", () => {

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,137 +0,0 @@
{
"saved_at": "2026-04-20T03:44:04+00:00",
"generation_id": "gen-ag04200344-0dc69f",
"mode": "saved_user_sessions",
"title": "AGENT | Phase 18 semantic dialog authority replay",
"agent_run": true,
"questions": [
"приветик - че как там дела",
"расскажи что можешь интересного",
"по какой компании мы сейчас работаем?",
"Альтернатива Плюс",
"по чепурнову покажи все доки",
"какой оборот был свк",
"а чем капибара отличается от утки?"
],
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "semantic_dialog_authority_answer_acceptance",
"architecture_phase": "turnaround_11_phase18",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase18_semantic_dialog_authority.json",
"scenario_id": "address_truth_harness_phase18_semantic_dialog_authority",
"semantic_tags": [
"answer_top_block_matches_current_user_intent",
"capability_meta",
"company_selected",
"counterparty_documents",
"current_turn_action_authority",
"current_turn_entity_authority",
"data_scope_meta",
"human_answer",
"multi_company_entry",
"off_domain_living_chat",
"organization_authority",
"smalltalk_entry",
"stale_entity_seed",
"stale_replay_forbidden"
]
},
"source_session_id": null,
"session": {
"session_id": null,
"mode": "agent_semantic_run",
"items": [
{
"message_id": "agent-user-001",
"role": "user",
"text": "приветик - че как там дела",
"created_at": "2026-04-20T03:44:04+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-002",
"role": "user",
"text": "расскажи что можешь интересного",
"created_at": "2026-04-20T03:44:04+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-003",
"role": "user",
"text": "по какой компании мы сейчас работаем?",
"created_at": "2026-04-20T03:44:04+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-004",
"role": "user",
"text": "Альтернатива Плюс",
"created_at": "2026-04-20T03:44:04+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-005",
"role": "user",
"text": "по чепурнову покажи все доки",
"created_at": "2026-04-20T03:44:04+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-006",
"role": "user",
"text": "какой оборот был свк",
"created_at": "2026-04-20T03:44:04+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
},
{
"message_id": "agent-user-007",
"role": "user",
"text": "а чем капибара отличается от утки?",
"created_at": "2026-04-20T03:44:04+00:00",
"reply_type": null,
"trace_id": null,
"debug": null
}
],
"agent_run": true,
"metadata": {
"assistant_prompt_version": null,
"decomposition_prompt_version": null,
"prompt_fingerprint": null,
"agent_focus": "semantic_dialog_authority_answer_acceptance",
"architecture_phase": "turnaround_11_phase18",
"source_spec_file": "X:\\1C\\NDC_1C\\docs\\orchestration\\address_truth_harness_phase18_semantic_dialog_authority.json",
"scenario_id": "address_truth_harness_phase18_semantic_dialog_authority",
"semantic_tags": [
"answer_top_block_matches_current_user_intent",
"capability_meta",
"company_selected",
"counterparty_documents",
"current_turn_action_authority",
"current_turn_entity_authority",
"data_scope_meta",
"human_answer",
"multi_company_entry",
"off_domain_living_chat",
"organization_authority",
"smalltalk_entry",
"stale_entity_seed",
"stale_replay_forbidden"
]
}
}
}

View File

@ -2,7 +2,7 @@
"suite_id": "assistant_saved_session_gen-mo1t93wq-jy0453e",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_suite_v0_1",
"generated_at": "2026-04-18T14:55:46.799Z",
"generated_at": "2026-04-21T04:50:45.064Z",
"generation_id": "gen-mo1t93wq-jy0453e",
"mode": "saved_user_sessions",
"title": "БОЛЬШОЙ ОБЩИЙ Ручная сессия 16.04.2026, 21:26:06",
@ -116,6 +116,9 @@
},
{
"user_message": "Как ты оценишь деятельность компании?"
},
{
"user_message": "какое нетто по деньгам с Группа СВК за 2020 год: сколько получили и сколько заплатили?"
}
]
}

View File

@ -1,46 +0,0 @@
{
"suite_id": "assistant_saved_session_gen-ag04200344-0dc69f",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_suite_v0_1",
"generated_at": "2026-04-20T03:44:04+00:00",
"generation_id": "gen-ag04200344-0dc69f",
"mode": "saved_user_sessions",
"title": "AGENT | Phase 18 semantic dialog authority replay",
"domain": "address_phase18_semantic_dialog_authority",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "agent_saved_user_sessions",
"title": "AGENT | Phase 18 semantic dialog authority replay",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "приветик - че как там дела"
},
{
"user_message": "расскажи что можешь интересного"
},
{
"user_message": "по какой компании мы сейчас работаем?"
},
{
"user_message": "Альтернатива Плюс"
},
{
"user_message": "по чепурнову покажи все доки"
},
{
"user_message": "какой оборот был свк"
},
{
"user_message": "а чем капибара отличается от утки?"
}
]
}
]
}

View File

@ -0,0 +1,45 @@
{
"suite_id": "assistant_saved_session_gen-moa1y0lw-m30gdsz",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_suite_v0_1",
"generated_at": "2026-04-22T12:51:54.645Z",
"generation_id": "gen-moa1y0lw-m30gdsz",
"mode": "saved_user_sessions",
"title": "СВК - Ручная сессия 22.04.2026, 15:50:39",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "saved_user_sessions",
"title": "СВК - Ручная сессия 22.04.2026, 15:50:39",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "найди в 1С контрагента СВК"
},
{
"user_message": "сколько получили по нему за 2020 год"
},
{
"user_message": "а теперь сколько заплатили?"
},
{
"user_message": "а за 2021?"
},
{
"user_message": "а какое нетто?"
},
{
"user_message": "а по документам?"
},
{
"user_message": "а по движениям?"
}
]
}
]
}

View File

@ -0,0 +1,123 @@
{
"suite_id": "assistant_saved_session_runtime_job-5IOPh9mD8u",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_runtime_v0_1",
"title": "БОЛЬШОЙ ОБЩИЙ Ручная сессия 16.04.2026, 21:26:06",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "saved_user_sessions_runtime",
"title": "БОЛЬШОЙ ОБЩИЙ Ручная сессия 16.04.2026, 21:26:06",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "приветик - че как там дела"
},
{
"user_message": "расскажи что можешь интересного"
},
{
"user_message": "кайф - что там на складе по остаткам?"
},
{
"user_message": "АЛЬТЕРНАТИВА"
},
{
"user_message": "а исторические остатки на другие даты умеешь?"
},
{
"user_message": "давай на июль 2017"
},
{
"user_message": "март 2016"
},
{
"user_message": "По выбранному объекту \"Рабочая станция универсального специалиста (индивидуальное изготовление)\": где взяли это?"
},
{
"user_message": "а кому продали?"
},
{
"user_message": "у тебя написано кто контрагент: рабочая станция - это ошибка?"
},
{
"user_message": "ндс можешь прикинуть на дату покупки рабочей станции?"
},
{
"user_message": "а какой ндс мы должны сгрузить на март 2020?"
},
{
"user_message": "прикинь какой ндс нам надо заплатить на февраль 2017"
},
{
"user_message": "кто у нас самый доходный клиент за все время"
},
{
"user_message": "кто нам должен денег на май 2017"
},
{
"user_message": "а какой ндс мы должны примерно заплатить за этот период?"
},
{
"user_message": "мы должны комуто денег на сегодня?"
},
{
"user_message": "а нам?"
},
{
"user_message": "какой у нас самый доходный год"
},
{
"user_message": "а за 2017 мы скок заработали?"
},
{
"user_message": "сколько вообще денег мы заработали за все время?"
},
{
"user_message": "ты умеешь считать дельту по договорам?"
},
{
"user_message": "по чепурнову покажи все доки"
},
{
"user_message": "а по свк"
},
{
"user_message": "а сейчас у нас есть что на складе?"
},
{
"user_message": "что нам отгружал чепурнов? какой товар или услугу?"
},
{
"user_message": "какие остатки на складе на сегодня"
},
{
"user_message": "остатки на март 2016"
},
{
"user_message": "хвосты покажи по счету 60 на август 2022"
},
{
"user_message": "Есть ли остатки товара, которые закупались очень давно"
},
{
"user_message": "Какие конкретно номенклатуры формируют остаток по складу на май 2020"
},
{
"user_message": "а по Альтернативе Плюс сколько лет активности в базе 1С?"
},
{
"user_message": "Как ты оценишь деятельность компании?"
},
{
"user_message": "какое нетто по деньгам с Группа СВК за 2020 год: сколько получили и сколько заплатили?"
}
]
}
]
}

View File

@ -0,0 +1,123 @@
{
"suite_id": "assistant_saved_session_runtime_job-GSMnTyNRlL",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_runtime_v0_1",
"title": "БОЛЬШОЙ ОБЩИЙ Ручная сессия 16.04.2026, 21:26:06",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "saved_user_sessions_runtime",
"title": "БОЛЬШОЙ ОБЩИЙ Ручная сессия 16.04.2026, 21:26:06",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "приветик - че как там дела"
},
{
"user_message": "расскажи что можешь интересного"
},
{
"user_message": "кайф - что там на складе по остаткам?"
},
{
"user_message": "АЛЬТЕРНАТИВА"
},
{
"user_message": "а исторические остатки на другие даты умеешь?"
},
{
"user_message": "давай на июль 2017"
},
{
"user_message": "март 2016"
},
{
"user_message": "По выбранному объекту \"Рабочая станция универсального специалиста (индивидуальное изготовление)\": где взяли это?"
},
{
"user_message": "а кому продали?"
},
{
"user_message": "у тебя написано кто контрагент: рабочая станция - это ошибка?"
},
{
"user_message": "ндс можешь прикинуть на дату покупки рабочей станции?"
},
{
"user_message": "а какой ндс мы должны сгрузить на март 2020?"
},
{
"user_message": "прикинь какой ндс нам надо заплатить на февраль 2017"
},
{
"user_message": "кто у нас самый доходный клиент за все время"
},
{
"user_message": "кто нам должен денег на май 2017"
},
{
"user_message": "а какой ндс мы должны примерно заплатить за этот период?"
},
{
"user_message": "мы должны комуто денег на сегодня?"
},
{
"user_message": "а нам?"
},
{
"user_message": "какой у нас самый доходный год"
},
{
"user_message": "а за 2017 мы скок заработали?"
},
{
"user_message": "сколько вообще денег мы заработали за все время?"
},
{
"user_message": "ты умеешь считать дельту по договорам?"
},
{
"user_message": "по чепурнову покажи все доки"
},
{
"user_message": "а по свк"
},
{
"user_message": "а сейчас у нас есть что на складе?"
},
{
"user_message": "что нам отгружал чепурнов? какой товар или услугу?"
},
{
"user_message": "какие остатки на складе на сегодня"
},
{
"user_message": "остатки на март 2016"
},
{
"user_message": "хвосты покажи по счету 60 на август 2022"
},
{
"user_message": "Есть ли остатки товара, которые закупались очень давно"
},
{
"user_message": "Какие конкретно номенклатуры формируют остаток по складу на май 2020"
},
{
"user_message": "а по Альтернативе Плюс сколько лет активности в базе 1С?"
},
{
"user_message": "Как ты оценишь деятельность компании?"
},
{
"user_message": "какое нетто по деньгам с Группа СВК за 2020 год: сколько получили и сколько заплатили?"
}
]
}
]
}

View File

@ -0,0 +1,123 @@
{
"suite_id": "assistant_saved_session_runtime_job-L3ayAQpJTm",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_runtime_v0_1",
"title": "БОЛЬШОЙ ОБЩИЙ Ручная сессия 16.04.2026, 21:26:06",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "saved_user_sessions_runtime",
"title": "БОЛЬШОЙ ОБЩИЙ Ручная сессия 16.04.2026, 21:26:06",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "приветик - че как там дела"
},
{
"user_message": "расскажи что можешь интересного"
},
{
"user_message": "кайф - что там на складе по остаткам?"
},
{
"user_message": "АЛЬТЕРНАТИВА"
},
{
"user_message": "а исторические остатки на другие даты умеешь?"
},
{
"user_message": "давай на июль 2017"
},
{
"user_message": "март 2016"
},
{
"user_message": "По выбранному объекту \"Рабочая станция универсального специалиста (индивидуальное изготовление)\": где взяли это?"
},
{
"user_message": "а кому продали?"
},
{
"user_message": "у тебя написано кто контрагент: рабочая станция - это ошибка?"
},
{
"user_message": "ндс можешь прикинуть на дату покупки рабочей станции?"
},
{
"user_message": "а какой ндс мы должны сгрузить на март 2020?"
},
{
"user_message": "прикинь какой ндс нам надо заплатить на февраль 2017"
},
{
"user_message": "кто у нас самый доходный клиент за все время"
},
{
"user_message": "кто нам должен денег на май 2017"
},
{
"user_message": "а какой ндс мы должны примерно заплатить за этот период?"
},
{
"user_message": "мы должны комуто денег на сегодня?"
},
{
"user_message": "а нам?"
},
{
"user_message": "какой у нас самый доходный год"
},
{
"user_message": "а за 2017 мы скок заработали?"
},
{
"user_message": "сколько вообще денег мы заработали за все время?"
},
{
"user_message": "ты умеешь считать дельту по договорам?"
},
{
"user_message": "по чепурнову покажи все доки"
},
{
"user_message": "а по свк"
},
{
"user_message": "а сейчас у нас есть что на складе?"
},
{
"user_message": "что нам отгружал чепурнов? какой товар или услугу?"
},
{
"user_message": "какие остатки на складе на сегодня"
},
{
"user_message": "остатки на март 2016"
},
{
"user_message": "хвосты покажи по счету 60 на август 2022"
},
{
"user_message": "Есть ли остатки товара, которые закупались очень давно"
},
{
"user_message": "Какие конкретно номенклатуры формируют остаток по складу на май 2020"
},
{
"user_message": "а по Альтернативе Плюс сколько лет активности в базе 1С?"
},
{
"user_message": "Как ты оценишь деятельность компании?"
},
{
"user_message": "какое нетто по деньгам с Группа СВК за 2020 год: сколько получили и сколько заплатили?"
}
]
}
]
}

View File

@ -0,0 +1,120 @@
{
"suite_id": "assistant_saved_session_runtime_job-SXBXqiDtH8",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_runtime_v0_1",
"title": "БОЛЬШОЙ ОБЩИЙ Ручная сессия 16.04.2026, 21:26:06",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "saved_user_sessions_runtime",
"title": "БОЛЬШОЙ ОБЩИЙ Ручная сессия 16.04.2026, 21:26:06",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "приветик - че как там дела"
},
{
"user_message": "расскажи что можешь интересного"
},
{
"user_message": "кайф - что там на складе по остаткам?"
},
{
"user_message": "АЛЬТЕРНАТИВА"
},
{
"user_message": "а исторические остатки на другие даты умеешь?"
},
{
"user_message": "давай на июль 2017"
},
{
"user_message": "март 2016"
},
{
"user_message": "По выбранному объекту \"Рабочая станция универсального специалиста (индивидуальное изготовление)\": где взяли это?"
},
{
"user_message": "а кому продали?"
},
{
"user_message": "у тебя написано кто контрагент: рабочая станция - это ошибка?"
},
{
"user_message": "ндс можешь прикинуть на дату покупки рабочей станции?"
},
{
"user_message": "а какой ндс мы должны сгрузить на март 2020?"
},
{
"user_message": "прикинь какой ндс нам надо заплатить на февраль 2017"
},
{
"user_message": "кто у нас самый доходный клиент за все время"
},
{
"user_message": "кто нам должен денег на май 2017"
},
{
"user_message": "а какой ндс мы должны примерно заплатить за этот период?"
},
{
"user_message": "мы должны комуто денег на сегодня?"
},
{
"user_message": "а нам?"
},
{
"user_message": "какой у нас самый доходный год"
},
{
"user_message": "а за 2017 мы скок заработали?"
},
{
"user_message": "сколько вообще денег мы заработали за все время?"
},
{
"user_message": "ты умеешь считать дельту по договорам?"
},
{
"user_message": "по чепурнову покажи все доки"
},
{
"user_message": "а по свк"
},
{
"user_message": "а сейчас у нас есть что на складе?"
},
{
"user_message": "что нам отгружал чепурнов? какой товар или услугу?"
},
{
"user_message": "какие остатки на складе на сегодня"
},
{
"user_message": "остатки на март 2016"
},
{
"user_message": "хвосты покажи по счету 60 на август 2022"
},
{
"user_message": "Есть ли остатки товара, которые закупались очень давно"
},
{
"user_message": "Какие конкретно номенклатуры формируют остаток по складу на май 2020"
},
{
"user_message": "а по Альтернативе Плюс сколько лет активности в базе 1С?"
},
{
"user_message": "Как ты оценишь деятельность компании?"
}
]
}
]
}

View File

@ -0,0 +1,48 @@
{
"suite_id": "assistant_saved_session_runtime_job-dQIwSalnID",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_runtime_v0_1",
"title": "Ручная сессия 19.04.2026, 18:58:20",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "saved_user_sessions_runtime",
"title": "Ручная сессия 19.04.2026, 18:58:20",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "привет как дела"
},
{
"user_message": "кто намс должен денег на сегодня"
},
{
"user_message": "что ты можешь"
},
{
"user_message": "кто нам должен денег на сегодня"
},
{
"user_message": "а мы кому"
},
{
"user_message": "какиек остатки на складе на сегодня"
},
{
"user_message": "альтернатива"
},
{
"user_message": "покажи документы по чепурнову"
},
{
"user_message": "какой оборот был свк"
}
]
}
]
}

View File

@ -0,0 +1,123 @@
{
"suite_id": "assistant_saved_session_runtime_job-g6HI083Yot",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_runtime_v0_1",
"title": "БОЛЬШОЙ ОБЩИЙ Ручная сессия 16.04.2026, 21:26:06",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "saved_user_sessions_runtime",
"title": "БОЛЬШОЙ ОБЩИЙ Ручная сессия 16.04.2026, 21:26:06",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "приветик - че как там дела"
},
{
"user_message": "расскажи что можешь интересного"
},
{
"user_message": "кайф - что там на складе по остаткам?"
},
{
"user_message": "АЛЬТЕРНАТИВА"
},
{
"user_message": "а исторические остатки на другие даты умеешь?"
},
{
"user_message": "давай на июль 2017"
},
{
"user_message": "март 2016"
},
{
"user_message": "По выбранному объекту \"Рабочая станция универсального специалиста (индивидуальное изготовление)\": где взяли это?"
},
{
"user_message": "а кому продали?"
},
{
"user_message": "у тебя написано кто контрагент: рабочая станция - это ошибка?"
},
{
"user_message": "ндс можешь прикинуть на дату покупки рабочей станции?"
},
{
"user_message": "а какой ндс мы должны сгрузить на март 2020?"
},
{
"user_message": "прикинь какой ндс нам надо заплатить на февраль 2017"
},
{
"user_message": "кто у нас самый доходный клиент за все время"
},
{
"user_message": "кто нам должен денег на май 2017"
},
{
"user_message": "а какой ндс мы должны примерно заплатить за этот период?"
},
{
"user_message": "мы должны комуто денег на сегодня?"
},
{
"user_message": "а нам?"
},
{
"user_message": "какой у нас самый доходный год"
},
{
"user_message": "а за 2017 мы скок заработали?"
},
{
"user_message": "сколько вообще денег мы заработали за все время?"
},
{
"user_message": "ты умеешь считать дельту по договорам?"
},
{
"user_message": "по чепурнову покажи все доки"
},
{
"user_message": "а по свк"
},
{
"user_message": "а сейчас у нас есть что на складе?"
},
{
"user_message": "что нам отгружал чепурнов? какой товар или услугу?"
},
{
"user_message": "какие остатки на складе на сегодня"
},
{
"user_message": "остатки на март 2016"
},
{
"user_message": "хвосты покажи по счету 60 на август 2022"
},
{
"user_message": "Есть ли остатки товара, которые закупались очень давно"
},
{
"user_message": "Какие конкретно номенклатуры формируют остаток по складу на май 2020"
},
{
"user_message": "а по Альтернативе Плюс сколько лет активности в базе 1С?"
},
{
"user_message": "Как ты оценишь деятельность компании?"
},
{
"user_message": "какое нетто по деньгам с Группа СВК за 2020 год: сколько получили и сколько заплатили?"
}
]
}
]
}

View File

@ -0,0 +1,123 @@
{
"suite_id": "assistant_saved_session_runtime_job-rucNsMWuUD",
"suite_version": "0.1.0",
"schema_version": "assistant_saved_session_runtime_v0_1",
"title": "БОЛЬШОЙ ОБЩИЙ Ручная сессия 16.04.2026, 21:26:06",
"scenario_count": 1,
"case_ids": [
"SAVED-001"
],
"cases": [
{
"case_id": "SAVED-001",
"scenario_tag": "saved_user_sessions_runtime",
"title": "БОЛЬШОЙ ОБЩИЙ Ручная сессия 16.04.2026, 21:26:06",
"question_type": "followup",
"broadness_level": "medium",
"turns": [
{
"user_message": "приветик - че как там дела"
},
{
"user_message": "расскажи что можешь интересного"
},
{
"user_message": "кайф - что там на складе по остаткам?"
},
{
"user_message": "АЛЬТЕРНАТИВА"
},
{
"user_message": "а исторические остатки на другие даты умеешь?"
},
{
"user_message": "давай на июль 2017"
},
{
"user_message": "март 2016"
},
{
"user_message": "По выбранному объекту \"Рабочая станция универсального специалиста (индивидуальное изготовление)\": где взяли это?"
},
{
"user_message": "а кому продали?"
},
{
"user_message": "у тебя написано кто контрагент: рабочая станция - это ошибка?"
},
{
"user_message": "ндс можешь прикинуть на дату покупки рабочей станции?"
},
{
"user_message": "а какой ндс мы должны сгрузить на март 2020?"
},
{
"user_message": "прикинь какой ндс нам надо заплатить на февраль 2017"
},
{
"user_message": "кто у нас самый доходный клиент за все время"
},
{
"user_message": "кто нам должен денег на май 2017"
},
{
"user_message": "а какой ндс мы должны примерно заплатить за этот период?"
},
{
"user_message": "мы должны комуто денег на сегодня?"
},
{
"user_message": "а нам?"
},
{
"user_message": "какой у нас самый доходный год"
},
{
"user_message": "а за 2017 мы скок заработали?"
},
{
"user_message": "сколько вообще денег мы заработали за все время?"
},
{
"user_message": "ты умеешь считать дельту по договорам?"
},
{
"user_message": "по чепурнову покажи все доки"
},
{
"user_message": "а по свк"
},
{
"user_message": "а сейчас у нас есть что на складе?"
},
{
"user_message": "что нам отгружал чепурнов? какой товар или услугу?"
},
{
"user_message": "какие остатки на складе на сегодня"
},
{
"user_message": "остатки на март 2016"
},
{
"user_message": "хвосты покажи по счету 60 на август 2022"
},
{
"user_message": "Есть ли остатки товара, которые закупались очень давно"
},
{
"user_message": "Какие конкретно номенклатуры формируют остаток по складу на май 2020"
},
{
"user_message": "а по Альтернативе Плюс сколько лет активности в базе 1С?"
},
{
"user_message": "Как ты оценишь деятельность компании?"
},
{
"user_message": "какое нетто по деньгам с Группа СВК за 2020 год: сколько получили и сколько заплатили?"
}
]
}
]
}

View File

File diff suppressed because one or more lines are too long