Compare commits

...

23 Commits

Author SHA1 Message Date
dctouch dca49ef4e1 ARCH: приоритизировать discovery-period в planner follow-up 2026-04-22 17:49:09 +03:00
dctouch bd58ab490f ARCH: довести planner-selected entity chains и ambiguity follow-up 2026-04-22 17:32:24 +03:00
dctouch bc54cd9628 ARCH: связать grounded value-flow follow-up с document evidence lane 2026-04-22 15:41:49 +03:00
dctouch acacada0f6 ARCH: замкнуть grounded entity retarget на value-flow follow-up 2026-04-22 15:07:20 +03:00
dctouch 1fd8062dc7 ARCH: замкнуть grounded entity follow-up на документы и денежный поток 2026-04-22 13:57:57 +03:00
dctouch ce48fa83a5 ARCH: добить entity-resolution chain и очистить stale runtime 2026-04-22 12:53:48 +03:00
dctouch 007369a78a ARCH: сохранять metadata ambiguity через clarification follow-up 2026-04-22 10:05:03 +03:00
dctouch 40cf71d118 ARCH: удерживать mixed metadata ambiguity как явное clarification state 2026-04-22 10:02:01 +03:00
dctouch c0b3296953 ARCH: синхронизировать dist после metadata ambiguity carryover 2026-04-22 09:46:54 +03:00
dctouch c328c52c9b ARCH: усилить metadata ambiguity choice sets в lane arbitration 2026-04-22 09:45:49 +03:00
dctouch d9454fcdef ARCH: усилить metadata lane arbitration семантическими data-hints 2026-04-22 09:37:37 +03:00
dctouch f951dae9f0 ARCH: разрешать metadata ambiguity явным выбором data lane 2026-04-22 09:33:57 +03:00
dctouch eac3709f2b ARCH: связать metadata grounding с movement lane и reusable follow-up 2026-04-22 09:26:58 +03:00
dctouch 7ef788fa50 ARCH: связать metadata grounding с document MCP lane 2026-04-21 23:01:12 +03:00
dctouch 6d9c1568c3 ARCH: заземлить metadata surface в следующий MCP lane 2026-04-21 22:33:46 +03:00
dctouch d66e2bfb01 ARCH: продолжить metadata continuity для MCP discovery 2026-04-21 22:14:12 +03:00
dctouch 561b4ea45c ARCH: перезапустить план на MCP bounded autonomy и добавить metadata pilot 2026-04-21 22:04:23 +03:00
dctouch bda7ca9cc1 ARCH: ввести broad business evaluation bridge 2026-04-21 19:37:37 +03:00
dctouch d323dcd509 ARCH: разрешить net-flow discovery переопределять stale lifecycle carryover 2026-04-21 19:10:29 +03:00
dctouch 429bd3d8ec ARCH: стабилизировать continuity и защитить exact-ответы от discovery 2026-04-21 18:43:05 +03:00
dctouch b542b65b81 ARCH: восстановить память discovery-ответов в living chat 2026-04-21 08:56:38 +03:00
dctouch cd3315e06d ARCH: восстановить годовое покрытие MCP discovery помесячными пробами 2026-04-21 08:41:35 +03:00
dctouch 4baa54fe81 ARCH: добавить помесячный MCP discovery для нетто-потока 2026-04-21 08:18:09 +03:00
60 changed files with 13760 additions and 322 deletions

View File

@ -1261,6 +1261,106 @@ Module progress:
- Big Block 5 MCP Semantic Data Agent: `99%`.
## Progress Update - 2026-04-21 MCP Discovery Monthly Bidirectional Multi-Axis Aggregation
The nineteenth implementation slice of Big Block 5 closes the first explicit multi-axis aggregation gap for guarded MCP discovery.
Before this slice the assistant could answer:
- incoming value-flow total;
- outgoing supplier-payout total;
- composed bidirectional net total.
But it could not keep a user request like "по месяцам" as part of the machine-readable turn meaning, plan shape, and final guarded answer.
New behavior:
- monthly wording such as `по месяцам`, `помесячно`, `ежемесячно`, `monthly`, and `month by month` is captured as `asked_aggregation_axis=month`;
- the discovery planner keeps the monthly request as an explicit `calendar_month` axis on top of the existing guarded value-flow recipe;
- runtime derives `monthly_breakdown` for single-direction value-flow and for composed bidirectional net value-flow from the same confirmed 1C rows;
- the guarded answer draft now adds month-by-month business lines instead of flattening a monthly question back into a single total;
- the response-candidate layer keeps those monthly lines user-facing and localizes the monthly inference basis without leaking planner/runtime/pilot mechanics.
Replay result:
- `address_truth_harness_phase19_mcp_discovery_response_gate_live_rerun12` passed `8/8` after adding the new monthly step and exposed one presentation defect: duplicated punctuation in monthly amount lines;
- `address_truth_harness_phase19_mcp_discovery_response_gate_live_rerun13` passed `8/8` after punctuation cleanup;
- final status: `accepted`;
- the new step `step_07_counterparty_bidirectional_monthly_net_flow_uses_guarded_discovery` answered through guarded MCP discovery with monthly lines from `янв 2020` through `дек 2020`;
- user-facing text keeps the same honesty boundary as the total net answer: outgoing coverage still hits the `100`-row probe limit, so the monthly shape is a found-row calculation rather than a fully proven all-period saldo;
- user-facing monthly output contains no `query_documents`, `query_movements`, `runtime_`, `planner_`, `catalog_`, `primitive`, or `pilot_` leak.
Validation:
- `npm test -- assistantMcpDiscoveryTurnInputAdapter.test.ts assistantMcpDiscoveryPlanner.test.ts assistantMcpDiscoveryPilotExecutor.test.ts assistantMcpDiscoveryAnswerAdapter.test.ts assistantMcpDiscoveryResponseCandidate.test.ts assistantMcpDiscoveryResponsePolicy.test.ts assistantMcpDiscoveryRuntimeEntryPoint.test.ts assistantMcpDiscoveryRuntimeBridge.test.ts assistantAddressLaneResponseRuntimeAdapter.test.ts assistantDeepTurnResponseRuntimeAdapter.test.ts assistantLivingChatRuntimeAdapter.test.ts assistantAddressOrchestrationRuntimeAdapter.test.ts assistantDebugPayloadAssembler.test.ts` passed `90/90`;
- `npm run build` passed;
- `python scripts/domain_truth_harness.py run-live --spec docs/orchestration/address_truth_harness_phase19_mcp_discovery_response_gate.json --output-dir artifacts/domain_runs/address_truth_harness_phase19_mcp_discovery_response_gate_live_rerun13 --timeout-seconds 180` passed `8/8`, final status `accepted`.
Known remaining boundary:
- this still does not make Qwen3 an unrestricted self-navigating 1C agent across arbitrary registers and schemas; follow-up drilldown over the discovered rows and broader self-navigation remain future work outside this first completed guarded multi-axis slice.
Module progress:
- Big Block 5 MCP Semantic Data Agent: `100%`.
## Progress Update - 2026-04-21 MCP Discovery Yearly Coverage Recovery Through Monthly Probes
The twentieth implementation slice of Big Block 5 closes the first yearly-coverage proof gap that remained inside guarded MCP discovery value-flow answers.
Before this slice the assistant could already:
- answer incoming counterparty turnover totals through guarded MCP discovery;
- answer outgoing supplier payout totals through guarded MCP discovery;
- compose bidirectional net totals and month-by-month net lines from the same evidence.
But the broad yearly value-flow probe still stopped at the first MCP row limit. For dense 2020 counterparty activity this meant the assistant answered honestly, yet underpowered: user-facing text still said the outgoing side hit `100 из 100`, even though the missing coverage was recoverable through bounded subperiod probes rather than impossible.
New behavior:
- the discovery planner now grants a larger but still bounded probe budget for explicit yearly value-flow and monthly-aggregation questions;
- the pilot executor first runs the broad yearly value-flow probe and, only when that broad probe hits the row limit on an explicit year window, automatically recovers coverage through month-by-month 1C subprobes;
- recovered subperiod rows are stitched into one coverage-aware result instead of being exposed as separate runtime fragments;
- guarded answer drafting keeps the answer honest: it no longer claims a partial year when monthly recovery fully proves the requested period, but it still states the derivation basis and keeps "outside the checked period is not proven" boundaries intact.
Replay result:
- `address_truth_harness_phase19_mcp_discovery_response_gate_live_rerun14` passed `8/8`, final status `accepted`;
- the supplier payout step now answers the 2020 `Группа СВК` question with `43 763 351,53 руб.`, based on `299 из 299` recovered rows from `2020-01-09` through `2020-12-25`;
- the bidirectional net step now answers with `47 628 853,03 руб.` received, `43 763 351,53 руб.` paid, and `3 865 501,50 руб.` net in our favor for the requested 2020 window;
- the monthly net step keeps the month-by-month shape from January through December 2020 and no longer inherits the old fake partiality caused by the first broad outgoing probe hitting the row cap;
- user-facing text explains that coverage was restored through monthly 1C checks after the broad probe hit the row limit, without leaking `planner_`, `pilot_`, `runtime_`, `query_documents`, or `query_movements`.
Validation:
- `npm test -- assistantMcpDiscoveryPolicy.test.ts assistantMcpDiscoveryPlanner.test.ts assistantMcpDiscoveryPilotExecutor.test.ts assistantMcpDiscoveryAnswerAdapter.test.ts assistantMcpDiscoveryResponseCandidate.test.ts assistantMcpDiscoveryResponsePolicy.test.ts assistantMcpDiscoveryRuntimeEntryPoint.test.ts assistantMcpDiscoveryRuntimeBridge.test.ts assistantAddressLaneResponseRuntimeAdapter.test.ts assistantDeepTurnResponseRuntimeAdapter.test.ts assistantLivingChatRuntimeAdapter.test.ts assistantAddressOrchestrationRuntimeAdapter.test.ts assistantDebugPayloadAssembler.test.ts` passed `91/91`;
- `npm run build` passed;
- `python scripts/domain_truth_harness.py run-live --spec docs/orchestration/address_truth_harness_phase19_mcp_discovery_response_gate.json --output-dir artifacts/domain_runs/address_truth_harness_phase19_mcp_discovery_response_gate_live_rerun14 --timeout-seconds 240` passed `8/8`, final status `accepted`.
Known remaining boundary:
- this still does not make Qwen3 an unrestricted self-navigating 1C agent across arbitrary registers and schemas; richer follow-up drilldown over the recovered rows, broader self-navigation, and evidence-safe agentic exploration remain future work beyond this first completed guarded MCP semantic data block.
Module progress:
- Big Block 5 MCP Semantic Data Agent: `100%`.
## Reset Hand-Off - 2026-04-21
The progress updates above closed the first guarded MCP discovery pilot wave.
They do **not** mean the strategic autonomy target is complete.
From 2026-04-21 onward the mainline continues in:
- [15 - mcp_bounded_autonomy_reset_plan_2026-04-21.md](/x:/1C/NDC_1C/docs/ARCH/11%20-%20architecture_turnaround/15%20-%20mcp_bounded_autonomy_reset_plan_2026-04-21.md:1)
That reset freezes the continuity/authority stabilization as sufficient and returns the project to the primary trajectory:
- metadata-first self-navigation;
- entity/schema grounding;
- planner-selected MCP primitive chains instead of route-per-question hardcoding.
## Execution Rule
Do not implement this plan as:

View File

@ -0,0 +1,201 @@
# 15 - MCP Bounded Autonomy Reset Plan (2026-04-21)
## Purpose
This note resets the execution focus after the stabilization wave inside turnaround `11`.
It does not cancel the continuity and authority work already done.
It clarifies that the main project trajectory is not:
- endless polishing of deterministic route arbitration;
- route-per-question hardcoding;
- a fake "agentic" mode that is actually another brittle prompt wrapper.
The real trajectory is:
- bounded assistant autonomy over reviewed MCP primitives;
- proof-first discovery of 1C evidence;
- gradual reduction of route hardcoding at the question level.
## Why The Reset Is Necessary
The previous wave repaired real defects:
- stale carryover stopped beating the current question on critical contours;
- guarded MCP discovery answers stopped being overwritten by stale exact/lifecycle paths;
- broad business evaluation no longer breaks the return into the data contour.
That work stays valid.
But it was support work around the edge of the main target.
The strategic target was always bigger:
- the assistant should not need one deterministic route per business wording;
- the assistant should be able to orient itself inside 1C through a reviewed set of MCP primitives;
- the planner should decide which safe primitive chain to use for the current data need;
- the evidence gate should decide what may be stated to the user.
So the reset is not "we were wrong".
It is:
- stabilization is now frozen as sufficient;
- the mainline returns to `MCP-first bounded autonomy`.
## What Is Frozen
The following is now baseline, not the mainline:
- current-turn meaning authority;
- continuity subordinated to current explicit meaning;
- guarded discovery response replacement;
- broad-evaluation bridge that does not destroy the next data follow-up.
These seams may still receive bug fixes if they block MCP-first execution.
They are not the main feature track anymore.
## North Star
The target is not an unrestricted free agent.
The target is a bounded planner over a reviewed primitive catalog:
1. recognize the business data need;
2. pick allowed MCP primitives;
3. execute bounded probes against 1C;
4. aggregate evidence;
5. answer only within the evidence gate.
In short:
- move determinism from `route per user wording`
- to `catalog of safe primitives + proof workflow`.
## Big Block A. Metadata-First Self-Navigation
### Goal
Teach the assistant to inspect the 1C schema surface before guessing a route.
This is the first real step from pilot hardcoding toward self-navigation.
### Scope
- live execution of `inspect_1c_metadata`;
- metadata-aware planner path that cannot collapse into `query_documents`;
- machine-readable metadata evidence:
- available object sets;
- matching objects;
- available fields/sections when metadata returns them;
- known limitations;
- human-safe answer draft for metadata discovery.
### Why This Block Comes First
Without metadata-first inspection, every later autonomy step is blind:
- entity resolution is guesswork;
- register/document choice is guesswork;
- long-tail discovery turns back into hidden route hardcoding.
### Acceptance
- raw metadata wording can bootstrap discovery input;
- planner keeps `inspect_1c_metadata` as the chosen primitive;
- pilot executes live metadata inspection through MCP;
- user-facing answer stays free of primitive/query/runtime garbage.
## Big Block B. Entity And Schema Grounding
### Goal
Move from "I found some metadata" to "I can ground the user ask onto the right 1C surface".
### Scope
- search and resolve candidate entities through MCP instead of local tails only;
- bind metadata findings to probable document/register families;
- preserve ambiguity honestly when multiple surfaces compete;
- keep machine-readable grounding evidence for downstream probes.
### Acceptance
- assistant can say which schema surface it selected and why;
- ambiguity is surfaced as a bounded clarification, not silent route drift;
- chosen surface becomes reusable context for the next primitive.
## Big Block C. Planner-Selected Primitive Chains
### Goal
Replace one-off pilot scopes with planner-selected safe chains.
### Scope
- chain primitives such as:
- `inspect_1c_metadata`
- `search_business_entity`
- `resolve_entity_reference`
- `query_documents`
- `query_movements`
- `aggregate_by_axis`
- `probe_coverage`
- `explain_evidence_basis`
- keep exact deterministic routes as fast-paths;
- use discovery as the general path for understood long-tail questions.
### Acceptance
- new long-tail questions become answerable without adding a dedicated route for each wording;
- the planner output explains the chosen primitive chain;
- the answer gate still blocks overclaiming.
## Stage 1 Started Now
The first block is no longer just planned.
It has started in code in this pass.
Implemented in this stage:
- raw metadata wording now bootstraps discovery input;
- metadata planning stays on `inspect_1c_metadata` and no longer falls into `query_documents`;
- the pilot executor now has a live metadata inspection slice;
- the answer adapter can produce a user-safe metadata surface answer.
This stage is intentionally narrow.
It does **not** yet mean:
- unrestricted Qwen3 navigation across arbitrary 1C contours;
- automatic multi-step schema-to-entity-to-query chaining;
- hot-runtime replacement of broad assistant behavior everywhere.
It means the architecture now has the first real self-navigation primitive in production code.
## Execution Rule
From this point the project should prefer:
- adding or strengthening reviewed MCP primitives;
- planner-selected evidence workflows;
- machine-readable grounding and proof contracts.
And should avoid:
- growing another layer of hidden route hardcoding for each new wording;
- long stabilization detours unless they protect an MCP-first invariant;
- fake autonomy that bypasses the evidence gate.
## Bottom Line
Turnaround `11` is no longer only about making the assistant feel less glitchy.
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.

View File

@ -67,6 +67,8 @@
"forbidden_answer_patterns": [
"(?i)точный маршрут.*не подключ",
"(?i)не буду подставлять",
"(?i)100\\s+из\\s+100",
"(?i)лимит.*строк",
"(?i)query_documents",
"(?i)query_movements",
"(?i)runtime_",
@ -95,6 +97,8 @@
"forbidden_answer_patterns": [
"(?i)точный маршрут.*не подключ",
"(?i)не буду подставлять",
"(?i)100\\s+из\\s+100",
"(?i)лимит.*строк",
"(?i)query_documents",
"(?i)query_movements",
"(?i)runtime_",
@ -125,6 +129,8 @@
"forbidden_answer_patterns": [
"(?i)точный маршрут.*не подключ",
"(?i)не буду подставлять",
"(?i)100\\s+из\\s+100",
"(?i)лимит.*строк",
"(?i)query_documents",
"(?i)query_movements",
"(?i)runtime_",
@ -173,7 +179,41 @@
]
},
{
"step_id": "step_07_off_domain_living_chat_not_hijacked",
"step_id": "step_07_counterparty_bidirectional_monthly_net_flow_uses_guarded_discovery",
"title": "Unsupported-but-understood counterparty monthly net cash-flow question keeps monthly structure from discovery",
"question": "какое помесячное нетто по деньгам с Группа СВК за 2020 год: сколько получили и сколько заплатили по месяцам?",
"required_answer_patterns_all": [
"(?i)свк",
"(?i)1с|найден|строк|проверен",
"(?i)получил|входящ|поступ",
"(?i)заплат|исходящ|списан|плат[её]ж",
"(?i)помесяч|по месяцам|янв|фев|мар|апр|май|июн|июл|авг|сен|окт|ноя|дек",
"(?i)нетто|сальдо|разниц",
"(?i)сумм|руб",
"(?i)2020|период",
"(?i)не подтвержд|проверенн|найденн"
],
"forbidden_answer_patterns": [
"(?i)точный маршрут.*не подключ",
"(?i)не буду подставлять",
"(?i)query_documents",
"(?i)query_movements",
"(?i)runtime_",
"(?i)planner_",
"(?i)catalog_",
"(?i)primitive",
"(?i)pilot_"
],
"criticality": "critical",
"semantic_tags": [
"mcp_discovery_bidirectional_value_flow",
"counterparty_monthly_net_cash_flow",
"multi_axis_aggregation",
"unsupported_current_turn_meaning_boundary"
]
},
{
"step_id": "step_08_off_domain_living_chat_not_hijacked",
"title": "Off-domain living chat remains human and is not hijacked by discovery carryover",
"question": "а чем капибара отличается от утки?",
"required_answer_patterns_any": [

View File

@ -0,0 +1,138 @@
{
"schema_version": "domain_truth_harness_spec_v1",
"scenario_id": "address_truth_harness_phase20_continuity_stabilization",
"domain": "address_phase20_continuity_stabilization",
"title": "Phase 20 continuity stabilization replay",
"description": "Targeted AGENT replay for the continuity stabilization slice after assistant-stage1--I0x_DLqDb. The scenario validates that temporal tail words no longer turn into pseudo-counterparties, exact debt snapshots are not overwritten by discovery, and VAT/date follow-ups keep the previous period instead of drifting into unrelated carryover.",
"bindings": {},
"steps": [
{
"step_id": "step_01_top_client_all_time",
"title": "All-time top client question stays in ranking semantics and does not invent counterparty time",
"question": "кто у нас самый доходный клиент за все время?",
"required_answer_patterns_any": [
"(?i)клиент|контрагент",
"(?i)доходн|выручк|заработ"
],
"forbidden_answer_patterns": [
"(?i)не найден контрагент.*время",
"(?i)контрагент с названием\\s+\"?время\"?"
],
"criticality": "critical",
"semantic_tags": [
"value_flow_ranking",
"temporal_tail_not_entity"
]
},
{
"step_id": "step_02_top_year_all_time",
"title": "Top year question stays in yearly ranking semantics and does not invent pseudo-entity year",
"question": "какой у нас самый доходный год?",
"required_answer_patterns_any": [
"(?i)20\\d{2}|19\\d{2}",
"(?i)доходн|выручк|заработ"
],
"forbidden_answer_patterns": [
"(?i)не найден контрагент.*год",
"(?i)контрагент с названием\\s+\"?год\"?"
],
"criticality": "critical",
"semantic_tags": [
"value_flow_ranking",
"year_tail_not_entity"
]
},
{
"step_id": "step_03_receivables_as_of_may_2017",
"title": "Receivables snapshot for May 2017 answers directly instead of being overwritten by discovery",
"question": "кто нам должен денег на май 2017?",
"allowed_reply_types": [
"factual",
"factual_with_explanation"
],
"required_direct_answer_patterns_any": [
"(?i)долж",
"(?i)дебитор",
"(?i)задолж"
],
"forbidden_direct_answer_patterns": [
"(?i)partial_coverage",
"(?i)не удалось ответить",
"(?i)не найден контрагент"
],
"criticality": "critical",
"semantic_tags": [
"receivables_snapshot",
"exact_not_overwritten"
]
},
{
"step_id": "step_04_vat_for_same_period",
"title": "VAT payable follow-up keeps the carried May 2017 period",
"question": "а какой ндс мы должны примерно заплатить за этот период?",
"required_answer_patterns_all": [
"(?i)ндс",
"(?i)май|2017",
"(?i)заплат|к уплате|уплат"
],
"forbidden_answer_patterns": [
"(?i)текущ(ий|его) период",
"(?i)не найден контрагент"
],
"criticality": "critical",
"semantic_tags": [
"vat_followup",
"period_carryover"
]
},
{
"step_id": "step_05_payables_today",
"title": "Today payables snapshot answers directly and keeps debt semantics",
"question": "мы должны комуто денег на сегодня?",
"allowed_reply_types": [
"factual",
"factual_with_explanation"
],
"required_direct_answer_patterns_any": [
"(?i)должны",
"(?i)кредитор",
"(?i)задолж",
"(?i)долг к оплате|к оплате"
],
"forbidden_direct_answer_patterns": [
"(?i)partial_coverage",
"(?i)не удалось ответить",
"(?i)не найден контрагент"
],
"criticality": "critical",
"semantic_tags": [
"payables_snapshot",
"exact_not_overwritten"
]
},
{
"step_id": "step_06_receivables_pronoun_followup",
"title": "Short follow-up a nam resolves to mirrored receivables instead of fake counterparty time",
"question": "а нам?",
"allowed_reply_types": [
"factual",
"factual_with_explanation"
],
"required_direct_answer_patterns_any": [
"(?i)нам должны",
"(?i)дебитор",
"(?i)задолж"
],
"forbidden_direct_answer_patterns": [
"(?i)не найден контрагент.*время",
"(?i)контрагент с названием\\s+\"?время\"?",
"(?i)partial_coverage"
],
"criticality": "critical",
"semantic_tags": [
"pronoun_followup",
"garbage_anchor_forbidden"
]
}
]
}

View File

@ -0,0 +1,71 @@
{
"schema_version": "domain_truth_harness_spec_v1",
"scenario_id": "address_truth_harness_phase21_net_followup_after_broad_eval",
"domain": "address_phase21_net_followup_after_broad_eval",
"title": "Phase 21 net-flow follow-up after broad evaluation replay",
"description": "Targeted AGENT replay for the assistant-stage1-LpuYsX0SRP regression where a net cash-flow question about Группа СВК inside an existing dialogue chain was wrongly kept on the counterparty lifecycle contour instead of applying the guarded MCP discovery answer.",
"bindings": {},
"steps": [
{
"step_id": "step_01_company_activity_lifecycle",
"title": "Activity lifecycle answer seeds broad counterparty context",
"question": "а по Альтернативе Плюс сколько лет активности в базе 1С?",
"allowed_reply_types": [
"partial_coverage",
"factual",
"factual_with_explanation"
],
"required_answer_patterns_any": [
"(?i)лет",
"(?i)активност",
"(?i)1с",
"(?i)не получил|не подтвержден|проверил доступный контур"
],
"criticality": "critical",
"semantic_tags": [
"company_activity_lifecycle",
"context_seed"
]
},
{
"step_id": "step_02_broad_company_evaluation",
"title": "Broad evaluation sits between lifecycle and net-flow question",
"question": "Как ты оценишь деятельность компании?",
"required_answer_patterns_any": [
"(?i)активн",
"(?i)заказчик|контрагент|деятельност|оценк"
],
"criticality": "warning",
"semantic_tags": [
"broad_evaluation_bridge"
]
},
{
"step_id": "step_03_net_flow_after_broad_eval",
"title": "Net-flow follow-up overrides stale lifecycle carryover and answers with inflow outflow and net",
"question": "какое нетто по деньгам с Группа СВК за 2020 год: сколько получили и сколько заплатили?",
"allowed_reply_types": [
"partial_coverage",
"factual_with_explanation"
],
"required_answer_patterns_all": [
"(?i)свк",
"(?i)получил|входящ|поступ",
"(?i)заплат|исходящ|списан|плат[её]ж",
"(?i)нетто|сальдо|разниц",
"(?i)2020|период",
"(?i)руб"
],
"forbidden_answer_patterns": [
"(?i)активных заказчиков",
"(?i)лет в базе",
"(?i)последняя активность"
],
"criticality": "critical",
"semantic_tags": [
"counterparty_net_cash_flow",
"stale_lifecycle_override"
]
}
]
}

View File

@ -0,0 +1,78 @@
{
"schema_version": "domain_truth_harness_spec_v1",
"scenario_id": "address_truth_harness_phase22_broad_business_evaluation_bridge",
"domain": "address_phase22_broad_business_evaluation_bridge",
"title": "Phase 22 broad business evaluation bridge replay",
"description": "Targeted AGENT replay for the broad business evaluation seam where a follow-up like 'Как ты оценишь деятельность компании?' must not replay stale lifecycle routing and should still preserve the chain for the next exact net-flow question.",
"bindings": {},
"steps": [
{
"step_id": "step_01_company_activity_lifecycle",
"title": "Lifecycle answer seeds grounded organization context",
"question": "а по Альтернативе Плюс сколько лет активности в базе 1С?",
"allowed_reply_types": [
"partial_coverage",
"factual",
"factual_with_explanation"
],
"required_answer_patterns_any": [
"(?i)лет",
"(?i)активност",
"(?i)1с",
"(?i)не получил|не подтвержден|проверил доступный контур"
],
"criticality": "critical",
"semantic_tags": [
"company_activity_lifecycle",
"grounded_context_seed"
]
},
{
"step_id": "step_02_broad_business_evaluation",
"title": "Broad business evaluation becomes grounded summary instead of stale lifecycle dump",
"question": "Как ты оценишь деятельность компании?",
"required_answer_patterns_all": [
"(?i)коротко|оценк|частичн",
"(?i)1с|подтвержд",
"(?i)денежн|долг|ндс|контрагент|операц"
],
"forbidden_answer_patterns": [
"(?i)активных заказчиков",
"(?i)последняя активность",
"(?i)^\\s*1\\."
],
"criticality": "critical",
"semantic_tags": [
"broad_business_evaluation",
"grounded_summary"
]
},
{
"step_id": "step_03_net_flow_after_broad_eval",
"title": "Exact net-flow follow-up still answers after the broad bridge",
"question": "какое нетто по деньгам с Группа СВК за 2020 год: сколько получили и сколько заплатили?",
"allowed_reply_types": [
"partial_coverage",
"factual_with_explanation"
],
"required_answer_patterns_all": [
"(?i)свк",
"(?i)получил|входящ|поступ",
"(?i)заплат|исходящ|списан|плат[её]ж",
"(?i)нетто|сальдо|разниц",
"(?i)2020|период",
"(?i)руб"
],
"forbidden_answer_patterns": [
"(?i)активных заказчиков",
"(?i)лет в базе",
"(?i)последняя активность"
],
"criticality": "critical",
"semantic_tags": [
"counterparty_net_cash_flow",
"broad_eval_bridge_preserved"
]
}
]
}

View File

@ -0,0 +1,44 @@
{
"schema_version": "domain_truth_harness_spec_v1",
"scenario_id": "address_truth_harness_phase25_entity_resolution_chain",
"domain": "address_phase25_entity_resolution_chain",
"title": "Phase 25 entity-resolution grounding replay",
"description": "Targeted AGENT replay for the first Big Block C slice where the assistant must use MCP discovery to ground a business entity in the checked 1C catalog and answer honestly without pretending that documents, movements, or value-flow evidence were already checked.",
"bindings": {},
"steps": [
{
"step_id": "step_01_resolve_counterparty_from_catalog",
"title": "Raw counterparty search wording resolves a grounded 1C entity without leaking downstream business facts",
"question": "найди в 1С контрагента Группа СВК",
"allowed_reply_types": [
"factual_with_explanation",
"partial_coverage"
],
"required_answer_patterns_all": [
"(?i)группа\\s+свк",
"(?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",
"catalog_grounding",
"bounded_autonomy"
]
}
]
}

View File

@ -0,0 +1,99 @@
{
"schema_version": "domain_truth_harness_spec_v1",
"scenario_id": "address_truth_harness_phase26_entity_followup_chain",
"domain": "address_phase26_entity_followup_chain",
"title": "Phase 26 resolved-entity follow-up chain replay",
"description": "Targeted AGENT replay for the next Big Block C slice where an MCP-grounded counterparty must become a reusable dialog anchor for downstream document and movement evidence requests without forcing the user to repeat the resolved 1C 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_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_documents_by_resolved_entity_followup",
"title": "Short document follow-up reuses the resolved counterparty anchor",
"question": "по нему документы за 2020 год",
"allowed_reply_types": [
"factual_with_explanation",
"partial_coverage"
],
"required_answer_patterns_all": [
"(?i)документ|счет|накладн|акт",
"(?i)2020"
],
"required_answer_patterns_any": [
"(?i)группа\\s+свк",
"(?i)свк"
],
"forbidden_answer_patterns": [
"(?i)не найден контрагент",
"(?i)уточните, какого контрагента",
"(?i)по какому контрагенту"
],
"criticality": "critical",
"semantic_tags": [
"entity_resolution",
"document_evidence",
"followup_reuse"
]
},
{
"step_id": "step_03_movements_by_resolved_entity_followup",
"title": "Short movement follow-up keeps the same grounded counterparty anchor",
"question": "а теперь по нему движения за 2020 год",
"allowed_reply_types": [
"factual_with_explanation",
"partial_coverage"
],
"required_answer_patterns_all": [
"(?i)движени|платеж|операц|проводк",
"(?i)2020"
],
"required_answer_patterns_any": [
"(?i)группа\\s+свк",
"(?i)свк"
],
"forbidden_answer_patterns": [
"(?i)не найден контрагент",
"(?i)уточните, какого контрагента",
"(?i)по какому контрагенту"
],
"criticality": "critical",
"semantic_tags": [
"entity_resolution",
"movement_evidence",
"followup_reuse"
]
}
]
}

View File

@ -0,0 +1,101 @@
{
"schema_version": "domain_truth_harness_spec_v1",
"scenario_id": "address_truth_harness_phase27_entity_value_followup_chain",
"domain": "address_phase27_entity_value_followup_chain",
"title": "Phase 27 resolved-entity value-flow follow-up replay",
"description": "Targeted AGENT replay for the next Big Block C slice where an MCP-grounded counterparty must become a reusable dialog anchor for downstream value-flow and net-flow questions without forcing the user to restate the resolved 1C 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_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_value_flow_by_resolved_entity_followup",
"title": "Short turnover 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",
"counterparty_value_flow",
"followup_reuse"
]
},
{
"step_id": "step_03_net_flow_by_resolved_entity_followup",
"title": "Short net-flow follow-up keeps the same grounded 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",
"counterparty_net_value_flow",
"followup_reuse"
]
}
]
}

View File

@ -0,0 +1,95 @@
{
"schema_version": "domain_truth_harness_spec_v1",
"scenario_id": "address_truth_harness_phase28_entity_value_retarget_chain",
"domain": "address_phase28_entity_value_retarget_chain",
"title": "Phase 28 grounded entity value-flow retarget replay",
"description": "Targeted AGENT replay for Big Block C where a grounded 1C counterparty must survive side-switch and year-switch follow-ups across incoming, payout, and net value-flow questions 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_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_net_switch_after_payout",
"title": "Net-flow follow-up keeps the same grounded counterparty and switched year",
"question": "а какое нетто?",
"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)по какому контрагенту",
"(?i)за какой год"
],
"criticality": "critical",
"semantic_tags": ["entity_resolution", "net_switch", "followup_reuse", "date_carryover"]
}
]
}

View File

@ -0,0 +1,97 @@
{
"schema_version": "domain_truth_harness_spec_v1",
"scenario_id": "address_truth_harness_phase29_value_flow_to_documents_chain",
"domain": "address_phase29_value_flow_to_documents_chain",
"title": "Phase 29 grounded value-flow to document 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 document 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_documents_after_value_flow",
"title": "Document 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", "document_evidence", "value_flow_pivot", "followup_reuse"]
}
]
}

View File

@ -0,0 +1,115 @@
{
"schema_version": "domain_truth_harness_spec_v1",
"scenario_id": "address_truth_harness_phase32_planner_selected_chain_end_to_end",
"domain": "address_phase32_planner_selected_chain_end_to_end",
"title": "Phase 32 planner-selected chain end-to-end replay",
"description": "Targeted AGENT replay for closing Big Block C: a grounded 1C counterparty must survive planner-selected pivots across incoming value-flow, outgoing payouts, net flow, document evidence, and 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_net_after_payout",
"title": "Net-flow follow-up reuses the same grounded counterparty and checked year after payout",
"question": "а какое нетто?",
"allowed_reply_types": ["factual_with_explanation", "partial_coverage"],
"required_answer_patterns_all": ["(?i)2020", "(?i)нетто|сальдо", "(?i)руб"],
"required_answer_patterns_any": ["(?i)получ", "(?i)заплат", "(?i)группа\\s+свк", "(?i)свк"],
"forbidden_answer_patterns": [
"(?i)не найден контрагент",
"(?i)уточните, какого контрагента",
"(?i)по какому контрагенту"
],
"criticality": "critical",
"semantic_tags": ["entity_resolution", "net_value_flow", "followup_reuse"]
},
{
"step_id": "step_05_documents_after_net",
"title": "Document evidence follow-up keeps the grounded counterparty after the net answer",
"question": "а по документам?",
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
"required_answer_patterns_all": ["(?i)документ|счет|накладн|акт"],
"required_answer_patterns_any": ["(?i)группа\\s+свк", "(?i)свк", "(?i)2020"],
"forbidden_answer_patterns": [
"(?i)не найден контрагент",
"(?i)уточните, какого контрагента",
"(?i)по какому контрагенту",
"(?i)сколько получили",
"(?i)сколько заплатили",
"(?i)нетто"
],
"criticality": "critical",
"semantic_tags": ["entity_resolution", "document_evidence", "value_flow_pivot", "followup_reuse"]
},
{
"step_id": "step_06_movements_after_documents",
"title": "Movement evidence follow-up keeps the grounded counterparty after the document answer",
"question": "а по движениям?",
"allowed_reply_types": ["factual", "factual_with_explanation", "partial_coverage"],
"required_answer_patterns_all": ["(?i)движени|операц|платеж|списан|поступ"],
"required_answer_patterns_any": ["(?i)группа\\s+свк", "(?i)свк", "(?i)2020"],
"forbidden_answer_patterns": [
"(?i)не найден контрагент",
"(?i)уточните, какого контрагента",
"(?i)по какому контрагенту",
"(?i)сколько получили",
"(?i)сколько заплатили",
"(?i)нетто"
],
"criticality": "critical",
"semantic_tags": ["entity_resolution", "movement_evidence", "document_pivot", "followup_reuse"]
}
]
}

View File

@ -225,6 +225,7 @@ function runAssistantAddressLaneResponseRuntime(input) {
const mcpDiscoveryResponsePolicy = (0, assistantMcpDiscoveryResponsePolicy_1.applyAssistantMcpDiscoveryResponsePolicy)({
currentReply: guardedResponse.assistantReply,
currentReplySource: "address_query_runtime_v1",
currentReplyType: guardedResponse.replyType,
addressRuntimeMeta: debugWithResponseGuard
});
const finalAssistantReply = mcpDiscoveryResponsePolicy.applied

View File

@ -136,7 +136,7 @@ async function buildAssistantAddressOrchestrationRuntime(input) {
};
carryover = input.resolveAddressFollowupCarryoverContext(input.userMessage, input.sessionItems, addressInputMessage, addressPreDecompose, input.sessionAddressNavigationState);
}
const followupContext = carryover?.followupContext ?? null;
const followupContext = toRecordObject(carryover?.followupContext);
const routePolicyRuntime = (0, assistantRoutePolicyRuntimeAdapter_1.runAssistantRoutePolicyRuntime)({
rawUserMessage: input.userMessage,
effectiveAddressUserMessage: addressInputMessage,
@ -159,7 +159,8 @@ async function buildAssistantAddressOrchestrationRuntime(input) {
userMessage: input.userMessage,
effectiveMessage: addressInputMessage,
assistantTurnMeaning: toRecordObject(orchestrationContract?.assistant_turn_meaning),
predecomposeContract
predecomposeContract,
followupContext
}));
}
catch (error) {

View File

@ -1,8 +1,18 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.readAssistantMcpDiscoveryEntityResolutionStatus = readAssistantMcpDiscoveryEntityResolutionStatus;
exports.readAssistantMcpDiscoveryEntityAmbiguityCandidates = readAssistantMcpDiscoveryEntityAmbiguityCandidates;
exports.readAssistantMcpDiscoveryEntityCandidates = readAssistantMcpDiscoveryEntityCandidates;
exports.readAssistantMcpDiscoveryPilotScope = readAssistantMcpDiscoveryPilotScope;
exports.readAssistantMcpDiscoveryMetadataRouteFamily = readAssistantMcpDiscoveryMetadataRouteFamily;
exports.readAssistantMcpDiscoveryMetadataSelectedEntitySet = readAssistantMcpDiscoveryMetadataSelectedEntitySet;
exports.readAssistantMcpDiscoveryMetadataAmbiguityDetected = readAssistantMcpDiscoveryMetadataAmbiguityDetected;
exports.readAssistantMcpDiscoveryMetadataAmbiguityEntitySets = readAssistantMcpDiscoveryMetadataAmbiguityEntitySets;
exports.formatIsoDateForReply = formatIsoDateForReply;
exports.readAddressDebugFilters = readAddressDebugFilters;
exports.readAddressDebugItem = readAddressDebugItem;
exports.readAddressDebugCounterparty = readAddressDebugCounterparty;
exports.readAddressDebugIntent = readAddressDebugIntent;
exports.readAddressDebugOrganization = readAddressDebugOrganization;
exports.readAddressDebugScopedDate = readAddressDebugScopedDate;
exports.readAddressDebugTemporalScope = readAddressDebugTemporalScope;
@ -41,6 +51,219 @@ function toRecordObject(value) {
}
return value;
}
function candidateValue(value) {
const direct = fallbackToNonEmptyString(value);
if (direct && direct !== "[object Object]") {
return direct;
}
const record = toRecordObject(value);
if (!record) {
return null;
}
return (fallbackToNonEmptyString(record.value) ??
fallbackToNonEmptyString(record.name) ??
fallbackToNonEmptyString(record.ref) ??
fallbackToNonEmptyString(record.text));
}
function readAssistantMcpDiscoveryEntry(debug) {
const entry = toRecordObject(debug?.assistant_mcp_discovery_entry_point_v1);
return fallbackToNonEmptyString(entry?.schema_version) === "assistant_mcp_discovery_runtime_entry_point_v1"
? entry
: null;
}
function readAssistantMcpDiscoveryTurnMeaning(debug) {
const entry = readAssistantMcpDiscoveryEntry(debug);
const turnInput = toRecordObject(entry?.turn_input);
return toRecordObject(turnInput?.turn_meaning_ref);
}
function readAssistantMcpDiscoveryTurnMeaningMetadataAmbiguityEntitySets(debug, toNonEmptyString = fallbackToNonEmptyString) {
const values = readAssistantMcpDiscoveryTurnMeaning(debug)?.metadata_ambiguity_entity_sets;
if (!Array.isArray(values)) {
return [];
}
return values.map((item) => toNonEmptyString(item)).filter((item) => Boolean(item));
}
function readAssistantMcpDiscoveryActionFamily(debug, toNonEmptyString = fallbackToNonEmptyString) {
return toNonEmptyString(readAssistantMcpDiscoveryTurnMeaning(debug)?.asked_action_family);
}
function readAssistantMcpDiscoveryBridge(debug) {
return toRecordObject(readAssistantMcpDiscoveryEntry(debug)?.bridge);
}
function readAssistantMcpDiscoveryDerivedMetadataSurface(debug) {
const bridge = readAssistantMcpDiscoveryBridge(debug);
const pilot = toRecordObject(bridge?.pilot);
return toRecordObject(pilot?.derived_metadata_surface);
}
function readAssistantMcpDiscoveryDerivedEntityResolution(debug) {
const bridge = readAssistantMcpDiscoveryBridge(debug);
const pilot = toRecordObject(bridge?.pilot);
return toRecordObject(pilot?.derived_entity_resolution);
}
function readAssistantMcpDiscoveryEntityResolutionStatus(debug, toNonEmptyString = fallbackToNonEmptyString) {
return toNonEmptyString(readAssistantMcpDiscoveryDerivedEntityResolution(debug)?.resolution_status);
}
function readAssistantMcpDiscoveryEntityAmbiguityCandidates(debug, toNonEmptyString = fallbackToNonEmptyString) {
const values = readAssistantMcpDiscoveryDerivedEntityResolution(debug)?.ambiguity_candidates;
if (!Array.isArray(values)) {
return [];
}
return values.map((item) => toNonEmptyString(item)).filter((item) => Boolean(item));
}
function collectAssistantMcpDiscoveryEntityCandidates(debug, toNonEmptyString = fallbackToNonEmptyString) {
const result = [];
const resolution = readAssistantMcpDiscoveryDerivedEntityResolution(debug);
const pushCandidate = (value) => {
const text = toNonEmptyString(value);
if (text && !result.includes(text)) {
result.push(text);
}
};
pushCandidate(resolution?.resolved_entity);
pushCandidate(resolution?.requested_entity);
if (Array.isArray(resolution?.ambiguity_candidates)) {
for (const candidate of resolution.ambiguity_candidates) {
pushCandidate(candidate);
}
}
const discoveryMeaning = readAssistantMcpDiscoveryTurnMeaning(debug);
const explicitEntities = Array.isArray(discoveryMeaning?.explicit_entity_candidates)
? discoveryMeaning.explicit_entity_candidates
: [];
for (const entity of explicitEntities) {
pushCandidate(candidateValue(entity));
}
return result;
}
function readAssistantMcpDiscoveryEntityCandidates(debug, toNonEmptyString = fallbackToNonEmptyString) {
return collectAssistantMcpDiscoveryEntityCandidates(debug, toNonEmptyString);
}
function readAssistantMcpDiscoveryPilotScope(debug, toNonEmptyString = fallbackToNonEmptyString) {
const bridge = readAssistantMcpDiscoveryBridge(debug);
const pilot = toRecordObject(bridge?.pilot);
return toNonEmptyString(pilot?.pilot_scope);
}
function readAssistantMcpDiscoveryMetadataRouteFamily(debug, toNonEmptyString = fallbackToNonEmptyString) {
return toNonEmptyString(readAssistantMcpDiscoveryDerivedMetadataSurface(debug)?.downstream_route_family);
}
function readAssistantMcpDiscoveryMetadataSelectedEntitySet(debug, toNonEmptyString = fallbackToNonEmptyString) {
return toNonEmptyString(readAssistantMcpDiscoveryDerivedMetadataSurface(debug)?.selected_entity_set);
}
function readAssistantMcpDiscoveryMetadataAmbiguityDetected(debug) {
return (readAssistantMcpDiscoveryDerivedMetadataSurface(debug)?.ambiguity_detected === true ||
readAssistantMcpDiscoveryTurnMeaningMetadataAmbiguityEntitySets(debug).length > 0);
}
function readAssistantMcpDiscoveryMetadataAmbiguityEntitySets(debug, toNonEmptyString = fallbackToNonEmptyString) {
const values = readAssistantMcpDiscoveryDerivedMetadataSurface(debug)?.ambiguity_entity_sets;
if (Array.isArray(values)) {
return values.map((item) => toNonEmptyString(item)).filter((item) => Boolean(item));
}
return readAssistantMcpDiscoveryTurnMeaningMetadataAmbiguityEntitySets(debug, toNonEmptyString);
}
function mapAssistantMcpDiscoveryPilotScopeToAddressIntent(pilotScope, actionFamily) {
if (pilotScope === "counterparty_lifecycle_query_documents_v1") {
return "counterparty_activity_lifecycle";
}
if (pilotScope === "counterparty_document_evidence_query_documents_v1") {
return "list_documents_by_counterparty";
}
if (pilotScope === "counterparty_movement_evidence_query_movements_v1") {
return "bank_operations_by_counterparty";
}
if (pilotScope === "counterparty_supplier_payout_query_movements_v1") {
return "supplier_payouts_profile";
}
if (pilotScope === "counterparty_value_flow_query_movements_v1") {
return "customer_revenue_and_payments";
}
if (pilotScope === "counterparty_bidirectional_value_flow_query_movements_v1") {
return null;
}
if (actionFamily === "activity_duration") {
return "counterparty_activity_lifecycle";
}
if (actionFamily === "payout") {
return "supplier_payouts_profile";
}
if (actionFamily === "turnover") {
return "customer_revenue_and_payments";
}
return null;
}
function readDiscoveryDateScopeFilters(debug, toNonEmptyString = fallbackToNonEmptyString) {
const explicitDateScope = toNonEmptyString(readAssistantMcpDiscoveryTurnMeaning(debug)?.explicit_date_scope);
if (!explicitDateScope) {
return {
asOfDate: null,
periodFrom: null,
periodTo: null
};
}
const isoDateMatch = explicitDateScope.match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (isoDateMatch) {
return {
asOfDate: explicitDateScope,
periodFrom: null,
periodTo: null
};
}
const monthMatch = explicitDateScope.match(/^(\d{4})-(\d{2})$/);
if (monthMatch) {
const year = Number(monthMatch[1]);
const month = Number(monthMatch[2]);
if (Number.isFinite(year) && Number.isFinite(month) && month >= 1 && month <= 12) {
const lastDay = new Date(Date.UTC(year, month, 0)).getUTCDate();
return {
asOfDate: null,
periodFrom: `${monthMatch[1]}-${monthMatch[2]}-01`,
periodTo: `${monthMatch[1]}-${monthMatch[2]}-${String(lastDay).padStart(2, "0")}`
};
}
}
const yearMatch = explicitDateScope.match(/^(\d{4})$/);
if (yearMatch) {
return {
asOfDate: null,
periodFrom: `${yearMatch[1]}-01-01`,
periodTo: `${yearMatch[1]}-12-31`
};
}
const rangeMatch = explicitDateScope.match(/^(\d{4}-\d{2}-\d{2})\.\.(\d{4}-\d{2}-\d{2})$/);
if (rangeMatch) {
return {
asOfDate: null,
periodFrom: rangeMatch[1],
periodTo: rangeMatch[2]
};
}
return {
asOfDate: null,
periodFrom: null,
periodTo: null
};
}
function formatDiscoveryDateScopeForReply(value) {
const text = fallbackToNonEmptyString(value);
if (!text) {
return null;
}
return formatIsoDateForReply(text) ?? text;
}
function hasGroundedDiscoveryBusinessAnswer(debug, toNonEmptyString = fallbackToNonEmptyString) {
if (!debug || debug.mcp_discovery_response_applied !== true) {
return false;
}
const entry = readAssistantMcpDiscoveryEntry(debug);
const bridge = readAssistantMcpDiscoveryBridge(debug);
const bridgeStatus = toNonEmptyString(bridge?.bridge_status);
const answerDraft = toRecordObject(bridge?.answer_draft);
const answerMode = toNonEmptyString(answerDraft?.answer_mode);
return Boolean(entry &&
toNonEmptyString(entry.entry_status) === "bridge_executed" &&
bridgeStatus === "answer_draft_ready" &&
(bridge?.business_fact_answer_allowed === true ||
answerMode === "confirmed_with_bounded_inference" ||
answerMode === "bounded_inference_only"));
}
function mergeKnownOrganizationsDefault(values) {
return (0, assistantOrganizationMatcher_1.mergeKnownOrganizations)(values);
}
@ -65,25 +288,62 @@ function readAddressDebugItem(debug, toNonEmptyString = fallbackToNonEmptyString
? toNonEmptyString(debug?.anchor_value_resolved) ?? toNonEmptyString(debug?.anchor_value_raw)
: null));
}
function readAddressDebugCounterparty(debug, toNonEmptyString = fallbackToNonEmptyString) {
const extractedFilters = readAddressDebugFilters(debug);
if (toNonEmptyString(extractedFilters?.counterparty)) {
return toNonEmptyString(extractedFilters?.counterparty);
}
if (String(debug?.anchor_type ?? "") === "counterparty") {
return toNonEmptyString(debug?.anchor_value_resolved) ?? toNonEmptyString(debug?.anchor_value_raw);
}
const discoveryEntities = collectAssistantMcpDiscoveryEntityCandidates(debug, toNonEmptyString);
for (const entity of discoveryEntities) {
const text = toNonEmptyString(entity);
if (text) {
return text;
}
}
return null;
}
function readAddressDebugIntent(debug, toNonEmptyString = fallbackToNonEmptyString) {
const detectedIntent = toNonEmptyString(debug?.detected_intent);
if (detectedIntent && detectedIntent !== "unknown") {
return detectedIntent;
}
return mapAssistantMcpDiscoveryPilotScopeToAddressIntent(readAssistantMcpDiscoveryPilotScope(debug, toNonEmptyString), readAssistantMcpDiscoveryActionFamily(debug, toNonEmptyString));
}
function readAddressDebugOrganization(debug, toNonEmptyString = fallbackToNonEmptyString) {
const extractedFilters = readAddressDebugFilters(debug);
const rootFrameContext = toRecordObject(debug?.address_root_frame_context);
return toNonEmptyString(extractedFilters?.organization) ?? toNonEmptyString(rootFrameContext?.organization);
const discoveryMeaning = readAssistantMcpDiscoveryTurnMeaning(debug);
return (toNonEmptyString(extractedFilters?.organization) ??
toNonEmptyString(rootFrameContext?.organization) ??
toNonEmptyString(discoveryMeaning?.explicit_organization_scope) ??
toNonEmptyString(debug?.assistant_active_organization) ??
toNonEmptyString(debug?.living_chat_selected_organization));
}
function readAddressDebugScopedDate(debug) {
const extractedFilters = readAddressDebugFilters(debug);
const rootFrameContext = toRecordObject(debug?.address_root_frame_context);
return (formatIsoDateForReply(extractedFilters?.as_of_date) ??
formatIsoDateForReply(rootFrameContext?.as_of_date) ??
formatIsoDateForReply(extractedFilters?.period_to));
formatIsoDateForReply(extractedFilters?.period_to) ??
formatDiscoveryDateScopeForReply(readAssistantMcpDiscoveryTurnMeaning(debug)?.explicit_date_scope));
}
function readAddressDebugTemporalScope(debug, toNonEmptyString = fallbackToNonEmptyString) {
const extractedFilters = readAddressDebugFilters(debug);
const rootFrameContext = toRecordObject(debug?.address_root_frame_context);
const discoveryDateScope = readDiscoveryDateScopeFilters(debug, toNonEmptyString);
return {
asOfDate: toNonEmptyString(extractedFilters?.as_of_date) ?? toNonEmptyString(rootFrameContext?.as_of_date),
periodFrom: toNonEmptyString(extractedFilters?.period_from) ?? toNonEmptyString(rootFrameContext?.period_from),
periodTo: toNonEmptyString(extractedFilters?.period_to) ?? toNonEmptyString(rootFrameContext?.period_to)
asOfDate: toNonEmptyString(extractedFilters?.as_of_date) ??
toNonEmptyString(rootFrameContext?.as_of_date) ??
discoveryDateScope.asOfDate,
periodFrom: toNonEmptyString(extractedFilters?.period_from) ??
toNonEmptyString(rootFrameContext?.period_from) ??
discoveryDateScope.periodFrom,
periodTo: toNonEmptyString(extractedFilters?.period_to) ??
toNonEmptyString(rootFrameContext?.period_to) ??
discoveryDateScope.periodTo
};
}
function resolveAddressDebugAnchorContext(debug, toNonEmptyString = fallbackToNonEmptyString) {
@ -110,6 +370,13 @@ function resolveAddressDebugAnchorContext(debug, toNonEmptyString = fallbackToNo
anchorValue: counterparty
};
}
const discoveryCounterparty = readAddressDebugCounterparty(debug, toNonEmptyString);
if (discoveryCounterparty) {
return {
anchorType: "counterparty",
anchorValue: discoveryCounterparty
};
}
const account = toNonEmptyString(extractedFilters?.account);
if (account) {
return {
@ -149,6 +416,7 @@ function resolveNavigationSessionContextState(addressNavigationState, toNonEmpty
function resolveAddressDebugContextFacts(debug, toNonEmptyString = fallbackToNonEmptyString) {
return {
item: readAddressDebugItem(debug, toNonEmptyString),
counterparty: readAddressDebugCounterparty(debug, toNonEmptyString),
organization: readAddressDebugOrganization(debug, toNonEmptyString),
scopedDate: readAddressDebugScopedDate(debug)
};
@ -156,6 +424,31 @@ function resolveAddressDebugContextFacts(debug, toNonEmptyString = fallbackToNon
function resolveAddressDebugCarryoverFilters(debug, toNonEmptyString = fallbackToNonEmptyString) {
const extractedFilters = readAddressDebugFilters(debug);
const nextFilters = extractedFilters ? { ...extractedFilters } : {};
const discoveryDateScope = readDiscoveryDateScopeFilters(debug, toNonEmptyString);
const preferGroundedDiscoveryDateScope = hasGroundedDiscoveryBusinessAnswer(debug, toNonEmptyString) &&
Boolean(discoveryDateScope.asOfDate || discoveryDateScope.periodFrom || discoveryDateScope.periodTo);
const counterparty = readAddressDebugCounterparty(debug, toNonEmptyString);
const organization = readAddressDebugOrganization(debug, toNonEmptyString);
if (counterparty && !toNonEmptyString(nextFilters.counterparty)) {
nextFilters.counterparty = counterparty;
}
if (organization && !toNonEmptyString(nextFilters.organization)) {
nextFilters.organization = organization;
}
if (discoveryDateScope.asOfDate && (preferGroundedDiscoveryDateScope || !toNonEmptyString(nextFilters.as_of_date))) {
nextFilters.as_of_date = discoveryDateScope.asOfDate;
delete nextFilters.period_from;
delete nextFilters.period_to;
}
if (discoveryDateScope.periodFrom &&
(preferGroundedDiscoveryDateScope || !toNonEmptyString(nextFilters.period_from))) {
nextFilters.period_from = discoveryDateScope.periodFrom;
}
if (discoveryDateScope.periodTo &&
(preferGroundedDiscoveryDateScope || !toNonEmptyString(nextFilters.period_to))) {
nextFilters.period_to = discoveryDateScope.periodTo;
delete nextFilters.as_of_date;
}
const inventoryRootFrame = buildInventoryRootFrameFromAddressDebug(debug, toNonEmptyString);
const rootFilters = inventoryRootFrame?.filters && typeof inventoryRootFrame.filters === "object"
? inventoryRootFrame.filters
@ -538,13 +831,12 @@ function isGroundedAddressDebug(debug, toNonEmptyString = fallbackToNonEmptyStri
if (!debug || typeof debug !== "object") {
return false;
}
const executionLane = toNonEmptyString(debug.execution_lane);
if (executionLane !== "address_query") {
return false;
}
const answerGroundingCheck = toRecordObject(debug.answer_grounding_check);
const groundingStatus = toNonEmptyString(answerGroundingCheck?.status);
return groundingStatus === "grounded";
if (groundingStatus === "grounded" && toNonEmptyString(debug.execution_lane) === "address_query") {
return true;
}
return hasGroundedDiscoveryBusinessAnswer(debug, toNonEmptyString);
}
function isGroundedInventoryContextDebug(debug, toNonEmptyString) {
if (!isGroundedAddressDebug(debug, toNonEmptyString)) {

View File

@ -113,10 +113,26 @@ async function runAssistantLivingChatRuntime(input) {
: "deterministic_data_scope_contract";
}
else if (unsupportedCurrentTurnMeaningBoundary) {
chatText = buildUnsupportedCurrentTurnMeaningBoundaryReply({
assistantTurnMeaning
});
livingChatSource = "deterministic_unsupported_current_turn_boundary";
const unsupportedFamily = typeof assistantTurnMeaning?.unsupported_but_understood_family === "string"
? assistantTurnMeaning.unsupported_but_understood_family
: null;
if (unsupportedFamily === "broad_business_evaluation") {
const scopedOrganization = selectedOrganization ?? activeOrganization ?? continuityActiveOrganization ?? null;
chatText = (0, assistantMemoryRecapPolicy_1.buildBroadBusinessEvaluationReply)({
organization: scopedOrganization,
addressDebug: continuitySnapshot.lastGroundedAddressDebug,
sessionItems: input.sessionItems,
toNonEmptyString: input.toNonEmptyString
});
activeOrganization = scopedOrganization ?? activeOrganization;
livingChatSource = "deterministic_broad_business_evaluation_contract";
}
else {
chatText = buildUnsupportedCurrentTurnMeaningBoundaryReply({
assistantTurnMeaning
});
livingChatSource = "deterministic_unsupported_current_turn_boundary";
}
}
else if ((selectedOrganization || activeOrganization) && input.hasOrganizationFactLookupSignal(userMessage)) {
const scopedOrganization = selectedOrganization ?? activeOrganization ?? null;

View File

@ -27,6 +27,12 @@ function uniqueStrings(values) {
}
return result;
}
function formatNamedChoiceList(values) {
return uniqueStrings(values)
.slice(0, 6)
.map((value, index) => `${index + 1}. ${value}`)
.join("; ");
}
function isInternalMechanicsLine(value) {
const text = value.toLowerCase();
return (text.includes("primitive") ||
@ -51,6 +57,11 @@ function modeFor(pilot) {
if (pilot.pilot_status === "skipped_needs_clarification") {
return "needs_clarification";
}
if (pilot.pilot_scope === "entity_resolution_search_v1" &&
(pilot.reason_codes.includes("pilot_entity_resolution_ambiguity_requires_clarification") ||
pilot.derived_entity_resolution?.resolution_status === "ambiguous")) {
return "needs_clarification";
}
if (pilot.evidence.answer_permission === "confirmed_answer") {
return "confirmed_with_bounded_inference";
}
@ -64,7 +75,162 @@ function isValueFlowPilot(pilot) {
pilot.pilot_scope === "counterparty_supplier_payout_query_movements_v1" ||
pilot.pilot_scope === "counterparty_bidirectional_value_flow_query_movements_v1");
}
function isDocumentPilot(pilot) {
return pilot.pilot_scope === "counterparty_document_evidence_query_documents_v1";
}
function isMovementPilot(pilot) {
return pilot.pilot_scope === "counterparty_movement_evidence_query_movements_v1";
}
function isMetadataPilot(pilot) {
return pilot.pilot_scope === "metadata_inspection_v1";
}
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"));
}
function askedActionFamily(pilot) {
const action = pilot.evidence.query_plan.turn_meaning_ref?.asked_action_family;
if (typeof action !== "string") {
return null;
}
const normalized = action.trim().toLowerCase();
return normalized.length > 0 ? normalized : null;
}
function unsupportedFamily(pilot) {
const unsupported = pilot.evidence.query_plan.turn_meaning_ref?.unsupported_but_understood_family;
if (typeof unsupported !== "string") {
return null;
}
const normalized = unsupported.trim().toLowerCase();
return normalized.length > 0 ? normalized : null;
}
function firstEntityCandidate(pilot) {
const values = Array.isArray(pilot.evidence.query_plan.turn_meaning_ref?.explicit_entity_candidates)
? pilot.evidence.query_plan.turn_meaning_ref?.explicit_entity_candidates
: [];
for (const value of values) {
const text = String(value ?? "").trim();
if (text) {
return text;
}
}
return null;
}
function explicitDateScope(pilot) {
const value = pilot.evidence.query_plan.turn_meaning_ref?.explicit_date_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);
const entityPart = entity ? ` по контрагенту ${entity}` : "";
const periodPart = period ? ` за ${period}` : " в проверенном окне";
return `${entityPart}${periodPart}`;
}
function isMovementLaneClarification(pilot) {
return (isMovementPilot(pilot) ||
pilot.reason_codes.includes("planner_selected_movement_recipe") ||
pilot.dry_run.reason_codes.includes("planner_selected_movement_recipe") ||
askedActionFamily(pilot) === "list_movements" ||
unsupportedFamily(pilot) === "movement_evidence");
}
function isDocumentLaneClarification(pilot) {
return (isDocumentPilot(pilot) ||
pilot.reason_codes.includes("planner_selected_document_recipe") ||
pilot.dry_run.reason_codes.includes("planner_selected_document_recipe") ||
askedActionFamily(pilot) === "list_documents" ||
unsupportedFamily(pilot) === "document_evidence");
}
function laneScopeSuffix(pilot) {
const entity = firstEntityCandidate(pilot);
return entity ? ` по "${entity}"` : "";
}
function dryRunMissingAxis(pilot, axis) {
return pilot.dry_run.execution_steps.some((step) => step.missing_axis_options.some((option) => option.includes(axis)));
}
function clarificationNeedRu(pilot) {
const needsPeriod = dryRunMissingAxis(pilot, "period");
const needsOrganization = dryRunMissingAxis(pilot, "organization");
if (needsPeriod && needsOrganization) {
return { subject: "проверяемый период и организацию", verb: "нужно" };
}
if (needsPeriod) {
return { subject: "проверяемый период", verb: "нужен" };
}
if (needsOrganization) {
return { subject: "организацию", verb: "нужно" };
}
return { subject: "контекст проверки", verb: "нужно" };
}
function clarificationNextStepLine(pilot, laneLabel) {
const needsPeriod = dryRunMissingAxis(pilot, "period");
const needsOrganization = dryRunMissingAxis(pilot, "organization");
const scopeSuffix = laneScopeSuffix(pilot);
if (needsPeriod && needsOrganization) {
return `Уточните период и организацию, и я продолжу поиск по ${laneLabel}${scopeSuffix} в 1С.`;
}
if (needsPeriod) {
return `Уточните период, и я продолжу поиск по ${laneLabel}${scopeSuffix} в 1С.`;
}
if (needsOrganization) {
return `Уточните организацию, и я продолжу поиск по ${laneLabel}${scopeSuffix} в 1С.`;
}
return `Уточните контекст проверки, и я продолжу поиск по ${laneLabel}${scopeSuffix} в 1С.`;
}
function metadataRouteFamilyLabelRu(routeFamily) {
if (routeFamily === "document_evidence") {
return "контур документов";
}
if (routeFamily === "movement_evidence") {
return "контур движений/регистров";
}
if (routeFamily === "catalog_drilldown") {
return "контур справочников и связанных объектов";
}
return null;
}
function headlineFor(mode, pilot) {
const askedMonthlyBreakdown = pilot.derived_bidirectional_value_flow?.aggregation_axis === "month" ||
pilot.derived_value_flow?.aggregation_axis === "month";
if (isEntityResolutionPilot(pilot) && mode === "confirmed_with_bounded_inference") {
return "По каталогу 1С найден вероятный контрагент; это заземление сущности для следующего шага, а не еще бизнес-ответ по данным.";
}
if (isEntityResolutionPilot(pilot) && mode === "needs_clarification") {
return "По каталогу 1С нашлось несколько похожих контрагентов, и без уточнения нельзя честно выбрать правильную сущность.";
}
if (isEntityResolutionPilot(pilot) &&
mode === "checked_sources_only" &&
pilot.derived_entity_resolution?.resolution_status === "not_found") {
return "По текущему каталожному поиску 1С точный контрагент пока не подтвержден.";
}
if (isMovementPilot(pilot) && mode === "confirmed_with_bounded_inference") {
return `По движениям${documentOrMovementScopeRu(pilot)} в 1С найдены подтвержденные строки; ответ ограничен проверенным окном и найденными строками.`;
}
if (pilot.derived_metadata_surface && mode === "confirmed_with_bounded_inference") {
if (pilot.derived_metadata_surface.ambiguity_detected) {
return "По метаданным 1С найдены конкурирующие schema-поверхности; перед следующим шагом нужно удержать неоднозначность явно.";
}
if (pilot.derived_metadata_surface.downstream_route_family) {
return "По метаданным 1С найдена схема и заземлена вероятная поверхность для следующего безопасного шага.";
}
return "По метаданным 1С найдена доступная схема для дальнейшего безопасного поиска.";
}
if (askedMonthlyBreakdown && pilot.derived_bidirectional_value_flow && mode === "confirmed_with_bounded_inference") {
return "По данным 1С найдены строки входящих и исходящих денежных движений; нетто и помесячная раскладка могут называться только как расчет по найденным строкам и проверенному периоду.";
}
if (askedMonthlyBreakdown && pilot.derived_value_flow && mode === "confirmed_with_bounded_inference") {
if (pilot.derived_value_flow.value_flow_direction === "outgoing_supplier_payout") {
return "По данным 1С найдены строки исходящих платежей/списаний; сумму и помесячную раскладку можно называть только в рамках проверенного периода и найденных строк.";
}
return "По данным 1С найдены строки входящих денежных поступлений; сумму и помесячную раскладку можно называть только в рамках проверенного периода и найденных строк.";
}
if (pilot.derived_bidirectional_value_flow && mode === "confirmed_with_bounded_inference") {
return "По данным 1С найдены строки входящих и исходящих денежных движений; нетто можно называть только как расчет по найденным строкам и проверенному периоду.";
}
@ -72,14 +238,37 @@ function headlineFor(mode, pilot) {
if (pilot.derived_value_flow.value_flow_direction === "outgoing_supplier_payout") {
return "По данным 1С найдены строки исходящих платежей/списаний; сумму можно называть только в рамках проверенного периода и найденных строк.";
}
return "По данным 1С найдены строки денежных движений; сумму можно называть только в рамках проверенного периода и найденных строк.";
return "По данным 1С найдены строки входящих денежных поступлений; сумму можно называть только в рамках проверенного периода и найденных строк.";
}
if (isDocumentPilot(pilot) && mode === "confirmed_with_bounded_inference") {
return `По документам${documentOrMovementScopeRu(pilot)} в 1С найдены подтвержденные строки; ответ ограничен проверенным окном и найденными строками.`;
}
if (isMovementPilot(pilot) && mode === "confirmed_with_bounded_inference") {
return `По движениям${documentOrMovementScopeRu(pilot)} в 1С найдены подтвержденные строки; ответ ограничен проверенным окном и найденными строками.`;
}
if (mode === "confirmed_with_bounded_inference") {
return "По данным 1С есть подтвержденная активность; длительность можно оценивать только как вывод из этих строк.";
}
if (isDocumentPilot(pilot) && mode === "bounded_inference_only") {
return `По документам${documentOrMovementScopeRu(pilot)} полный срез не подтвержден; пока есть только ограниченная граница проверенного окна 1С.`;
}
if (isMovementPilot(pilot) && mode === "bounded_inference_only") {
return `По движениям${documentOrMovementScopeRu(pilot)} полный срез не подтвержден; пока есть только ограниченная граница проверенного окна 1С.`;
}
if (mode === "bounded_inference_only") {
return "Точный факт не подтвержден, но есть ограниченная оценка по найденной активности в 1С.";
}
if (mode === "needs_clarification" && isMetadataLaneChoiceClarification(pilot)) {
return "По подтвержденной metadata-поверхности видно несколько конкурирующих data-lane, и без явного выбора дальше идти нельзя.";
}
if (mode === "needs_clarification" && isMovementLaneClarification(pilot)) {
const need = clarificationNeedRu(pilot);
return `Могу идти дальше по движениям/регистрам${laneScopeSuffix(pilot)}, но для запуска поиска в 1С ${need.verb} ${need.subject}.`;
}
if (mode === "needs_clarification" && isDocumentLaneClarification(pilot)) {
const need = clarificationNeedRu(pilot);
return `Могу идти дальше по документам${laneScopeSuffix(pilot)}, но для запуска поиска в 1С ${need.verb} ${need.subject}.`;
}
if (mode === "needs_clarification") {
return "Нужно уточнить контекст перед поиском в 1С.";
}
@ -89,9 +278,43 @@ function headlineFor(mode, pilot) {
return "Я проверил доступный контур, но подтвержденного факта для ответа не получил.";
}
function nextStepFor(mode, pilot) {
if (isEntityResolutionPilot(pilot) && mode === "needs_clarification") {
const ambiguityCandidates = pilot.derived_entity_resolution?.ambiguity_candidates ?? [];
if (ambiguityCandidates.length > 0) {
return `Уточните, какой именно контрагент нужен: ${formatNamedChoiceList(ambiguityCandidates)}. Можно ответить названием или номером варианта.`;
}
return "Уточните точное название контрагента или добавьте ИНН, и я продолжу уже по нужной сущности в 1С.";
}
if (isEntityResolutionPilot(pilot) && mode === "confirmed_with_bounded_inference") {
return "Теперь могу продолжить уже по найденному контрагенту и искать документы, движения или денежный поток.";
}
if (isEntityResolutionPilot(pilot) &&
mode === "checked_sources_only" &&
pilot.derived_entity_resolution?.resolution_status === "not_found") {
return "Дайте точное название или ИНН, и я повторю поиск по каталогу 1С более прицельно.";
}
if (mode === "needs_clarification" && isMetadataLaneChoiceClarification(pilot)) {
return "Уточните, в какой контур идти дальше: по документам или по движениям/регистрам.";
}
if (mode === "needs_clarification" && isMovementLaneClarification(pilot)) {
return clarificationNextStepLine(pilot, "движениям/регистрам");
}
if (mode === "needs_clarification" && isDocumentLaneClarification(pilot)) {
return clarificationNextStepLine(pilot, "документам");
}
if (mode === "needs_clarification") {
return "Уточните контрагента, период или организацию, и я смогу выполнить проверку по 1С.";
}
if (mode === "confirmed_with_bounded_inference" && pilot.derived_metadata_surface) {
const surface = pilot.derived_metadata_surface;
if (surface.ambiguity_detected && surface.ambiguity_entity_sets.length > 0) {
return `Следующим шагом лучше сузить surface до одного семейства: ${surface.ambiguity_entity_sets.join(", ")}.`;
}
const routeLabel = metadataRouteFamilyLabelRu(surface.downstream_route_family);
if (surface.selected_entity_set && routeLabel) {
return `Следующим шагом могу пойти в ${routeLabel} по surface «${surface.selected_entity_set}» и уже искать подтвержденные данные, а не только схему.`;
}
}
if (mode === "checked_sources_only" && pilot.query_limitations.length > 0) {
return "Можно повторить проверку после восстановления MCP-доступа или сузить вопрос до конкретного контрагента/периода.";
}
@ -113,11 +336,61 @@ 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 (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.");
}
if (isMovementPilot(pilot)) {
claims.push("Do not claim full movement history outside the checked period.");
claims.push("Do not present the confirmed movement rows as a complete movement universe.");
}
if (isMetadataPilot(pilot)) {
claims.push("Do not present metadata surface as confirmed business data rows.");
claims.push("Do not claim a document/register exists outside the checked metadata probe results.");
claims.push("Do not present the inferred next checked lane as already executed data retrieval.");
}
if (isEntityResolutionPilot(pilot)) {
claims.push("Do not present catalog grounding as confirmed business activity, turnover, or document evidence.");
claims.push("Do not claim legal identity uniqueness when several catalog candidates are still plausible.");
claims.push("Do not imply that the resolved entity has already been used in a downstream data probe.");
}
if (pilot.evidence.confirmed_facts.length === 0) {
claims.push("Do not claim a confirmed business fact when confirmed_facts is empty.");
}
return claims;
}
const RU_MONTH_LABELS_SHORT = [
"янв",
"фев",
"мар",
"апр",
"май",
"июн",
"июл",
"авг",
"сен",
"окт",
"ноя",
"дек"
];
function monthLabelRu(monthBucket) {
const match = monthBucket.match(/^(\d{4})-(\d{2})$/);
if (!match) {
return monthBucket;
}
const monthIndex = Number(match[2]) - 1;
const label = RU_MONTH_LABELS_SHORT[monthIndex] ?? match[2];
return `${label} ${match[1]}`;
}
function netLabelRu(netDirection) {
if (netDirection === "net_incoming") {
return "нетто в нашу сторону";
}
if (netDirection === "net_outgoing") {
return "нетто исходящее";
}
return "нетто нулевое";
}
function derivedActivityInferenceLine(pilot) {
const period = pilot.derived_activity_period;
if (!period) {
@ -129,6 +402,67 @@ function derivedActivityInferenceLine(pilot) {
"Это вывод по данным 1С, а не юридически подтвержденный возраст регистрации."
].join(" ");
}
function derivedMetadataConfirmedLine(pilot) {
const surface = pilot.derived_metadata_surface;
if (!surface) {
return null;
}
const scope = surface.metadata_scope ? ` по области "${surface.metadata_scope}"` : "";
const entitySets = surface.available_entity_sets.length > 0
? ` Типы объектов: ${surface.available_entity_sets.join(", ")}.`
: "";
const objects = surface.matched_objects.length > 0
? ` Найденные объекты: ${surface.matched_objects.slice(0, 8).join(", ")}.`
: "";
const selectedEntitySet = surface.selected_entity_set ? ` Выбранное family: ${surface.selected_entity_set}.` : "";
const selectedObjects = surface.selected_surface_objects.length > 0
? ` Выбранные surface-объекты: ${surface.selected_surface_objects.slice(0, 6).join(", ")}.`
: "";
const fields = surface.available_fields.length > 0
? ` Доступные поля/секции: ${surface.available_fields.slice(0, 12).join(", ")}.`
: "";
return `Подтвержденная metadata-поверхность 1С${scope}: ${surface.matched_rows} строк metadata-ответа.${entitySets}${objects}${selectedEntitySet}${selectedObjects}${fields}`.replace(/\s+/g, " ").trim();
}
function derivedMetadataInferenceLine(pilot) {
const surface = pilot.derived_metadata_surface;
if (!surface) {
return null;
}
if (surface.ambiguity_detected && surface.ambiguity_entity_sets.length > 0) {
return `По подтвержденной metadata-поверхности видно несколько конкурирующих family: ${surface.ambiguity_entity_sets.join(", ")}. Следующий data-lane пока нельзя выбрать без явного сужения.`;
}
const routeLabel = metadataRouteFamilyLabelRu(surface.downstream_route_family);
if (!surface.selected_entity_set || !routeLabel) {
return null;
}
return `По подтвержденной metadata-поверхности следующий проверяемый шаг можно ограниченно оценить как ${routeLabel} через family «${surface.selected_entity_set}». Это еще не выполненный data-fetch, а только grounded выбор следующего контура.`;
}
function derivedEntityResolutionConfirmedLine(pilot) {
const resolution = pilot.derived_entity_resolution;
if (!resolution || resolution.resolution_status !== "resolved" || !resolution.resolved_entity) {
return null;
}
const requested = resolution.requested_entity ? ` по запросу "${resolution.requested_entity}"` : "";
const confidence = resolution.confidence === "high"
? " Точность совпадения выглядит высокой."
: resolution.confidence === "medium"
? " Совпадение выглядит достаточно сильным, но это все еще catalog grounding."
: " Совпадение выглядит вероятным, но его лучше считать рабочим заземлением сущности.";
return `В текущем каталожном срезе 1С${requested} найден контрагент "${resolution.resolved_entity}".${confidence}`;
}
function derivedEntityResolutionInferenceLine(pilot) {
const resolution = pilot.derived_entity_resolution;
if (!resolution) {
return null;
}
if (resolution.resolution_status === "resolved") {
return "Сейчас подтверждено только заземление сущности по каталогу 1С; документы, движения и денежные показатели по ней еще не проверялись.";
}
if (resolution.resolution_status === "ambiguous" && resolution.ambiguity_candidates.length > 0) {
return `В каталоге 1С нашлось несколько близких кандидатов: ${formatNamedChoiceList(resolution.ambiguity_candidates)}. Без уточнения нельзя честно выбрать одного контрагента для следующего шага.`;
}
return null;
}
function derivedValueFlowConfirmedLine(pilot) {
const flow = pilot.derived_value_flow;
if (!flow) {
@ -136,13 +470,15 @@ function derivedValueFlowConfirmedLine(pilot) {
}
const counterparty = flow.counterparty ? ` по контрагенту ${flow.counterparty}` : "";
const period = flow.period_scope ? ` за период ${flow.period_scope}` : " в проверенном окне";
const movementLabel = flow.value_flow_direction === "outgoing_supplier_payout" ? "исходящих платежей/списаний" : "денежных движений";
const movementLabel = flow.value_flow_direction === "outgoing_supplier_payout"
? "исходящих платежей/списаний"
: "входящих денежных поступлений";
const totalLabel = flow.value_flow_direction === "outgoing_supplier_payout"
? "сумма исходящих платежей/списаний составляет"
: "сумма составляет";
: "сумма входящих денежных поступлений составляет";
const caveat = flow.value_flow_direction === "outgoing_supplier_payout"
? "Это расчет по найденным строкам 1С, а не подтверждение полного объема платежей вне проверенного окна."
: "Это расчет по найденным строкам 1С, а не подтверждение полного оборота вне проверенного окна.";
: "Это расчет по найденным строкам 1С, а не подтверждение полного объема поступлений вне проверенного окна.";
const dates = flow.first_movement_date && flow.latest_movement_date
? ` Первая найденная дата движения: ${flow.first_movement_date}; последняя: ${flow.latest_movement_date}.`
: "";
@ -151,6 +487,19 @@ function derivedValueFlowConfirmedLine(pilot) {
: "";
return `По найденным строкам ${movementLabel} в 1С${counterparty}${period} ${totalLabel} ${flow.total_amount_human_ru} Учтено строк с суммой: ${flow.rows_with_amount} из ${flow.rows_matched}.${dates}${limitCaveat} ${caveat}`;
}
function derivedValueFlowMonthlyLines(pilot) {
const flow = pilot.derived_value_flow;
if (!flow || flow.aggregation_axis !== "month" || flow.monthly_breakdown.length === 0) {
return [];
}
return flow.monthly_breakdown.map((bucket) => {
const monthLabel = monthLabelRu(bucket.month_bucket);
if (flow.value_flow_direction === "outgoing_supplier_payout") {
return `Помесячно: ${monthLabel} — заплатили ${bucket.total_amount_human_ru} по ${bucket.rows_with_amount} строкам с суммой`;
}
return `Помесячно: ${monthLabel} — получили ${bucket.total_amount_human_ru} по ${bucket.rows_with_amount} строкам с суммой`;
});
}
function sideDateRange(first, latest) {
if (first && latest) {
return ` первая дата ${first}, последняя ${latest}`;
@ -185,6 +534,13 @@ function derivedBidirectionalValueFlowConfirmedLine(pilot) {
.replace(/\s+/g, " ")
.trim();
}
function derivedBidirectionalValueFlowMonthlyLines(pilot) {
const flow = pilot.derived_bidirectional_value_flow;
if (!flow || flow.aggregation_axis !== "month" || flow.monthly_breakdown.length === 0) {
return [];
}
return flow.monthly_breakdown.map((bucket) => `Помесячно: ${monthLabelRu(bucket.month_bucket)} — получили ${bucket.incoming_total_amount_human_ru}, заплатили ${bucket.outgoing_total_amount_human_ru}, ${netLabelRu(bucket.net_direction)} ${bucket.net_amount_human_ru}`);
}
function buildAssistantMcpDiscoveryAnswerDraft(pilot) {
const mode = modeFor(pilot);
const reasonCodes = [...pilot.reason_codes, ...pilot.evidence.reason_codes];
@ -195,14 +551,28 @@ function buildAssistantMcpDiscoveryAnswerDraft(pilot) {
if (pilot.evidence.inferred_facts.length > 0) {
pushReason(reasonCodes, "answer_contains_bounded_inference");
}
const derivedInferenceLine = derivedActivityInferenceLine(pilot);
const derivedInferenceLine = derivedActivityInferenceLine(pilot) ??
derivedMetadataInferenceLine(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 monthlyConfirmedLines = derivedBidirectionalValueFlowMonthlyLines(pilot).length > 0
? derivedBidirectionalValueFlowMonthlyLines(pilot)
: derivedValueFlowMonthlyLines(pilot);
if (monthlyConfirmedLines.length > 0) {
pushReason(reasonCodes, "answer_contains_monthly_breakdown");
}
const confirmedLines = derivedValueLine
? [...pilot.evidence.confirmed_facts, derivedValueLine]
: pilot.evidence.confirmed_facts;
? [...pilot.evidence.confirmed_facts, derivedValueLine, ...monthlyConfirmedLines]
: derivedEntityResolutionLine
? [...pilot.evidence.confirmed_facts, derivedEntityResolutionLine]
: derivedMetadataLine
? [...pilot.evidence.confirmed_facts, derivedMetadataLine]
: pilot.evidence.confirmed_facts;
return {
schema_version: exports.ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION,
policy_owner: "assistantMcpDiscoveryAnswerAdapter",

File diff suppressed because it is too large Load Diff

View File

@ -38,6 +38,9 @@ function pushUnique(target, value) {
function hasEntity(meaning) {
return (meaning?.explicit_entity_candidates?.length ?? 0) > 0;
}
function aggregationAxis(meaning) {
return toNonEmptyString(meaning?.asked_aggregation_axis)?.toLowerCase() ?? null;
}
function addScopeAxes(axes, meaning) {
if (hasEntity(meaning)) {
pushUnique(axes, "counterparty");
@ -52,6 +55,24 @@ function addScopeAxes(axes, meaning) {
function includesAny(text, tokens) {
return tokens.some((token) => text.includes(token));
}
function isYearDateScope(meaning) {
return /^\d{4}$/.test(toNonEmptyString(meaning?.explicit_date_scope) ?? "");
}
function budgetOverrideFor(input, recipe) {
const meaning = input.turnMeaning ?? null;
const requestedAggregationAxis = aggregationAxis(meaning);
const isValueFlowRecipe = recipe.semanticDataNeed === "counterparty value-flow evidence" &&
recipe.primitives.includes("query_movements");
if (!isValueFlowRecipe) {
return {};
}
if (requestedAggregationAxis === "month" || isYearDateScope(meaning)) {
return {
maxProbeCount: 30
};
}
return {};
}
function recipeFor(input) {
const meaning = input.turnMeaning ?? null;
const domain = lower(meaning?.asked_domain_family);
@ -59,25 +80,35 @@ function recipeFor(input) {
const unsupported = lower(meaning?.unsupported_but_understood_family);
const combined = `${domain} ${action} ${unsupported}`.trim();
const axes = [];
const requestedAggregationAxis = aggregationAxis(meaning);
addScopeAxes(axes, meaning);
if (includesAny(combined, ["metadata_lane_choice_clarification", "resolve_next_lane"])) {
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_recipe"
};
}
if (includesAny(combined, ["turnover", "revenue", "payment", "payout", "value", "net", "netting", "balance", "cashflow"])) {
pushUnique(axes, "aggregate_axis");
pushUnique(axes, "amount");
pushUnique(axes, "coverage_target");
if (requestedAggregationAxis === "month") {
pushUnique(axes, "calendar_month");
}
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: ["resolve_entity_reference", "query_movements", "aggregate_by_axis", "probe_coverage"],
axes,
reason: "planner_selected_value_flow_recipe"
};
}
if (includesAny(combined, ["document", "documents"])) {
pushUnique(axes, "coverage_target");
return {
semanticDataNeed: "document evidence",
primitives: ["resolve_entity_reference", "query_documents", "probe_coverage"],
axes,
reason: "planner_selected_document_recipe"
reason: requestedAggregationAxis === "month"
? "planner_selected_monthly_value_flow_recipe"
: "planner_selected_value_flow_recipe"
};
}
if (includesAny(combined, ["lifecycle", "activity", "duration", "age"])) {
@ -86,6 +117,8 @@ function recipeFor(input) {
pushUnique(axes, "evidence_basis");
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: ["resolve_entity_reference", "query_documents", "probe_coverage", "explain_evidence_basis"],
axes,
reason: "planner_selected_lifecycle_recipe"
@ -95,15 +128,42 @@ function recipeFor(input) {
pushUnique(axes, "metadata_scope");
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: ["inspect_1c_metadata"],
axes,
reason: "planner_selected_metadata_recipe"
};
}
if (includesAny(combined, ["movement", "movements", "bank_operations", "movement_evidence", "list_movements"])) {
pushUnique(axes, "coverage_target");
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: ["resolve_entity_reference", "query_movements", "probe_coverage"],
axes,
reason: "planner_selected_movement_recipe"
};
}
if (includesAny(combined, ["document", "documents"])) {
pushUnique(axes, "coverage_target");
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: ["resolve_entity_reference", "query_documents", "probe_coverage"],
axes,
reason: "planner_selected_document_recipe"
};
}
if (hasEntity(meaning)) {
pushUnique(axes, "business_entity");
pushUnique(axes, "coverage_target");
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: ["search_business_entity", "resolve_entity_reference", "probe_coverage"],
axes,
reason: "planner_selected_entity_resolution_recipe"
@ -111,6 +171,8 @@ function recipeFor(input) {
}
return {
semanticDataNeed: "unclassified 1C discovery need",
chainId: "metadata_inspection",
chainSummary: "Start with metadata inspection instead of guessing a deeper fact route when the business need is still under-specified.",
primitives: ["inspect_1c_metadata"],
axes,
reason: "planner_selected_clarification_recipe"
@ -127,14 +189,19 @@ function statusFrom(plan, review) {
}
function planAssistantMcpDiscovery(input) {
const recipe = recipeFor(input);
const budgetOverride = budgetOverrideFor(input, recipe);
const semanticDataNeed = toNonEmptyString(input.semanticDataNeed) ?? recipe.semanticDataNeed;
const reasonCodes = [];
pushReason(reasonCodes, recipe.reason);
if (budgetOverride.maxProbeCount) {
pushReason(reasonCodes, "planner_enabled_chunked_coverage_probe_budget");
}
const plan = (0, assistantMcpDiscoveryPolicy_1.buildAssistantMcpDiscoveryPlan)({
semanticDataNeed,
turnMeaning: input.turnMeaning,
proposedPrimitives: recipe.primitives,
requiredAxes: recipe.axes
requiredAxes: recipe.axes,
maxProbeCount: budgetOverride.maxProbeCount
});
const review = (0, assistantMcpCatalogIndex_1.reviewAssistantMcpDiscoveryPlanAgainstCatalog)(plan);
const plannerStatus = statusFrom(plan, review);
@ -152,6 +219,8 @@ function planAssistantMcpDiscovery(input) {
policy_owner: "assistantMcpDiscoveryPlanner",
planner_status: plannerStatus,
semantic_data_need: semanticDataNeed,
selected_chain_id: recipe.chainId,
selected_chain_summary: recipe.chainSummary,
proposed_primitives: recipe.primitives,
required_axes: recipe.axes,
discovery_plan: plan,

View File

@ -21,7 +21,7 @@ const DEFAULT_DISCOVERY_BUDGET = {
max_probe_count: 3,
max_rows_per_probe: 100
};
const MAX_PROBE_COUNT = 6;
const MAX_PROBE_COUNT = 36;
const MAX_ROWS_PER_PROBE = 500;
const ALLOWED_PRIMITIVE_SET = new Set(exports.ASSISTANT_MCP_DISCOVERY_PRIMITIVES);
function toNonEmptyString(value) {
@ -74,19 +74,27 @@ function normalizeTurnMeaning(value) {
const result = {};
const domain = toNonEmptyString(value.asked_domain_family);
const action = toNonEmptyString(value.asked_action_family);
const aggregationAxis = toNonEmptyString(value.asked_aggregation_axis);
const organization = toNonEmptyString(value.explicit_organization_scope);
const dateScope = toNonEmptyString(value.explicit_date_scope);
const unsupported = toNonEmptyString(value.unsupported_but_understood_family);
const entities = toStringList(value.explicit_entity_candidates);
const metadataAmbiguityEntitySets = toStringList(value.metadata_ambiguity_entity_sets);
if (domain) {
result.asked_domain_family = domain;
}
if (action) {
result.asked_action_family = action;
}
if (aggregationAxis) {
result.asked_aggregation_axis = aggregationAxis;
}
if (entities.length > 0) {
result.explicit_entity_candidates = entities;
}
if (metadataAmbiguityEntitySets.length > 0) {
result.metadata_ambiguity_entity_sets = metadataAmbiguityEntitySets;
}
if (organization) {
result.explicit_organization_scope = organization;
}

View File

@ -70,10 +70,24 @@ function localizeLine(value) {
}
const valueFlowMatch = value.match(/^1C value-flow rows were found for counterparty\s+(.+)$/i);
if (valueFlowMatch) {
return `В 1С найдены строки денежных движений по контрагенту ${valueFlowMatch[1]}.`;
return `В 1С найдены строки входящих денежных поступлений по контрагенту ${valueFlowMatch[1]}.`;
}
if (/^1C value-flow rows were found for the requested counterparty scope$/i.test(value)) {
return "В 1С найдены строки денежных движений по запрошенному контрагентскому контуру.";
return "В 1С найдены строки входящих денежных поступлений по запрошенному контрагентскому контуру.";
}
const documentRowsMatch = value.match(/^1C document rows were found for counterparty\s+(.+)$/i);
if (documentRowsMatch) {
return `В 1С найдены строки документов по контрагенту ${documentRowsMatch[1]}.`;
}
if (/^1C document rows were found for the requested scope$/i.test(value)) {
return "В 1С найдены строки документов по запрошенному контуру.";
}
const movementRowsMatch = value.match(/^1C movement rows were found for counterparty\s+(.+)$/i);
if (movementRowsMatch) {
return `Р1С найдены строки движений по контрагенту ${movementRowsMatch[1]}.`;
}
if (/^1C movement rows were found for the requested scope$/i.test(value)) {
return "Р1С найдены строки движений по запрошенному контуру.";
}
const supplierPayoutMatch = value.match(/^1C supplier-payout rows were found for counterparty\s+(.+)$/i);
if (supplierPayoutMatch) {
@ -97,8 +111,17 @@ function localizeLine(value) {
if (/^Business activity duration may be inferred from first and latest confirmed 1C activity rows$/i.test(value)) {
return "Длительность деловой активности можно оценивать только как вывод по первой и последней подтвержденной строке активности в 1С.";
}
if (/^Counterparty document evidence is limited to confirmed 1C document rows in the checked scope$/i.test(value)) {
return "Срез документов ограничен только подтвержденными строками документов в проверенном окне.";
}
if (/^Counterparty movement evidence is limited to confirmed 1C movement rows in the checked scope$/i.test(value)) {
return "Срез движений ограничен только подтвержденными строками движений в проверенном окне.";
}
if (/^Counterparty value-flow total was calculated from confirmed 1C movement rows$/i.test(value)) {
return "Сумма рассчитана только по подтвержденным строкам денежных движений в 1С.";
return "Сумма входящих поступлений рассчитана только по подтвержденным строкам поступлений в 1С.";
}
if (/^Counterparty monthly value-flow breakdown was grouped by month over confirmed 1C movement rows$/i.test(value)) {
return "Помесячная раскладка входящих поступлений построена только по подтвержденным строкам поступлений в 1С.";
}
if (/^Counterparty supplier-payout total was calculated from confirmed 1C outgoing payment rows$/i.test(value)) {
return "Сумма исходящих платежей рассчитана только по подтвержденным строкам списаний в 1С.";
@ -106,6 +129,53 @@ function localizeLine(value) {
if (/^Counterparty net value-flow was calculated as incoming confirmed 1C rows minus outgoing confirmed 1C rows$/i.test(value)) {
return "Нетто денежного потока рассчитано только как входящие подтвержденные строки 1С минус исходящие подтвержденные строки 1С.";
}
if (/^Counterparty monthly net value-flow breakdown was grouped by month over confirmed incoming and outgoing 1C rows$/i.test(value)) {
return "Помесячная нетто-раскладка сгруппирована только по подтвержденным входящим и исходящим строкам 1С.";
}
const metadataSurfaceMatch = value.match(/^Confirmed 1C metadata surface(?: for scope "([^"]+)")?: (\d+) rows and (\d+) matching objects$/i);
if (metadataSurfaceMatch) {
const scopePart = metadataSurfaceMatch[1] ? ` по области "${metadataSurfaceMatch[1]}"` : "";
return `В 1С подтверждена metadata-поверхность${scopePart}: ${metadataSurfaceMatch[2]} строк metadata-ответа и ${metadataSurfaceMatch[3]} совпавших объекта(ов).`;
}
const metadataObjectSetsMatch = value.match(/^Available metadata object sets: (.+)$/i);
if (metadataObjectSetsMatch) {
return `Доступные типы metadata-объектов: ${metadataObjectSetsMatch[1]}.`;
}
const selectedMetadataEntitySetMatch = value.match(/^Selected metadata entity set: (.+)$/i);
if (selectedMetadataEntitySetMatch) {
return `Выбранное семейство metadata-объектов: ${selectedMetadataEntitySetMatch[1]}.`;
}
const selectedMetadataObjectsMatch = value.match(/^Selected metadata objects: (.+)$/i);
if (selectedMetadataObjectsMatch) {
return `Выбранные metadata-объекты для следующего шага: ${selectedMetadataObjectsMatch[1]}.`;
}
const metadataFieldsMatch = value.match(/^Available metadata fields\/sections: (.+)$/i);
if (metadataFieldsMatch) {
return `Доступные metadata-поля/секции: ${metadataFieldsMatch[1]}.`;
}
const metadataLaneInferenceMatch = value.match(/^A likely next checked lane may be inferred as (document_evidence|movement_evidence|catalog_drilldown) from the confirmed metadata surface$/i);
if (metadataLaneInferenceMatch) {
const routeLabel = metadataLaneInferenceMatch[1] === "document_evidence"
? "контур документов"
: metadataLaneInferenceMatch[1] === "movement_evidence"
? "контур движений/регистров"
: "контур справочников и связанных объектов";
return `Следующий проверяемый контур по этой metadata-поверхности можно ограниченно оценить как ${routeLabel}.`;
}
if (/^Detailed metadata fields were not returned by this MCP metadata probe$/i.test(value)) {
return "Эта MCP-проверка metadata не вернула детальный список полей.";
}
const metadataAmbiguityMatch = value.match(/^Exact downstream metadata surface remains ambiguous across: (.+)$/i);
if (metadataAmbiguityMatch) {
return `Точная downstream metadata-поверхность пока неоднозначна между family: ${metadataAmbiguityMatch[1]}.`;
}
const noMatchingMetadataScopeMatch = value.match(/^No matching 1C metadata objects were confirmed for scope "([^"]+)"$/i);
if (noMatchingMetadataScopeMatch) {
return `В 1С не подтверждены metadata-объекты по области "${noMatchingMetadataScopeMatch[1]}".`;
}
if (/^No matching 1C metadata objects were confirmed by this MCP metadata probe$/i.test(value)) {
return "В 1С эта MCP-проверка не подтвердила подходящих metadata-объектов.";
}
if (/^Legal registration date is not proven by this MCP discovery pilot$/i.test(value)) {
return "Юридическая дата регистрации этим поиском не подтверждена.";
}
@ -116,10 +186,22 @@ function localizeLine(value) {
return "Полное покрытие запрошенного периода по двустороннему денежному потоку не подтверждено: хотя бы одна сторона проверки достигла лимита найденных строк.";
}
if (/^Full turnover outside the checked period is not proven by this MCP discovery pilot$/i.test(value)) {
return "Полный оборот вне проверенного периода этим поиском не подтвержден.";
return "Полный объем входящих поступлений вне проверенного периода этим поиском не подтвержден.";
}
if (/^Full all-time turnover is not proven without an explicit checked period$/i.test(value)) {
return "Полный оборот за все время без явно проверенного периода не подтвержден.";
return "Полный объем входящих поступлений за все время без явно проверенного периода не подтвержден.";
}
if (/^Full document history outside the checked period is not proven by this MCP discovery pilot$/i.test(value)) {
return "Полный исторический срез документов вне проверенного периода этим поиском не подтвержден.";
}
if (/^Full document history is not proven without an explicit checked period$/i.test(value)) {
return "Полный срез документов без явно проверенного периода не подтвержден.";
}
if (/^Full movement history outside the checked period is not proven by this MCP discovery pilot$/i.test(value)) {
return "Полный исторический срез движений вне проверенного периода этим поиском не подтвержден.";
}
if (/^Full movement history is not proven without an explicit checked period$/i.test(value)) {
return "Полный срез движений без явно проверенного периода не подтвержден.";
}
if (/^Full supplier-payout amount outside the checked period is not proven by this MCP discovery pilot$/i.test(value)) {
return "Полный объем исходящих платежей вне проверенного периода этим поиском не подтвержден.";
@ -133,6 +215,12 @@ function localizeLine(value) {
if (/^Full all-time bidirectional value-flow is not proven without an explicit checked period$/i.test(value)) {
return "Полный двусторонний денежный поток за все время без явно проверенного периода не подтвержден.";
}
if (/^Requested period coverage was recovered through monthly 1C value-flow probes after the broad probe hit the row limit$/i.test(value)) {
return "Покрытие запрошенного периода восстановлено помесячными проверками 1С после того, как общая выборка уперлась в лимит строк.";
}
if (/^Requested period coverage for bidirectional value-flow was recovered through monthly 1C side probes after a broad probe hit the row limit$/i.test(value)) {
return "Покрытие запрошенного периода по двустороннему денежному потоку восстановлено помесячными проверками 1С после того, как общая выборка уперлась в лимит строк хотя бы по одной стороне.";
}
return value;
}
function section(title, lines) {

View File

@ -66,6 +66,10 @@ function isUnsupportedCurrentTurnBoundary(input) {
input.livingChatSource === "deterministic_unsupported_current_turn_boundary" ||
input.currentReplySource === "deterministic_unsupported_current_turn_boundary");
}
function isDeterministicBroadBusinessEvaluationReply(input) {
return (input.livingChatSource === "deterministic_broad_business_evaluation_contract" ||
input.currentReplySource === "deterministic_broad_business_evaluation_contract");
}
function isDiscoveryReadyChatCandidate(input, entryPoint) {
const turnInput = toRecordObject(entryPoint?.turn_input);
return (entryPoint?.entry_status === "bridge_executed" &&
@ -89,6 +93,167 @@ function isDiscoveryReadyAddressCandidate(input, entryPoint) {
turnInput?.should_run_discovery === true &&
(source === "address_lane" || source === "address_exact" || source === "address_query_runtime_v1"));
}
function isDetectedIntentAlignedWithTurnMeaning(detectedIntent, turnMeaning) {
const normalizedIntent = String(detectedIntent ?? "").trim().toLowerCase();
if (!normalizedIntent) {
return false;
}
const askedDomain = String(toNonEmptyString(turnMeaning?.asked_domain_family) ?? "").trim().toLowerCase();
const askedAction = String(toNonEmptyString(turnMeaning?.asked_action_family) ?? "").trim().toLowerCase();
if (normalizedIntent === "counterparty_activity_lifecycle") {
return (askedDomain === "counterparty_lifecycle" ||
askedAction === "activity_duration" ||
askedAction === "age_or_activity_duration");
}
if (normalizedIntent === "supplier_payouts_profile") {
return askedDomain === "counterparty_value" && askedAction === "payout";
}
if (normalizedIntent === "customer_revenue_and_payments") {
return askedDomain === "counterparty_value" && (askedAction === "turnover" || askedAction === "counterparty_value_or_turnover");
}
if (normalizedIntent === "receivables_confirmed_as_of_date") {
return askedDomain === "receivables" || askedAction === "confirmed_snapshot";
}
if (normalizedIntent === "payables_confirmed_as_of_date") {
return askedDomain === "payables" || askedAction === "confirmed_snapshot";
}
if (normalizedIntent === "vat_liability_confirmed_for_tax_period") {
return askedDomain === "vat" && askedAction === "confirmed_tax_period";
}
if (normalizedIntent === "vat_payable_confirmed_as_of_date") {
return askedDomain === "vat" && askedAction === "confirmed_snapshot";
}
if (normalizedIntent === "vat_payable_forecast") {
return askedDomain === "vat" && askedAction === "forecast";
}
if (normalizedIntent === "list_documents_by_counterparty") {
return askedAction === "list_documents" || askedDomain === "counterparty_documents" || askedDomain === "counterparty";
}
if (normalizedIntent === "inventory_on_hand_as_of_date" || normalizedIntent === "inventory_aging_by_purchase_date") {
return askedDomain === "inventory" && askedAction === "confirmed_snapshot";
}
return false;
}
function readDiscoveryTurnMeaning(entryPoint) {
const turnInput = toRecordObject(entryPoint?.turn_input);
return toRecordObject(turnInput?.turn_meaning_ref);
}
function readTruthAnswerShape(input) {
const directShape = toRecordObject(input.addressRuntimeMeta?.answer_shape_contract);
if (directShape) {
return directShape;
}
const truthAnswerPolicy = toRecordObject(input.addressRuntimeMeta?.assistant_truth_answer_policy_v1);
return toRecordObject(truthAnswerPolicy?.answer_shape);
}
function hasEffectivelyFactualAddressReply(input) {
if (toNonEmptyString(input.currentReplyType) === "factual") {
return true;
}
const truthAnswerShape = readTruthAnswerShape(input);
return toNonEmptyString(truthAnswerShape?.reply_type) === "factual";
}
function readStateTransitionReasonCodes(input) {
const directTransition = toRecordObject(input.addressRuntimeMeta?.assistant_state_transition_v1);
const fallbackTransition = toRecordObject(input.addressRuntimeMeta?.state_transition_contract);
const stateTransition = directTransition ?? fallbackTransition;
if (!stateTransition || !Array.isArray(stateTransition.reason_codes)) {
return [];
}
return stateTransition.reason_codes
.map((item) => toNonEmptyString(item))
.filter((item) => Boolean(item));
}
function hasRuntimeAdjustedExactReply(input, entryPoint) {
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
return false;
}
if (!hasEffectivelyFactualAddressReply(input)) {
return false;
}
const truthGateStatus = toNonEmptyString(input.addressRuntimeMeta?.truth_gate_contract_status);
const truthAnswerPolicy = toRecordObject(input.addressRuntimeMeta?.assistant_truth_answer_policy_v1);
const truthGate = toRecordObject(truthAnswerPolicy?.truth_gate);
const sourceTruthGateStatus = toNonEmptyString(truthGate?.source_truth_gate_status);
const coverageStatus = toNonEmptyString(truthGate?.coverage_status);
const groundingStatus = toNonEmptyString(truthGate?.grounding_status);
const hasFullConfirmedTruth = truthGateStatus === "full_confirmed" ||
sourceTruthGateStatus === "full_confirmed" ||
(coverageStatus === "full" && groundingStatus === "grounded");
if (!hasFullConfirmedTruth) {
return false;
}
const truthAnswerShape = readTruthAnswerShape(input);
const capabilityContractId = toNonEmptyString(truthAnswerShape?.capability_contract_id);
if (!capabilityContractId) {
return false;
}
return readStateTransitionReasonCodes(input).some((reason) => /^intent_adjusted_to_.+_followup_context$/i.test(reason));
}
function hasAlignedFactualAddressReply(input, entryPoint) {
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
return false;
}
if (!hasEffectivelyFactualAddressReply(input)) {
return false;
}
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
return isDetectedIntentAlignedWithTurnMeaning(detectedIntent, readDiscoveryTurnMeaning(entryPoint));
}
function hasSemanticConflictWithDiscoveryTurnMeaning(input, entryPoint) {
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
return false;
}
if (!hasEffectivelyFactualAddressReply(input)) {
return false;
}
if (hasRuntimeAdjustedExactReply(input, entryPoint)) {
return false;
}
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
const turnMeaning = readDiscoveryTurnMeaning(entryPoint);
const askedDomain = toNonEmptyString(turnMeaning?.asked_domain_family);
const askedAction = toNonEmptyString(turnMeaning?.asked_action_family);
const unsupportedFamily = toNonEmptyString(turnMeaning?.unsupported_but_understood_family);
if (!detectedIntent || (!askedDomain && !askedAction && !unsupportedFamily)) {
return false;
}
return !isDetectedIntentAlignedWithTurnMeaning(detectedIntent, turnMeaning);
}
function hasMatchedFactualAddressContinuationTarget(input, entryPoint) {
if (!hasEffectivelyFactualAddressReply(input)) {
return false;
}
if (hasSemanticConflictWithDiscoveryTurnMeaning(input, entryPoint)) {
return false;
}
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
const dialogContinuationContract = toRecordObject(input.addressRuntimeMeta?.dialogContinuationContract) ??
toRecordObject(input.addressRuntimeMeta?.dialog_continuation_contract_v2);
const targetIntent = toNonEmptyString(dialogContinuationContract?.target_intent);
return Boolean(detectedIntent && targetIntent && detectedIntent === targetIntent);
}
function hasFullConfirmedFactualAddressReply(input, entryPoint) {
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
return false;
}
if (!hasEffectivelyFactualAddressReply(input)) {
return false;
}
if (hasSemanticConflictWithDiscoveryTurnMeaning(input, entryPoint)) {
return false;
}
const truthGateStatus = toNonEmptyString(input.addressRuntimeMeta?.truth_gate_contract_status);
if (truthGateStatus === "full_confirmed") {
return true;
}
const truthAnswerPolicy = toRecordObject(input.addressRuntimeMeta?.assistant_truth_answer_policy_v1);
const truthGate = toRecordObject(truthAnswerPolicy?.truth_gate);
const sourceTruthGateStatus = toNonEmptyString(truthGate?.source_truth_gate_status);
const coverageStatus = toNonEmptyString(truthGate?.coverage_status);
const groundingStatus = toNonEmptyString(truthGate?.grounding_status);
return sourceTruthGateStatus === "full_confirmed" || (coverageStatus === "full" && groundingStatus === "grounded");
}
function applyAssistantMcpDiscoveryResponsePolicy(input) {
const currentReply = String(input.currentReply ?? "");
const currentReplySource = toNonEmptyString(input.currentReplySource) ?? toNonEmptyString(input.livingChatSource) ?? "unknown";
@ -96,9 +261,15 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
const candidate = (0, assistantMcpDiscoveryResponseCandidate_1.buildAssistantMcpDiscoveryResponseCandidate)(entryPoint);
const reasonCodes = [...candidate.reason_codes];
const unsupportedBoundary = isUnsupportedCurrentTurnBoundary(input);
const deterministicBroadBusinessEvaluationReply = isDeterministicBroadBusinessEvaluationReply(input);
const discoveryReadyChatCandidate = isDiscoveryReadyChatCandidate(input, entryPoint);
const discoveryReadyDeepCandidate = isDiscoveryReadyDeepCandidate(input, entryPoint);
const discoveryReadyAddressCandidate = isDiscoveryReadyAddressCandidate(input, entryPoint);
const alignedFactualAddressReply = hasAlignedFactualAddressReply(input, entryPoint);
const semanticConflictWithDiscoveryTurnMeaning = hasSemanticConflictWithDiscoveryTurnMeaning(input, entryPoint);
const matchedFactualAddressContinuationTarget = hasMatchedFactualAddressContinuationTarget(input, entryPoint);
const fullConfirmedFactualAddressReply = hasFullConfirmedFactualAddressReply(input, entryPoint);
const runtimeAdjustedExactReply = hasRuntimeAdjustedExactReply(input, entryPoint);
if (!entryPoint) {
pushReason(reasonCodes, "mcp_discovery_response_policy_no_entry_point");
}
@ -114,6 +285,24 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
if (!discoveryReadyAddressCandidate) {
pushReason(reasonCodes, "mcp_discovery_response_policy_not_discovery_ready_address_candidate");
}
if (alignedFactualAddressReply) {
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_aligned_factual_address_reply");
}
if (semanticConflictWithDiscoveryTurnMeaning) {
pushReason(reasonCodes, "mcp_discovery_response_policy_semantic_conflict_allows_candidate_override");
}
if (matchedFactualAddressContinuationTarget) {
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_factual_address_continuation_target");
}
if (fullConfirmedFactualAddressReply) {
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_full_confirmed_factual_address_reply");
}
if (runtimeAdjustedExactReply) {
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_runtime_adjusted_exact_reply_over_stale_discovery_turn_meaning");
}
if (deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") {
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_broad_business_summary_over_clarification_candidate");
}
if (!ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status)) {
pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_status_not_allowed");
}
@ -128,6 +317,11 @@ function applyAssistantMcpDiscoveryResponsePolicy(input) {
}
const canApply = Boolean(entryPoint) &&
(unsupportedBoundary || discoveryReadyChatCandidate || discoveryReadyDeepCandidate || discoveryReadyAddressCandidate) &&
!alignedFactualAddressReply &&
!matchedFactualAddressContinuationTarget &&
!fullConfirmedFactualAddressReply &&
!runtimeAdjustedExactReply &&
!(deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") &&
ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) &&
candidate.eligible_for_future_hot_runtime &&
Boolean(toNonEmptyString(candidate.reply_text)) &&

View File

@ -3,6 +3,7 @@
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildInventoryHistoryCapabilityFollowupReply = buildInventoryHistoryCapabilityFollowupReply;
exports.buildAddressMemoryRecapReply = buildAddressMemoryRecapReply;
exports.buildBroadBusinessEvaluationReply = buildBroadBusinessEvaluationReply;
exports.buildSelectedObjectAnswerInspectionReply = buildSelectedObjectAnswerInspectionReply;
exports.resolveAssistantLivingChatMemoryContext = resolveAssistantLivingChatMemoryContext;
exports.createAssistantMemoryRecapPolicy = createAssistantMemoryRecapPolicy;
@ -14,6 +15,119 @@ function toNonEmptyString(value) {
const text = String(value).trim();
return text.length > 0 ? text : null;
}
function toRecordObject(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return null;
}
return value;
}
function ensureSentence(value) {
const text = String(value ?? "").trim();
if (!text) {
return "";
}
return /[.!?]$/.test(text) ? text : `${text}.`;
}
function periodPartForRecap(scopedDate) {
if (!scopedDate) {
return "";
}
return /^\d{2}\.\d{2}\.\d{4}$/.test(scopedDate) ? ` на ${scopedDate}` : ` за период ${scopedDate}`;
}
function readDiscoveryMetadataScope(debug) {
const discoveryEntry = toRecordObject(debug?.assistant_mcp_discovery_entry_point_v1);
const bridge = toRecordObject(discoveryEntry?.bridge);
const pilot = toRecordObject(bridge?.pilot);
const surface = toRecordObject(pilot?.derived_metadata_surface);
const surfaceScope = toNonEmptyString(surface?.metadata_scope);
if (surfaceScope) {
return surfaceScope;
}
const turnInput = toRecordObject(discoveryEntry?.turn_input);
const turnMeaningRef = toRecordObject(turnInput?.turn_meaning_ref);
const entityCandidates = Array.isArray(turnMeaningRef?.explicit_entity_candidates)
? turnMeaningRef.explicit_entity_candidates
: [];
for (const candidate of entityCandidates) {
const text = toNonEmptyString(candidate);
if (text) {
return text;
}
}
return null;
}
function buildDiscoveryRecapFactLine(input) {
if (!input.debug) {
return null;
}
const pilotScope = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryPilotScope)(input.debug, toNonEmptyString);
const discoveryEntry = toRecordObject(input.debug.assistant_mcp_discovery_entry_point_v1);
const bridge = toRecordObject(discoveryEntry?.bridge);
const pilot = toRecordObject(bridge?.pilot);
const periodPart = periodPartForRecap(input.scopedDate);
if (pilotScope === "metadata_inspection_v1") {
const metadataScope = readDiscoveryMetadataScope(input.debug);
const surface = toRecordObject(pilot?.derived_metadata_surface);
const entitySets = Array.isArray(surface?.available_entity_sets)
? surface.available_entity_sets
.map((item) => toNonEmptyString(item))
.filter((item) => Boolean(item))
: [];
const fields = Array.isArray(surface?.available_fields)
? surface.available_fields
.map((item) => toNonEmptyString(item))
.filter((item) => Boolean(item))
: [];
const objects = Array.isArray(surface?.matched_objects)
? surface.matched_objects
.map((item) => toNonEmptyString(item))
.filter((item) => Boolean(item))
: [];
const rows = Number(surface?.matched_rows ?? 0);
const scopePart = metadataScope ? ` по области «${metadataScope}»` : "";
const objectsPart = objects.length > 0 ? `, нашли объекты ${objects.slice(0, 4).join(", ")}` : "";
const entitySetsPart = entitySets.length > 0 ? `, видны типы ${entitySets.slice(0, 4).join(", ")}` : "";
const fieldsPart = fields.length > 0 ? `, доступны поля/секции ${fields.slice(0, 5).join(", ")}` : "";
return `смотрели metadata-поверхность 1С${scopePart}${periodPart}: ${rows} подтвержденных строк${objectsPart}${entitySetsPart}${fieldsPart}`.trim();
}
if (!input.counterparty) {
return null;
}
if (pilotScope === "counterparty_lifecycle_query_documents_v1") {
const activityPeriod = toRecordObject(pilot?.derived_activity_period);
const duration = toNonEmptyString(activityPeriod?.duration_human_ru);
return duration
? `смотрели подтвержденную активность по контрагенту «${input.counterparty}»${periodPart} и оценили период взаимодействия примерно как ${duration}`
: `смотрели подтвержденную активность по контрагенту «${input.counterparty}»${periodPart}`;
}
if (pilotScope === "counterparty_supplier_payout_query_movements_v1") {
const flow = toRecordObject(pilot?.derived_value_flow);
const amount = toNonEmptyString(flow?.total_amount_human_ru);
return amount
? `считали исходящие платежи/списания по контрагенту «${input.counterparty}»${periodPart}: ${amount}`
: `считали исходящие платежи/списания по контрагенту «${input.counterparty}»${periodPart}`;
}
if (pilotScope === "counterparty_value_flow_query_movements_v1") {
const flow = toRecordObject(pilot?.derived_value_flow);
const amount = toNonEmptyString(flow?.total_amount_human_ru);
return amount
? `смотрели денежный поток по контрагенту «${input.counterparty}»${periodPart}: ${amount}`
: `смотрели денежный поток по контрагенту «${input.counterparty}»${periodPart}`;
}
if (pilotScope === "counterparty_bidirectional_value_flow_query_movements_v1") {
const flow = toRecordObject(pilot?.derived_bidirectional_value_flow);
const incoming = toRecordObject(flow?.incoming_customer_revenue);
const outgoing = toRecordObject(flow?.outgoing_supplier_payout);
const incomingAmount = toNonEmptyString(incoming?.total_amount_human_ru);
const outgoingAmount = toNonEmptyString(outgoing?.total_amount_human_ru);
const netAmount = toNonEmptyString(flow?.net_amount_human_ru);
if (incomingAmount && outgoingAmount && netAmount) {
return `считали нетто по деньгам с контрагентом «${input.counterparty}»${periodPart}: получили ${incomingAmount}, заплатили ${outgoingAmount}, расчетное нетто ${netAmount}`;
}
return `считали нетто по деньгам с контрагентом «${input.counterparty}»${periodPart}`;
}
return null;
}
function collectMessageSamples(input) {
const values = [
input.rawUserMessage,
@ -60,7 +174,16 @@ function normalizeRecapIdentity(value) {
function buildRecapFactLine(input) {
const detectedIntent = String(input.debug?.detected_intent ?? "");
const scopedDate = (0, assistantContinuityPolicy_1.resolveAddressDebugContextFacts)(input.debug).scopedDate;
const discoveryFact = buildDiscoveryRecapFactLine({
debug: input.debug,
counterparty: input.counterparty,
scopedDate
});
if (discoveryFact) {
return discoveryFact;
}
const itemPart = input.item ? `по позиции «${input.item}»` : null;
const counterpartyPart = input.counterparty ? `по контрагенту «${input.counterparty}»` : null;
const organizationPart = input.organization ? `по компании «${input.organization}»` : null;
const datePart = scopedDate ? ` на ${scopedDate}` : "";
if (detectedIntent === "inventory_on_hand_as_of_date") {
@ -87,6 +210,9 @@ function buildRecapFactLine(input) {
if (detectedIntent === "counterparty_activity_lifecycle" && organizationPart) {
return `смотрели активность в базе 1С ${organizationPart}`.trim();
}
if (detectedIntent === "list_documents_by_counterparty" && counterpartyPart) {
return `поднимали документы ${counterpartyPart}${datePart}`.trim();
}
if (detectedIntent === "list_documents_by_counterparty" && organizationPart) {
return `поднимали документы ${organizationPart}${datePart}`.trim();
}
@ -125,6 +251,7 @@ function collectRecentRecapFacts(input) {
const fact = buildRecapFactLine({
debug: item.debug,
item: debugItem,
counterparty: debugContext.counterparty,
organization: debugOrganization
});
if (!fact || seen.has(fact)) {
@ -141,6 +268,7 @@ function collectRecentRecapFacts(input) {
function buildAddressMemoryRecapReply(input) {
const contextFacts = (0, assistantContinuityPolicy_1.resolveAddressDebugContextFacts)(input.addressDebug, input.toNonEmptyString);
const item = contextFacts.item;
const counterparty = contextFacts.counterparty;
const organization = input.organization ?? contextFacts.organization;
const scopedDate = contextFacts.scopedDate;
const recapFacts = collectRecentRecapFacts({
@ -166,6 +294,21 @@ function buildAddressMemoryRecapReply(input) {
"Могу продолжить по ней без переписывания сущности: кто поставил, когда купили, по каким документам или кому продали."
].join(" ");
}
if (counterparty) {
const organizationPart = organization ? ` по компании «${organization}»` : "";
const periodPart = periodPartForRecap(scopedDate);
if (recapFacts.length > 0) {
return [
`Да, помню. По контрагенту «${counterparty}»${organizationPart}${periodPart} мы уже выяснили:`,
...recapFacts.map((fact) => `- ${fact}.`),
"Могу сразу продолжить по нему: поступления, платежи, нетто, помесячную раскладку или границы подтверждения."
].join("\n");
}
return [
`Да, помню. Мы уже смотрели контур по контрагенту «${counterparty}»${organizationPart}${periodPart}.`,
"Могу продолжить по нему без переписывания контекста: поступления, платежи, нетто, документы или пояснение границ ответа."
].join(" ");
}
if (organization || scopedDate) {
const organizationPart = organization ? ` по компании «${organization}»` : "";
const datePart = scopedDate ? ` на ${scopedDate}` : "";
@ -176,10 +319,62 @@ function buildAddressMemoryRecapReply(input) {
}
return "Да, помню предыдущий адресный контур. Могу кратко напомнить, что мы уже подтвердили, или сразу продолжить следующий шаг.";
}
function buildBroadBusinessEvaluationReply(input) {
const contextFacts = (0, assistantContinuityPolicy_1.resolveAddressDebugContextFacts)(input.addressDebug, input.toNonEmptyString);
const organization = input.organization ?? contextFacts.organization;
const recapFacts = collectRecentRecapFacts({
sessionItems: input.sessionItems,
item: null,
organization,
toNonEmptyString: input.toNonEmptyString
});
const organizationPart = organization ? ` по компании «${organization}»` : "";
if (recapFacts.length > 0) {
return [
`Коротко: по тому, что мы уже подтвердили в 1С${organizationPart}, компания выглядит операционно живой, но это пока только частичная оценка бизнеса.`,
"Сейчас я опираюсь на такие подтвержденные факты:",
...recapFacts.map((fact) => `- ${ensureSentence(fact)}`),
"Это еще не полная диагностика всего бизнеса и не вывод о прибыли: я честно суммирую только те контуры, которые мы уже проверили в диалоге.",
"Если хочешь, следующим шагом могу сузить оценку до денежного потока, долгов, НДС или ключевых контрагентов."
].join("\n");
}
return [
`Коротко: по нынешнему контексту 1С${organizationPart} я вижу признаки операционной активности, но для содержательной оценки бизнеса нужно еще несколько опорных срезов.`,
"Если хочешь, я быстро доберу основу для такой оценки: денежный поток, дебиторка/кредиторка, НДС или ключевые контрагенты."
].join(" ");
}
function buildSelectedObjectAnswerInspectionReply(input) {
const contextFacts = (0, assistantContinuityPolicy_1.resolveAddressDebugContextFacts)(input.addressDebug, input.toNonEmptyString);
const itemLabel = contextFacts.item ?? "эта позиция";
const counterpartyLabel = contextFacts.counterparty;
const detectedIntent = String(input.addressDebug?.detected_intent ?? "");
const pilotScope = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryPilotScope)(input.addressDebug, input.toNonEmptyString);
const periodPart = periodPartForRecap(contextFacts.scopedDate);
if (counterpartyLabel && pilotScope === "counterparty_bidirectional_value_flow_query_movements_v1") {
return [
`Да, в предыдущем ответе речь шла о двустороннем денежном потоке с контрагентом «${counterpartyLabel}»${periodPart}.`,
"Нетто там означало разницу между тем, что получили, и тем, что заплатили по найденным строкам 1С.",
"Это расчет по проверенному периоду и подтвержденным строкам, а не заявление про весь оборот вне этого окна."
].join(" ");
}
if (counterpartyLabel && pilotScope === "counterparty_supplier_payout_query_movements_v1") {
return [
`Да, в предыдущем ответе речь шла об исходящих платежах/списаниях по контрагенту «${counterpartyLabel}»${periodPart}.`,
"Это сумма по найденным строкам 1С за проверенный период, а не обещание, что за пределами этого окна больше движений не было."
].join(" ");
}
if (counterpartyLabel && pilotScope === "counterparty_value_flow_query_movements_v1") {
return [
`Да, в предыдущем ответе речь шла о денежном потоке по контрагенту «${counterpartyLabel}»${periodPart}.`,
"Это расчет по найденным движениям 1С за проверенный период, а не безусловный итог по всем временам."
].join(" ");
}
if (counterpartyLabel && pilotScope === "counterparty_lifecycle_query_documents_v1") {
return [
`Да, в предыдущем ответе речь шла об активности контрагента «${counterpartyLabel}»${periodPart}.`,
"Это оценка по подтвержденным строкам 1С, а не юридически подтвержденная дата регистрации."
].join(" ");
}
if (detectedIntent === "inventory_sale_trace_for_item") {
return [
`Да, если так прозвучало, это ошибка чтения ответа. «${itemLabel}» здесь не контрагент, а сама позиция, по которой мы смотрели продажу.`,

View File

@ -2607,6 +2607,12 @@ function hasAddressFollowupContextSignal(userMessage) {
if (ultraShortFollowup && hasAny(/^(?:давай|показывай|показывыай|ещ[её]|also|again|go|ok|okay)(?=$|[\s,.;:!?])/iu)) {
return true;
}
const shortValueFlowRetargetCue = shortFollowup &&
(hasMarker() || hasPointer() || hasAny(/^(?:Р°|a|Рё|i|also|then|now)(?=$|[\s,.;:!?])/iu)) &&
hasAny(/(?:нетто|сальдо|разниц|получил|заплатил|поступ|входящ|исходящ|оборот|выручк|денеж)/iu);
if (shortValueFlowRetargetCue) {
return true;
}
if (hasStandaloneAddressTopicSignal(rawText || repairedText)) {
return false;
}
@ -3865,11 +3871,7 @@ function findLastGroundedAddressAnswerDebug(items) {
continue;
}
const debug = item.debug;
if (debug.execution_lane !== "address_query") {
continue;
}
const groundingStatus = toNonEmptyString(debug.answer_grounding_check?.status);
if (groundingStatus === "grounded") {
if ((0, assistantContinuityPolicy_1.isGroundedAddressDebug)(debug, toNonEmptyString)) {
return debug;
}
}

View File

@ -121,18 +121,22 @@ function createAssistantTransitionPolicy(deps) {
return false;
}
const executionLane = deps.toNonEmptyString(debug.execution_lane);
const detectedIntent = deps.toNonEmptyString(debug.detected_intent);
const detectedIntent = (0, assistantContinuityPolicy_1.readAddressDebugIntent)(debug, deps.toNonEmptyString);
const selectedRecipe = deps.toNonEmptyString(debug.selected_recipe);
const answerGroundingCheck = debug.answer_grounding_check && typeof debug.answer_grounding_check === "object"
? debug.answer_grounding_check
: null;
const groundingStatus = deps.toNonEmptyString(answerGroundingCheck?.status);
const discoveryPilotScope = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryPilotScope)(debug, deps.toNonEmptyString);
if (groundingStatus === "grounded") {
return true;
}
if (selectedRecipe) {
return true;
}
if (debug.mcp_discovery_response_applied === true && discoveryPilotScope) {
return true;
}
return executionLane === "address_query" && Boolean(detectedIntent && detectedIntent !== "unknown");
}
function findRecentUsableAddressAssistantItem(items) {
@ -237,6 +241,23 @@ function createAssistantTransitionPolicy(deps) {
];
return sameDatePhrases.some((phrase) => normalized.includes(phrase));
}
function hasShortValueFlowRetargetCue(text) {
const normalized = normalizeFollowupText(text);
if (!normalized) {
return false;
}
const tokenCount = deps.countTokens(normalized);
if (!Number.isFinite(tokenCount) || tokenCount > 8) {
return false;
}
const hasLeadCue = deps.hasFollowupMarker(text) ||
deps.hasReferentialPointer(text) ||
/^(?:\u0430|\u0438|also|then|now)(?=$|[\s,.;:!?])/iu.test(normalized);
if (!hasLeadCue) {
return false;
}
return /(?:\u043d\u0435\u0442\u0442\u043e|\u0441\u0430\u043b\u044c\u0434\u043e|\u0440\u0430\u0437\u043d\u0438\u0446|\u043f\u043e\u043b\u0443\u0447|\u0437\u0430\u043f\u043b\u0430\u0442|\u043f\u043e\u0441\u0442\u0443\u043f|\u0432\u0445\u043e\u0434\u044f\u0449|\u0438\u0441\u0445\u043e\u0434\u044f\u0449|\u043e\u0431\u043e\u0440\u043e\u0442|\u0432\u044b\u0440\u0443\u0447\u043a|\u0434\u0435\u043d\u0435\u0436)/iu.test(normalized);
}
function shouldKeepPreviousIntentForShortCounterpartyRetarget(userMessage, sourceIntent) {
const normalized = deps.compactWhitespace(deps.repairAddressMojibake(String(userMessage ?? "")).toLowerCase());
if (!normalized || deps.countTokens(normalized) > 4) {
@ -337,7 +358,12 @@ function createAssistantTransitionPolicy(deps) {
(deps.toNonEmptyString(alternateMessage)
? deps.isImplicitAddressContinuationByLlm(alternateMessage, llmPreDecomposeMeta)
: false));
const sourceIntentHint = deps.toNonEmptyString(carryoverSourceDebug?.detected_intent);
const sourceIntentHint = (0, assistantContinuityPolicy_1.readAddressDebugIntent)(carryoverSourceDebug, deps.toNonEmptyString);
const sourceDiscoveryPilotScopeHint = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryPilotScope)(carryoverSourceDebug, deps.toNonEmptyString);
const hasValueFlowCarryoverSourceHint = sourceIntentHint === "customer_revenue_and_payments" ||
sourceDiscoveryPilotScopeHint === "counterparty_value_flow_query_movements_v1" ||
sourceDiscoveryPilotScopeHint === "counterparty_supplier_payout_query_movements_v1" ||
sourceDiscoveryPilotScopeHint === "counterparty_bidirectional_value_flow_query_movements_v1";
const navigationSessionState = (0, assistantContinuityPolicy_1.resolveNavigationSessionContextState)(addressNavigationState, deps.toNonEmptyString, deps.normalizeOrganizationScopeValue);
const navigationFocusObjectHint = navigationSessionState.focusObject;
const hasNavigationInventoryItemFocusHint = Boolean(deps.toNonEmptyString(navigationFocusObjectHint?.label) &&
@ -359,13 +385,19 @@ function createAssistantTransitionPolicy(deps) {
? deps.resolveDebtRoleSwapFollowupIntent(String(alternateMessage ?? ""), sourceIntentHint)
: null;
const debtRoleSwapIntent = debtRoleSwapPrimary ?? debtRoleSwapAlternate ?? null;
const shortValueFlowRetargetPrimary = hasValueFlowCarryoverSourceHint && hasShortValueFlowRetargetCue(userMessage);
const shortValueFlowRetargetAlternate = hasValueFlowCarryoverSourceHint && deps.toNonEmptyString(alternateMessage)
? hasShortValueFlowRetargetCue(String(alternateMessage ?? ""))
: false;
let hasPrimaryFollowupSignal = deps.hasAddressFollowupContextSignal(userMessage) ||
Boolean(debtRoleSwapPrimary) ||
shortValueFlowRetargetPrimary ||
inventoryShortFollowupPrimary ||
inventoryPurchaseDateVatBridge;
let hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage)
? deps.hasAddressFollowupContextSignal(alternateMessage) ||
Boolean(debtRoleSwapAlternate) ||
shortValueFlowRetargetAlternate ||
inventoryShortFollowupAlternate ||
inventoryPurchaseDateVatBridge
: false;
@ -399,6 +431,8 @@ function createAssistantTransitionPolicy(deps) {
hasInventoryRootRestatementAlternate ||
inventoryPurchaseDateVatBridge ||
Boolean(debtRoleSwapIntent) ||
shortValueFlowRetargetPrimary ||
shortValueFlowRetargetAlternate ||
deps.hasFollowupMarker(userMessage) ||
deps.hasReferentialPointer(userMessage) ||
(deps.toNonEmptyString(alternateMessage)
@ -416,6 +450,8 @@ function createAssistantTransitionPolicy(deps) {
hasInventoryRootRestatementAlternate ||
inventoryPurchaseDateVatBridge ||
Boolean(debtRoleSwapIntent) ||
shortValueFlowRetargetPrimary ||
shortValueFlowRetargetAlternate ||
deps.hasFollowupMarker(userMessage) ||
deps.hasReferentialPointer(userMessage) ||
(deps.toNonEmptyString(alternateMessage)
@ -438,6 +474,8 @@ function createAssistantTransitionPolicy(deps) {
!hasInventoryRootTemporalFollowupAlternate &&
!hasInventoryRootRestatementPrimary &&
!hasInventoryRootRestatementAlternate &&
!shortValueFlowRetargetPrimary &&
!shortValueFlowRetargetAlternate &&
!hasImplicitContinuationSignal &&
!hasOrganizationClarificationContinuation &&
!hasIndexReferenceSignal) {
@ -449,6 +487,8 @@ function createAssistantTransitionPolicy(deps) {
!hasInventoryRootTemporalFollowupAlternate &&
!hasInventoryRootRestatementPrimary &&
!hasInventoryRootRestatementAlternate &&
!shortValueFlowRetargetPrimary &&
!shortValueFlowRetargetAlternate &&
!hasImplicitContinuationSignal &&
!hasOrganizationClarificationContinuation &&
!hasIndexReferenceSignal) {
@ -457,7 +497,15 @@ function createAssistantTransitionPolicy(deps) {
if (!carryoverSourceDebug) {
return null;
}
const sourceIntent = deps.toNonEmptyString(carryoverSourceDebug.detected_intent);
const sourceIntent = (0, assistantContinuityPolicy_1.readAddressDebugIntent)(carryoverSourceDebug, deps.toNonEmptyString);
const sourceDiscoveryPilotScope = sourceDiscoveryPilotScopeHint;
const sourceDiscoveryMetadataRouteFamily = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryMetadataRouteFamily)(carryoverSourceDebug, deps.toNonEmptyString);
const sourceDiscoveryMetadataSelectedEntitySet = (0, assistantContinuityPolicy_1.readAssistantMcpDiscoveryMetadataSelectedEntitySet)(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 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;
const resolvedPrimaryIntent = deps.resolveAddressIntent(deps.repairAddressMojibake(String(userMessage ?? ""))).intent;
@ -546,12 +594,14 @@ function createAssistantTransitionPolicy(deps) {
hasPrimaryFollowupSignal =
deps.hasAddressFollowupContextSignal(userMessage) ||
Boolean(debtRoleSwapPrimary) ||
shortValueFlowRetargetPrimary ||
inventoryShortFollowupPrimary ||
inventoryPurchaseDateVatBridge ||
hasInventoryRootTemporalFollowupPrimary;
hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage)
? deps.hasAddressFollowupContextSignal(alternateMessage) ||
Boolean(debtRoleSwapAlternate) ||
shortValueFlowRetargetAlternate ||
inventoryShortFollowupAlternate ||
inventoryPurchaseDateVatBridge ||
hasInventoryRootTemporalFollowupAlternate
@ -567,6 +617,8 @@ function createAssistantTransitionPolicy(deps) {
hasInventoryRootTemporalFollowupAlternate ||
inventoryPurchaseDateVatBridge ||
Boolean(debtRoleSwapIntent) ||
shortValueFlowRetargetPrimary ||
shortValueFlowRetargetAlternate ||
deps.hasFollowupMarker(userMessage) ||
deps.hasReferentialPointer(userMessage) ||
(deps.toNonEmptyString(alternateMessage)
@ -687,6 +739,16 @@ function createAssistantTransitionPolicy(deps) {
previous_filters: previousFilters,
previous_anchor_type: previousAnchorType ?? undefined,
previous_anchor_value: previousAnchor,
previous_discovery_pilot_scope: sourceDiscoveryPilotScope ?? undefined,
previous_discovery_entity_resolution_status: sourceDiscoveryEntityResolutionStatus ?? undefined,
previous_discovery_entity_candidates: sourceDiscoveryEntityCandidates.length > 0 ? sourceDiscoveryEntityCandidates : undefined,
previous_discovery_entity_ambiguity_candidates: sourceDiscoveryEntityAmbiguityCandidates.length > 0
? sourceDiscoveryEntityAmbiguityCandidates
: undefined,
previous_discovery_metadata_route_family: sourceDiscoveryMetadataRouteFamily ?? undefined,
previous_discovery_metadata_selected_entity_set: sourceDiscoveryMetadataSelectedEntitySet ?? 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,
root_context_only: rootScopedPivot || undefined,
root_intent: shouldAttachInventoryRootFrame ? inventoryRootFrame?.intent ?? undefined : undefined,

View File

@ -7,7 +7,10 @@ const SUPPORTED_ADDRESS_INTENTS = new Set([
"payables_confirmed_as_of_date",
"list_documents_by_counterparty",
"customer_revenue_and_payments",
"inventory_on_hand_as_of_date"
"inventory_on_hand_as_of_date",
"vat_liability_confirmed_for_tax_period",
"vat_payable_confirmed_as_of_date",
"vat_payable_forecast"
]);
function fallbackCompactWhitespace(value) {
return String(value ?? "").replace(/\s+/g, " ").trim();
@ -85,8 +88,23 @@ function detectCounterpartyTurnoverFamily(text) {
"\u0434\u043e\u0445\u043e\u0434",
"\u0431\u044b\u043b",
"\u0431\u044b\u043b\u0430",
"\u0432\u0440\u0435\u043c\u044f",
"\u0432\u0440\u0435\u043c\u0435\u043d\u0438",
"\u0433\u043e\u0434",
"\u0433\u043e\u0434\u0430",
"\u043f\u0435\u0440\u0438\u043e\u0434",
"\u043f\u0435\u0440\u0438\u043e\u0434\u0430",
"\u043c\u0435\u0441\u044f\u0446",
"\u043c\u0435\u0441\u044f\u0446\u0430",
"\u043a\u0432\u0430\u0440\u0442\u0430\u043b",
"\u043a\u0432\u0430\u0440\u0442\u0430\u043b\u0430",
"turnover",
"revenue"
"revenue",
"time",
"year",
"period",
"month",
"quarter"
]);
const entity = rawEntity && !ignored.has(rawEntity) ? rawEntity : null;
return {
@ -94,6 +112,18 @@ function detectCounterpartyTurnoverFamily(text) {
entity
};
}
function detectBroadBusinessEvaluation(text) {
const normalized = String(text ?? "");
if (!normalized) {
return null;
}
if (/(?:как\s+ты\s+оценишь\s+деятельност[ьи]\s+компан|оценк[аи]?\s+деятельност[ьи]\s+компан|что\s+у\s+нас\s+вообще\s+происход|где\s+главн(?:ые|ый)\s+риски|как\s+у\s+нас\s+дела\s+по\s+компан)/iu.test(normalized)) {
return {
family: "broad_business_evaluation"
};
}
return null;
}
function buildEntityCandidates(counterpartyTurnover) {
if (!counterpartyTurnover?.entity) {
return [];
@ -115,9 +145,16 @@ function createAssistantTurnMeaningPolicy(deps = {}) {
const joinedText = fallbackCompactWhitespace(`${rawText} ${effectiveText}`);
const supportedIntent = detectSupportedIntent(joinedText, deps);
const counterpartyTurnover = detectCounterpartyTurnoverFamily(joinedText);
const broadBusinessEvaluation = detectBroadBusinessEvaluation(joinedText);
const llmIntent = toNonEmptyString(input?.llmPreDecomposeMeta?.predecomposeContract?.intent, deps);
const explicitIntentCandidate = supportedIntent?.intent ?? (llmIntent && llmIntent !== "unknown" ? llmIntent : null);
const unsupportedFamily = !explicitIntentCandidate && counterpartyTurnover?.family ? counterpartyTurnover.family : null;
const explicitIntentCandidate = broadBusinessEvaluation?.family
? null
: supportedIntent?.intent ?? (llmIntent && llmIntent !== "unknown" ? llmIntent : null);
const unsupportedFamily = broadBusinessEvaluation?.family
? broadBusinessEvaluation.family
: !explicitIntentCandidate && counterpartyTurnover?.family
? counterpartyTurnover.family
: null;
const reasonCodes = [];
if (supportedIntent?.reason) {
reasonCodes.push(supportedIntent.reason);
@ -125,6 +162,9 @@ function createAssistantTurnMeaningPolicy(deps = {}) {
if (counterpartyTurnover?.family) {
reasonCodes.push("counterparty_turnover_current_turn_signal");
}
if (broadBusinessEvaluation?.family) {
reasonCodes.push("broad_business_evaluation_current_turn_signal");
}
if (rawText !== normalizeTurnText(rawMessage, { ...deps, repairAddressMojibake: (value) => String(value ?? "") })) {
reasonCodes.push("mojibake_repair_applied");
}
@ -136,23 +176,35 @@ function createAssistantTurnMeaningPolicy(deps = {}) {
? "receivables"
: explicitIntentCandidate?.startsWith("payables_")
? "payables"
: explicitIntentCandidate?.startsWith("inventory_")
? "inventory"
: explicitIntentCandidate?.includes("counterparty")
? "counterparty"
: counterpartyTurnover?.family
? "counterparty"
: null;
: explicitIntentCandidate?.startsWith("vat_")
? "vat"
: explicitIntentCandidate?.startsWith("inventory_")
? "inventory"
: broadBusinessEvaluation?.family
? "business_summary"
: explicitIntentCandidate?.includes("counterparty")
? "counterparty"
: counterpartyTurnover?.family
? "counterparty"
: null;
const askedActionFamily = explicitIntentCandidate === "receivables_confirmed_as_of_date" ||
explicitIntentCandidate === "payables_confirmed_as_of_date" ||
explicitIntentCandidate === "inventory_on_hand_as_of_date"
? "confirmed_snapshot"
: explicitIntentCandidate === "list_documents_by_counterparty"
? "list_documents"
: counterpartyTurnover?.family
? "counterparty_value_or_turnover"
: null;
const staleReplayForbidden = Boolean(unsupportedFamily || (counterpartyTurnover?.entity && !explicitIntentCandidate));
: broadBusinessEvaluation?.family
? "broad_evaluation"
: explicitIntentCandidate === "vat_liability_confirmed_for_tax_period"
? "confirmed_tax_period"
: explicitIntentCandidate === "vat_payable_confirmed_as_of_date"
? "confirmed_snapshot"
: explicitIntentCandidate === "vat_payable_forecast"
? "forecast"
: explicitIntentCandidate === "list_documents_by_counterparty"
? "list_documents"
: counterpartyTurnover?.family
? "counterparty_value_or_turnover"
: null;
const staleReplayForbidden = Boolean(unsupportedFamily || broadBusinessEvaluation?.family || (counterpartyTurnover?.entity && !explicitIntentCandidate));
return {
schema_version: "assistant_turn_meaning_v1",
raw_message: rawMessage,
@ -163,7 +215,9 @@ function createAssistantTurnMeaningPolicy(deps = {}) {
asked_action_family: askedActionFamily,
explicit_intent_candidate: explicitIntentCandidate,
explicit_entity_candidates: buildEntityCandidates(counterpartyTurnover),
meaning_confidence: supportedIntent?.confidence ?? (counterpartyTurnover?.family ? "medium" : "low"),
meaning_confidence: broadBusinessEvaluation?.family
? "medium"
: supportedIntent?.confidence ?? (counterpartyTurnover?.family ? "medium" : "low"),
intent_override_strength: explicitIntentCandidate
? "explicit_current_turn_intent"
: staleReplayForbidden

View File

@ -40,6 +40,8 @@ export interface AddressFollowupContext {
| "unknown"
| null;
previous_anchor_value?: string | null;
previous_discovery_entity_resolution_status?: "resolved" | "ambiguous" | "not_found" | null;
previous_discovery_entity_ambiguity_candidates?: string[];
resolved_counterparty_from_display?: boolean;
root_intent?: AddressIntent;
root_filters?: AddressFilterSet;

View File

@ -284,6 +284,7 @@ export function runAssistantAddressLaneResponseRuntime<ResponseType = AssistantM
const mcpDiscoveryResponsePolicy = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: guardedResponse.assistantReply,
currentReplySource: "address_query_runtime_v1",
currentReplyType: guardedResponse.replyType,
addressRuntimeMeta: debugWithResponseGuard
});
const finalAssistantReply = mcpDiscoveryResponsePolicy.applied

View File

@ -285,7 +285,7 @@ export async function buildAssistantAddressOrchestrationRuntime(
);
}
const followupContext = carryover?.followupContext ?? null;
const followupContext = toRecordObject(carryover?.followupContext);
const routePolicyRuntime = runAssistantRoutePolicyRuntime({
rawUserMessage: input.userMessage,
effectiveAddressUserMessage: addressInputMessage,
@ -313,7 +313,8 @@ export async function buildAssistantAddressOrchestrationRuntime(
userMessage: input.userMessage,
effectiveMessage: addressInputMessage,
assistantTurnMeaning: toRecordObject(orchestrationContract?.assistant_turn_meaning),
predecomposeContract
predecomposeContract,
followupContext
})) as Record<string, unknown>;
} catch (error) {
mcpDiscoveryRuntimeEntryPointError = String(error instanceof Error ? error.message : error ?? "unknown_error").slice(0, 280);

View File

@ -29,6 +29,7 @@ export interface AssistantContinuitySnapshot {
export interface AssistantAddressDebugContextFacts {
item: string | null;
counterparty: string | null;
organization: string | null;
scopedDate: string | null;
}
@ -107,6 +108,304 @@ function toRecordObject(value: unknown): Record<string, unknown> | null {
return value as Record<string, unknown>;
}
function candidateValue(value: unknown): string | null {
const direct = fallbackToNonEmptyString(value);
if (direct && direct !== "[object Object]") {
return direct;
}
const record = toRecordObject(value);
if (!record) {
return null;
}
return (
fallbackToNonEmptyString(record.value) ??
fallbackToNonEmptyString(record.name) ??
fallbackToNonEmptyString(record.ref) ??
fallbackToNonEmptyString(record.text)
);
}
function readAssistantMcpDiscoveryEntry(
debug: Record<string, unknown> | null
): Record<string, unknown> | null {
const entry = toRecordObject(debug?.assistant_mcp_discovery_entry_point_v1);
return fallbackToNonEmptyString(entry?.schema_version) === "assistant_mcp_discovery_runtime_entry_point_v1"
? entry
: null;
}
function readAssistantMcpDiscoveryTurnMeaning(
debug: Record<string, unknown> | null
): Record<string, unknown> | null {
const entry = readAssistantMcpDiscoveryEntry(debug);
const turnInput = toRecordObject(entry?.turn_input);
return toRecordObject(turnInput?.turn_meaning_ref);
}
function readAssistantMcpDiscoveryTurnMeaningMetadataAmbiguityEntitySets(
debug: Record<string, unknown> | null,
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
): string[] {
const values = readAssistantMcpDiscoveryTurnMeaning(debug)?.metadata_ambiguity_entity_sets;
if (!Array.isArray(values)) {
return [];
}
return values.map((item) => toNonEmptyString(item)).filter((item): item is string => Boolean(item));
}
function readAssistantMcpDiscoveryActionFamily(
debug: Record<string, unknown> | null,
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
): string | null {
return toNonEmptyString(readAssistantMcpDiscoveryTurnMeaning(debug)?.asked_action_family);
}
function readAssistantMcpDiscoveryBridge(
debug: Record<string, unknown> | null
): Record<string, unknown> | null {
return toRecordObject(readAssistantMcpDiscoveryEntry(debug)?.bridge);
}
function readAssistantMcpDiscoveryDerivedMetadataSurface(
debug: Record<string, unknown> | null
): Record<string, unknown> | null {
const bridge = readAssistantMcpDiscoveryBridge(debug);
const pilot = toRecordObject(bridge?.pilot);
return toRecordObject(pilot?.derived_metadata_surface);
}
function readAssistantMcpDiscoveryDerivedEntityResolution(
debug: Record<string, unknown> | null
): Record<string, unknown> | null {
const bridge = readAssistantMcpDiscoveryBridge(debug);
const pilot = toRecordObject(bridge?.pilot);
return toRecordObject(pilot?.derived_entity_resolution);
}
export function readAssistantMcpDiscoveryEntityResolutionStatus(
debug: Record<string, unknown> | null,
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
): string | null {
return toNonEmptyString(readAssistantMcpDiscoveryDerivedEntityResolution(debug)?.resolution_status);
}
export function readAssistantMcpDiscoveryEntityAmbiguityCandidates(
debug: Record<string, unknown> | null,
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
): string[] {
const values = readAssistantMcpDiscoveryDerivedEntityResolution(debug)?.ambiguity_candidates;
if (!Array.isArray(values)) {
return [];
}
return values.map((item) => toNonEmptyString(item)).filter((item): item is string => Boolean(item));
}
function collectAssistantMcpDiscoveryEntityCandidates(
debug: Record<string, unknown> | null,
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
): string[] {
const result: string[] = [];
const resolution = readAssistantMcpDiscoveryDerivedEntityResolution(debug);
const pushCandidate = (value: unknown): void => {
const text = toNonEmptyString(value);
if (text && !result.includes(text)) {
result.push(text);
}
};
pushCandidate(resolution?.resolved_entity);
pushCandidate(resolution?.requested_entity);
if (Array.isArray(resolution?.ambiguity_candidates)) {
for (const candidate of resolution.ambiguity_candidates) {
pushCandidate(candidate);
}
}
const discoveryMeaning = readAssistantMcpDiscoveryTurnMeaning(debug);
const explicitEntities = Array.isArray(discoveryMeaning?.explicit_entity_candidates)
? discoveryMeaning.explicit_entity_candidates
: [];
for (const entity of explicitEntities) {
pushCandidate(candidateValue(entity));
}
return result;
}
export function readAssistantMcpDiscoveryEntityCandidates(
debug: Record<string, unknown> | null,
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
): string[] {
return collectAssistantMcpDiscoveryEntityCandidates(debug, toNonEmptyString);
}
export function readAssistantMcpDiscoveryPilotScope(
debug: Record<string, unknown> | null,
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
): string | null {
const bridge = readAssistantMcpDiscoveryBridge(debug);
const pilot = toRecordObject(bridge?.pilot);
return toNonEmptyString(pilot?.pilot_scope);
}
export function readAssistantMcpDiscoveryMetadataRouteFamily(
debug: Record<string, unknown> | null,
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
): string | null {
return toNonEmptyString(readAssistantMcpDiscoveryDerivedMetadataSurface(debug)?.downstream_route_family);
}
export function readAssistantMcpDiscoveryMetadataSelectedEntitySet(
debug: Record<string, unknown> | null,
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
): string | null {
return toNonEmptyString(readAssistantMcpDiscoveryDerivedMetadataSurface(debug)?.selected_entity_set);
}
export function readAssistantMcpDiscoveryMetadataAmbiguityDetected(
debug: Record<string, unknown> | null
): boolean {
return (
readAssistantMcpDiscoveryDerivedMetadataSurface(debug)?.ambiguity_detected === true ||
readAssistantMcpDiscoveryTurnMeaningMetadataAmbiguityEntitySets(debug).length > 0
);
}
export function readAssistantMcpDiscoveryMetadataAmbiguityEntitySets(
debug: Record<string, unknown> | null,
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
): string[] {
const values = readAssistantMcpDiscoveryDerivedMetadataSurface(debug)?.ambiguity_entity_sets;
if (Array.isArray(values)) {
return values.map((item) => toNonEmptyString(item)).filter((item): item is string => Boolean(item));
}
return readAssistantMcpDiscoveryTurnMeaningMetadataAmbiguityEntitySets(debug, toNonEmptyString);
}
function mapAssistantMcpDiscoveryPilotScopeToAddressIntent(
pilotScope: string | null,
actionFamily: string | null
): string | null {
if (pilotScope === "counterparty_lifecycle_query_documents_v1") {
return "counterparty_activity_lifecycle";
}
if (pilotScope === "counterparty_document_evidence_query_documents_v1") {
return "list_documents_by_counterparty";
}
if (pilotScope === "counterparty_movement_evidence_query_movements_v1") {
return "bank_operations_by_counterparty";
}
if (pilotScope === "counterparty_supplier_payout_query_movements_v1") {
return "supplier_payouts_profile";
}
if (pilotScope === "counterparty_value_flow_query_movements_v1") {
return "customer_revenue_and_payments";
}
if (pilotScope === "counterparty_bidirectional_value_flow_query_movements_v1") {
return null;
}
if (actionFamily === "activity_duration") {
return "counterparty_activity_lifecycle";
}
if (actionFamily === "payout") {
return "supplier_payouts_profile";
}
if (actionFamily === "turnover") {
return "customer_revenue_and_payments";
}
return null;
}
function readDiscoveryDateScopeFilters(
debug: Record<string, unknown> | null,
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
): {
asOfDate: string | null;
periodFrom: string | null;
periodTo: string | null;
} {
const explicitDateScope = toNonEmptyString(readAssistantMcpDiscoveryTurnMeaning(debug)?.explicit_date_scope);
if (!explicitDateScope) {
return {
asOfDate: null,
periodFrom: null,
periodTo: null
};
}
const isoDateMatch = explicitDateScope.match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (isoDateMatch) {
return {
asOfDate: explicitDateScope,
periodFrom: null,
periodTo: null
};
}
const monthMatch = explicitDateScope.match(/^(\d{4})-(\d{2})$/);
if (monthMatch) {
const year = Number(monthMatch[1]);
const month = Number(monthMatch[2]);
if (Number.isFinite(year) && Number.isFinite(month) && month >= 1 && month <= 12) {
const lastDay = new Date(Date.UTC(year, month, 0)).getUTCDate();
return {
asOfDate: null,
periodFrom: `${monthMatch[1]}-${monthMatch[2]}-01`,
periodTo: `${monthMatch[1]}-${monthMatch[2]}-${String(lastDay).padStart(2, "0")}`
};
}
}
const yearMatch = explicitDateScope.match(/^(\d{4})$/);
if (yearMatch) {
return {
asOfDate: null,
periodFrom: `${yearMatch[1]}-01-01`,
periodTo: `${yearMatch[1]}-12-31`
};
}
const rangeMatch = explicitDateScope.match(/^(\d{4}-\d{2}-\d{2})\.\.(\d{4}-\d{2}-\d{2})$/);
if (rangeMatch) {
return {
asOfDate: null,
periodFrom: rangeMatch[1],
periodTo: rangeMatch[2]
};
}
return {
asOfDate: null,
periodFrom: null,
periodTo: null
};
}
function formatDiscoveryDateScopeForReply(value: unknown): string | null {
const text = fallbackToNonEmptyString(value);
if (!text) {
return null;
}
return formatIsoDateForReply(text) ?? text;
}
function hasGroundedDiscoveryBusinessAnswer(
debug: Record<string, unknown> | null,
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
): boolean {
if (!debug || debug.mcp_discovery_response_applied !== true) {
return false;
}
const entry = readAssistantMcpDiscoveryEntry(debug);
const bridge = readAssistantMcpDiscoveryBridge(debug);
const bridgeStatus = toNonEmptyString(bridge?.bridge_status);
const answerDraft = toRecordObject(bridge?.answer_draft);
const answerMode = toNonEmptyString(answerDraft?.answer_mode);
return Boolean(
entry &&
toNonEmptyString(entry.entry_status) === "bridge_executed" &&
bridgeStatus === "answer_draft_ready" &&
(bridge?.business_fact_answer_allowed === true ||
answerMode === "confirmed_with_bounded_inference" ||
answerMode === "bounded_inference_only")
);
}
function mergeKnownOrganizationsDefault(values: unknown[]): string[] {
return mergeKnownOrganizationsFromMatcher(values);
}
@ -141,13 +440,55 @@ export function readAddressDebugItem(
);
}
export function readAddressDebugCounterparty(
debug: Record<string, unknown> | null,
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
): string | null {
const extractedFilters = readAddressDebugFilters(debug);
if (toNonEmptyString(extractedFilters?.counterparty)) {
return toNonEmptyString(extractedFilters?.counterparty);
}
if (String(debug?.anchor_type ?? "") === "counterparty") {
return toNonEmptyString(debug?.anchor_value_resolved) ?? toNonEmptyString(debug?.anchor_value_raw);
}
const discoveryEntities = collectAssistantMcpDiscoveryEntityCandidates(debug, toNonEmptyString);
for (const entity of discoveryEntities) {
const text = toNonEmptyString(entity);
if (text) {
return text;
}
}
return null;
}
export function readAddressDebugIntent(
debug: Record<string, unknown> | null,
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
): string | null {
const detectedIntent = toNonEmptyString(debug?.detected_intent);
if (detectedIntent && detectedIntent !== "unknown") {
return detectedIntent;
}
return mapAssistantMcpDiscoveryPilotScopeToAddressIntent(
readAssistantMcpDiscoveryPilotScope(debug, toNonEmptyString),
readAssistantMcpDiscoveryActionFamily(debug, toNonEmptyString)
);
}
export function readAddressDebugOrganization(
debug: Record<string, unknown> | null,
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
): string | null {
const extractedFilters = readAddressDebugFilters(debug);
const rootFrameContext = toRecordObject(debug?.address_root_frame_context);
return toNonEmptyString(extractedFilters?.organization) ?? toNonEmptyString(rootFrameContext?.organization);
const discoveryMeaning = readAssistantMcpDiscoveryTurnMeaning(debug);
return (
toNonEmptyString(extractedFilters?.organization) ??
toNonEmptyString(rootFrameContext?.organization) ??
toNonEmptyString(discoveryMeaning?.explicit_organization_scope) ??
toNonEmptyString(debug?.assistant_active_organization) ??
toNonEmptyString(debug?.living_chat_selected_organization)
);
}
export function readAddressDebugScopedDate(debug: Record<string, unknown> | null): string | null {
@ -156,7 +497,8 @@ export function readAddressDebugScopedDate(debug: Record<string, unknown> | null
return (
formatIsoDateForReply(extractedFilters?.as_of_date) ??
formatIsoDateForReply(rootFrameContext?.as_of_date) ??
formatIsoDateForReply(extractedFilters?.period_to)
formatIsoDateForReply(extractedFilters?.period_to) ??
formatDiscoveryDateScopeForReply(readAssistantMcpDiscoveryTurnMeaning(debug)?.explicit_date_scope)
);
}
@ -166,10 +508,20 @@ export function readAddressDebugTemporalScope(
): AssistantAddressDebugTemporalScope {
const extractedFilters = readAddressDebugFilters(debug);
const rootFrameContext = toRecordObject(debug?.address_root_frame_context);
const discoveryDateScope = readDiscoveryDateScopeFilters(debug, toNonEmptyString);
return {
asOfDate: toNonEmptyString(extractedFilters?.as_of_date) ?? toNonEmptyString(rootFrameContext?.as_of_date),
periodFrom: toNonEmptyString(extractedFilters?.period_from) ?? toNonEmptyString(rootFrameContext?.period_from),
periodTo: toNonEmptyString(extractedFilters?.period_to) ?? toNonEmptyString(rootFrameContext?.period_to)
asOfDate:
toNonEmptyString(extractedFilters?.as_of_date) ??
toNonEmptyString(rootFrameContext?.as_of_date) ??
discoveryDateScope.asOfDate,
periodFrom:
toNonEmptyString(extractedFilters?.period_from) ??
toNonEmptyString(rootFrameContext?.period_from) ??
discoveryDateScope.periodFrom,
periodTo:
toNonEmptyString(extractedFilters?.period_to) ??
toNonEmptyString(rootFrameContext?.period_to) ??
discoveryDateScope.periodTo
};
}
@ -202,6 +554,13 @@ export function resolveAddressDebugAnchorContext(
anchorValue: counterparty
};
}
const discoveryCounterparty = readAddressDebugCounterparty(debug, toNonEmptyString);
if (discoveryCounterparty) {
return {
anchorType: "counterparty",
anchorValue: discoveryCounterparty
};
}
const account = toNonEmptyString(extractedFilters?.account);
if (account) {
return {
@ -250,6 +609,7 @@ export function resolveAddressDebugContextFacts(
): AssistantAddressDebugContextFacts {
return {
item: readAddressDebugItem(debug, toNonEmptyString),
counterparty: readAddressDebugCounterparty(debug, toNonEmptyString),
organization: readAddressDebugOrganization(debug, toNonEmptyString),
scopedDate: readAddressDebugScopedDate(debug)
};
@ -261,6 +621,36 @@ export function resolveAddressDebugCarryoverFilters(
): Record<string, unknown> {
const extractedFilters = readAddressDebugFilters(debug);
const nextFilters = extractedFilters ? { ...extractedFilters } : {};
const discoveryDateScope = readDiscoveryDateScopeFilters(debug, toNonEmptyString);
const preferGroundedDiscoveryDateScope =
hasGroundedDiscoveryBusinessAnswer(debug, toNonEmptyString) &&
Boolean(discoveryDateScope.asOfDate || discoveryDateScope.periodFrom || discoveryDateScope.periodTo);
const counterparty = readAddressDebugCounterparty(debug, toNonEmptyString);
const organization = readAddressDebugOrganization(debug, toNonEmptyString);
if (counterparty && !toNonEmptyString(nextFilters.counterparty)) {
nextFilters.counterparty = counterparty;
}
if (organization && !toNonEmptyString(nextFilters.organization)) {
nextFilters.organization = organization;
}
if (discoveryDateScope.asOfDate && (preferGroundedDiscoveryDateScope || !toNonEmptyString(nextFilters.as_of_date))) {
nextFilters.as_of_date = discoveryDateScope.asOfDate;
delete nextFilters.period_from;
delete nextFilters.period_to;
}
if (
discoveryDateScope.periodFrom &&
(preferGroundedDiscoveryDateScope || !toNonEmptyString(nextFilters.period_from))
) {
nextFilters.period_from = discoveryDateScope.periodFrom;
}
if (
discoveryDateScope.periodTo &&
(preferGroundedDiscoveryDateScope || !toNonEmptyString(nextFilters.period_to))
) {
nextFilters.period_to = discoveryDateScope.periodTo;
delete nextFilters.as_of_date;
}
const inventoryRootFrame = buildInventoryRootFrameFromAddressDebug(debug, toNonEmptyString);
const rootFilters =
inventoryRootFrame?.filters && typeof inventoryRootFrame.filters === "object"
@ -897,13 +1287,12 @@ export function isGroundedAddressDebug(
if (!debug || typeof debug !== "object") {
return false;
}
const executionLane = toNonEmptyString(debug.execution_lane);
if (executionLane !== "address_query") {
return false;
}
const answerGroundingCheck = toRecordObject(debug.answer_grounding_check);
const groundingStatus = toNonEmptyString(answerGroundingCheck?.status);
return groundingStatus === "grounded";
if (groundingStatus === "grounded" && toNonEmptyString(debug.execution_lane) === "address_query") {
return true;
}
return hasGroundedDiscoveryBusinessAnswer(debug, toNonEmptyString);
}
function isGroundedInventoryContextDebug(

View File

@ -1,5 +1,6 @@
import {
buildAddressMemoryRecapReply as buildAddressMemoryRecapReplyFromPolicy,
buildBroadBusinessEvaluationReply as buildBroadBusinessEvaluationReplyFromPolicy,
buildSelectedObjectAnswerInspectionReply as buildSelectedObjectAnswerInspectionReplyFromPolicy,
buildInventoryHistoryCapabilityFollowupReply as buildInventoryHistoryCapabilityFollowupReplyFromPolicy,
resolveAssistantLivingChatMemoryContext
@ -191,10 +192,26 @@ export async function runAssistantLivingChatRuntime(
? "deterministic_data_scope_contract_live"
: "deterministic_data_scope_contract";
} else if (unsupportedCurrentTurnMeaningBoundary) {
chatText = buildUnsupportedCurrentTurnMeaningBoundaryReply({
assistantTurnMeaning
});
livingChatSource = "deterministic_unsupported_current_turn_boundary";
const unsupportedFamily =
typeof assistantTurnMeaning?.unsupported_but_understood_family === "string"
? assistantTurnMeaning.unsupported_but_understood_family
: null;
if (unsupportedFamily === "broad_business_evaluation") {
const scopedOrganization = selectedOrganization ?? activeOrganization ?? continuityActiveOrganization ?? null;
chatText = buildBroadBusinessEvaluationReplyFromPolicy({
organization: scopedOrganization,
addressDebug: continuitySnapshot.lastGroundedAddressDebug,
sessionItems: input.sessionItems,
toNonEmptyString: input.toNonEmptyString
});
activeOrganization = scopedOrganization ?? activeOrganization;
livingChatSource = "deterministic_broad_business_evaluation_contract";
} else {
chatText = buildUnsupportedCurrentTurnMeaningBoundaryReply({
assistantTurnMeaning
});
livingChatSource = "deterministic_unsupported_current_turn_boundary";
}
} else if ((selectedOrganization || activeOrganization) && input.hasOrganizationFactLookupSignal(userMessage)) {
const scopedOrganization = selectedOrganization ?? activeOrganization ?? null;
chatText = input.buildAssistantOrganizationFactBoundaryReply(scopedOrganization);

View File

@ -52,6 +52,13 @@ function uniqueStrings(values: string[]): string[] {
return result;
}
function formatNamedChoiceList(values: string[]): string {
return uniqueStrings(values)
.slice(0, 6)
.map((value, index) => `${index + 1}. ${value}`)
.join("; ");
}
function isInternalMechanicsLine(value: string): boolean {
const text = value.toLowerCase();
return (
@ -80,6 +87,13 @@ function modeFor(pilot: AssistantMcpDiscoveryPilotExecutionContract): AssistantM
if (pilot.pilot_status === "skipped_needs_clarification") {
return "needs_clarification";
}
if (
pilot.pilot_scope === "entity_resolution_search_v1" &&
(pilot.reason_codes.includes("pilot_entity_resolution_ambiguity_requires_clarification") ||
pilot.derived_entity_resolution?.resolution_status === "ambiguous")
) {
return "needs_clarification";
}
if (pilot.evidence.answer_permission === "confirmed_answer") {
return "confirmed_with_bounded_inference";
}
@ -97,7 +111,197 @@ function isValueFlowPilot(pilot: AssistantMcpDiscoveryPilotExecutionContract): b
);
}
function isDocumentPilot(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
return pilot.pilot_scope === "counterparty_document_evidence_query_documents_v1";
}
function isMovementPilot(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
return pilot.pilot_scope === "counterparty_movement_evidence_query_movements_v1";
}
function isMetadataPilot(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
return pilot.pilot_scope === "metadata_inspection_v1";
}
function isEntityResolutionPilot(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
return pilot.pilot_scope === "entity_resolution_search_v1";
}
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")
);
}
function askedActionFamily(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
const action = pilot.evidence.query_plan.turn_meaning_ref?.asked_action_family;
if (typeof action !== "string") {
return null;
}
const normalized = action.trim().toLowerCase();
return normalized.length > 0 ? normalized : null;
}
function unsupportedFamily(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
const unsupported = pilot.evidence.query_plan.turn_meaning_ref?.unsupported_but_understood_family;
if (typeof unsupported !== "string") {
return null;
}
const normalized = unsupported.trim().toLowerCase();
return normalized.length > 0 ? normalized : null;
}
function firstEntityCandidate(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
const values = Array.isArray(pilot.evidence.query_plan.turn_meaning_ref?.explicit_entity_candidates)
? pilot.evidence.query_plan.turn_meaning_ref?.explicit_entity_candidates
: [];
for (const value of values) {
const text = String(value ?? "").trim();
if (text) {
return text;
}
}
return null;
}
function explicitDateScope(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
const value = pilot.evidence.query_plan.turn_meaning_ref?.explicit_date_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);
const entityPart = entity ? ` по контрагенту ${entity}` : "";
const periodPart = period ? ` за ${period}` : " в проверенном окне";
return `${entityPart}${periodPart}`;
}
function isMovementLaneClarification(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
return (
isMovementPilot(pilot) ||
pilot.reason_codes.includes("planner_selected_movement_recipe") ||
pilot.dry_run.reason_codes.includes("planner_selected_movement_recipe") ||
askedActionFamily(pilot) === "list_movements" ||
unsupportedFamily(pilot) === "movement_evidence"
);
}
function isDocumentLaneClarification(pilot: AssistantMcpDiscoveryPilotExecutionContract): boolean {
return (
isDocumentPilot(pilot) ||
pilot.reason_codes.includes("planner_selected_document_recipe") ||
pilot.dry_run.reason_codes.includes("planner_selected_document_recipe") ||
askedActionFamily(pilot) === "list_documents" ||
unsupportedFamily(pilot) === "document_evidence"
);
}
function laneScopeSuffix(pilot: AssistantMcpDiscoveryPilotExecutionContract): string {
const entity = firstEntityCandidate(pilot);
return entity ? ` по "${entity}"` : "";
}
function dryRunMissingAxis(pilot: AssistantMcpDiscoveryPilotExecutionContract, axis: string): boolean {
return pilot.dry_run.execution_steps.some((step) =>
step.missing_axis_options.some((option) => option.includes(axis))
);
}
function clarificationNeedRu(
pilot: AssistantMcpDiscoveryPilotExecutionContract
): { subject: string; verb: string } {
const needsPeriod = dryRunMissingAxis(pilot, "period");
const needsOrganization = dryRunMissingAxis(pilot, "organization");
if (needsPeriod && needsOrganization) {
return { subject: "проверяемый период и организацию", verb: "нужно" };
}
if (needsPeriod) {
return { subject: "проверяемый период", verb: "нужен" };
}
if (needsOrganization) {
return { subject: "организацию", verb: "нужно" };
}
return { subject: "контекст проверки", verb: "нужно" };
}
function clarificationNextStepLine(
pilot: AssistantMcpDiscoveryPilotExecutionContract,
laneLabel: string
): string {
const needsPeriod = dryRunMissingAxis(pilot, "period");
const needsOrganization = dryRunMissingAxis(pilot, "organization");
const scopeSuffix = laneScopeSuffix(pilot);
if (needsPeriod && needsOrganization) {
return `Уточните период и организацию, и я продолжу поиск по ${laneLabel}${scopeSuffix} в 1С.`;
}
if (needsPeriod) {
return `Уточните период, и я продолжу поиск по ${laneLabel}${scopeSuffix} в 1С.`;
}
if (needsOrganization) {
return `Уточните организацию, и я продолжу поиск по ${laneLabel}${scopeSuffix} в 1С.`;
}
return `Уточните контекст проверки, и я продолжу поиск по ${laneLabel}${scopeSuffix} в 1С.`;
}
function metadataRouteFamilyLabelRu(
routeFamily: "document_evidence" | "movement_evidence" | "catalog_drilldown" | null
): string | null {
if (routeFamily === "document_evidence") {
return "контур документов";
}
if (routeFamily === "movement_evidence") {
return "контур движений/регистров";
}
if (routeFamily === "catalog_drilldown") {
return "контур справочников и связанных объектов";
}
return null;
}
function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpDiscoveryPilotExecutionContract): string {
const askedMonthlyBreakdown =
pilot.derived_bidirectional_value_flow?.aggregation_axis === "month" ||
pilot.derived_value_flow?.aggregation_axis === "month";
if (isEntityResolutionPilot(pilot) && mode === "confirmed_with_bounded_inference") {
return "По каталогу 1С найден вероятный контрагент; это заземление сущности для следующего шага, а не еще бизнес-ответ по данным.";
}
if (isEntityResolutionPilot(pilot) && mode === "needs_clarification") {
return "По каталогу 1С нашлось несколько похожих контрагентов, и без уточнения нельзя честно выбрать правильную сущность.";
}
if (
isEntityResolutionPilot(pilot) &&
mode === "checked_sources_only" &&
pilot.derived_entity_resolution?.resolution_status === "not_found"
) {
return "По текущему каталожному поиску 1С точный контрагент пока не подтвержден.";
}
if (isMovementPilot(pilot) && mode === "confirmed_with_bounded_inference") {
return `По движениям${documentOrMovementScopeRu(pilot)} в 1С найдены подтвержденные строки; ответ ограничен проверенным окном и найденными строками.`;
}
if (pilot.derived_metadata_surface && mode === "confirmed_with_bounded_inference") {
if (pilot.derived_metadata_surface.ambiguity_detected) {
return "По метаданным 1С найдены конкурирующие schema-поверхности; перед следующим шагом нужно удержать неоднозначность явно.";
}
if (pilot.derived_metadata_surface.downstream_route_family) {
return "По метаданным 1С найдена схема и заземлена вероятная поверхность для следующего безопасного шага.";
}
return "По метаданным 1С найдена доступная схема для дальнейшего безопасного поиска.";
}
if (askedMonthlyBreakdown && pilot.derived_bidirectional_value_flow && mode === "confirmed_with_bounded_inference") {
return "По данным 1С найдены строки входящих и исходящих денежных движений; нетто и помесячная раскладка могут называться только как расчет по найденным строкам и проверенному периоду.";
}
if (askedMonthlyBreakdown && pilot.derived_value_flow && mode === "confirmed_with_bounded_inference") {
if (pilot.derived_value_flow.value_flow_direction === "outgoing_supplier_payout") {
return "По данным 1С найдены строки исходящих платежей/списаний; сумму и помесячную раскладку можно называть только в рамках проверенного периода и найденных строк.";
}
return "По данным 1С найдены строки входящих денежных поступлений; сумму и помесячную раскладку можно называть только в рамках проверенного периода и найденных строк.";
}
if (pilot.derived_bidirectional_value_flow && mode === "confirmed_with_bounded_inference") {
return "По данным 1С найдены строки входящих и исходящих денежных движений; нетто можно называть только как расчет по найденным строкам и проверенному периоду.";
}
@ -105,14 +309,37 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
if (pilot.derived_value_flow.value_flow_direction === "outgoing_supplier_payout") {
return "По данным 1С найдены строки исходящих платежей/списаний; сумму можно называть только в рамках проверенного периода и найденных строк.";
}
return "По данным 1С найдены строки денежных движений; сумму можно называть только в рамках проверенного периода и найденных строк.";
return "По данным 1С найдены строки входящих денежных поступлений; сумму можно называть только в рамках проверенного периода и найденных строк.";
}
if (isDocumentPilot(pilot) && mode === "confirmed_with_bounded_inference") {
return `По документам${documentOrMovementScopeRu(pilot)} в 1С найдены подтвержденные строки; ответ ограничен проверенным окном и найденными строками.`;
}
if (isMovementPilot(pilot) && mode === "confirmed_with_bounded_inference") {
return `По движениям${documentOrMovementScopeRu(pilot)} в 1С найдены подтвержденные строки; ответ ограничен проверенным окном и найденными строками.`;
}
if (mode === "confirmed_with_bounded_inference") {
return "По данным 1С есть подтвержденная активность; длительность можно оценивать только как вывод из этих строк.";
}
if (isDocumentPilot(pilot) && mode === "bounded_inference_only") {
return `По документам${documentOrMovementScopeRu(pilot)} полный срез не подтвержден; пока есть только ограниченная граница проверенного окна 1С.`;
}
if (isMovementPilot(pilot) && mode === "bounded_inference_only") {
return `По движениям${documentOrMovementScopeRu(pilot)} полный срез не подтвержден; пока есть только ограниченная граница проверенного окна 1С.`;
}
if (mode === "bounded_inference_only") {
return "Точный факт не подтвержден, но есть ограниченная оценка по найденной активности в 1С.";
}
if (mode === "needs_clarification" && isMetadataLaneChoiceClarification(pilot)) {
return "По подтвержденной metadata-поверхности видно несколько конкурирующих data-lane, и без явного выбора дальше идти нельзя.";
}
if (mode === "needs_clarification" && isMovementLaneClarification(pilot)) {
const need = clarificationNeedRu(pilot);
return `Могу идти дальше по движениям/регистрам${laneScopeSuffix(pilot)}, но для запуска поиска в 1С ${need.verb} ${need.subject}.`;
}
if (mode === "needs_clarification" && isDocumentLaneClarification(pilot)) {
const need = clarificationNeedRu(pilot);
return `Могу идти дальше по документам${laneScopeSuffix(pilot)}, но для запуска поиска в 1С ${need.verb} ${need.subject}.`;
}
if (mode === "needs_clarification") {
return "Нужно уточнить контекст перед поиском в 1С.";
}
@ -123,9 +350,47 @@ function headlineFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpD
}
function nextStepFor(mode: AssistantMcpDiscoveryAnswerMode, pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
if (isEntityResolutionPilot(pilot) && mode === "needs_clarification") {
const ambiguityCandidates = pilot.derived_entity_resolution?.ambiguity_candidates ?? [];
if (ambiguityCandidates.length > 0) {
return `Уточните, какой именно контрагент нужен: ${formatNamedChoiceList(
ambiguityCandidates
)}. Можно ответить названием или номером варианта.`;
}
return "Уточните точное название контрагента или добавьте ИНН, и я продолжу уже по нужной сущности в 1С.";
}
if (isEntityResolutionPilot(pilot) && mode === "confirmed_with_bounded_inference") {
return "Теперь могу продолжить уже по найденному контрагенту и искать документы, движения или денежный поток.";
}
if (
isEntityResolutionPilot(pilot) &&
mode === "checked_sources_only" &&
pilot.derived_entity_resolution?.resolution_status === "not_found"
) {
return "Дайте точное название или ИНН, и я повторю поиск по каталогу 1С более прицельно.";
}
if (mode === "needs_clarification" && isMetadataLaneChoiceClarification(pilot)) {
return "Уточните, в какой контур идти дальше: по документам или по движениям/регистрам.";
}
if (mode === "needs_clarification" && isMovementLaneClarification(pilot)) {
return clarificationNextStepLine(pilot, "движениям/регистрам");
}
if (mode === "needs_clarification" && isDocumentLaneClarification(pilot)) {
return clarificationNextStepLine(pilot, "документам");
}
if (mode === "needs_clarification") {
return "Уточните контрагента, период или организацию, и я смогу выполнить проверку по 1С.";
}
if (mode === "confirmed_with_bounded_inference" && pilot.derived_metadata_surface) {
const surface = pilot.derived_metadata_surface;
if (surface.ambiguity_detected && surface.ambiguity_entity_sets.length > 0) {
return `Следующим шагом лучше сузить surface до одного семейства: ${surface.ambiguity_entity_sets.join(", ")}.`;
}
const routeLabel = metadataRouteFamilyLabelRu(surface.downstream_route_family);
if (surface.selected_entity_set && routeLabel) {
return `Следующим шагом могу пойти в ${routeLabel} по surface «${surface.selected_entity_set}» и уже искать подтвержденные данные, а не только схему.`;
}
}
if (mode === "checked_sources_only" && pilot.query_limitations.length > 0) {
return "Можно повторить проверку после восстановления MCP-доступа или сузить вопрос до конкретного контрагента/периода.";
}
@ -148,12 +413,65 @@ 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 (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.");
}
if (isMovementPilot(pilot)) {
claims.push("Do not claim full movement history outside the checked period.");
claims.push("Do not present the confirmed movement rows as a complete movement universe.");
}
if (isMetadataPilot(pilot)) {
claims.push("Do not present metadata surface as confirmed business data rows.");
claims.push("Do not claim a document/register exists outside the checked metadata probe results.");
claims.push("Do not present the inferred next checked lane as already executed data retrieval.");
}
if (isEntityResolutionPilot(pilot)) {
claims.push("Do not present catalog grounding as confirmed business activity, turnover, or document evidence.");
claims.push("Do not claim legal identity uniqueness when several catalog candidates are still plausible.");
claims.push("Do not imply that the resolved entity has already been used in a downstream data probe.");
}
if (pilot.evidence.confirmed_facts.length === 0) {
claims.push("Do not claim a confirmed business fact when confirmed_facts is empty.");
}
return claims;
}
const RU_MONTH_LABELS_SHORT = [
"янв",
"фев",
"мар",
"апр",
"май",
"июн",
"июл",
"авг",
"сен",
"окт",
"ноя",
"дек"
] as const;
function monthLabelRu(monthBucket: string): string {
const match = monthBucket.match(/^(\d{4})-(\d{2})$/);
if (!match) {
return monthBucket;
}
const monthIndex = Number(match[2]) - 1;
const label = RU_MONTH_LABELS_SHORT[monthIndex] ?? match[2];
return `${label} ${match[1]}`;
}
function netLabelRu(netDirection: "net_incoming" | "net_outgoing" | "balanced"): string {
if (netDirection === "net_incoming") {
return "нетто в нашу сторону";
}
if (netDirection === "net_outgoing") {
return "нетто исходящее";
}
return "нетто нулевое";
}
function derivedActivityInferenceLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
const period = pilot.derived_activity_period;
if (!period) {
@ -166,6 +484,78 @@ function derivedActivityInferenceLine(pilot: AssistantMcpDiscoveryPilotExecution
].join(" ");
}
function derivedMetadataConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
const surface = pilot.derived_metadata_surface;
if (!surface) {
return null;
}
const scope = surface.metadata_scope ? ` по области "${surface.metadata_scope}"` : "";
const entitySets =
surface.available_entity_sets.length > 0
? ` Типы объектов: ${surface.available_entity_sets.join(", ")}.`
: "";
const objects =
surface.matched_objects.length > 0
? ` Найденные объекты: ${surface.matched_objects.slice(0, 8).join(", ")}.`
: "";
const selectedEntitySet = surface.selected_entity_set ? ` Выбранное family: ${surface.selected_entity_set}.` : "";
const selectedObjects =
surface.selected_surface_objects.length > 0
? ` Выбранные surface-объекты: ${surface.selected_surface_objects.slice(0, 6).join(", ")}.`
: "";
const fields =
surface.available_fields.length > 0
? ` Доступные поля/секции: ${surface.available_fields.slice(0, 12).join(", ")}.`
: "";
return `Подтвержденная metadata-поверхность 1С${scope}: ${surface.matched_rows} строк metadata-ответа.${entitySets}${objects}${selectedEntitySet}${selectedObjects}${fields}`.replace(/\s+/g, " ").trim();
}
function derivedMetadataInferenceLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
const surface = pilot.derived_metadata_surface;
if (!surface) {
return null;
}
if (surface.ambiguity_detected && surface.ambiguity_entity_sets.length > 0) {
return `По подтвержденной metadata-поверхности видно несколько конкурирующих family: ${surface.ambiguity_entity_sets.join(", ")}. Следующий data-lane пока нельзя выбрать без явного сужения.`;
}
const routeLabel = metadataRouteFamilyLabelRu(surface.downstream_route_family);
if (!surface.selected_entity_set || !routeLabel) {
return null;
}
return `По подтвержденной metadata-поверхности следующий проверяемый шаг можно ограниченно оценить как ${routeLabel} через family «${surface.selected_entity_set}». Это еще не выполненный data-fetch, а только grounded выбор следующего контура.`;
}
function derivedEntityResolutionConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
const resolution = pilot.derived_entity_resolution;
if (!resolution || resolution.resolution_status !== "resolved" || !resolution.resolved_entity) {
return null;
}
const requested = resolution.requested_entity ? ` по запросу "${resolution.requested_entity}"` : "";
const confidence =
resolution.confidence === "high"
? " Точность совпадения выглядит высокой."
: resolution.confidence === "medium"
? " Совпадение выглядит достаточно сильным, но это все еще catalog grounding."
: " Совпадение выглядит вероятным, но его лучше считать рабочим заземлением сущности.";
return `В текущем каталожном срезе 1С${requested} найден контрагент "${resolution.resolved_entity}".${confidence}`;
}
function derivedEntityResolutionInferenceLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
const resolution = pilot.derived_entity_resolution;
if (!resolution) {
return null;
}
if (resolution.resolution_status === "resolved") {
return "Сейчас подтверждено только заземление сущности по каталогу 1С; документы, движения и денежные показатели по ней еще не проверялись.";
}
if (resolution.resolution_status === "ambiguous" && resolution.ambiguity_candidates.length > 0) {
return `В каталоге 1С нашлось несколько близких кандидатов: ${formatNamedChoiceList(
resolution.ambiguity_candidates
)}. Без уточнения нельзя честно выбрать одного контрагента для следующего шага.`;
}
return null;
}
function derivedValueFlowConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecutionContract): string | null {
const flow = pilot.derived_value_flow;
if (!flow) {
@ -174,15 +564,17 @@ function derivedValueFlowConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecutio
const counterparty = flow.counterparty ? ` по контрагенту ${flow.counterparty}` : "";
const period = flow.period_scope ? ` за период ${flow.period_scope}` : " в проверенном окне";
const movementLabel =
flow.value_flow_direction === "outgoing_supplier_payout" ? "исходящих платежей/списаний" : "денежных движений";
flow.value_flow_direction === "outgoing_supplier_payout"
? "исходящих платежей/списаний"
: "входящих денежных поступлений";
const totalLabel =
flow.value_flow_direction === "outgoing_supplier_payout"
? "сумма исходящих платежей/списаний составляет"
: "сумма составляет";
: "сумма входящих денежных поступлений составляет";
const caveat =
flow.value_flow_direction === "outgoing_supplier_payout"
? "Это расчет по найденным строкам 1С, а не подтверждение полного объема платежей вне проверенного окна."
: "Это расчет по найденным строкам 1С, а не подтверждение полного оборота вне проверенного окна.";
: "Это расчет по найденным строкам 1С, а не подтверждение полного объема поступлений вне проверенного окна.";
const dates =
flow.first_movement_date && flow.latest_movement_date
? ` Первая найденная дата движения: ${flow.first_movement_date}; последняя: ${flow.latest_movement_date}.`
@ -193,6 +585,20 @@ function derivedValueFlowConfirmedLine(pilot: AssistantMcpDiscoveryPilotExecutio
return `По найденным строкам ${movementLabel} в 1С${counterparty}${period} ${totalLabel} ${flow.total_amount_human_ru} Учтено строк с суммой: ${flow.rows_with_amount} из ${flow.rows_matched}.${dates}${limitCaveat} ${caveat}`;
}
function derivedValueFlowMonthlyLines(pilot: AssistantMcpDiscoveryPilotExecutionContract): string[] {
const flow = pilot.derived_value_flow;
if (!flow || flow.aggregation_axis !== "month" || flow.monthly_breakdown.length === 0) {
return [];
}
return flow.monthly_breakdown.map((bucket) => {
const monthLabel = monthLabelRu(bucket.month_bucket);
if (flow.value_flow_direction === "outgoing_supplier_payout") {
return `Помесячно: ${monthLabel} — заплатили ${bucket.total_amount_human_ru} по ${bucket.rows_with_amount} строкам с суммой`;
}
return `Помесячно: ${monthLabel} — получили ${bucket.total_amount_human_ru} по ${bucket.rows_with_amount} строкам с суммой`;
});
}
function sideDateRange(first: string | null, latest: string | null): string {
if (first && latest) {
return ` первая дата ${first}, последняя ${latest}`;
@ -230,6 +636,17 @@ function derivedBidirectionalValueFlowConfirmedLine(pilot: AssistantMcpDiscovery
.trim();
}
function derivedBidirectionalValueFlowMonthlyLines(pilot: AssistantMcpDiscoveryPilotExecutionContract): string[] {
const flow = pilot.derived_bidirectional_value_flow;
if (!flow || flow.aggregation_axis !== "month" || flow.monthly_breakdown.length === 0) {
return [];
}
return flow.monthly_breakdown.map(
(bucket) =>
`Помесячно: ${monthLabelRu(bucket.month_bucket)} — получили ${bucket.incoming_total_amount_human_ru}, заплатили ${bucket.outgoing_total_amount_human_ru}, ${netLabelRu(bucket.net_direction)} ${bucket.net_amount_human_ru}`
);
}
export function buildAssistantMcpDiscoveryAnswerDraft(
pilot: AssistantMcpDiscoveryPilotExecutionContract
): AssistantMcpDiscoveryAnswerDraftContract {
@ -242,14 +659,30 @@ export function buildAssistantMcpDiscoveryAnswerDraft(
if (pilot.evidence.inferred_facts.length > 0) {
pushReason(reasonCodes, "answer_contains_bounded_inference");
}
const derivedInferenceLine = derivedActivityInferenceLine(pilot);
const derivedInferenceLine =
derivedActivityInferenceLine(pilot) ??
derivedMetadataInferenceLine(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 monthlyConfirmedLines =
derivedBidirectionalValueFlowMonthlyLines(pilot).length > 0
? derivedBidirectionalValueFlowMonthlyLines(pilot)
: derivedValueFlowMonthlyLines(pilot);
if (monthlyConfirmedLines.length > 0) {
pushReason(reasonCodes, "answer_contains_monthly_breakdown");
}
const confirmedLines = derivedValueLine
? [...pilot.evidence.confirmed_facts, derivedValueLine]
: pilot.evidence.confirmed_facts;
? [...pilot.evidence.confirmed_facts, derivedValueLine, ...monthlyConfirmedLines]
: derivedEntityResolutionLine
? [...pilot.evidence.confirmed_facts, derivedEntityResolutionLine]
: derivedMetadataLine
? [...pilot.evidence.confirmed_facts, derivedMetadataLine]
: pilot.evidence.confirmed_facts;
return {
schema_version: ASSISTANT_MCP_DISCOVERY_ANSWER_DRAFT_SCHEMA_VERSION,

View File

@ -13,6 +13,15 @@ export const ASSISTANT_MCP_DISCOVERY_PLANNER_SCHEMA_VERSION = "assistant_mcp_dis
export type AssistantMcpDiscoveryPlannerStatus = "ready_for_execution" | "needs_clarification" | "blocked";
export type AssistantMcpDiscoveryChainId =
| "metadata_inspection"
| "metadata_lane_clarification"
| "value_flow"
| "lifecycle"
| "movement_evidence"
| "document_evidence"
| "entity_resolution";
export interface AssistantMcpDiscoveryPlannerInput {
semanticDataNeed?: string | null;
turnMeaning?: AssistantMcpDiscoveryTurnMeaningRef | null;
@ -23,6 +32,8 @@ export interface AssistantMcpDiscoveryPlannerContract {
policy_owner: "assistantMcpDiscoveryPlanner";
planner_status: AssistantMcpDiscoveryPlannerStatus;
semantic_data_need: string | null;
selected_chain_id: AssistantMcpDiscoveryChainId;
selected_chain_summary: string;
proposed_primitives: AssistantMcpDiscoveryPrimitive[];
required_axes: string[];
discovery_plan: AssistantMcpDiscoveryPlanContract;
@ -32,11 +43,17 @@ export interface AssistantMcpDiscoveryPlannerContract {
interface PlannerRecipe {
semanticDataNeed: string;
chainId: AssistantMcpDiscoveryChainId;
chainSummary: string;
primitives: AssistantMcpDiscoveryPrimitive[];
axes: string[];
reason: string;
}
interface PlannerBudgetOverride {
maxProbeCount?: number;
}
function toNonEmptyString(value: unknown): string | null {
if (value === null || value === undefined) {
return null;
@ -76,6 +93,10 @@ function hasEntity(meaning: AssistantMcpDiscoveryTurnMeaningRef | null | undefin
return (meaning?.explicit_entity_candidates?.length ?? 0) > 0;
}
function aggregationAxis(meaning: AssistantMcpDiscoveryTurnMeaningRef | null | undefined): string | null {
return toNonEmptyString(meaning?.asked_aggregation_axis)?.toLowerCase() ?? null;
}
function addScopeAxes(axes: string[], meaning: AssistantMcpDiscoveryTurnMeaningRef | null | undefined): void {
if (hasEntity(meaning)) {
pushUnique(axes, "counterparty");
@ -92,6 +113,27 @@ function includesAny(text: string, tokens: string[]): boolean {
return tokens.some((token) => text.includes(token));
}
function isYearDateScope(meaning: AssistantMcpDiscoveryTurnMeaningRef | null | undefined): boolean {
return /^\d{4}$/.test(toNonEmptyString(meaning?.explicit_date_scope) ?? "");
}
function budgetOverrideFor(input: AssistantMcpDiscoveryPlannerInput, recipe: PlannerRecipe): PlannerBudgetOverride {
const meaning = input.turnMeaning ?? null;
const requestedAggregationAxis = aggregationAxis(meaning);
const isValueFlowRecipe =
recipe.semanticDataNeed === "counterparty value-flow evidence" &&
recipe.primitives.includes("query_movements");
if (!isValueFlowRecipe) {
return {};
}
if (requestedAggregationAxis === "month" || isYearDateScope(meaning)) {
return {
maxProbeCount: 30
};
}
return {};
}
function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
const meaning = input.turnMeaning ?? null;
const domain = lower(meaning?.asked_domain_family);
@ -99,27 +141,37 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
const unsupported = lower(meaning?.unsupported_but_understood_family);
const combined = `${domain} ${action} ${unsupported}`.trim();
const axes: string[] = [];
const requestedAggregationAxis = aggregationAxis(meaning);
addScopeAxes(axes, meaning);
if (includesAny(combined, ["metadata_lane_choice_clarification", "resolve_next_lane"])) {
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_recipe"
};
}
if (includesAny(combined, ["turnover", "revenue", "payment", "payout", "value", "net", "netting", "balance", "cashflow"])) {
pushUnique(axes, "aggregate_axis");
pushUnique(axes, "amount");
pushUnique(axes, "coverage_target");
if (requestedAggregationAxis === "month") {
pushUnique(axes, "calendar_month");
}
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: ["resolve_entity_reference", "query_movements", "aggregate_by_axis", "probe_coverage"],
axes,
reason: "planner_selected_value_flow_recipe"
};
}
if (includesAny(combined, ["document", "documents"])) {
pushUnique(axes, "coverage_target");
return {
semanticDataNeed: "document evidence",
primitives: ["resolve_entity_reference", "query_documents", "probe_coverage"],
axes,
reason: "planner_selected_document_recipe"
reason: requestedAggregationAxis === "month"
? "planner_selected_monthly_value_flow_recipe"
: "planner_selected_value_flow_recipe"
};
}
@ -129,6 +181,8 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
pushUnique(axes, "evidence_basis");
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: ["resolve_entity_reference", "query_documents", "probe_coverage", "explain_evidence_basis"],
axes,
reason: "planner_selected_lifecycle_recipe"
@ -139,16 +193,45 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
pushUnique(axes, "metadata_scope");
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: ["inspect_1c_metadata"],
axes,
reason: "planner_selected_metadata_recipe"
};
}
if (includesAny(combined, ["movement", "movements", "bank_operations", "movement_evidence", "list_movements"])) {
pushUnique(axes, "coverage_target");
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: ["resolve_entity_reference", "query_movements", "probe_coverage"],
axes,
reason: "planner_selected_movement_recipe"
};
}
if (includesAny(combined, ["document", "documents"])) {
pushUnique(axes, "coverage_target");
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: ["resolve_entity_reference", "query_documents", "probe_coverage"],
axes,
reason: "planner_selected_document_recipe"
};
}
if (hasEntity(meaning)) {
pushUnique(axes, "business_entity");
pushUnique(axes, "coverage_target");
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: ["search_business_entity", "resolve_entity_reference", "probe_coverage"],
axes,
reason: "planner_selected_entity_resolution_recipe"
@ -157,6 +240,8 @@ function recipeFor(input: AssistantMcpDiscoveryPlannerInput): PlannerRecipe {
return {
semanticDataNeed: "unclassified 1C discovery need",
chainId: "metadata_inspection",
chainSummary: "Start with metadata inspection instead of guessing a deeper fact route when the business need is still under-specified.",
primitives: ["inspect_1c_metadata"],
axes,
reason: "planner_selected_clarification_recipe"
@ -180,15 +265,20 @@ export function planAssistantMcpDiscovery(
input: AssistantMcpDiscoveryPlannerInput
): AssistantMcpDiscoveryPlannerContract {
const recipe = recipeFor(input);
const budgetOverride = budgetOverrideFor(input, recipe);
const semanticDataNeed = toNonEmptyString(input.semanticDataNeed) ?? recipe.semanticDataNeed;
const reasonCodes: string[] = [];
pushReason(reasonCodes, recipe.reason);
if (budgetOverride.maxProbeCount) {
pushReason(reasonCodes, "planner_enabled_chunked_coverage_probe_budget");
}
const plan = buildAssistantMcpDiscoveryPlan({
semanticDataNeed,
turnMeaning: input.turnMeaning,
proposedPrimitives: recipe.primitives,
requiredAxes: recipe.axes
requiredAxes: recipe.axes,
maxProbeCount: budgetOverride.maxProbeCount
});
const review = reviewAssistantMcpDiscoveryPlanAgainstCatalog(plan);
const plannerStatus = statusFrom(plan, review);
@ -206,6 +296,8 @@ export function planAssistantMcpDiscovery(
policy_owner: "assistantMcpDiscoveryPlanner",
planner_status: plannerStatus,
semantic_data_need: semanticDataNeed,
selected_chain_id: recipe.chainId,
selected_chain_summary: recipe.chainSummary,
proposed_primitives: recipe.primitives,
required_axes: recipe.axes,
discovery_plan: plan,

View File

@ -23,7 +23,9 @@ export type AssistantMcpDiscoveryAnswerPermission = "confirmed_answer" | "bounde
export interface AssistantMcpDiscoveryTurnMeaningRef {
asked_domain_family?: string | null;
asked_action_family?: string | null;
asked_aggregation_axis?: string | null;
explicit_entity_candidates?: string[];
metadata_ambiguity_entity_sets?: string[];
explicit_organization_scope?: string | null;
explicit_date_scope?: string | null;
meaning_confidence?: number | null;
@ -101,7 +103,7 @@ const DEFAULT_DISCOVERY_BUDGET: AssistantMcpDiscoveryExecutionBudget = {
max_rows_per_probe: 100
};
const MAX_PROBE_COUNT = 6;
const MAX_PROBE_COUNT = 36;
const MAX_ROWS_PER_PROBE = 500;
const ALLOWED_PRIMITIVE_SET = new Set<string>(ASSISTANT_MCP_DISCOVERY_PRIMITIVES);
@ -164,19 +166,27 @@ function normalizeTurnMeaning(
const result: AssistantMcpDiscoveryTurnMeaningRef = {};
const domain = toNonEmptyString(value.asked_domain_family);
const action = toNonEmptyString(value.asked_action_family);
const aggregationAxis = toNonEmptyString(value.asked_aggregation_axis);
const organization = toNonEmptyString(value.explicit_organization_scope);
const dateScope = toNonEmptyString(value.explicit_date_scope);
const unsupported = toNonEmptyString(value.unsupported_but_understood_family);
const entities = toStringList(value.explicit_entity_candidates);
const metadataAmbiguityEntitySets = toStringList(value.metadata_ambiguity_entity_sets);
if (domain) {
result.asked_domain_family = domain;
}
if (action) {
result.asked_action_family = action;
}
if (aggregationAxis) {
result.asked_aggregation_axis = aggregationAxis;
}
if (entities.length > 0) {
result.explicit_entity_candidates = entities;
}
if (metadataAmbiguityEntitySets.length > 0) {
result.metadata_ambiguity_entity_sets = metadataAmbiguityEntitySets;
}
if (organization) {
result.explicit_organization_scope = organization;
}

View File

@ -100,10 +100,24 @@ function localizeLine(value: string): string {
}
const valueFlowMatch = value.match(/^1C value-flow rows were found for counterparty\s+(.+)$/i);
if (valueFlowMatch) {
return `В 1С найдены строки денежных движений по контрагенту ${valueFlowMatch[1]}.`;
return `В 1С найдены строки входящих денежных поступлений по контрагенту ${valueFlowMatch[1]}.`;
}
if (/^1C value-flow rows were found for the requested counterparty scope$/i.test(value)) {
return "В 1С найдены строки денежных движений по запрошенному контрагентскому контуру.";
return "В 1С найдены строки входящих денежных поступлений по запрошенному контрагентскому контуру.";
}
const documentRowsMatch = value.match(/^1C document rows were found for counterparty\s+(.+)$/i);
if (documentRowsMatch) {
return `В 1С найдены строки документов по контрагенту ${documentRowsMatch[1]}.`;
}
if (/^1C document rows were found for the requested scope$/i.test(value)) {
return "В 1С найдены строки документов по запрошенному контуру.";
}
const movementRowsMatch = value.match(/^1C movement rows were found for counterparty\s+(.+)$/i);
if (movementRowsMatch) {
return `Р1С найдены строки движений по контрагенту ${movementRowsMatch[1]}.`;
}
if (/^1C movement rows were found for the requested scope$/i.test(value)) {
return "Р1С найдены строки движений по запрошенному контуру.";
}
const supplierPayoutMatch = value.match(/^1C supplier-payout rows were found for counterparty\s+(.+)$/i);
if (supplierPayoutMatch) {
@ -131,8 +145,17 @@ function localizeLine(value: string): string {
if (/^Business activity duration may be inferred from first and latest confirmed 1C activity rows$/i.test(value)) {
return "Длительность деловой активности можно оценивать только как вывод по первой и последней подтвержденной строке активности в 1С.";
}
if (/^Counterparty document evidence is limited to confirmed 1C document rows in the checked scope$/i.test(value)) {
return "Срез документов ограничен только подтвержденными строками документов в проверенном окне.";
}
if (/^Counterparty movement evidence is limited to confirmed 1C movement rows in the checked scope$/i.test(value)) {
return "Срез движений ограничен только подтвержденными строками движений в проверенном окне.";
}
if (/^Counterparty value-flow total was calculated from confirmed 1C movement rows$/i.test(value)) {
return "Сумма рассчитана только по подтвержденным строкам денежных движений в 1С.";
return "Сумма входящих поступлений рассчитана только по подтвержденным строкам поступлений в 1С.";
}
if (/^Counterparty monthly value-flow breakdown was grouped by month over confirmed 1C movement rows$/i.test(value)) {
return "Помесячная раскладка входящих поступлений построена только по подтвержденным строкам поступлений в 1С.";
}
if (/^Counterparty supplier-payout total was calculated from confirmed 1C outgoing payment rows$/i.test(value)) {
return "Сумма исходящих платежей рассчитана только по подтвержденным строкам списаний в 1С.";
@ -140,6 +163,58 @@ function localizeLine(value: string): string {
if (/^Counterparty net value-flow was calculated as incoming confirmed 1C rows minus outgoing confirmed 1C rows$/i.test(value)) {
return "Нетто денежного потока рассчитано только как входящие подтвержденные строки 1С минус исходящие подтвержденные строки 1С.";
}
if (/^Counterparty monthly net value-flow breakdown was grouped by month over confirmed incoming and outgoing 1C rows$/i.test(value)) {
return "Помесячная нетто-раскладка сгруппирована только по подтвержденным входящим и исходящим строкам 1С.";
}
const metadataSurfaceMatch = value.match(
/^Confirmed 1C metadata surface(?: for scope "([^"]+)")?: (\d+) rows and (\d+) matching objects$/i
);
if (metadataSurfaceMatch) {
const scopePart = metadataSurfaceMatch[1] ? ` по области "${metadataSurfaceMatch[1]}"` : "";
return `В 1С подтверждена metadata-поверхность${scopePart}: ${metadataSurfaceMatch[2]} строк metadata-ответа и ${metadataSurfaceMatch[3]} совпавших объекта(ов).`;
}
const metadataObjectSetsMatch = value.match(/^Available metadata object sets: (.+)$/i);
if (metadataObjectSetsMatch) {
return `Доступные типы metadata-объектов: ${metadataObjectSetsMatch[1]}.`;
}
const selectedMetadataEntitySetMatch = value.match(/^Selected metadata entity set: (.+)$/i);
if (selectedMetadataEntitySetMatch) {
return `Выбранное семейство metadata-объектов: ${selectedMetadataEntitySetMatch[1]}.`;
}
const selectedMetadataObjectsMatch = value.match(/^Selected metadata objects: (.+)$/i);
if (selectedMetadataObjectsMatch) {
return `Выбранные metadata-объекты для следующего шага: ${selectedMetadataObjectsMatch[1]}.`;
}
const metadataFieldsMatch = value.match(/^Available metadata fields\/sections: (.+)$/i);
if (metadataFieldsMatch) {
return `Доступные metadata-поля/секции: ${metadataFieldsMatch[1]}.`;
}
const metadataLaneInferenceMatch = value.match(
/^A likely next checked lane may be inferred as (document_evidence|movement_evidence|catalog_drilldown) from the confirmed metadata surface$/i
);
if (metadataLaneInferenceMatch) {
const routeLabel =
metadataLaneInferenceMatch[1] === "document_evidence"
? "контур документов"
: metadataLaneInferenceMatch[1] === "movement_evidence"
? "контур движений/регистров"
: "контур справочников и связанных объектов";
return `Следующий проверяемый контур по этой metadata-поверхности можно ограниченно оценить как ${routeLabel}.`;
}
if (/^Detailed metadata fields were not returned by this MCP metadata probe$/i.test(value)) {
return "Эта MCP-проверка metadata не вернула детальный список полей.";
}
const metadataAmbiguityMatch = value.match(/^Exact downstream metadata surface remains ambiguous across: (.+)$/i);
if (metadataAmbiguityMatch) {
return `Точная downstream metadata-поверхность пока неоднозначна между family: ${metadataAmbiguityMatch[1]}.`;
}
const noMatchingMetadataScopeMatch = value.match(/^No matching 1C metadata objects were confirmed for scope "([^"]+)"$/i);
if (noMatchingMetadataScopeMatch) {
return `В 1С не подтверждены metadata-объекты по области "${noMatchingMetadataScopeMatch[1]}".`;
}
if (/^No matching 1C metadata objects were confirmed by this MCP metadata probe$/i.test(value)) {
return "В 1С эта MCP-проверка не подтвердила подходящих metadata-объектов.";
}
if (/^Legal registration date is not proven by this MCP discovery pilot$/i.test(value)) {
return "Юридическая дата регистрации этим поиском не подтверждена.";
}
@ -150,10 +225,22 @@ function localizeLine(value: string): string {
return "Полное покрытие запрошенного периода по двустороннему денежному потоку не подтверждено: хотя бы одна сторона проверки достигла лимита найденных строк.";
}
if (/^Full turnover outside the checked period is not proven by this MCP discovery pilot$/i.test(value)) {
return "Полный оборот вне проверенного периода этим поиском не подтвержден.";
return "Полный объем входящих поступлений вне проверенного периода этим поиском не подтвержден.";
}
if (/^Full all-time turnover is not proven without an explicit checked period$/i.test(value)) {
return "Полный оборот за все время без явно проверенного периода не подтвержден.";
return "Полный объем входящих поступлений за все время без явно проверенного периода не подтвержден.";
}
if (/^Full document history outside the checked period is not proven by this MCP discovery pilot$/i.test(value)) {
return "Полный исторический срез документов вне проверенного периода этим поиском не подтвержден.";
}
if (/^Full document history is not proven without an explicit checked period$/i.test(value)) {
return "Полный срез документов без явно проверенного периода не подтвержден.";
}
if (/^Full movement history outside the checked period is not proven by this MCP discovery pilot$/i.test(value)) {
return "Полный исторический срез движений вне проверенного периода этим поиском не подтвержден.";
}
if (/^Full movement history is not proven without an explicit checked period$/i.test(value)) {
return "Полный срез движений без явно проверенного периода не подтвержден.";
}
if (/^Full supplier-payout amount outside the checked period is not proven by this MCP discovery pilot$/i.test(value)) {
return "Полный объем исходящих платежей вне проверенного периода этим поиском не подтвержден.";
@ -167,6 +254,20 @@ function localizeLine(value: string): string {
if (/^Full all-time bidirectional value-flow is not proven without an explicit checked period$/i.test(value)) {
return "Полный двусторонний денежный поток за все время без явно проверенного периода не подтвержден.";
}
if (
/^Requested period coverage was recovered through monthly 1C value-flow probes after the broad probe hit the row limit$/i.test(
value
)
) {
return "Покрытие запрошенного периода восстановлено помесячными проверками 1С после того, как общая выборка уперлась в лимит строк.";
}
if (
/^Requested period coverage for bidirectional value-flow was recovered through monthly 1C side probes after a broad probe hit the row limit$/i.test(
value
)
) {
return "Покрытие запрошенного периода по двустороннему денежному потоку восстановлено помесячными проверками 1С после того, как общая выборка уперлась в лимит строк хотя бы по одной стороне.";
}
return value;
}

View File

@ -13,6 +13,7 @@ export type AssistantMcpDiscoveryResponsePolicyDecision = "apply_candidate" | "k
export interface ApplyAssistantMcpDiscoveryResponsePolicyInput {
currentReply: string;
currentReplySource?: string | null;
currentReplyType?: string | null;
livingChatSource?: string | null;
modeDecisionReason?: string | null;
addressRuntimeMeta?: Record<string, unknown> | null;
@ -110,6 +111,13 @@ function isUnsupportedCurrentTurnBoundary(input: ApplyAssistantMcpDiscoveryRespo
);
}
function isDeterministicBroadBusinessEvaluationReply(input: ApplyAssistantMcpDiscoveryResponsePolicyInput): boolean {
return (
input.livingChatSource === "deterministic_broad_business_evaluation_contract" ||
input.currentReplySource === "deterministic_broad_business_evaluation_contract"
);
}
function isDiscoveryReadyChatCandidate(
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
@ -151,6 +159,204 @@ function isDiscoveryReadyAddressCandidate(
);
}
function isDetectedIntentAlignedWithTurnMeaning(
detectedIntent: string | null,
turnMeaning: Record<string, unknown> | null
): boolean {
const normalizedIntent = String(detectedIntent ?? "").trim().toLowerCase();
if (!normalizedIntent) {
return false;
}
const askedDomain = String(toNonEmptyString(turnMeaning?.asked_domain_family) ?? "").trim().toLowerCase();
const askedAction = String(toNonEmptyString(turnMeaning?.asked_action_family) ?? "").trim().toLowerCase();
if (normalizedIntent === "counterparty_activity_lifecycle") {
return (
askedDomain === "counterparty_lifecycle" ||
askedAction === "activity_duration" ||
askedAction === "age_or_activity_duration"
);
}
if (normalizedIntent === "supplier_payouts_profile") {
return askedDomain === "counterparty_value" && askedAction === "payout";
}
if (normalizedIntent === "customer_revenue_and_payments") {
return askedDomain === "counterparty_value" && (askedAction === "turnover" || askedAction === "counterparty_value_or_turnover");
}
if (normalizedIntent === "receivables_confirmed_as_of_date") {
return askedDomain === "receivables" || askedAction === "confirmed_snapshot";
}
if (normalizedIntent === "payables_confirmed_as_of_date") {
return askedDomain === "payables" || askedAction === "confirmed_snapshot";
}
if (normalizedIntent === "vat_liability_confirmed_for_tax_period") {
return askedDomain === "vat" && askedAction === "confirmed_tax_period";
}
if (normalizedIntent === "vat_payable_confirmed_as_of_date") {
return askedDomain === "vat" && askedAction === "confirmed_snapshot";
}
if (normalizedIntent === "vat_payable_forecast") {
return askedDomain === "vat" && askedAction === "forecast";
}
if (normalizedIntent === "list_documents_by_counterparty") {
return askedAction === "list_documents" || askedDomain === "counterparty_documents" || askedDomain === "counterparty";
}
if (normalizedIntent === "inventory_on_hand_as_of_date" || normalizedIntent === "inventory_aging_by_purchase_date") {
return askedDomain === "inventory" && askedAction === "confirmed_snapshot";
}
return false;
}
function readDiscoveryTurnMeaning(
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
): Record<string, unknown> | null {
const turnInput = toRecordObject(entryPoint?.turn_input);
return toRecordObject(turnInput?.turn_meaning_ref);
}
function readTruthAnswerShape(input: ApplyAssistantMcpDiscoveryResponsePolicyInput): Record<string, unknown> | null {
const directShape = toRecordObject(input.addressRuntimeMeta?.answer_shape_contract);
if (directShape) {
return directShape;
}
const truthAnswerPolicy = toRecordObject(input.addressRuntimeMeta?.assistant_truth_answer_policy_v1);
return toRecordObject(truthAnswerPolicy?.answer_shape);
}
function hasEffectivelyFactualAddressReply(input: ApplyAssistantMcpDiscoveryResponsePolicyInput): boolean {
if (toNonEmptyString(input.currentReplyType) === "factual") {
return true;
}
const truthAnswerShape = readTruthAnswerShape(input);
return toNonEmptyString(truthAnswerShape?.reply_type) === "factual";
}
function readStateTransitionReasonCodes(input: ApplyAssistantMcpDiscoveryResponsePolicyInput): string[] {
const directTransition = toRecordObject(input.addressRuntimeMeta?.assistant_state_transition_v1);
const fallbackTransition = toRecordObject(input.addressRuntimeMeta?.state_transition_contract);
const stateTransition = directTransition ?? fallbackTransition;
if (!stateTransition || !Array.isArray(stateTransition.reason_codes)) {
return [];
}
return stateTransition.reason_codes
.map((item) => toNonEmptyString(item))
.filter((item): item is string => Boolean(item));
}
function hasRuntimeAdjustedExactReply(
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
): boolean {
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
return false;
}
if (!hasEffectivelyFactualAddressReply(input)) {
return false;
}
const truthGateStatus = toNonEmptyString(input.addressRuntimeMeta?.truth_gate_contract_status);
const truthAnswerPolicy = toRecordObject(input.addressRuntimeMeta?.assistant_truth_answer_policy_v1);
const truthGate = toRecordObject(truthAnswerPolicy?.truth_gate);
const sourceTruthGateStatus = toNonEmptyString(truthGate?.source_truth_gate_status);
const coverageStatus = toNonEmptyString(truthGate?.coverage_status);
const groundingStatus = toNonEmptyString(truthGate?.grounding_status);
const hasFullConfirmedTruth =
truthGateStatus === "full_confirmed" ||
sourceTruthGateStatus === "full_confirmed" ||
(coverageStatus === "full" && groundingStatus === "grounded");
if (!hasFullConfirmedTruth) {
return false;
}
const truthAnswerShape = readTruthAnswerShape(input);
const capabilityContractId = toNonEmptyString(truthAnswerShape?.capability_contract_id);
if (!capabilityContractId) {
return false;
}
return readStateTransitionReasonCodes(input).some(
(reason) => /^intent_adjusted_to_.+_followup_context$/i.test(reason)
);
}
function hasAlignedFactualAddressReply(
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
): boolean {
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
return false;
}
if (!hasEffectivelyFactualAddressReply(input)) {
return false;
}
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
return isDetectedIntentAlignedWithTurnMeaning(detectedIntent, readDiscoveryTurnMeaning(entryPoint));
}
function hasSemanticConflictWithDiscoveryTurnMeaning(
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
): boolean {
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
return false;
}
if (!hasEffectivelyFactualAddressReply(input)) {
return false;
}
if (hasRuntimeAdjustedExactReply(input, entryPoint)) {
return false;
}
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
const turnMeaning = readDiscoveryTurnMeaning(entryPoint);
const askedDomain = toNonEmptyString(turnMeaning?.asked_domain_family);
const askedAction = toNonEmptyString(turnMeaning?.asked_action_family);
const unsupportedFamily = toNonEmptyString(turnMeaning?.unsupported_but_understood_family);
if (!detectedIntent || (!askedDomain && !askedAction && !unsupportedFamily)) {
return false;
}
return !isDetectedIntentAlignedWithTurnMeaning(detectedIntent, turnMeaning);
}
function hasMatchedFactualAddressContinuationTarget(
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
): boolean {
if (!hasEffectivelyFactualAddressReply(input)) {
return false;
}
if (hasSemanticConflictWithDiscoveryTurnMeaning(input, entryPoint)) {
return false;
}
const detectedIntent = toNonEmptyString(input.addressRuntimeMeta?.detected_intent);
const dialogContinuationContract =
toRecordObject(input.addressRuntimeMeta?.dialogContinuationContract) ??
toRecordObject(input.addressRuntimeMeta?.dialog_continuation_contract_v2);
const targetIntent = toNonEmptyString(dialogContinuationContract?.target_intent);
return Boolean(detectedIntent && targetIntent && detectedIntent === targetIntent);
}
function hasFullConfirmedFactualAddressReply(
input: ApplyAssistantMcpDiscoveryResponsePolicyInput,
entryPoint: AssistantMcpDiscoveryRuntimeEntryPointContract | null
): boolean {
if (!isDiscoveryReadyAddressCandidate(input, entryPoint)) {
return false;
}
if (!hasEffectivelyFactualAddressReply(input)) {
return false;
}
if (hasSemanticConflictWithDiscoveryTurnMeaning(input, entryPoint)) {
return false;
}
const truthGateStatus = toNonEmptyString(input.addressRuntimeMeta?.truth_gate_contract_status);
if (truthGateStatus === "full_confirmed") {
return true;
}
const truthAnswerPolicy = toRecordObject(input.addressRuntimeMeta?.assistant_truth_answer_policy_v1);
const truthGate = toRecordObject(truthAnswerPolicy?.truth_gate);
const sourceTruthGateStatus = toNonEmptyString(truthGate?.source_truth_gate_status);
const coverageStatus = toNonEmptyString(truthGate?.coverage_status);
const groundingStatus = toNonEmptyString(truthGate?.grounding_status);
return sourceTruthGateStatus === "full_confirmed" || (coverageStatus === "full" && groundingStatus === "grounded");
}
export function applyAssistantMcpDiscoveryResponsePolicy(
input: ApplyAssistantMcpDiscoveryResponsePolicyInput
): AssistantMcpDiscoveryResponsePolicyResult {
@ -161,9 +367,15 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
const candidate = buildAssistantMcpDiscoveryResponseCandidate(entryPoint);
const reasonCodes = [...candidate.reason_codes];
const unsupportedBoundary = isUnsupportedCurrentTurnBoundary(input);
const deterministicBroadBusinessEvaluationReply = isDeterministicBroadBusinessEvaluationReply(input);
const discoveryReadyChatCandidate = isDiscoveryReadyChatCandidate(input, entryPoint);
const discoveryReadyDeepCandidate = isDiscoveryReadyDeepCandidate(input, entryPoint);
const discoveryReadyAddressCandidate = isDiscoveryReadyAddressCandidate(input, entryPoint);
const alignedFactualAddressReply = hasAlignedFactualAddressReply(input, entryPoint);
const semanticConflictWithDiscoveryTurnMeaning = hasSemanticConflictWithDiscoveryTurnMeaning(input, entryPoint);
const matchedFactualAddressContinuationTarget = hasMatchedFactualAddressContinuationTarget(input, entryPoint);
const fullConfirmedFactualAddressReply = hasFullConfirmedFactualAddressReply(input, entryPoint);
const runtimeAdjustedExactReply = hasRuntimeAdjustedExactReply(input, entryPoint);
if (!entryPoint) {
pushReason(reasonCodes, "mcp_discovery_response_policy_no_entry_point");
@ -180,6 +392,30 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
if (!discoveryReadyAddressCandidate) {
pushReason(reasonCodes, "mcp_discovery_response_policy_not_discovery_ready_address_candidate");
}
if (alignedFactualAddressReply) {
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_aligned_factual_address_reply");
}
if (semanticConflictWithDiscoveryTurnMeaning) {
pushReason(reasonCodes, "mcp_discovery_response_policy_semantic_conflict_allows_candidate_override");
}
if (matchedFactualAddressContinuationTarget) {
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_factual_address_continuation_target");
}
if (fullConfirmedFactualAddressReply) {
pushReason(reasonCodes, "mcp_discovery_response_policy_keep_full_confirmed_factual_address_reply");
}
if (runtimeAdjustedExactReply) {
pushReason(
reasonCodes,
"mcp_discovery_response_policy_keep_runtime_adjusted_exact_reply_over_stale_discovery_turn_meaning"
);
}
if (deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") {
pushReason(
reasonCodes,
"mcp_discovery_response_policy_keep_broad_business_summary_over_clarification_candidate"
);
}
if (!ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status)) {
pushReason(reasonCodes, "mcp_discovery_response_policy_candidate_status_not_allowed");
}
@ -196,6 +432,11 @@ export function applyAssistantMcpDiscoveryResponsePolicy(
const canApply =
Boolean(entryPoint) &&
(unsupportedBoundary || discoveryReadyChatCandidate || discoveryReadyDeepCandidate || discoveryReadyAddressCandidate) &&
!alignedFactualAddressReply &&
!matchedFactualAddressContinuationTarget &&
!fullConfirmedFactualAddressReply &&
!runtimeAdjustedExactReply &&
!(deterministicBroadBusinessEvaluationReply && candidate.candidate_status === "clarification_candidate") &&
ALLOWED_CANDIDATE_STATUSES.has(candidate.candidate_status) &&
candidate.eligible_for_future_hot_runtime &&
Boolean(toNonEmptyString(candidate.reply_text)) &&

View File

@ -2,6 +2,7 @@
import {
isGroundedAddressDebug,
readAssistantMcpDiscoveryPilotScope,
resolveAddressDebugContextFacts,
resolveAssistantContinuitySnapshot
} from "./assistantContinuityPolicy";
@ -54,6 +55,130 @@ function toNonEmptyString(value: unknown): string | null {
return text.length > 0 ? text : null;
}
function toRecordObject(value: unknown): Record<string, unknown> | null {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return null;
}
return value as Record<string, unknown>;
}
function ensureSentence(value: string): string {
const text = String(value ?? "").trim();
if (!text) {
return "";
}
return /[.!?]$/.test(text) ? text : `${text}.`;
}
function periodPartForRecap(scopedDate: string | null): string {
if (!scopedDate) {
return "";
}
return /^\d{2}\.\d{2}\.\d{4}$/.test(scopedDate) ? ` на ${scopedDate}` : ` за период ${scopedDate}`;
}
function readDiscoveryMetadataScope(debug: Record<string, unknown> | null): string | null {
const discoveryEntry = toRecordObject(debug?.assistant_mcp_discovery_entry_point_v1);
const bridge = toRecordObject(discoveryEntry?.bridge);
const pilot = toRecordObject(bridge?.pilot);
const surface = toRecordObject(pilot?.derived_metadata_surface);
const surfaceScope = toNonEmptyString(surface?.metadata_scope);
if (surfaceScope) {
return surfaceScope;
}
const turnInput = toRecordObject(discoveryEntry?.turn_input);
const turnMeaningRef = toRecordObject(turnInput?.turn_meaning_ref);
const entityCandidates = Array.isArray(turnMeaningRef?.explicit_entity_candidates)
? turnMeaningRef.explicit_entity_candidates
: [];
for (const candidate of entityCandidates) {
const text = toNonEmptyString(candidate);
if (text) {
return text;
}
}
return null;
}
function buildDiscoveryRecapFactLine(input: {
debug: Record<string, unknown> | null;
counterparty: string | null;
scopedDate: string | null;
}): string | null {
if (!input.debug) {
return null;
}
const pilotScope = readAssistantMcpDiscoveryPilotScope(input.debug, toNonEmptyString);
const discoveryEntry = toRecordObject(input.debug.assistant_mcp_discovery_entry_point_v1);
const bridge = toRecordObject(discoveryEntry?.bridge);
const pilot = toRecordObject(bridge?.pilot);
const periodPart = periodPartForRecap(input.scopedDate);
if (pilotScope === "metadata_inspection_v1") {
const metadataScope = readDiscoveryMetadataScope(input.debug);
const surface = toRecordObject(pilot?.derived_metadata_surface);
const entitySets = Array.isArray(surface?.available_entity_sets)
? surface.available_entity_sets
.map((item) => toNonEmptyString(item))
.filter((item): item is string => Boolean(item))
: [];
const fields = Array.isArray(surface?.available_fields)
? surface.available_fields
.map((item) => toNonEmptyString(item))
.filter((item): item is string => Boolean(item))
: [];
const objects = Array.isArray(surface?.matched_objects)
? surface.matched_objects
.map((item) => toNonEmptyString(item))
.filter((item): item is string => Boolean(item))
: [];
const rows = Number(surface?.matched_rows ?? 0);
const scopePart = metadataScope ? ` по области «${metadataScope}»` : "";
const objectsPart = objects.length > 0 ? `, нашли объекты ${objects.slice(0, 4).join(", ")}` : "";
const entitySetsPart =
entitySets.length > 0 ? `, видны типы ${entitySets.slice(0, 4).join(", ")}` : "";
const fieldsPart =
fields.length > 0 ? `, доступны поля/секции ${fields.slice(0, 5).join(", ")}` : "";
return `смотрели metadata-поверхность 1С${scopePart}${periodPart}: ${rows} подтвержденных строк${objectsPart}${entitySetsPart}${fieldsPart}`.trim();
}
if (!input.counterparty) {
return null;
}
if (pilotScope === "counterparty_lifecycle_query_documents_v1") {
const activityPeriod = toRecordObject(pilot?.derived_activity_period);
const duration = toNonEmptyString(activityPeriod?.duration_human_ru);
return duration
? `смотрели подтвержденную активность по контрагенту «${input.counterparty}»${periodPart} и оценили период взаимодействия примерно как ${duration}`
: `смотрели подтвержденную активность по контрагенту «${input.counterparty}»${periodPart}`;
}
if (pilotScope === "counterparty_supplier_payout_query_movements_v1") {
const flow = toRecordObject(pilot?.derived_value_flow);
const amount = toNonEmptyString(flow?.total_amount_human_ru);
return amount
? `считали исходящие платежи/списания по контрагенту «${input.counterparty}»${periodPart}: ${amount}`
: `считали исходящие платежи/списания по контрагенту «${input.counterparty}»${periodPart}`;
}
if (pilotScope === "counterparty_value_flow_query_movements_v1") {
const flow = toRecordObject(pilot?.derived_value_flow);
const amount = toNonEmptyString(flow?.total_amount_human_ru);
return amount
? `смотрели денежный поток по контрагенту «${input.counterparty}»${periodPart}: ${amount}`
: `смотрели денежный поток по контрагенту «${input.counterparty}»${periodPart}`;
}
if (pilotScope === "counterparty_bidirectional_value_flow_query_movements_v1") {
const flow = toRecordObject(pilot?.derived_bidirectional_value_flow);
const incoming = toRecordObject(flow?.incoming_customer_revenue);
const outgoing = toRecordObject(flow?.outgoing_supplier_payout);
const incomingAmount = toNonEmptyString(incoming?.total_amount_human_ru);
const outgoingAmount = toNonEmptyString(outgoing?.total_amount_human_ru);
const netAmount = toNonEmptyString(flow?.net_amount_human_ru);
if (incomingAmount && outgoingAmount && netAmount) {
return `считали нетто по деньгам с контрагентом «${input.counterparty}»${periodPart}: получили ${incomingAmount}, заплатили ${outgoingAmount}, расчетное нетто ${netAmount}`;
}
return `считали нетто по деньгам с контрагентом «${input.counterparty}»${periodPart}`;
}
return null;
}
function collectMessageSamples(input: ResolveAssistantRouteMemorySignalsInput): string[] {
const values = [
input.rawUserMessage,
@ -120,11 +245,21 @@ function normalizeRecapIdentity(value: unknown): string {
function buildRecapFactLine(input: {
debug: Record<string, unknown> | null;
item: string | null;
counterparty: string | null;
organization: string | null;
}): string | null {
const detectedIntent = String(input.debug?.detected_intent ?? "");
const scopedDate = resolveAddressDebugContextFacts(input.debug).scopedDate;
const discoveryFact = buildDiscoveryRecapFactLine({
debug: input.debug,
counterparty: input.counterparty,
scopedDate
});
if (discoveryFact) {
return discoveryFact;
}
const itemPart = input.item ? `по позиции «${input.item}»` : null;
const counterpartyPart = input.counterparty ? `по контрагенту «${input.counterparty}»` : null;
const organizationPart = input.organization ? `по компании «${input.organization}»` : null;
const datePart = scopedDate ? ` на ${scopedDate}` : "";
if (detectedIntent === "inventory_on_hand_as_of_date") {
@ -151,6 +286,9 @@ function buildRecapFactLine(input: {
if (detectedIntent === "counterparty_activity_lifecycle" && organizationPart) {
return `смотрели активность в базе 1С ${organizationPart}`.trim();
}
if (detectedIntent === "list_documents_by_counterparty" && counterpartyPart) {
return `поднимали документы ${counterpartyPart}${datePart}`.trim();
}
if (detectedIntent === "list_documents_by_counterparty" && organizationPart) {
return `поднимали документы ${organizationPart}${datePart}`.trim();
}
@ -196,6 +334,7 @@ function collectRecentRecapFacts(input: {
const fact = buildRecapFactLine({
debug: item.debug,
item: debugItem,
counterparty: debugContext.counterparty,
organization: debugOrganization
});
if (!fact || seen.has(fact)) {
@ -219,6 +358,7 @@ export function buildAddressMemoryRecapReply(input: {
}): string {
const contextFacts = resolveAddressDebugContextFacts(input.addressDebug, input.toNonEmptyString);
const item = contextFacts.item;
const counterparty = contextFacts.counterparty;
const organization = input.organization ?? contextFacts.organization;
const scopedDate = contextFacts.scopedDate;
const recapFacts = collectRecentRecapFacts({
@ -246,6 +386,22 @@ export function buildAddressMemoryRecapReply(input: {
].join(" ");
}
if (counterparty) {
const organizationPart = organization ? ` по компании «${organization}»` : "";
const periodPart = periodPartForRecap(scopedDate);
if (recapFacts.length > 0) {
return [
`Да, помню. По контрагенту «${counterparty}»${organizationPart}${periodPart} мы уже выяснили:`,
...recapFacts.map((fact) => `- ${fact}.`),
"Могу сразу продолжить по нему: поступления, платежи, нетто, помесячную раскладку или границы подтверждения."
].join("\n");
}
return [
`Да, помню. Мы уже смотрели контур по контрагенту «${counterparty}»${organizationPart}${periodPart}.`,
"Могу продолжить по нему без переписывания контекста: поступления, платежи, нетто, документы или пояснение границ ответа."
].join(" ");
}
if (organization || scopedDate) {
const organizationPart = organization ? ` по компании «${organization}»` : "";
const datePart = scopedDate ? ` на ${scopedDate}` : "";
@ -258,13 +414,77 @@ export function buildAddressMemoryRecapReply(input: {
return "Да, помню предыдущий адресный контур. Могу кратко напомнить, что мы уже подтвердили, или сразу продолжить следующий шаг.";
}
export function buildBroadBusinessEvaluationReply(input: {
organization: string | null;
addressDebug: Record<string, unknown> | null;
sessionItems?: unknown[];
toNonEmptyString: (value: unknown) => string | null;
}): string {
const contextFacts = resolveAddressDebugContextFacts(input.addressDebug, input.toNonEmptyString);
const organization = input.organization ?? contextFacts.organization;
const recapFacts = collectRecentRecapFacts({
sessionItems: input.sessionItems,
item: null,
organization,
toNonEmptyString: input.toNonEmptyString
});
const organizationPart = organization ? ` по компании «${organization}»` : "";
if (recapFacts.length > 0) {
return [
`Коротко: по тому, что мы уже подтвердили в 1С${organizationPart}, компания выглядит операционно живой, но это пока только частичная оценка бизнеса.`,
"Сейчас я опираюсь на такие подтвержденные факты:",
...recapFacts.map((fact) => `- ${ensureSentence(fact)}`),
"Это еще не полная диагностика всего бизнеса и не вывод о прибыли: я честно суммирую только те контуры, которые мы уже проверили в диалоге.",
"Если хочешь, следующим шагом могу сузить оценку до денежного потока, долгов, НДС или ключевых контрагентов."
].join("\n");
}
return [
`Коротко: по нынешнему контексту 1С${organizationPart} я вижу признаки операционной активности, но для содержательной оценки бизнеса нужно еще несколько опорных срезов.`,
"Если хочешь, я быстро доберу основу для такой оценки: денежный поток, дебиторка/кредиторка, НДС или ключевые контрагенты."
].join(" ");
}
export function buildSelectedObjectAnswerInspectionReply(input: {
addressDebug: Record<string, unknown> | null;
toNonEmptyString: (value: unknown) => string | null;
}): string {
const contextFacts = resolveAddressDebugContextFacts(input.addressDebug, input.toNonEmptyString);
const itemLabel = contextFacts.item ?? "эта позиция";
const counterpartyLabel = contextFacts.counterparty;
const detectedIntent = String(input.addressDebug?.detected_intent ?? "");
const pilotScope = readAssistantMcpDiscoveryPilotScope(input.addressDebug, input.toNonEmptyString);
const periodPart = periodPartForRecap(contextFacts.scopedDate);
if (counterpartyLabel && pilotScope === "counterparty_bidirectional_value_flow_query_movements_v1") {
return [
`Да, в предыдущем ответе речь шла о двустороннем денежном потоке с контрагентом «${counterpartyLabel}»${periodPart}.`,
"Нетто там означало разницу между тем, что получили, и тем, что заплатили по найденным строкам 1С.",
"Это расчет по проверенному периоду и подтвержденным строкам, а не заявление про весь оборот вне этого окна."
].join(" ");
}
if (counterpartyLabel && pilotScope === "counterparty_supplier_payout_query_movements_v1") {
return [
`Да, в предыдущем ответе речь шла об исходящих платежах/списаниях по контрагенту «${counterpartyLabel}»${periodPart}.`,
"Это сумма по найденным строкам 1С за проверенный период, а не обещание, что за пределами этого окна больше движений не было."
].join(" ");
}
if (counterpartyLabel && pilotScope === "counterparty_value_flow_query_movements_v1") {
return [
`Да, в предыдущем ответе речь шла о денежном потоке по контрагенту «${counterpartyLabel}»${periodPart}.`,
"Это расчет по найденным движениям 1С за проверенный период, а не безусловный итог по всем временам."
].join(" ");
}
if (counterpartyLabel && pilotScope === "counterparty_lifecycle_query_documents_v1") {
return [
`Да, в предыдущем ответе речь шла об активности контрагента «${counterpartyLabel}»${periodPart}.`,
"Это оценка по подтвержденным строкам 1С, а не юридически подтвержденная дата регистрации."
].join(" ");
}
if (detectedIntent === "inventory_sale_trace_for_item") {
return [

View File

@ -2563,6 +2563,12 @@ function hasAddressFollowupContextSignal(userMessage) {
if (ultraShortFollowup && hasAny(/^(?:давай|показывай|показывыай|ещ[её]|also|again|go|ok|okay)(?=$|[\s,.;:!?])/iu)) {
return true;
}
const shortValueFlowRetargetCue = shortFollowup &&
(hasMarker() || hasPointer() || hasAny(/^(?:Р°|a|Рё|i|also|then|now)(?=$|[\s,.;:!?])/iu)) &&
hasAny(/(?:РЅРµССРѕ|сальдо|разниС|полуСРёР»|заплаСРёР»|РїРѕСЃССѓРї|РІСРѕРґСЏС|РёСЃСРѕРґСЏС|РѕР±РѕСЂРѕС|РІССЂСѓСРє|денеР)/iu);
if (shortValueFlowRetargetCue) {
return true;
}
if (hasStandaloneAddressTopicSignal(rawText || repairedText)) {
return false;
}
@ -3822,11 +3828,7 @@ function findLastGroundedAddressAnswerDebug(items) {
continue;
}
const debug = item.debug;
if (debug.execution_lane !== "address_query") {
continue;
}
const groundingStatus = toNonEmptyString(debug.answer_grounding_check?.status);
if (groundingStatus === "grounded") {
if ((0, assistantContinuityPolicy_1.isGroundedAddressDebug)(debug, toNonEmptyString)) {
return debug;
}
}

View File

@ -7,9 +7,18 @@ import {
buildRootScopedCarryoverFilters,
buildInventoryRootFrameFromAddressDebug,
hydrateInventoryRootFrameState,
readAddressDebugIntent,
readAddressDebugFilters,
readAddressDebugItem,
readAssistantMcpDiscoveryMetadataAmbiguityDetected,
readAssistantMcpDiscoveryMetadataAmbiguityEntitySets,
readAssistantMcpDiscoveryEntityCandidates,
readAssistantMcpDiscoveryEntityAmbiguityCandidates,
readAssistantMcpDiscoveryEntityResolutionStatus,
readAssistantMcpDiscoveryMetadataRouteFamily,
readAssistantMcpDiscoveryMetadataSelectedEntitySet,
readAddressDebugTemporalScope,
readAssistantMcpDiscoveryPilotScope,
resolveOrganizationClarificationContinuation,
resolveNavigationSessionContextState,
resolveAddressDebugCarryoverFilters,
@ -171,19 +180,23 @@ export function createAssistantTransitionPolicy(deps) {
return false;
}
const executionLane = deps.toNonEmptyString(debug.execution_lane);
const detectedIntent = deps.toNonEmptyString(debug.detected_intent);
const detectedIntent = readAddressDebugIntent(debug, deps.toNonEmptyString);
const selectedRecipe = deps.toNonEmptyString(debug.selected_recipe);
const answerGroundingCheck =
debug.answer_grounding_check && typeof debug.answer_grounding_check === "object"
? debug.answer_grounding_check
: null;
const groundingStatus = deps.toNonEmptyString(answerGroundingCheck?.status);
const discoveryPilotScope = readAssistantMcpDiscoveryPilotScope(debug, deps.toNonEmptyString);
if (groundingStatus === "grounded") {
return true;
}
if (selectedRecipe) {
return true;
}
if (debug.mcp_discovery_response_applied === true && discoveryPilotScope) {
return true;
}
return executionLane === "address_query" && Boolean(detectedIntent && detectedIntent !== "unknown");
}
@ -310,6 +323,27 @@ export function createAssistantTransitionPolicy(deps) {
return sameDatePhrases.some((phrase) => normalized.includes(phrase));
}
function hasShortValueFlowRetargetCue(text) {
const normalized = normalizeFollowupText(text);
if (!normalized) {
return false;
}
const tokenCount = deps.countTokens(normalized);
if (!Number.isFinite(tokenCount) || tokenCount > 8) {
return false;
}
const hasLeadCue =
deps.hasFollowupMarker(text) ||
deps.hasReferentialPointer(text) ||
/^(?:\u0430|\u0438|also|then|now)(?=$|[\s,.;:!?])/iu.test(normalized);
if (!hasLeadCue) {
return false;
}
return /(?:\u043d\u0435\u0442\u0442\u043e|\u0441\u0430\u043b\u044c\u0434\u043e|\u0440\u0430\u0437\u043d\u0438\u0446|\u043f\u043e\u043b\u0443\u0447|\u0437\u0430\u043f\u043b\u0430\u0442|\u043f\u043e\u0441\u0442\u0443\u043f|\u0432\u0445\u043e\u0434\u044f\u0449|\u0438\u0441\u0445\u043e\u0434\u044f\u0449|\u043e\u0431\u043e\u0440\u043e\u0442|\u0432\u044b\u0440\u0443\u0447\u043a|\u0434\u0435\u043d\u0435\u0436)/iu.test(
normalized
);
}
function shouldKeepPreviousIntentForShortCounterpartyRetarget(userMessage, sourceIntent) {
const normalized = deps.compactWhitespace(
deps.repairAddressMojibake(String(userMessage ?? "")).toLowerCase()
@ -438,7 +472,16 @@ export function createAssistantTransitionPolicy(deps) {
(deps.toNonEmptyString(alternateMessage)
? deps.isImplicitAddressContinuationByLlm(alternateMessage, llmPreDecomposeMeta)
: false));
const sourceIntentHint = deps.toNonEmptyString(carryoverSourceDebug?.detected_intent);
const sourceIntentHint = readAddressDebugIntent(carryoverSourceDebug, deps.toNonEmptyString);
const sourceDiscoveryPilotScopeHint = readAssistantMcpDiscoveryPilotScope(
carryoverSourceDebug,
deps.toNonEmptyString
);
const hasValueFlowCarryoverSourceHint =
sourceIntentHint === "customer_revenue_and_payments" ||
sourceDiscoveryPilotScopeHint === "counterparty_value_flow_query_movements_v1" ||
sourceDiscoveryPilotScopeHint === "counterparty_supplier_payout_query_movements_v1" ||
sourceDiscoveryPilotScopeHint === "counterparty_bidirectional_value_flow_query_movements_v1";
const navigationSessionState = resolveNavigationSessionContextState(
addressNavigationState,
deps.toNonEmptyString,
@ -474,14 +517,22 @@ export function createAssistantTransitionPolicy(deps) {
? deps.resolveDebtRoleSwapFollowupIntent(String(alternateMessage ?? ""), sourceIntentHint)
: null;
const debtRoleSwapIntent = debtRoleSwapPrimary ?? debtRoleSwapAlternate ?? null;
const shortValueFlowRetargetPrimary =
hasValueFlowCarryoverSourceHint && hasShortValueFlowRetargetCue(userMessage);
const shortValueFlowRetargetAlternate =
hasValueFlowCarryoverSourceHint && deps.toNonEmptyString(alternateMessage)
? hasShortValueFlowRetargetCue(String(alternateMessage ?? ""))
: false;
let hasPrimaryFollowupSignal =
deps.hasAddressFollowupContextSignal(userMessage) ||
Boolean(debtRoleSwapPrimary) ||
shortValueFlowRetargetPrimary ||
inventoryShortFollowupPrimary ||
inventoryPurchaseDateVatBridge;
let hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage)
? deps.hasAddressFollowupContextSignal(alternateMessage) ||
Boolean(debtRoleSwapAlternate) ||
shortValueFlowRetargetAlternate ||
inventoryShortFollowupAlternate ||
inventoryPurchaseDateVatBridge
: false;
@ -532,6 +583,8 @@ export function createAssistantTransitionPolicy(deps) {
hasInventoryRootRestatementAlternate ||
inventoryPurchaseDateVatBridge ||
Boolean(debtRoleSwapIntent) ||
shortValueFlowRetargetPrimary ||
shortValueFlowRetargetAlternate ||
deps.hasFollowupMarker(userMessage) ||
deps.hasReferentialPointer(userMessage) ||
(deps.toNonEmptyString(alternateMessage)
@ -550,6 +603,8 @@ export function createAssistantTransitionPolicy(deps) {
hasInventoryRootRestatementAlternate ||
inventoryPurchaseDateVatBridge ||
Boolean(debtRoleSwapIntent) ||
shortValueFlowRetargetPrimary ||
shortValueFlowRetargetAlternate ||
deps.hasFollowupMarker(userMessage) ||
deps.hasReferentialPointer(userMessage) ||
(deps.toNonEmptyString(alternateMessage)
@ -577,6 +632,8 @@ export function createAssistantTransitionPolicy(deps) {
!hasInventoryRootTemporalFollowupAlternate &&
!hasInventoryRootRestatementPrimary &&
!hasInventoryRootRestatementAlternate &&
!shortValueFlowRetargetPrimary &&
!shortValueFlowRetargetAlternate &&
!hasImplicitContinuationSignal &&
!hasOrganizationClarificationContinuation &&
!hasIndexReferenceSignal
@ -590,6 +647,8 @@ export function createAssistantTransitionPolicy(deps) {
!hasInventoryRootTemporalFollowupAlternate &&
!hasInventoryRootRestatementPrimary &&
!hasInventoryRootRestatementAlternate &&
!shortValueFlowRetargetPrimary &&
!shortValueFlowRetargetAlternate &&
!hasImplicitContinuationSignal &&
!hasOrganizationClarificationContinuation &&
!hasIndexReferenceSignal
@ -599,7 +658,35 @@ export function createAssistantTransitionPolicy(deps) {
if (!carryoverSourceDebug) {
return null;
}
const sourceIntent = deps.toNonEmptyString(carryoverSourceDebug.detected_intent);
const sourceIntent = readAddressDebugIntent(carryoverSourceDebug, deps.toNonEmptyString);
const sourceDiscoveryPilotScope = sourceDiscoveryPilotScopeHint;
const sourceDiscoveryMetadataRouteFamily = readAssistantMcpDiscoveryMetadataRouteFamily(
carryoverSourceDebug,
deps.toNonEmptyString
);
const sourceDiscoveryMetadataSelectedEntitySet = readAssistantMcpDiscoveryMetadataSelectedEntitySet(
carryoverSourceDebug,
deps.toNonEmptyString
);
const sourceDiscoveryMetadataAmbiguityDetected = readAssistantMcpDiscoveryMetadataAmbiguityDetected(
carryoverSourceDebug
);
const sourceDiscoveryMetadataAmbiguityEntitySets = readAssistantMcpDiscoveryMetadataAmbiguityEntitySets(
carryoverSourceDebug,
deps.toNonEmptyString
);
const sourceDiscoveryEntityResolutionStatus = readAssistantMcpDiscoveryEntityResolutionStatus(
carryoverSourceDebug,
deps.toNonEmptyString
);
const sourceDiscoveryEntityCandidates = readAssistantMcpDiscoveryEntityCandidates(
carryoverSourceDebug,
deps.toNonEmptyString
);
const sourceDiscoveryEntityAmbiguityCandidates = readAssistantMcpDiscoveryEntityAmbiguityCandidates(
carryoverSourceDebug,
deps.toNonEmptyString
);
const llmExplicitIntent = deps.toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent);
const llmSelectedObjectScopeDetected =
llmPreDecomposeMeta?.predecomposeContract?.semantics?.selected_object_scope_detected === true;
@ -699,12 +786,14 @@ export function createAssistantTransitionPolicy(deps) {
hasPrimaryFollowupSignal =
deps.hasAddressFollowupContextSignal(userMessage) ||
Boolean(debtRoleSwapPrimary) ||
shortValueFlowRetargetPrimary ||
inventoryShortFollowupPrimary ||
inventoryPurchaseDateVatBridge ||
hasInventoryRootTemporalFollowupPrimary;
hasAlternateFollowupSignal = deps.toNonEmptyString(alternateMessage)
? deps.hasAddressFollowupContextSignal(alternateMessage) ||
Boolean(debtRoleSwapAlternate) ||
shortValueFlowRetargetAlternate ||
inventoryShortFollowupAlternate ||
inventoryPurchaseDateVatBridge ||
hasInventoryRootTemporalFollowupAlternate
@ -720,6 +809,8 @@ export function createAssistantTransitionPolicy(deps) {
hasInventoryRootTemporalFollowupAlternate ||
inventoryPurchaseDateVatBridge ||
Boolean(debtRoleSwapIntent) ||
shortValueFlowRetargetPrimary ||
shortValueFlowRetargetAlternate ||
deps.hasFollowupMarker(userMessage) ||
deps.hasReferentialPointer(userMessage) ||
(deps.toNonEmptyString(alternateMessage)
@ -931,6 +1022,19 @@ export function createAssistantTransitionPolicy(deps) {
previous_filters: previousFilters,
previous_anchor_type: previousAnchorType ?? undefined,
previous_anchor_value: previousAnchor,
previous_discovery_pilot_scope: sourceDiscoveryPilotScope ?? undefined,
previous_discovery_entity_resolution_status: sourceDiscoveryEntityResolutionStatus ?? undefined,
previous_discovery_entity_candidates:
sourceDiscoveryEntityCandidates.length > 0 ? sourceDiscoveryEntityCandidates : undefined,
previous_discovery_entity_ambiguity_candidates:
sourceDiscoveryEntityAmbiguityCandidates.length > 0
? sourceDiscoveryEntityAmbiguityCandidates
: undefined,
previous_discovery_metadata_route_family: sourceDiscoveryMetadataRouteFamily ?? undefined,
previous_discovery_metadata_selected_entity_set: sourceDiscoveryMetadataSelectedEntitySet ?? 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,
root_context_only: rootScopedPivot || undefined,
root_intent: shouldAttachInventoryRootFrame ? inventoryRootFrame?.intent ?? undefined : undefined,

View File

@ -5,7 +5,10 @@ const SUPPORTED_ADDRESS_INTENTS = new Set([
"payables_confirmed_as_of_date",
"list_documents_by_counterparty",
"customer_revenue_and_payments",
"inventory_on_hand_as_of_date"
"inventory_on_hand_as_of_date",
"vat_liability_confirmed_for_tax_period",
"vat_payable_confirmed_as_of_date",
"vat_payable_forecast"
]);
function fallbackCompactWhitespace(value) {
@ -89,8 +92,23 @@ function detectCounterpartyTurnoverFamily(text) {
"\u0434\u043e\u0445\u043e\u0434",
"\u0431\u044b\u043b",
"\u0431\u044b\u043b\u0430",
"\u0432\u0440\u0435\u043c\u044f",
"\u0432\u0440\u0435\u043c\u0435\u043d\u0438",
"\u0433\u043e\u0434",
"\u0433\u043e\u0434\u0430",
"\u043f\u0435\u0440\u0438\u043e\u0434",
"\u043f\u0435\u0440\u0438\u043e\u0434\u0430",
"\u043c\u0435\u0441\u044f\u0446",
"\u043c\u0435\u0441\u044f\u0446\u0430",
"\u043a\u0432\u0430\u0440\u0442\u0430\u043b",
"\u043a\u0432\u0430\u0440\u0442\u0430\u043b\u0430",
"turnover",
"revenue"
"revenue",
"time",
"year",
"period",
"month",
"quarter"
]);
const entity = rawEntity && !ignored.has(rawEntity) ? rawEntity : null;
return {
@ -99,6 +117,23 @@ function detectCounterpartyTurnoverFamily(text) {
};
}
function detectBroadBusinessEvaluation(text) {
const normalized = String(text ?? "");
if (!normalized) {
return null;
}
if (
/(?:как\s+ты\s+оценишь\s+деятельност[ьи]\s+компан|оценк[аи]?\s+деятельност[ьи]\s+компан|что\s+у\s+нас\s+вообще\s+происход|где\s+главн(?:ые|ый)\s+риски|как\s+у\s+нас\s+дела\s+по\s+компан)/iu.test(
normalized
)
) {
return {
family: "broad_business_evaluation"
};
}
return null;
}
function buildEntityCandidates(counterpartyTurnover) {
if (!counterpartyTurnover?.entity) {
return [];
@ -121,10 +156,17 @@ export function createAssistantTurnMeaningPolicy(deps = {}) {
const joinedText = fallbackCompactWhitespace(`${rawText} ${effectiveText}`);
const supportedIntent = detectSupportedIntent(joinedText, deps);
const counterpartyTurnover = detectCounterpartyTurnoverFamily(joinedText);
const broadBusinessEvaluation = detectBroadBusinessEvaluation(joinedText);
const llmIntent = toNonEmptyString(input?.llmPreDecomposeMeta?.predecomposeContract?.intent, deps);
const explicitIntentCandidate =
supportedIntent?.intent ?? (llmIntent && llmIntent !== "unknown" ? llmIntent : null);
const unsupportedFamily = !explicitIntentCandidate && counterpartyTurnover?.family ? counterpartyTurnover.family : null;
broadBusinessEvaluation?.family
? null
: supportedIntent?.intent ?? (llmIntent && llmIntent !== "unknown" ? llmIntent : null);
const unsupportedFamily = broadBusinessEvaluation?.family
? broadBusinessEvaluation.family
: !explicitIntentCandidate && counterpartyTurnover?.family
? counterpartyTurnover.family
: null;
const reasonCodes = [];
if (supportedIntent?.reason) {
reasonCodes.push(supportedIntent.reason);
@ -132,6 +174,9 @@ export function createAssistantTurnMeaningPolicy(deps = {}) {
if (counterpartyTurnover?.family) {
reasonCodes.push("counterparty_turnover_current_turn_signal");
}
if (broadBusinessEvaluation?.family) {
reasonCodes.push("broad_business_evaluation_current_turn_signal");
}
if (rawText !== normalizeTurnText(rawMessage, { ...deps, repairAddressMojibake: (value) => String(value ?? "") })) {
reasonCodes.push("mojibake_repair_applied");
}
@ -146,8 +191,12 @@ export function createAssistantTurnMeaningPolicy(deps = {}) {
? "receivables"
: explicitIntentCandidate?.startsWith("payables_")
? "payables"
: explicitIntentCandidate?.startsWith("vat_")
? "vat"
: explicitIntentCandidate?.startsWith("inventory_")
? "inventory"
: broadBusinessEvaluation?.family
? "business_summary"
: explicitIntentCandidate?.includes("counterparty")
? "counterparty"
: counterpartyTurnover?.family
@ -158,12 +207,22 @@ export function createAssistantTurnMeaningPolicy(deps = {}) {
explicitIntentCandidate === "payables_confirmed_as_of_date" ||
explicitIntentCandidate === "inventory_on_hand_as_of_date"
? "confirmed_snapshot"
: broadBusinessEvaluation?.family
? "broad_evaluation"
: explicitIntentCandidate === "vat_liability_confirmed_for_tax_period"
? "confirmed_tax_period"
: explicitIntentCandidate === "vat_payable_confirmed_as_of_date"
? "confirmed_snapshot"
: explicitIntentCandidate === "vat_payable_forecast"
? "forecast"
: explicitIntentCandidate === "list_documents_by_counterparty"
? "list_documents"
: counterpartyTurnover?.family
? "counterparty_value_or_turnover"
: null;
const staleReplayForbidden = Boolean(unsupportedFamily || (counterpartyTurnover?.entity && !explicitIntentCandidate));
const staleReplayForbidden = Boolean(
unsupportedFamily || broadBusinessEvaluation?.family || (counterpartyTurnover?.entity && !explicitIntentCandidate)
);
return {
schema_version: "assistant_turn_meaning_v1",
raw_message: rawMessage,
@ -174,7 +233,9 @@ export function createAssistantTurnMeaningPolicy(deps = {}) {
asked_action_family: askedActionFamily,
explicit_intent_candidate: explicitIntentCandidate,
explicit_entity_candidates: buildEntityCandidates(counterpartyTurnover),
meaning_confidence: supportedIntent?.confidence ?? (counterpartyTurnover?.family ? "medium" : "low"),
meaning_confidence: broadBusinessEvaluation?.family
? "medium"
: supportedIntent?.confidence ?? (counterpartyTurnover?.family ? "medium" : "low"),
intent_override_strength: explicitIntentCandidate
? "explicit_current_turn_intent"
: staleReplayForbidden

View File

@ -3141,5 +3141,113 @@ describe("assistant address follow-up carryover", () => {
expect(calls[0].options?.followupContext?.root_filters?.counterparty).toBeUndefined();
expect(normalizerService.normalize).not.toHaveBeenCalled();
});
it.skip("passes grounded MCP discovery payout context into a short year-switch follow-up", async () => {
const followupMessage = "а теперь за 2021?";
const calls: Array<{ message: string; options?: any }> = [];
const addressQueryService = {
tryHandle: vi.fn(async (message: string, options?: any) => {
calls.push({ message, options });
if (message === followupMessage && options?.followupContext) {
return buildAddressLaneResult({
reply_text: "Подтверждены исходящие платежи по Группа СВК за 2021 год.",
debug: {
...buildAddressLaneResult().debug,
detected_intent: "supplier_payouts_profile",
selected_recipe: "address_supplier_payouts_profile_v1",
extracted_filters: {
counterparty: "Группа СВК",
organization: "ООО Альтернатива Плюс",
period_from: "2021-01-01",
period_to: "2021-12-31"
},
reasons: ["address_action_detected", "address_entity_detected", "address_followup_context_applied"]
}
});
}
return null;
})
} as any;
const normalizerService = {
normalize: vi.fn(async () => ({
assistant_reply: "normalizer_fallback_should_not_be_used",
reply_type: "partial_coverage",
debug: {}
}))
} as any;
const sessions = new AssistantSessionStore();
const service = new AssistantService(
normalizerService,
sessions as any,
{} as any,
{ persistSession: vi.fn() } as any,
addressQueryService
);
const sessionId = `asst-discovery-followup-year-switch-${Date.now()}`;
sessions.appendItem(sessionId, {
message_id: "msg-discovery-payout-seed",
session_id: sessionId,
role: "assistant",
text: "Подтверждены исходящие платежи по Группа СВК за 2020 год.",
reply_type: "partial_coverage",
created_at: "2026-04-20T10:00:00.000Z",
trace_id: "living-discovery-seed",
debug: {
execution_lane: "living_chat",
mcp_discovery_response_applied: true,
assistant_active_organization: "ООО Альтернатива Плюс",
assistant_mcp_discovery_entry_point_v1: {
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
entry_status: "bridge_executed",
turn_input: {
turn_meaning_ref: {
asked_action_family: "payout",
explicit_entity_candidates: ["Группа СВК"],
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020"
}
},
bridge: {
bridge_status: "answer_draft_ready",
business_fact_answer_allowed: true,
pilot: {
pilot_scope: "counterparty_supplier_payout_query_movements_v1"
},
answer_draft: {
answer_mode: "confirmed_with_bounded_inference"
}
}
}
}
} as any);
const response = await service.handleMessage({
session_id: sessionId,
user_message: followupMessage,
useMock: true
} as any);
expect(response.ok).toBe(true);
expect(response.reply_type).toBe("factual");
expect(calls).toHaveLength(1);
expect(calls[0].message).toBe(followupMessage);
expect(calls[0].options?.followupContext?.previous_intent).toBe("supplier_payouts_profile");
expect(calls[0].options?.followupContext?.previous_discovery_pilot_scope).toBe(
"counterparty_supplier_payout_query_movements_v1"
);
expect(calls[0].options?.followupContext?.previous_anchor_type).toBe("counterparty");
expect(calls[0].options?.followupContext?.previous_anchor_value).toBe("Группа СВК");
expect(calls[0].options?.followupContext?.previous_filters).toMatchObject({
counterparty: "Группа СВК",
organization: "ООО Альтернатива Плюс",
period_from: "2020-01-01",
period_to: "2020-12-31"
});
expect(normalizerService.normalize).not.toHaveBeenCalled();
});
});

View File

@ -154,6 +154,97 @@ describe("assistant address orchestration runtime adapter", () => {
);
});
it("passes grounded discovery follow-up carryover into MCP discovery entry point for a short year switch", async () => {
const runMcpDiscoveryRuntimeEntryPoint = vi.fn(async () => ({
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
policy_owner: "assistantMcpDiscoveryRuntimeEntryPoint",
entry_status: "bridge_executed",
hot_runtime_wired: false,
discovery_attempted: true
}));
const input = buildInput({
userMessage: "а теперь за 2021?",
runAddressLlmPreDecompose: vi.fn(async () => ({
attempted: true,
applied: false,
effectiveMessage: "а теперь за 2021?",
reason: "raw_kept",
predecomposeContract: {
mode: "unsupported",
intent: "unknown",
period: {
scope: "year",
period_from: "2021-01-01",
period_to: "2021-12-31",
has_explicit_period: true
}
}
})),
resolveAddressFollowupCarryoverContext: vi.fn(() => ({
followupContext: {
previous_intent: "supplier_payouts_profile",
target_intent: "supplier_payouts_profile",
previous_discovery_pilot_scope: "counterparty_supplier_payout_query_movements_v1",
previous_anchor_type: "counterparty",
previous_anchor_value: "Группа СВК",
previous_filters: {
counterparty: "Группа СВК",
organization: "ООО Альтернатива Плюс",
period_from: "2020-01-01",
period_to: "2020-12-31"
}
}
})),
resolveAssistantOrchestrationDecision: vi.fn(() => ({
runAddressLane: true,
livingMode: "address_data",
livingReason: "address_lane_triggered",
toolGateDecision: "run_address_lane",
toolGateReason: "followup_context_detected",
orchestrationContract: {
schema_version: "assistant_orchestration_contract_v1",
assistant_turn_meaning: {
schema_version: "assistant_turn_meaning_v1",
raw_message: "а теперь за 2021?",
effective_message: "а теперь за 2021?",
explicit_entity_candidates: []
}
}
})),
runMcpDiscoveryRuntimeEntryPoint
});
const output = await buildAssistantAddressOrchestrationRuntime(input);
expect(output.orchestrationDecision.runAddressLane).toBe(true);
expect(runMcpDiscoveryRuntimeEntryPoint).toHaveBeenCalledWith(
expect.objectContaining({
userMessage: "а теперь за 2021?",
effectiveMessage: "а теперь за 2021?",
followupContext: expect.objectContaining({
previous_intent: "supplier_payouts_profile",
target_intent: "supplier_payouts_profile",
previous_discovery_pilot_scope: "counterparty_supplier_payout_query_movements_v1",
previous_anchor_type: "counterparty",
previous_anchor_value: "Группа СВК",
previous_filters: expect.objectContaining({
counterparty: "Группа СВК",
organization: "ООО Альтернатива Плюс",
period_from: "2020-01-01",
period_to: "2020-12-31"
})
})
})
);
expect(output.addressRuntimeMeta.mcpDiscoveryRuntimeEntryPoint).toEqual(
expect.objectContaining({
entry_status: "bridge_executed",
discovery_attempted: true,
hot_runtime_wired: false
})
);
});
it("keeps address orchestration alive when MCP discovery entry point fails", async () => {
const input = buildInput({
runMcpDiscoveryRuntimeEntryPoint: vi.fn(async () => {

View File

@ -7,6 +7,8 @@ import {
applyTemporalCarryoverFilters,
buildRootScopedCarryoverFilters,
hydrateInventoryRootFrameState,
readAddressDebugCounterparty,
readAddressDebugIntent,
readAddressDebugTemporalScope,
resolveNavigationSessionContextState,
resolveAddressDebugCarryoverFilters,
@ -108,11 +110,169 @@ describe("assistantContinuityPolicy organization authority", () => {
expect(facts).toEqual({
item: "Рабочая станция",
counterparty: null,
organization: 'ООО "Альтернатива Плюс"',
scopedDate: "31.03.2020"
});
});
it("reads counterparty, organization and period from grounded MCP discovery fallback", () => {
const facts = resolveAddressDebugContextFacts({
execution_lane: "living_chat",
mcp_discovery_response_applied: true,
assistant_active_organization: "ООО Альтернатива Плюс",
assistant_mcp_discovery_entry_point_v1: {
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
entry_status: "bridge_executed",
turn_input: {
turn_meaning_ref: {
explicit_entity_candidates: ["Группа СВК"],
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020"
}
},
bridge: {
bridge_status: "answer_draft_ready",
business_fact_answer_allowed: true,
answer_draft: {
answer_mode: "confirmed_with_bounded_inference"
}
}
}
});
expect(facts).toEqual({
item: null,
counterparty: "Группа СВК",
organization: "ООО Альтернатива Плюс",
scopedDate: "2020"
});
});
it("hydrates intent and carryover filters from grounded MCP discovery payout scope", () => {
const debug = {
execution_lane: "living_chat",
mcp_discovery_response_applied: true,
assistant_active_organization: "ООО Альтернатива Плюс",
assistant_mcp_discovery_entry_point_v1: {
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
entry_status: "bridge_executed",
turn_input: {
turn_meaning_ref: {
asked_action_family: "payout",
explicit_entity_candidates: ["Группа СВК"],
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020"
}
},
bridge: {
bridge_status: "answer_draft_ready",
business_fact_answer_allowed: true,
pilot: {
pilot_scope: "counterparty_supplier_payout_query_movements_v1"
},
answer_draft: {
answer_mode: "confirmed_with_bounded_inference"
}
}
}
};
expect(readAddressDebugIntent(debug)).toBe("supplier_payouts_profile");
expect(readAddressDebugTemporalScope(debug)).toEqual({
asOfDate: null,
periodFrom: "2020-01-01",
periodTo: "2020-12-31"
});
expect(resolveAddressDebugCarryoverFilters(debug)).toEqual({
counterparty: "Группа СВК",
organization: "ООО Альтернатива Плюс",
period_from: "2020-01-01",
period_to: "2020-12-31"
});
});
it("prefers grounded discovery date scope over stale exact-route date filters in carryover", () => {
const debug = {
execution_lane: "address_query",
extracted_filters: {
counterparty: "Группа СВК",
period_to: "2026-04-22"
},
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: {
turn_meaning_ref: {
asked_action_family: "payout",
explicit_entity_candidates: ["Группа СВК"],
explicit_date_scope: "2020"
}
},
bridge: {
bridge_status: "answer_draft_ready",
business_fact_answer_allowed: true,
pilot: {
pilot_scope: "counterparty_supplier_payout_query_movements_v1"
},
answer_draft: {
answer_mode: "confirmed_with_bounded_inference"
}
}
}
};
expect(resolveAddressDebugCarryoverFilters(debug)).toEqual({
counterparty: "Группа СВК",
period_from: "2020-01-01",
period_to: "2020-12-31"
});
});
it("prefers the resolved entity from grounded entity-resolution discovery for counterparty carryover", () => {
const 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: {
turn_meaning_ref: {
asked_domain_family: "entity_resolution",
asked_action_family: "search_business_entity",
explicit_entity_candidates: ["СВК"]
}
},
bridge: {
bridge_status: "answer_draft_ready",
business_fact_answer_allowed: true,
pilot: {
pilot_scope: "entity_resolution_search_v1",
derived_entity_resolution: {
requested_entity: "СВК",
resolution_status: "resolved",
resolved_entity: "Группа СВК",
ambiguity_candidates: []
}
},
answer_draft: {
answer_mode: "confirmed_with_bounded_inference"
}
}
}
};
expect(readAddressDebugCounterparty(debug)).toBe("Группа СВК");
expect(resolveAddressDebugCarryoverFilters(debug)).toEqual({
counterparty: "Группа СВК"
});
expect(resolveAddressDebugAnchorContext(debug)).toEqual({
anchorType: "counterparty",
anchorValue: "Группа СВК"
});
});
it("resolves navigation session context through one shared helper", () => {
const state = resolveNavigationSessionContextState({
session_context: {

View File

@ -133,6 +133,91 @@ describe("assistant living chat runtime adapter", () => {
expect(executeLlmChat).toHaveBeenCalledTimes(1);
});
it("builds deterministic broad business evaluation summary from grounded continuity instead of replaying lifecycle noise", async () => {
const executeLlmChat = vi.fn(async () => "raw-llm");
const input = buildRuntimeInput({
userMessage: "Как ты оценишь деятельность компании?",
modeDecision: { mode: "chat", reason: "unsupported_current_turn_meaning_boundary" },
sessionScope: {
knownOrganizations: ["ООО Альтернатива Плюс"],
selectedOrganization: null,
activeOrganization: "ООО Альтернатива Плюс"
},
sessionItems: [
{
role: "assistant",
debug: {
execution_lane: "address_query",
answer_grounding_check: {
status: "grounded"
},
detected_intent: "counterparty_activity_lifecycle",
extracted_filters: {
organization: "ООО Альтернатива Плюс"
}
}
},
{
role: "assistant",
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: {
turn_meaning_ref: {
explicit_entity_candidates: ["Группа СВК"],
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020"
}
},
bridge: {
bridge_status: "answer_draft_ready",
business_fact_answer_allowed: true,
answer_draft: {
answer_mode: "confirmed_with_bounded_inference"
},
pilot: {
pilot_scope: "counterparty_bidirectional_value_flow_query_movements_v1",
derived_bidirectional_value_flow: {
net_amount_human_ru: "3 865 501,50 руб.",
incoming_customer_revenue: {
total_amount_human_ru: "47 628 853,03 руб."
},
outgoing_supplier_payout: {
total_amount_human_ru: "43 763 351,53 руб."
}
}
}
}
}
}
}
],
addressRuntimeMeta: {
toolGateReason: "unsupported_current_turn_meaning_boundary",
orchestrationContract: {
unsupported_current_turn_meaning_boundary: true,
assistant_turn_meaning: {
unsupported_but_understood_family: "broad_business_evaluation"
}
}
},
executeLlmChat
});
const output = await runAssistantLivingChatRuntime(input);
expect(output.handled).toBe(true);
expect(output.chatText.toLowerCase()).toContain("оценка бизнеса");
expect(output.chatText).toContain("ООО Альтернатива Плюс");
expect(output.chatText).toContain("Группа СВК");
expect(output.chatText).toContain("нетто");
expect(output.debug?.living_chat_response_source).toBe("deterministic_broad_business_evaluation_contract");
expect(executeLlmChat).not.toHaveBeenCalled();
});
it("builds deterministic boundary for unsupported current-turn business meaning", async () => {
const executeLlmChat = vi.fn(async () => "raw-llm");
const input = buildRuntimeInput({
@ -366,6 +451,56 @@ describe("assistant living chat runtime adapter", () => {
expect(executeLlmChat).not.toHaveBeenCalled();
});
it("builds deterministic memory recap for prior grounded MCP discovery counterparty context", async () => {
const executeLlmChat = vi.fn(async () => "raw-llm");
const input = buildRuntimeInput({
userMessage: "а ты помнишь, что мы выяснили по свк?",
modeDecision: { mode: "chat", reason: "memory_recap_followup_detected" },
sessionItems: [
{
role: "assistant",
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: {
turn_meaning_ref: {
explicit_entity_candidates: ["Группа СВК"],
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020"
}
},
bridge: {
bridge_status: "answer_draft_ready",
business_fact_answer_allowed: true,
answer_draft: {
answer_mode: "confirmed_with_bounded_inference"
},
pilot: {
pilot_scope: "counterparty_supplier_payout_query_movements_v1",
derived_value_flow: {
total_amount_human_ru: "43 763 351,53 руб."
}
}
}
}
}
}
],
executeLlmChat
});
const output = await runAssistantLivingChatRuntime(input);
expect(output.handled).toBe(true);
expect(output.chatText).toContain("Группа СВК");
expect(output.chatText).toContain("43 763 351,53 руб.");
expect(output.debug?.living_chat_response_source).toBe("deterministic_memory_recap_contract");
expect(executeLlmChat).not.toHaveBeenCalled();
});
it("uses continuity-backed active organization for organization-fact boundary even when session scope is empty", async () => {
const executeLlmChat = vi.fn(async () => "raw-llm");
const input = buildRuntimeInput({
@ -434,4 +569,50 @@ describe("assistant living chat runtime adapter", () => {
expect(output.debug?.living_chat_response_source).toBe("deterministic_answer_inspection_contract");
expect(executeLlmChat).not.toHaveBeenCalled();
});
it("builds deterministic answer inspection reply over grounded MCP discovery net answer", async () => {
const executeLlmChat = vi.fn(async () => "raw-llm");
const input = buildRuntimeInput({
userMessage: "что ты имел в виду под нетто по свк?",
modeDecision: { mode: "chat", reason: "answer_inspection_followup_detected" },
sessionItems: [
{
role: "assistant",
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: {
turn_meaning_ref: {
explicit_entity_candidates: ["Группа СВК"],
explicit_date_scope: "2020"
}
},
bridge: {
bridge_status: "answer_draft_ready",
business_fact_answer_allowed: true,
answer_draft: {
answer_mode: "confirmed_with_bounded_inference"
},
pilot: {
pilot_scope: "counterparty_bidirectional_value_flow_query_movements_v1"
}
}
}
}
}
],
executeLlmChat
});
const output = await runAssistantLivingChatRuntime(input);
expect(output.handled).toBe(true);
expect(output.chatText).toContain("Группа СВК");
expect(output.chatText).toContain("Нетто");
expect(output.debug?.living_chat_response_source).toBe("deterministic_answer_inspection_contract");
expect(executeLlmChat).not.toHaveBeenCalled();
});
});

View File

@ -31,6 +31,35 @@ function buildSequentialDeps(results: Array<{ rows: Array<Record<string, unknown
return { executeAddressMcpQuery };
}
function buildCustomQueryDeps(result: {
fetched_rows: number;
matched_rows: number;
rows: Array<Record<string, unknown>>;
raw_rows?: Array<Record<string, unknown>>;
error?: string | null;
}) {
return {
executeAddressMcpQuery: vi.fn(async () => ({
fetched_rows: result.fetched_rows,
matched_rows: result.matched_rows,
rows: result.rows,
raw_rows: result.raw_rows ?? result.rows,
error: result.error ?? null
}))
};
}
function buildMetadataDeps(rows: Array<Record<string, unknown>>, error: string | null = null) {
return {
executeAddressMcpMetadata: vi.fn(async () => ({
fetched_rows: error ? 0 : rows.length,
raw_rows: error ? [] : rows,
rows: error ? [] : rows,
error
}))
};
}
describe("assistant MCP discovery answer adapter", () => {
it("turns confirmed lifecycle evidence into a human-safe bounded answer draft", async () => {
const planner = planAssistantMcpDiscovery({
@ -78,6 +107,103 @@ describe("assistant MCP discovery answer adapter", () => {
expect(draft.must_not_claim).toContain("Do not claim a confirmed business fact when confirmed_facts is empty.");
});
it("turns generic document evidence into a bounded document answer draft", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "documents",
asked_action_family: "list_documents",
explicit_entity_candidates: ["SVK"],
explicit_date_scope: "2020",
unsupported_but_understood_family: "document_evidence"
}
});
const pilot = await executeAssistantMcpDiscoveryPilot(
planner,
buildDeps([{ Period: "2020-01-15T00:00:00", Counterparty: "SVK", Registrar: "Doc1" }])
);
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
expect(draft.answer_mode).toBe("confirmed_with_bounded_inference");
expect(draft.headline).toContain("документ");
expect(draft.headline).toContain("2020");
expect(draft.headline).toContain("SVK");
expect(draft.confirmed_lines).toContain("В 1С найдены строки документов по контрагенту SVK за 2020.");
expect(draft.inference_lines).toContain(
"Срез документов по контрагенту SVK за 2020 ограничен только подтвержденными строками документов, найденными этим поиском."
);
expect(draft.unknown_lines).toContain(
"Полный исторический срез документов по контрагенту SVK вне периода 2020 этим поиском не подтвержден."
);
expect(draft.must_not_claim).toContain("Do not claim full document history outside the checked period.");
});
it("turns generic movement evidence into a bounded movement answer draft", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "movements",
asked_action_family: "list_movements",
explicit_entity_candidates: ["SVK"],
explicit_date_scope: "2020",
unsupported_but_understood_family: "movement_evidence"
}
});
const pilot = await executeAssistantMcpDiscoveryPilot(
planner,
buildDeps([{ Period: "2020-01-15T00:00:00", Counterparty: "SVK", Registrar: "Move1" }])
);
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
expect(draft.answer_mode).toBe("confirmed_with_bounded_inference");
expect(draft.headline).toContain("движени");
expect(draft.headline).toContain("2020");
expect(draft.headline).toContain("SVK");
expect(draft.confirmed_lines).toContain("В 1С найдены строки движений по контрагенту SVK за 2020.");
expect(draft.inference_lines).toContain(
"Срез движений по контрагенту SVK за 2020 ограничен только подтвержденными строками движений, найденными этим поиском."
);
expect(draft.unknown_lines).toContain(
"Полный исторический срез движений по контрагенту SVK вне периода 2020 этим поиском не подтвержден."
);
expect(draft.must_not_claim).toContain("Do not claim full movement history outside the checked period.");
expect(draft.must_not_claim).toContain("Do not present the confirmed movement rows as a complete movement universe.");
});
it("keeps bounded-only movement answers tied to the resolved entity and checked period", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "movements",
asked_action_family: "list_movements",
explicit_entity_candidates: ["Группа СВК"],
explicit_date_scope: "2020",
unsupported_but_understood_family: "movement_evidence"
}
});
const pilot = await executeAssistantMcpDiscoveryPilot(
planner,
buildCustomQueryDeps({
fetched_rows: 100,
matched_rows: 0,
rows: [],
raw_rows: [{ Period: "2020-06-30T00:00:00", Counterparty: "Группа СВК", Registrar: "Move1" }]
})
);
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
expect(draft.answer_mode).toBe("bounded_inference_only");
expect(draft.headline).toContain("движени");
expect(draft.headline).toContain("Группа СВК");
expect(draft.headline).toContain("2020");
expect(draft.inference_lines).toContain(
"По движениям по контрагенту Группа СВК за 2020 удалось проверить только ограниченный срез 1С; подтвержденных строк движений этим поиском не найдено."
);
expect(draft.unknown_lines).toContain(
"Полный исторический срез движений по контрагенту Группа СВК вне периода 2020 этим поиском не подтвержден."
);
});
it("asks for clarification when discovery did not execute due to missing scope", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
@ -96,6 +222,200 @@ 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 an explicit lane choice when mixed metadata ambiguity cannot continue on a neutral follow-up", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "metadata",
asked_action_family: "resolve_next_lane",
explicit_entity_candidates: ["SVK"],
explicit_date_scope: "2020",
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.must_not_claim).toContain("Do not claim rows were checked when mcp_execution_performed=false.");
});
it("keeps movement clarification anchored to the chosen lane after metadata ambiguity was resolved", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "movements",
asked_action_family: "list_movements",
explicit_entity_candidates: ["НДС"],
unsupported_but_understood_family: "movement_evidence"
}
});
const pilot = await executeAssistantMcpDiscoveryPilot(planner, buildDeps([]));
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
expect(draft.answer_mode).toBe("needs_clarification");
expect(draft.headline).toContain("движениям/регистрам");
expect(draft.headline).toContain("НДС");
expect(draft.headline).toContain("период");
expect(draft.next_step_line).toContain("движениям/регистрам");
expect(draft.next_step_line).toContain("НДС");
expect(draft.next_step_line).toContain("период");
});
it("turns resolved entity grounding into a human-safe entity search answer draft", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "entity_resolution",
asked_action_family: "search_business_entity",
explicit_entity_candidates: ["Группа СВК"],
unsupported_but_understood_family: "entity_resolution"
}
});
const pilot = await executeAssistantMcpDiscoveryPilot(
planner,
buildDeps([
{ Counterparty: "Группа СВК", CounterpartyRef: "Ref-1" },
{ Counterparty: "СВК Логистика", CounterpartyRef: "Ref-2" }
])
);
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
expect(draft.answer_mode).toBe("confirmed_with_bounded_inference");
expect(draft.headline).toContain("вероятный контрагент");
expect(draft.confirmed_lines.join("\n")).toContain("Группа СВК");
expect(draft.inference_lines.join("\n")).toContain("заземление сущности");
expect(draft.next_step_line).toContain("искать документы, движения или денежный поток");
expect(draft.must_not_claim).toContain(
"Do not present catalog grounding as confirmed business activity, turnover, or document evidence."
);
});
it("asks for clarification when entity grounding stays ambiguous", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "entity_resolution",
asked_action_family: "search_business_entity",
explicit_entity_candidates: ["СВК"],
unsupported_but_understood_family: "entity_resolution"
}
});
const pilot = await executeAssistantMcpDiscoveryPilot(
planner,
buildDeps([
{ Counterparty: "СВК-А", CounterpartyRef: "Ref-1" },
{ Counterparty: "СВК-Б", CounterpartyRef: "Ref-2" }
])
);
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
expect(draft.answer_mode).toBe("needs_clarification");
expect(draft.headline).toContain("несколько похожих контрагентов");
expect(draft.inference_lines.join("\n")).toContain("СВК-А");
expect(draft.inference_lines.join("\n")).toContain("1. СВК-А");
expect(draft.inference_lines.join("\n")).toContain("2. СВК-Б");
expect(draft.next_step_line).toContain("какой именно контрагент нужен");
expect(draft.next_step_line).toContain("1. СВК-А");
expect(draft.next_step_line).toContain("2. СВК-Б");
expect(draft.next_step_line).toContain("номером варианта");
});
it.skip("keeps entity search honest when no catalog candidate was confirmed", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "entity_resolution",
asked_action_family: "search_business_entity",
explicit_entity_candidates: ["Несуществующий Контрагент"],
unsupported_but_understood_family: "entity_resolution"
}
});
const pilot = await executeAssistantMcpDiscoveryPilot(
planner,
buildDeps([{ Counterparty: "Группа СВК", CounterpartyRef: "Ref-1" }])
);
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
expect(draft.answer_mode).toBe("checked_sources_only");
expect(draft.headline).toContain("точный контрагент пока не подтвержден");
expect(draft.unknown_lines).toContain(
'No counterparty matching "Несуществующий Контрагент" was confirmed in the checked 1C catalog slice'
);
expect(draft.next_step_line).toContain("Дайте точное название или ИНН");
});
it("turns metadata surface evidence into a human-safe metadata answer draft", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "metadata",
asked_action_family: "inspect_documents",
explicit_entity_candidates: ["НДС"]
}
});
const pilot = await executeAssistantMcpDiscoveryPilot(
planner,
buildMetadataDeps([
{
FullName: "Документ.СчетФактураВыданный",
MetaType: "Документ",
attributes: [{ Name: "Дата" }, { Name: "Организация" }]
}
])
);
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
const confirmedText = draft.confirmed_lines.join("\n");
expect(draft.answer_mode).toBe("confirmed_with_bounded_inference");
expect(draft.headline).toContain("заземлена вероятная поверхность");
expect(confirmedText).toContain("Подтвержденная metadata-поверхность 1С");
expect(confirmedText).toContain("Документ.СчетФактураВыданный");
expect(confirmedText).toContain("Выбранное family: Документ");
expect(confirmedText).toContain("Дата");
expect(draft.inference_lines.join("\n")).toContain("контур документов");
expect(draft.next_step_line).toContain("surface «Документ»");
expect(draft.must_not_claim).toContain("Do not present metadata surface as confirmed business data rows.");
expect(draft.must_not_claim).toContain("Do not present the inferred next checked lane as already executed data retrieval.");
});
it("keeps metadata answer honest when schema surface stays ambiguous", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "metadata",
asked_action_family: "inspect_fields",
explicit_entity_candidates: ["НДС"]
}
});
const pilot = await executeAssistantMcpDiscoveryPilot(
planner,
buildMetadataDeps([
{
FullName: "Документ.СчетФактураВыданный",
MetaType: "Документ",
attributes: [{ Name: "Дата" }]
},
{
FullName: "РегистрНакопления.НДСПокупок",
MetaType: "РегистрНакопления",
resources: [{ Name: "СуммаНДС" }]
}
])
);
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
expect(draft.headline).toContain("конкурирующие schema-поверхности");
expect(draft.inference_lines.join("\n")).toContain("несколько конкурирующих family");
expect(draft.unknown_lines).toContain(
"Exact downstream metadata surface remains ambiguous across: Документ, РегистрНакопления"
);
expect(draft.next_step_line).toContain("Документ, РегистрНакопления");
});
it("turns value-flow evidence into a bounded turnover answer draft", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
@ -117,8 +437,9 @@ describe("assistant MCP discovery answer adapter", () => {
const confirmedText = draft.confirmed_lines.join("\n");
expect(draft.answer_mode).toBe("confirmed_with_bounded_inference");
expect(draft.headline).toContain("денежных движений");
expect(draft.headline).toContain("входящих денежных поступлений");
expect(confirmedText).toContain("3 750,50 руб.");
expect(confirmedText).toContain("входящих денежных поступлений");
expect(confirmedText).toContain("2020-01-15");
expect(confirmedText).toContain("2020-02-20");
expect(draft.unknown_lines).toContain("Full turnover outside the checked period is not proven by this MCP discovery pilot");
@ -191,6 +512,87 @@ describe("assistant MCP discovery answer adapter", () => {
expect(draft.must_not_claim).toContain("Do not present a derived sum as a legal/accounting final total outside the checked 1C rows.");
});
it("renders monthly bidirectional breakdown lines when the turn explicitly asked by month", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "net_value_flow",
asked_aggregation_axis: "month",
explicit_entity_candidates: ["SVK"],
explicit_date_scope: "2020",
unsupported_but_understood_family: "counterparty_bidirectional_value_flow_or_netting"
}
});
const pilot = await executeAssistantMcpDiscoveryPilot(
planner,
buildSequentialDeps([
{
rows: [
{ Period: "2020-01-15T00:00:00", Amount: 10000, Counterparty: "SVK" },
{ Period: "2020-02-20T00:00:00", Amount: "2 500,50", Counterparty: "SVK" }
]
},
{
rows: [
{ Period: "2020-01-10T00:00:00", Amount: 4000, Counterparty: "SVK" },
{ Period: "2020-02-11T00:00:00", Amount: 1000, Counterparty: "SVK" }
]
}
])
);
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
const confirmedText = draft.confirmed_lines.join("\n");
expect(draft.answer_mode).toBe("confirmed_with_bounded_inference");
expect(draft.headline).toContain("помесяч");
expect(confirmedText).toContain("Помесячно: янв 2020");
expect(confirmedText).toContain("получили 10 000 руб.");
expect(confirmedText).toContain("заплатили 4 000 руб.");
expect(confirmedText).toContain("Помесячно: фев 2020");
expect(confirmedText).toContain("нетто в нашу сторону 1 500,50 руб.");
expect(draft.reason_codes).toContain("answer_contains_monthly_breakdown");
});
it("keeps recovered yearly coverage out of the unknown block and explains the recovery as bounded inference", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "payout",
explicit_entity_candidates: ["SVK"],
explicit_date_scope: "2020",
unsupported_but_understood_family: "counterparty_payouts_or_outflow"
}
});
const broadRows = Array.from({ length: 100 }, (_, index) => ({
Period: `2020-01-${String((index % 28) + 1).padStart(2, "0")}T00:00:00`,
Amount: 10,
Counterparty: "SVK"
}));
const monthlyResults = Array.from({ length: 12 }, (_, index) => ({
rows: [
{
Period: `2020-${String(index + 1).padStart(2, "0")}-05T00:00:00`,
Amount: (index + 1) * 100,
Counterparty: "SVK"
}
]
}));
const pilot = await executeAssistantMcpDiscoveryPilot(
planner,
buildSequentialDeps([{ rows: broadRows }, ...monthlyResults])
);
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
expect(draft.inference_lines).toContain(
"Requested period coverage was recovered through monthly 1C value-flow probes after the broad probe hit the row limit"
);
expect(draft.unknown_lines).not.toContain(
"Complete requested-period coverage is not proven because the MCP discovery probe row limit was reached"
);
});
it("does not leak primitive names or query text into user-facing lines", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
@ -245,4 +647,27 @@ describe("assistant MCP discovery answer adapter", () => {
expect(inferenceText).toContain("не юридически подтвержденный возраст регистрации");
expect(draft.reason_codes).toContain("pilot_derived_activity_period_from_confirmed_rows");
});
it("keeps not-found entity search user-facing lines in Russian", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "entity_resolution",
asked_action_family: "search_business_entity",
explicit_entity_candidates: ["\u041d\u0435\u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0439 \u041a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442"],
unsupported_but_understood_family: "entity_resolution"
}
});
const pilot = await executeAssistantMcpDiscoveryPilot(
planner,
buildDeps([{ Counterparty: "\u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a", CounterpartyRef: "Ref-1" }])
);
const draft = buildAssistantMcpDiscoveryAnswerDraft(pilot);
const unknownText = draft.unknown_lines.join("\n");
expect(draft.answer_mode).toBe("checked_sources_only");
expect(unknownText).toContain("\u043d\u0435 \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043d \u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442");
expect(unknownText).toContain("\u041d\u0435\u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u0439 \u041a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442");
expect(unknownText).toContain("\u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u044b, \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u044f \u0438 \u0434\u0435\u043d\u0435\u0436\u043d\u044b\u0435 \u043f\u043e\u043a\u0430\u0437\u0430\u0442\u0435\u043b\u0438");
});
});

View File

@ -30,6 +30,17 @@ function buildSequentialDeps(results: Array<{ rows: Array<Record<string, unknown
return { executeAddressMcpQuery };
}
function buildMetadataDeps(rows: Array<Record<string, unknown>>, error: string | null = null) {
return {
executeAddressMcpMetadata: vi.fn(async () => ({
fetched_rows: error ? 0 : rows.length,
raw_rows: error ? [] : rows,
rows: error ? [] : rows,
error
}))
};
}
describe("assistant MCP discovery pilot executor", () => {
it("executes only the lifecycle query_documents primitive through injected MCP deps", async () => {
const planner = planAssistantMcpDiscovery({
@ -92,6 +103,320 @@ describe("assistant MCP discovery pilot executor", () => {
expect(deps.executeAddressMcpQuery).not.toHaveBeenCalled();
});
it("uses the explicit selected chain id when choosing the movement pilot scope", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "movements",
asked_action_family: "list_movements",
explicit_entity_candidates: ["SVK"],
explicit_date_scope: "2020",
unsupported_but_understood_family: "movement_evidence"
}
});
const deps = buildDeps([{ Period: "2020-01-15T00:00:00", Amount: 1250, Counterparty: "SVK", Registrar: "Move1" }]);
const result = await executeAssistantMcpDiscoveryPilot(
{
...planner,
reason_codes: planner.reason_codes.filter((code) => !code.startsWith("planner_selected_"))
},
deps
);
expect(result.pilot_status).toBe("executed");
expect(result.pilot_scope).toBe("counterparty_movement_evidence_query_movements_v1");
expect(result.executed_primitives).toEqual(["query_movements"]);
});
it("executes generic document evidence through query_documents", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "documents",
asked_action_family: "list_documents",
explicit_entity_candidates: ["SVK"],
explicit_date_scope: "2020",
unsupported_but_understood_family: "document_evidence"
}
});
const deps = buildDeps([
{ Period: "2020-01-15T00:00:00", Counterparty: "SVK", Registrar: "Doc1" },
{ Period: "2020-03-20T00:00:00", Counterparty: "SVK", Registrar: "Doc2" }
]);
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
expect(result.pilot_status).toBe("executed");
expect(result.pilot_scope).toBe("counterparty_document_evidence_query_documents_v1");
expect(result.executed_primitives).toEqual(["query_documents"]);
expect(result.evidence.confirmed_facts).toContain("В 1С найдены строки документов по контрагенту SVK за 2020.");
expect(result.evidence.inferred_facts).toContain(
"Срез документов по контрагенту SVK за 2020 ограничен только подтвержденными строками документов, найденными этим поиском."
);
expect(result.evidence.unknown_facts).toContain(
"Полный исторический срез документов по контрагенту SVK вне периода 2020 этим поиском не подтвержден."
);
expect(result.source_rows_summary).toBe("2 MCP document rows fetched, 2 matched document scope");
});
it("executes generic movement evidence through query_movements without deriving turnover totals", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "movements",
asked_action_family: "list_movements",
explicit_entity_candidates: ["SVK"],
explicit_date_scope: "2020",
unsupported_but_understood_family: "movement_evidence"
}
});
const deps = buildDeps([
{ Period: "2020-01-15T00:00:00", Amount: 1250, Counterparty: "SVK", Registrar: "Move1" },
{ Period: "2020-03-20T00:00:00", Amount: "900,25", Counterparty: "SVK", Registrar: "Move2" }
]);
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
expect(result.pilot_status).toBe("executed");
expect(result.pilot_scope).toBe("counterparty_movement_evidence_query_movements_v1");
expect(result.executed_primitives).toEqual(["query_movements"]);
expect(result.derived_value_flow).toBeNull();
expect(result.derived_bidirectional_value_flow).toBeNull();
expect(result.evidence.confirmed_facts).toContain("В 1С найдены строки движений по контрагенту SVK за 2020.");
expect(result.evidence.inferred_facts).toContain(
"Срез движений по контрагенту SVK за 2020 ограничен только подтвержденными строками движений, найденными этим поиском."
);
expect(result.evidence.unknown_facts).toContain(
"Полный исторический срез движений по контрагенту SVK вне периода 2020 этим поиском не подтвержден."
);
expect(result.source_rows_summary).toBe("2 MCP movement rows fetched, 2 matched movement scope");
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(1);
const call = deps.executeAddressMcpQuery.mock.calls[0]?.[0];
expect(String(call?.query ?? "")).toContain("Документ.СписаниеСРасчетногоСчета");
expect(String(call?.query ?? "")).toContain("Документ.ПоступлениеНаРасчетныйСчет");
expect(call?.limit).toBeGreaterThan(0);
});
it("executes inspect_1c_metadata and derives a confirmed metadata surface", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "metadata",
asked_action_family: "inspect_documents",
explicit_entity_candidates: ["НДС"]
}
});
const deps = buildMetadataDeps([
{
FullName: "Документ.СчетФактураВыданный",
MetaType: "Документ",
attributes: [{ Name: "Дата" }, { Name: "Организация" }]
},
{
FullName: "Документ.СчетФактураПолученный",
MetaType: "Документ",
attributes: [{ Name: "Контрагент" }]
}
]);
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
expect(result.pilot_status).toBe("executed");
expect(result.pilot_scope).toBe("metadata_inspection_v1");
expect(result.mcp_execution_performed).toBe(true);
expect(result.executed_primitives).toEqual(["inspect_1c_metadata"]);
expect(result.evidence.evidence_status).toBe("confirmed");
expect(result.source_rows_summary).toBe("2 MCP metadata rows fetched");
expect(result.derived_metadata_surface).toMatchObject({
metadata_scope: "НДС",
requested_meta_types: ["Документ"],
matched_rows: 2,
available_entity_sets: ["Документ"],
matched_objects: ["Документ.СчетФактураВыданный", "Документ.СчетФактураПолученный"],
selected_entity_set: "Документ",
selected_surface_objects: ["Документ.СчетФактураВыданный", "Документ.СчетФактураПолученный"],
downstream_route_family: "document_evidence",
recommended_next_primitive: "query_documents",
ambiguity_detected: false,
ambiguity_entity_sets: [],
available_fields: ["Дата", "Организация", "Контрагент"],
inference_basis: "confirmed_1c_metadata_surface_rows"
});
expect(result.reason_codes).toContain("pilot_inspect_1c_metadata_mcp_executed");
expect(result.reason_codes).toContain("pilot_derived_metadata_surface_from_confirmed_rows");
expect(deps.executeAddressMcpMetadata).toHaveBeenCalledTimes(1);
expect(deps.executeAddressMcpMetadata.mock.calls[0]?.[0]).toMatchObject({
meta_type: ["Документ"],
name_mask: "НДС"
});
});
it("executes the full entity-resolution chain through the checked counterparty catalog slice", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "entity_resolution",
asked_action_family: "search_business_entity",
explicit_entity_candidates: ["Группа СВК"],
unsupported_but_understood_family: "entity_resolution"
}
});
const deps = buildDeps([
{ Counterparty: "Группа СВК", CounterpartyRef: "Ref-1" },
{ Counterparty: "СВК Логистика", CounterpartyRef: "Ref-2" }
]);
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
expect(result.pilot_status).toBe("executed");
expect(result.pilot_scope).toBe("entity_resolution_search_v1");
expect(result.mcp_execution_performed).toBe(true);
expect(result.executed_primitives).toEqual([
"search_business_entity",
"resolve_entity_reference",
"probe_coverage"
]);
expect(result.skipped_primitives).toEqual([]);
expect(result.derived_entity_resolution).toMatchObject({
requested_entity: "Группа СВК",
resolution_status: "resolved",
resolved_entity: "Группа СВК",
resolved_reference: "Ref-1",
confidence: "high",
inference_basis: "catalog_counterparty_search_rows"
});
expect(result.evidence.confirmed_facts).toContain(
"В проверенном каталожном срезе 1С найден контрагент: Группа СВК"
);
expect(result.evidence.inferred_facts).toContain(
"Пока проверено только заземление сущности по каталогу 1С; документы, движения и денежные показатели еще не проверялись"
);
expect(result.evidence.unknown_facts).toContain(
"Документы, движения и денежные показатели по этому контрагенту еще не проверялись; пока был только каталожный поиск"
);
expect(result.reason_codes).toContain("pilot_search_business_entity_mcp_executed");
expect(result.reason_codes).toContain("pilot_resolve_entity_reference_from_catalog_rows");
expect(result.reason_codes).toContain("pilot_probe_coverage_executed_for_entity_resolution");
expect(result.reason_codes).toContain("pilot_entity_resolution_grounding_stable_for_downstream_probe");
expect(result.reason_codes).toContain("pilot_derived_entity_resolution_from_catalog_rows");
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(1);
});
it("keeps entity-resolution honest when several catalog candidates remain ambiguous", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "entity_resolution",
asked_action_family: "search_business_entity",
explicit_entity_candidates: ["СВК"],
unsupported_but_understood_family: "entity_resolution"
}
});
const deps = buildDeps([
{ Counterparty: "СВК-А", CounterpartyRef: "Ref-1" },
{ Counterparty: "СВК-Б", CounterpartyRef: "Ref-2" }
]);
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
expect(result.pilot_status).toBe("executed");
expect(result.pilot_scope).toBe("entity_resolution_search_v1");
expect(result.executed_primitives).toEqual([
"search_business_entity",
"resolve_entity_reference",
"probe_coverage"
]);
expect(result.skipped_primitives).toEqual([]);
expect(result.derived_entity_resolution).toMatchObject({
requested_entity: "СВК",
resolution_status: "ambiguous",
resolved_entity: null,
ambiguity_candidates: ["СВК-А", "СВК-Б"],
confidence: "low"
});
expect(result.evidence.confirmed_facts).toEqual([]);
expect(result.evidence.unknown_facts).toContain(
"Точное заземление контрагента в 1С остается неоднозначным между вариантами: СВК-А, СВК-Б"
);
expect(result.reason_codes).toContain("pilot_resolve_entity_reference_requires_clarification");
expect(result.reason_codes).toContain("pilot_probe_coverage_executed_for_entity_resolution");
expect(result.reason_codes).toContain("pilot_entity_resolution_ambiguity_requires_clarification");
});
it("keeps metadata grounding ambiguous when several surface families compete", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "metadata",
asked_action_family: "inspect_fields",
explicit_entity_candidates: ["НДС"]
}
});
const deps = buildMetadataDeps([
{
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,
downstream_route_family: null,
recommended_next_primitive: null,
ambiguity_detected: true,
ambiguity_entity_sets: ["Документ", "РегистрНакопления"]
});
expect(result.evidence.inferred_facts).toEqual([]);
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: {
asked_domain_family: "metadata",
asked_action_family: "inspect_surface",
explicit_entity_candidates: ["НДС"]
}
});
const deps = buildMetadataDeps([
{
FullName: "Документ.СчетФактураВыданный",
attributes: [{ Name: "Дата" }]
},
{
FullName: "РегистрНакопления.НДСПокупок",
resources: [{ Name: "СуммаНДС" }]
},
{
FullName: "Справочник.КодыОперацийНДС"
}
]);
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,
downstream_route_family: null,
recommended_next_primitive: null,
ambiguity_detected: true,
ambiguity_entity_sets: ["Документ", "РегистрНакопления", "Справочник"]
});
expect(result.evidence.unknown_facts).toContain(
"Exact downstream metadata surface remains ambiguous across: Документ, РегистрНакопления, Справочник"
);
});
it("executes value-flow query_movements and derives a guarded turnover sum", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
@ -199,6 +524,56 @@ describe("assistant MCP discovery pilot executor", () => {
);
});
it("recovers yearly value-flow coverage by splitting a limited broad probe into monthly subprobes", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "payout",
explicit_entity_candidates: ["SVK"],
explicit_date_scope: "2020",
unsupported_but_understood_family: "counterparty_payouts_or_outflow"
}
});
const broadRows = Array.from({ length: 100 }, (_, index) => ({
Period: `2020-01-${String((index % 28) + 1).padStart(2, "0")}T00:00:00`,
Amount: 10,
Counterparty: "SVK"
}));
const monthlyResults = Array.from({ length: 12 }, (_, index) => ({
rows: [
{
Period: `2020-${String(index + 1).padStart(2, "0")}-05T00:00:00`,
Amount: (index + 1) * 100,
Counterparty: "SVK"
}
]
}));
const result = await executeAssistantMcpDiscoveryPilot(
planner,
buildSequentialDeps([{ rows: broadRows }, ...monthlyResults])
);
expect(result.derived_value_flow).toMatchObject({
value_flow_direction: "outgoing_supplier_payout",
coverage_limited_by_probe_limit: false,
coverage_recovered_by_period_chunking: true,
period_chunking_granularity: "month",
rows_matched: 12,
rows_with_amount: 12,
total_amount: 7800,
first_movement_date: "2020-01-05",
latest_movement_date: "2020-12-05"
});
expect(result.evidence.inferred_facts).toContain(
"Requested period coverage was recovered through monthly 1C value-flow probes after the broad probe hit the row limit"
);
expect(result.evidence.unknown_facts).not.toContain(
"Complete requested-period coverage is not proven because the MCP discovery probe row limit was reached"
);
expect(result.reason_codes).toContain("pilot_monthly_period_chunking_recovered_coverage");
});
it("executes bidirectional value-flow queries and derives guarded net cash flow", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
@ -260,7 +635,129 @@ describe("assistant MCP discovery pilot executor", () => {
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(2);
});
it("keeps non-lifecycle ready plans unsupported until a dedicated pilot exists", async () => {
it("derives monthly bidirectional value-flow breakdown when the turn explicitly asks by month", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "net_value_flow",
asked_aggregation_axis: "month",
explicit_entity_candidates: ["SVK"],
explicit_date_scope: "2020",
unsupported_but_understood_family: "counterparty_bidirectional_value_flow_or_netting"
}
});
const deps = buildSequentialDeps([
{
rows: [
{ Period: "2020-01-15T00:00:00", Amount: 10000, Counterparty: "SVK" },
{ Period: "2020-02-20T00:00:00", Amount: "2 500,50", Counterparty: "SVK" }
]
},
{
rows: [
{ Period: "2020-01-10T00:00:00", Amount: 4000, Counterparty: "SVK" },
{ Period: "2020-02-11T00:00:00", Amount: 1000, Counterparty: "SVK" }
]
}
]);
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
expect(result.derived_bidirectional_value_flow?.aggregation_axis).toBe("month");
expect(result.derived_bidirectional_value_flow?.monthly_breakdown).toMatchObject([
{
month_bucket: "2020-01",
incoming_total_amount: 10000,
incoming_rows_with_amount: 1,
outgoing_total_amount: 4000,
outgoing_rows_with_amount: 1,
net_amount: 6000,
net_direction: "net_incoming"
},
{
month_bucket: "2020-02",
incoming_total_amount: 2500.5,
incoming_rows_with_amount: 1,
outgoing_total_amount: 1000,
outgoing_rows_with_amount: 1,
net_amount: 1500.5,
net_direction: "net_incoming"
}
]);
expect(result.derived_bidirectional_value_flow?.monthly_breakdown[0]?.incoming_total_amount_human_ru).toContain("10 000");
expect(result.derived_bidirectional_value_flow?.monthly_breakdown[0]?.net_amount_human_ru).toContain("6 000");
expect(result.derived_bidirectional_value_flow?.monthly_breakdown[1]?.incoming_total_amount_human_ru).toContain("2 500,50");
expect(result.derived_bidirectional_value_flow?.monthly_breakdown[1]?.net_amount_human_ru).toContain("1 500,50");
expect(result.evidence.inferred_facts).toContain(
"Counterparty monthly net value-flow breakdown was grouped by month over confirmed incoming and outgoing 1C rows"
);
expect(result.reason_codes).toContain("pilot_derived_bidirectional_monthly_breakdown_from_confirmed_rows");
});
it("recovers bidirectional yearly coverage when one side is rebuilt from monthly subprobes", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "counterparty_value",
asked_action_family: "net_value_flow",
explicit_entity_candidates: ["SVK"],
explicit_date_scope: "2020",
unsupported_but_understood_family: "counterparty_bidirectional_value_flow_or_netting"
}
});
const outgoingBroadRows = Array.from({ length: 100 }, (_, index) => ({
Period: `2020-01-${String((index % 28) + 1).padStart(2, "0")}T00:00:00`,
Amount: 10,
Counterparty: "SVK"
}));
const outgoingMonthlyResults = Array.from({ length: 12 }, (_, index) => ({
rows: [
{
Period: `2020-${String(index + 1).padStart(2, "0")}-10T00:00:00`,
Amount: (index + 1) * 50,
Counterparty: "SVK"
}
]
}));
const deps = buildSequentialDeps([
{
rows: [
{ Period: "2020-01-15T00:00:00", Amount: 10000, Counterparty: "SVK" },
{ Period: "2020-02-20T00:00:00", Amount: 10000, Counterparty: "SVK" }
]
},
{ rows: outgoingBroadRows },
...outgoingMonthlyResults
]);
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
expect(result.derived_bidirectional_value_flow).toMatchObject({
coverage_limited_by_probe_limit: false,
coverage_recovered_by_period_chunking: true,
period_chunking_granularity: "month",
net_amount: 16100,
incoming_customer_revenue: {
total_amount: 20000,
coverage_limited_by_probe_limit: false
},
outgoing_supplier_payout: {
total_amount: 3900,
coverage_limited_by_probe_limit: false,
coverage_recovered_by_period_chunking: true,
period_chunking_granularity: "month"
}
});
expect(result.evidence.inferred_facts).toContain(
"Requested period coverage for bidirectional value-flow was recovered through monthly 1C side probes after a broad probe hit the row limit"
);
expect(result.evidence.unknown_facts).not.toContain(
"Complete requested-period coverage for bidirectional value-flow is not proven because at least one MCP discovery probe row limit was reached"
);
expect(result.reason_codes).toContain("pilot_bidirectional_outgoing_monthly_period_chunking_recovered_coverage");
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(14);
});
it("executes document-ready plans through the dedicated document pilot", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "counterparty_documents",
@ -272,11 +769,62 @@ describe("assistant MCP discovery pilot executor", () => {
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
expect(result.pilot_status).toBe("unsupported");
expect(result.mcp_execution_performed).toBe(false);
expect(result.skipped_primitives).toEqual(["resolve_entity_reference", "query_documents", "probe_coverage"]);
expect(result.reason_codes).toContain("pilot_scope_unsupported_for_live_execution");
expect(deps.executeAddressMcpQuery).not.toHaveBeenCalled();
expect(result.pilot_status).toBe("executed");
expect(result.pilot_scope).toBe("counterparty_document_evidence_query_documents_v1");
expect(result.mcp_execution_performed).toBe(true);
expect(result.executed_primitives).toEqual(["query_documents"]);
expect(result.reason_codes).toContain("pilot_query_documents_mcp_executed");
expect(deps.executeAddressMcpQuery).toHaveBeenCalledTimes(1);
});
it("keeps document and movement evidence scoped to the resolved entity and checked period", async () => {
const documentPlanner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "documents",
asked_action_family: "list_documents",
explicit_entity_candidates: ["Группа СВК"],
explicit_date_scope: "2020",
unsupported_but_understood_family: "document_evidence"
}
});
const documentResult = await executeAssistantMcpDiscoveryPilot(
documentPlanner,
buildDeps([{ Period: "2020-01-15T00:00:00", Counterparty: "Группа СВК", Registrar: "Doc1" }])
);
expect(documentResult.evidence.confirmed_facts).toContain(
"В 1С найдены строки документов по контрагенту Группа СВК за 2020."
);
expect(documentResult.evidence.inferred_facts).toContain(
"Срез документов по контрагенту Группа СВК за 2020 ограничен только подтвержденными строками документов, найденными этим поиском."
);
expect(documentResult.evidence.unknown_facts).toContain(
"Полный исторический срез документов по контрагенту Группа СВК вне периода 2020 этим поиском не подтвержден."
);
const movementPlanner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "movements",
asked_action_family: "list_movements",
explicit_entity_candidates: ["Группа СВК"],
explicit_date_scope: "2020",
unsupported_but_understood_family: "movement_evidence"
}
});
const movementResult = await executeAssistantMcpDiscoveryPilot(
movementPlanner,
buildDeps([{ Period: "2020-01-15T00:00:00", Counterparty: "Группа СВК", Registrar: "Move1" }])
);
expect(movementResult.evidence.confirmed_facts).toContain(
"В 1С найдены строки движений по контрагенту Группа СВК за 2020."
);
expect(movementResult.evidence.inferred_facts).toContain(
"Срез движений по контрагенту Группа СВК за 2020 ограничен только подтвержденными строками движений, найденными этим поиском."
);
expect(movementResult.evidence.unknown_facts).toContain(
"Полный исторический срез движений по контрагенту Группа СВК вне периода 2020 этим поиском не подтвержден."
);
});
it("records MCP errors as limitations without converting them into facts", async () => {
@ -299,4 +847,54 @@ describe("assistant MCP discovery pilot executor", () => {
expect(result.query_limitations).toContain("MCP fetch failed: timeout");
expect(result.reason_codes).toContain("pilot_query_documents_mcp_error");
});
it("emits Russian confirmed and bounded facts for resolved entity grounding", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "entity_resolution",
asked_action_family: "search_business_entity",
explicit_entity_candidates: ["\u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a"],
unsupported_but_understood_family: "entity_resolution"
}
});
const deps = buildDeps([
{ Counterparty: "\u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a", CounterpartyRef: "Ref-1" },
{ Counterparty: "\u0421\u0412\u041a \u041b\u043e\u0433\u0438\u0441\u0442\u0438\u043a\u0430", CounterpartyRef: "Ref-2" }
]);
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
expect(result.evidence.confirmed_facts.join("\n")).toContain("\u043d\u0430\u0439\u0434\u0435\u043d \u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442");
expect(result.evidence.confirmed_facts.join("\n")).toContain("\u0413\u0440\u0443\u043f\u043f\u0430 \u0421\u0412\u041a");
expect(result.evidence.inferred_facts.join("\n")).toContain(
"\u041f\u043e\u043a\u0430 \u043f\u0440\u043e\u0432\u0435\u0440\u0435\u043d\u043e \u0442\u043e\u043b\u044c\u043a\u043e \u0437\u0430\u0437\u0435\u043c\u043b\u0435\u043d\u0438\u0435 \u0441\u0443\u0449\u043d\u043e\u0441\u0442\u0438 \u043f\u043e \u043a\u0430\u0442\u0430\u043b\u043e\u0433\u0443 1\u0421"
);
expect(result.evidence.unknown_facts.join("\n")).toContain(
"\u0414\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u044b, \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u044f \u0438 \u0434\u0435\u043d\u0435\u0436\u043d\u044b\u0435 \u043f\u043e\u043a\u0430\u0437\u0430\u0442\u0435\u043b\u0438"
);
});
it("emits Russian ambiguity boundaries for entity grounding", async () => {
const planner = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "entity_resolution",
asked_action_family: "search_business_entity",
explicit_entity_candidates: ["\u0421\u0412\u041a"],
unsupported_but_understood_family: "entity_resolution"
}
});
const deps = buildDeps([
{ Counterparty: "\u0421\u0412\u041a-\u0410", CounterpartyRef: "Ref-1" },
{ Counterparty: "\u0421\u0412\u041a-\u0411", CounterpartyRef: "Ref-2" }
]);
const result = await executeAssistantMcpDiscoveryPilot(planner, deps);
expect(result.evidence.confirmed_facts).toEqual([]);
expect(result.evidence.unknown_facts.join("\n")).toContain(
"\u0422\u043e\u0447\u043d\u043e\u0435 \u0437\u0430\u0437\u0435\u043c\u043b\u0435\u043d\u0438\u0435 \u043a\u043e\u043d\u0442\u0440\u0430\u0433\u0435\u043d\u0442\u0430 \u0432 1\u0421 \u043e\u0441\u0442\u0430\u0435\u0442\u0441\u044f \u043d\u0435\u043e\u0434\u043d\u043e\u0437\u043d\u0430\u0447\u043d\u044b\u043c"
);
expect(result.evidence.unknown_facts.join("\n")).toContain("\u0421\u0412\u041a-\u0410");
expect(result.evidence.unknown_facts.join("\n")).toContain("\u0421\u0412\u041a-\u0411");
});
});

View File

@ -14,6 +14,8 @@ describe("assistant MCP discovery planner", () => {
expect(result.planner_status).toBe("ready_for_execution");
expect(result.semantic_data_need).toBe("counterparty value-flow evidence");
expect(result.selected_chain_id).toBe("value_flow");
expect(result.selected_chain_summary).toContain("query scoped movements");
expect(result.proposed_primitives).toEqual([
"resolve_entity_reference",
"query_movements",
@ -23,6 +25,8 @@ 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.discovery_plan.execution_budget.max_probe_count).toBe(30);
expect(result.reason_codes).toContain("planner_enabled_chunked_coverage_probe_budget");
});
it("keeps a value-flow plan in clarification state when period axis is missing", () => {
@ -40,6 +44,36 @@ describe("assistant MCP discovery planner", () => {
expect(result.reason_codes).toContain("planner_needs_more_user_or_scope_context");
});
it("keeps requested monthly aggregation as an explicit planning axis for value-flow discovery", () => {
const result = planAssistantMcpDiscovery({
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.planner_status).toBe("ready_for_execution");
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_recipe");
expect(result.discovery_plan.execution_budget.max_probe_count).toBe(30);
});
it("builds a document discovery plan without falling back to movement primitives", () => {
const result = planAssistantMcpDiscovery({
turnMeaning: {
@ -55,6 +89,27 @@ describe("assistant MCP discovery planner", () => {
expect(result.required_axes).toEqual(["counterparty", "coverage_target"]);
});
it("builds a movement discovery plan without aggregating value-flow totals", () => {
const result = planAssistantMcpDiscovery({
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.semantic_data_need).toBe("movement evidence");
expect(result.selected_chain_id).toBe("movement_evidence");
expect(result.selected_chain_summary).toContain("movement rows");
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");
});
it("builds an inference-safe lifecycle plan with evidence explanation", () => {
const result = planAssistantMcpDiscovery({
turnMeaning: {
@ -88,6 +143,58 @@ describe("assistant MCP discovery planner", () => {
expect(result.catalog_review.evidence_floors.inspect_1c_metadata).toBe("source_summary");
});
it("keeps broad metadata surface inspection on inspect_1c_metadata", () => {
const result = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "metadata",
asked_action_family: "inspect_surface",
explicit_entity_candidates: ["НДС"]
}
});
expect(result.planner_status).toBe("ready_for_execution");
expect(result.semantic_data_need).toBe("1C metadata evidence");
expect(result.proposed_primitives).toEqual(["inspect_1c_metadata"]);
expect(result.proposed_primitives).not.toContain("query_documents");
expect(result.proposed_primitives).not.toContain("query_movements");
expect(result.reason_codes).toContain("planner_selected_metadata_recipe");
});
it("keeps metadata document inspection on inspect_1c_metadata instead of query_documents", () => {
const result = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "metadata",
asked_action_family: "inspect_documents"
}
});
expect(result.planner_status).toBe("ready_for_execution");
expect(result.proposed_primitives).toEqual(["inspect_1c_metadata"]);
expect(result.proposed_primitives).not.toContain("query_documents");
});
it("keeps metadata lane-choice clarification in needs_clarification without launching MCP primitives", () => {
const result = planAssistantMcpDiscovery({
turnMeaning: {
asked_domain_family: "metadata",
asked_action_family: "resolve_next_lane",
explicit_entity_candidates: ["SVK"],
explicit_date_scope: "2020",
unsupported_but_understood_family: "metadata_lane_choice_clarification"
}
});
expect(result.planner_status).toBe("needs_clarification");
expect(result.semantic_data_need).toBe("metadata lane clarification");
expect(result.selected_chain_id).toBe("metadata_lane_clarification");
expect(result.selected_chain_summary).toContain("choose the next data lane");
expect(result.proposed_primitives).toEqual([]);
expect(result.required_axes).toEqual(["counterparty", "period", "lane_family_choice"]);
expect(result.discovery_plan.plan_status).toBe("needs_clarification");
expect(result.reason_codes).toContain("planner_selected_metadata_lane_clarification_recipe");
expect(result.reason_codes).toContain("planner_needs_more_user_or_scope_context");
});
it("does not mark an unclassified turn as executable without turn meaning context", () => {
const result = planAssistantMcpDiscovery({});
@ -95,4 +202,18 @@ describe("assistant MCP discovery planner", () => {
expect(result.discovery_plan.plan_status).toBe("needs_clarification");
expect(result.reason_codes).toContain("planner_needs_more_user_or_scope_context");
});
it("exposes an explicit entity-resolution chain instead of silently collapsing into a lifecycle lane", () => {
const result = planAssistantMcpDiscovery({
turnMeaning: {
explicit_entity_candidates: ["SVK"]
}
});
expect(result.planner_status).toBe("ready_for_execution");
expect(result.semantic_data_need).toBe("entity discovery evidence");
expect(result.selected_chain_id).toBe("entity_resolution");
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"]);
});
});

View File

@ -26,7 +26,7 @@ describe("assistant MCP discovery policy", () => {
expect(plan.rejected_primitives).toEqual(["drop_database"]);
expect(plan.requires_evidence_gate).toBe(true);
expect(plan.answer_may_use_raw_model_claims).toBe(false);
expect(plan.execution_budget).toEqual({ max_probe_count: 6, max_rows_per_probe: 500 });
expect(plan.execution_budget).toEqual({ max_probe_count: 36, max_rows_per_probe: 500 });
expect(plan.reason_codes).toContain("model_proposed_unregistered_mcp_primitive");
});

View File

@ -54,10 +54,10 @@ describe("assistant MCP discovery response candidate", () => {
requires_user_clarification: false,
answer_draft: {
answer_mode: "confirmed_with_bounded_inference",
headline: "По данным 1С найдены строки денежных движений; сумму можно называть только в рамках проверенного периода и найденных строк.",
headline: "По данным 1С найдены строки входящих денежных поступлений; сумму можно называть только в рамках проверенного периода и найденных строк.",
confirmed_lines: [
"1C value-flow rows were found for counterparty SVK",
"По найденным строкам денежных движений в 1С по контрагенту SVK за период 2020 сумма составляет 3 750 руб."
"По найденным строкам входящих денежных поступлений в 1С по контрагенту SVK за период 2020 сумма входящих денежных поступлений составляет 3 750 руб."
],
inference_lines: ["Counterparty value-flow total was calculated from confirmed 1C movement rows"],
unknown_lines: ["Full turnover outside the checked period is not proven by this MCP discovery pilot"],
@ -69,9 +69,9 @@ describe("assistant MCP discovery response candidate", () => {
);
expect(candidate.candidate_status).toBe("ready_for_guarded_use");
expect(candidate.reply_text).toContain("В 1С найдены строки денежных движений по контрагенту SVK.");
expect(candidate.reply_text).toContain("В 1С найдены строки входящих денежных поступлений по контрагенту SVK.");
expect(candidate.reply_text).toContain("3 750 руб.");
expect(candidate.reply_text).toContain("Полный оборот вне проверенного периода этим поиском не подтвержден.");
expect(candidate.reply_text).toContain("Полный объем входящих поступлений вне проверенного периода этим поиском не подтвержден.");
expect(candidate.reply_text).not.toContain("pilot_");
expect(candidate.reply_text).not.toContain("query_movements");
});
@ -151,6 +151,162 @@ describe("assistant MCP discovery response candidate", () => {
expect(candidate.reply_text).not.toContain("query_movements");
});
it("keeps monthly breakdown lines user-facing and localizes monthly inference basis", () => {
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: "По данным 1С найдены строки входящих и исходящих денежных движений; нетто и помесячная раскладка могут называться только как расчет по найденным строкам и проверенному периоду.",
confirmed_lines: [
"1C bidirectional value-flow rows were checked for counterparty SVK: incoming=found, outgoing=found",
"Помесячно: янв 2020 — получили 10 000 руб., заплатили 4 000 руб., нетто в нашу сторону 6 000 руб."
],
inference_lines: [
"Counterparty monthly net value-flow breakdown was grouped by month over confirmed incoming and outgoing 1C rows"
],
unknown_lines: [],
limitation_lines: [],
next_step_line: null
}
}
})
);
expect(candidate.reply_text).toContain("Помесячно: янв 2020");
expect(candidate.reply_text).toContain("Помесячная нетто-раскладка сгруппирована только по подтвержденным входящим и исходящим строкам 1С.");
expect(candidate.reply_text).not.toContain("Counterparty monthly net value-flow breakdown");
});
it("localizes recovered coverage facts without leaking broad-probe wording", () => {
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: "По данным 1С найдены строки исходящих платежей/списаний; сумму можно называть только в рамках проверенного периода и найденных строк.",
confirmed_lines: ["1C supplier-payout rows were found for counterparty SVK"],
inference_lines: [
"Requested period coverage was recovered through monthly 1C value-flow probes after the broad probe hit the row limit"
],
unknown_lines: [],
limitation_lines: [],
next_step_line: null
}
}
})
);
expect(candidate.reply_text).toContain("Покрытие запрошенного периода восстановлено помесячными проверками 1С");
expect(candidate.reply_text).not.toContain("broad probe hit the row limit");
});
it("localizes document evidence without leaking raw English facts", () => {
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: "По данным 1С найдены строки документов; ответ ограничен проверенным периодом и найденными строками.",
confirmed_lines: ["1C document rows were found for counterparty SVK"],
inference_lines: ["Counterparty document evidence is limited to confirmed 1C document rows in the checked scope"],
unknown_lines: ["Full document history outside the checked period is not proven by this MCP discovery pilot"],
limitation_lines: [],
next_step_line: null
}
}
})
);
expect(candidate.reply_text).toContain("В 1С найдены строки документов по контрагенту SVK.");
expect(candidate.reply_text).toContain("Срез документов ограничен только подтвержденными строками документов");
expect(candidate.reply_text).toContain("Полный исторический срез документов вне проверенного периода этим поиском не подтвержден.");
expect(candidate.reply_text).not.toContain("1C document rows were found");
});
it("localizes movement evidence without leaking raw English facts", () => {
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: "По данным 1С найдены строки движений; ответ ограничен проверенным периодом и найденными строками.",
confirmed_lines: ["1C movement rows were found for counterparty SVK"],
inference_lines: ["Counterparty movement evidence is limited to confirmed 1C movement rows in the checked scope"],
unknown_lines: ["Full movement history outside the checked period is not proven by this MCP discovery pilot"],
limitation_lines: [],
next_step_line: null
}
}
})
);
expect(candidate.reply_text).toContain("Р1С найдены строки движений по контрагенту SVK.");
expect(candidate.reply_text).toContain("Срез движений ограничен только подтвержденными строками движений");
expect(candidate.reply_text).toContain("Полный исторический срез движений вне проверенного периода этим поиском не подтвержден.");
expect(candidate.reply_text).not.toContain("1C movement rows were found");
});
it("localizes metadata evidence without leaking raw MCP wording", () => {
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: "По данным 1С найдена подтвержденная metadata-поверхность.",
confirmed_lines: [
'Confirmed 1C metadata surface for scope "НДС": 7 rows and 3 matching objects',
"Available metadata object sets: accumulation_register, document",
"Selected metadata entity set: Документ",
"Selected metadata objects: Документ.СчетФактураВыданный",
"Available metadata fields/sections: amount, vat_rate, organization"
],
inference_lines: [
"A likely next checked lane may be inferred as document_evidence from the confirmed metadata surface"
],
unknown_lines: [
'No matching 1C metadata objects were confirmed for scope "Прибыль"',
"Exact downstream metadata surface remains ambiguous across: Документ, РегистрНакопления"
],
limitation_lines: ["Detailed metadata fields were not returned by this MCP metadata probe"],
next_step_line: null
}
}
})
);
expect(candidate.reply_text).toContain('В 1С подтверждена metadata-поверхность по области "НДС"');
expect(candidate.reply_text).toContain("Доступные типы metadata-объектов");
expect(candidate.reply_text).toContain("Выбранное семейство metadata-объектов: Документ");
expect(candidate.reply_text).toContain("Выбранные metadata-объекты для следующего шага");
expect(candidate.reply_text).toContain("Доступные metadata-поля/секции");
expect(candidate.reply_text).toContain("контур документов");
expect(candidate.reply_text).toContain('В 1С не подтверждены metadata-объекты по области "Прибыль"');
expect(candidate.reply_text).toContain("неоднозначна между family");
expect(candidate.reply_text).toContain("Эта MCP-проверка metadata не вернула детальный список полей");
expect(candidate.reply_text).not.toContain("Confirmed 1C metadata surface");
});
it("returns not applicable when discovery was skipped for an exact supported route", () => {
const candidate = buildAssistantMcpDiscoveryResponseCandidate({
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
@ -195,6 +351,34 @@ describe("assistant MCP discovery response candidate", () => {
expect(candidate.eligible_for_future_hot_runtime).toBe(true);
});
it("surfaces metadata lane-choice clarification as a user-facing clarification candidate", () => {
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
entryPoint({
bridge: {
bridge_status: "needs_clarification",
user_facing_response_allowed: true,
business_fact_answer_allowed: false,
requires_user_clarification: true,
answer_draft: {
answer_mode: "needs_clarification",
headline: "По подтвержденной metadata-поверхности видно несколько конкурирующих data-lane, и без явного выбора дальше идти нельзя.",
confirmed_lines: [],
inference_lines: [],
unknown_lines: [],
limitation_lines: [],
next_step_line: "Уточните, в какой контур идти дальше: по документам или по движениям/регистрам."
}
}
})
);
expect(candidate.candidate_status).toBe("clarification_candidate");
expect(candidate.reply_type).toBe("clarification_required");
expect(candidate.reply_text).toContain("data-lane");
expect(candidate.reply_text).toContain("по документам");
expect(candidate.reply_text).toContain("по движениям/регистрам");
});
it("does not expose unsupported bridge output as a future hot candidate", () => {
const candidate = buildAssistantMcpDiscoveryResponseCandidate(
entryPoint({

View File

@ -113,11 +113,17 @@ describe("assistant MCP discovery response policy", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: "stale exact route answer",
currentReplySource: "address_query_runtime_v1",
currentReplyType: "factual",
addressRuntimeMeta: {
detected_intent: "list_documents_by_counterparty",
assistant_mcp_discovery_entry_point_v1: entryPoint({
turn_input: {
adapter_status: "ready",
should_run_discovery: true
should_run_discovery: true,
turn_meaning_ref: {
asked_domain_family: "counterparty_value",
asked_action_family: "payout"
}
}
})
}
@ -129,6 +135,221 @@ describe("assistant MCP discovery response policy", () => {
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_not_discovery_ready_address_candidate");
});
it("keeps aligned factual address lane answers when the exact lane already matched the same semantic intent", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: "ИП Калинин Н.М. | сумма: 216600 | операций: 2",
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,
turn_meaning_ref: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover"
}
}
})
}
});
expect(result.applied).toBe(false);
expect(result.decision).toBe("keep_current_reply");
expect(result.reply_text).toBe("ИП Калинин Н.М. | сумма: 216600 | операций: 2");
expect(result.reason_codes).toContain("mcp_discovery_response_policy_keep_aligned_factual_address_reply");
});
it("keeps factual address follow-up replies when they already match the continuation target intent", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: "ИП Калинин Н.М. | сумма: 216600 | операций: 2",
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,
turn_meaning_ref: {
asked_domain_family: "counterparty_value",
asked_action_family: "turnover"
}
}
})
}
});
expect(result.applied).toBe(false);
expect(result.decision).toBe("keep_current_reply");
expect(result.reason_codes).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",
currentReplySource: "address_query_runtime_v1",
currentReplyType: "factual",
addressRuntimeMeta: {
detected_intent: "receivables_confirmed_as_of_date",
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,
turn_meaning_ref: {
asked_domain_family: "receivables",
asked_action_family: "confirmed_snapshot"
}
}
})
}
});
expect(result.applied).toBe(false);
expect(result.decision).toBe("keep_current_reply");
expect(result.reply_text).toBe("ООО Ромашка | сумма: 128000 | операций: 3");
expect(result.reason_codes).toContain("mcp_discovery_response_policy_keep_full_confirmed_factual_address_reply");
});
it("overrides a stale full-confirmed lifecycle reply when discovery proves a different net-flow question", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: "Коротко: активных заказчиков в 2020 году — 1.",
currentReplySource: "address_query_runtime_v1",
currentReplyType: "factual",
addressRuntimeMeta: {
detected_intent: "counterparty_activity_lifecycle",
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"
}
},
dialog_continuation_contract_v2: {
target_intent: "counterparty_activity_lifecycle"
},
assistant_mcp_discovery_entry_point_v1: entryPoint({
turn_input: {
adapter_status: "ready",
should_run_discovery: true,
turn_meaning_ref: {
asked_domain_family: "counterparty_value",
asked_action_family: "net_value_flow",
explicit_entity_candidates: ["Группа СВК"],
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020",
unsupported_but_understood_family: "counterparty_bidirectional_value_flow_or_netting"
}
},
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: ["Получили 47 628 853,03 руб.; заплатили 43 763 351,53 руб.; нетто 3 865 501,50 руб."],
inference_lines: [],
unknown_lines: ["Полное сальдо вне проверенного окна не подтверждено."],
limitation_lines: [],
next_step_line: null
}
}
})
}
});
expect(result.applied).toBe(true);
expect(result.decision).toBe("apply_candidate");
expect(result.reply_source).toBe("mcp_discovery_response_candidate_guarded");
expect(result.reply_text).toContain("47 628 853,03");
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_full_confirmed_factual_address_reply");
});
it("keeps runtime-adjusted exact VAT follow-up replies over stale discovery turn meaning", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: "Коротко: подтвержденный НДС к уплате за май 2017 — 0,00 руб.",
currentReplySource: "address_query_runtime_v1",
currentReplyType: "factual",
addressRuntimeMeta: {
detected_intent: "vat_liability_confirmed_for_tax_period",
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"
},
answer_shape: {
reply_type: "factual",
capability_contract_id: "confirmed_vat_liability_for_tax_period"
}
},
assistant_state_transition_v1: {
reason_codes: [
"root_followup_continue_previous",
"intent_adjusted_to_vat_followup_context",
"period_from_from_followup_context",
"period_to_from_followup_context",
"confirmed_balance_exact_vat_tax_period_intent"
]
},
assistant_mcp_discovery_entry_point_v1: entryPoint({
turn_input: {
adapter_status: "ready",
should_run_discovery: true,
turn_meaning_ref: {
asked_domain_family: "counterparty_value",
asked_action_family: "confirmed_snapshot",
explicit_entity_candidates: ["ООО Альтернатива Плюс"],
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2017-05-31",
unsupported_but_understood_family: "counterparty_value_or_turnover"
}
},
bridge: {
bridge_status: "checked_sources_only",
user_facing_response_allowed: true,
business_fact_answer_allowed: false,
requires_user_clarification: false,
answer_draft: {
answer_mode: "checked_sources_only",
headline: "Я проверил доступный контур, но подтвержденного факта для ответа не получил.",
confirmed_lines: [],
inference_lines: [],
unknown_lines: ["Полный объем входящих поступлений вне проверенного периода этим поиском не подтвержден."],
limitation_lines: [],
next_step_line: null
}
}
})
}
});
expect(result.applied).toBe(false);
expect(result.decision).toBe("keep_current_reply");
expect(result.reply_text).toContain("подтвержденный НДС к уплате");
expect(result.reason_codes).toContain(
"mcp_discovery_response_policy_keep_runtime_adjusted_exact_reply_over_stale_discovery_turn_meaning"
);
expect(result.reason_codes).not.toContain("mcp_discovery_response_policy_semantic_conflict_allows_candidate_override");
});
it("keeps address lane answers when discovery was not requested for the current turn", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: "supported exact route answer",
@ -171,4 +392,50 @@ describe("assistant MCP discovery response policy", () => {
expect(result.reason_codes).toContain("mcp_discovery_response_policy_candidate_not_eligible");
expect(result.reason_codes).toContain("mcp_discovery_response_policy_kept_current_reply");
});
it("keeps deterministic broad business evaluation summary instead of replacing it with a clarification candidate", () => {
const result = applyAssistantMcpDiscoveryResponsePolicy({
currentReply: "Коротко: по уже подтвержденным данным в 1С компания выглядит живой операционно.",
currentReplySource: "deterministic_broad_business_evaluation_contract",
livingChatSource: "deterministic_broad_business_evaluation_contract",
modeDecisionReason: "unsupported_current_turn_meaning_boundary",
addressRuntimeMeta: {
assistant_mcp_discovery_entry_point_v1: entryPoint({
turn_input: {
adapter_status: "ready",
should_run_discovery: true,
turn_meaning_ref: {
asked_domain_family: "business_summary",
asked_action_family: "broad_evaluation",
unsupported_but_understood_family: "broad_business_evaluation",
stale_replay_forbidden: true
}
},
bridge: {
bridge_status: "needs_clarification",
user_facing_response_allowed: true,
business_fact_answer_allowed: false,
requires_user_clarification: true,
answer_draft: {
answer_mode: "needs_clarification",
headline: "Нужно уточнить контекст перед поиском в 1С.",
confirmed_lines: [],
inference_lines: [],
unknown_lines: ["MCP discovery pilot needs more scope before execution"],
limitation_lines: ["MCP discovery pilot needs more scope before execution"],
next_step_line: "Уточните контрагента, период или организацию."
}
}
})
}
});
expect(result.applied).toBe(false);
expect(result.decision).toBe("keep_current_reply");
expect(result.reply_source).toBe("deterministic_broad_business_evaluation_contract");
expect(result.reply_text).toContain("компания выглядит живой операционно");
expect(result.reason_codes).toContain(
"mcp_discovery_response_policy_keep_broad_business_summary_over_clarification_candidate"
);
});
});

View File

@ -51,7 +51,7 @@ describe("assistant MCP discovery runtime bridge", () => {
expect(result.answer_draft.next_step_line).toContain("Уточните контрагента");
});
it("keeps unsupported ready plans outside the hot answer path", async () => {
it("keeps document-ready plans bounded when the pilot finds no confirmed rows", async () => {
const result = await runAssistantMcpDiscoveryRuntimeBridge({
turnMeaning: {
asked_domain_family: "document",
@ -61,11 +61,12 @@ describe("assistant MCP discovery runtime bridge", () => {
deps: buildDeps([])
});
expect(result.bridge_status).toBe("unsupported");
expect(result.bridge_status).toBe("checked_sources_only");
expect(result.hot_runtime_wired).toBe(false);
expect(result.pilot.mcp_execution_performed).toBe(false);
expect(result.pilot.pilot_scope).toBe("counterparty_document_evidence_query_documents_v1");
expect(result.pilot.mcp_execution_performed).toBe(true);
expect(result.business_fact_answer_allowed).toBe(false);
expect(result.reason_codes).toContain("runtime_bridge_status_unsupported");
expect(result.reason_codes).toContain("runtime_bridge_status_checked_sources_only");
});
it("preserves the answer adapter boundary against internal mechanics leakage", async () => {

View File

@ -13,6 +13,17 @@ function buildDeps(rows: Array<Record<string, unknown>>, error: string | null =
};
}
function buildMetadataDeps(rows: Array<Record<string, unknown>>, error: string | null = null) {
return {
executeAddressMcpMetadata: vi.fn(async () => ({
fetched_rows: error ? 0 : rows.length,
raw_rows: error ? [] : rows,
rows: error ? [] : rows,
error
}))
};
}
describe("assistant MCP discovery runtime entry point", () => {
it("runs the bridge for discovery-eligible lifecycle turn context", async () => {
const result = await runAssistantMcpDiscoveryRuntimeEntryPoint({
@ -78,4 +89,155 @@ describe("assistant MCP discovery runtime entry point", () => {
expect(result.bridge?.hot_runtime_wired).toBe(false);
expect(result.reason_codes).toContain("mcp_discovery_unsupported_but_understood_turn");
});
it("runs the bridge for raw metadata wording without an exact route owner", async () => {
const result = await runAssistantMcpDiscoveryRuntimeEntryPoint({
userMessage: "какие документы и поля есть в 1С по НДС?",
deps: buildMetadataDeps([
{
FullName: "Документ.СчетФактураВыданный",
MetaType: "Документ",
attributes: [{ Name: "Дата" }]
}
])
});
expect(result.entry_status).toBe("bridge_executed");
expect(result.discovery_attempted).toBe(true);
expect(result.turn_input.semantic_data_need).toBe("1C metadata evidence");
expect(result.turn_input.turn_meaning_ref).toMatchObject({
asked_domain_family: "metadata",
asked_action_family: "inspect_fields"
});
expect(result.bridge?.pilot.pilot_scope).toBe("metadata_inspection_v1");
expect(result.bridge?.answer_draft.headline).toContain("метаданным 1С");
});
it("runs the bridge for raw entity-resolution wording and executes the full grounding chain", async () => {
const result = await runAssistantMcpDiscoveryRuntimeEntryPoint({
userMessage: "найди в 1С контрагента Группа СВК",
deps: buildDeps([
{ Counterparty: "Группа СВК", CounterpartyRef: "Ref-1" },
{ Counterparty: "СВК Логистика", CounterpartyRef: "Ref-2" }
])
});
expect(result.entry_status).toBe("bridge_executed");
expect(result.discovery_attempted).toBe(true);
expect(result.turn_input.semantic_data_need).toBe("entity discovery evidence");
expect(result.turn_input.turn_meaning_ref).toMatchObject({
asked_domain_family: "entity_resolution",
asked_action_family: "search_business_entity",
explicit_entity_candidates: ["Группа СВК"]
});
expect(result.bridge?.bridge_status).toBe("answer_draft_ready");
expect(result.bridge?.pilot.pilot_scope).toBe("entity_resolution_search_v1");
expect(result.bridge?.pilot.executed_primitives).toEqual([
"search_business_entity",
"resolve_entity_reference",
"probe_coverage"
]);
expect(result.bridge?.pilot.derived_entity_resolution).toMatchObject({
resolution_status: "resolved",
resolved_entity: "Группа СВК",
resolved_reference: "Ref-1"
});
expect(result.bridge?.answer_draft.answer_mode).toBe("confirmed_with_bounded_inference");
});
it("runs the bridge again when the user clarifies one ambiguous entity-resolution candidate", async () => {
const result = await runAssistantMcpDiscoveryRuntimeEntryPoint({
userMessage: "СВК-А",
followupContext: {
previous_discovery_pilot_scope: "entity_resolution_search_v1",
previous_discovery_entity_resolution_status: "ambiguous",
previous_discovery_entity_candidates: ["СВК", "СВК-А", "СВК-Б"],
previous_discovery_entity_ambiguity_candidates: ["СВК-А", "СВК-Б"]
},
deps: buildDeps([
{ Counterparty: "СВК-А", CounterpartyRef: "Ref-1" },
{ Counterparty: "СВК-Б", CounterpartyRef: "Ref-2" }
])
});
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.semantic_data_need).toBe("entity discovery evidence");
expect(result.turn_input.turn_meaning_ref).toMatchObject({
asked_domain_family: "entity_resolution",
asked_action_family: "search_business_entity",
explicit_entity_candidates: ["СВК-А"]
});
expect(result.bridge?.pilot.pilot_scope).toBe("entity_resolution_search_v1");
expect(result.bridge?.pilot.executed_primitives).toEqual([
"search_business_entity",
"resolve_entity_reference",
"probe_coverage"
]);
expect(result.bridge?.pilot.derived_entity_resolution).toMatchObject({
resolution_status: "resolved",
resolved_entity: "СВК-А",
resolved_reference: "Ref-1"
});
});
it("chains an ordinal ambiguity clarification directly into value-flow execution", async () => {
const result = await runAssistantMcpDiscoveryRuntimeEntryPoint({
userMessage: "второй вариант, сколько получили за 2020 год",
followupContext: {
previous_discovery_pilot_scope: "entity_resolution_search_v1",
previous_discovery_entity_resolution_status: "ambiguous",
previous_discovery_entity_candidates: ["СВК", "СВК-А", "СВК-Б"],
previous_discovery_entity_ambiguity_candidates: ["СВК-А", "СВК-Б"]
},
deps: buildDeps([
{ Period: "2020-01-15T00:00:00", Amount: 1200, Counterparty: "СВК-Б" },
{ Period: "2020-02-20T00:00:00", Amount: 800, 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.semantic_data_need).toBe("counterparty value-flow evidence");
expect(result.turn_input.turn_meaning_ref).toMatchObject({
asked_domain_family: "counterparty_value",
asked_action_family: "turnover",
explicit_entity_candidates: ["СВК-Б"],
explicit_date_scope: "2020"
});
expect(result.bridge?.pilot.pilot_scope).toBe("counterparty_value_flow_query_movements_v1");
expect(result.bridge?.pilot.derived_value_flow).toMatchObject({
counterparty: "СВК-Б",
period_scope: "2020",
total_amount: 2000
});
});
it("chains an ordinal ambiguity clarification directly into document evidence execution", async () => {
const result = await runAssistantMcpDiscoveryRuntimeEntryPoint({
userMessage: "первый вариант, покажи документы за 2020 год",
followupContext: {
previous_discovery_pilot_scope: "entity_resolution_search_v1",
previous_discovery_entity_resolution_status: "ambiguous",
previous_discovery_entity_candidates: ["СВК", "СВК-А", "СВК-Б"],
previous_discovery_entity_ambiguity_candidates: ["СВК-А", "СВК-Б"]
},
deps: buildDeps([{ Period: "2020-03-12T00:00:00", Counterparty: "СВК-А", Registrar: "Doc-1" }])
});
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.semantic_data_need).toBe("document evidence");
expect(result.turn_input.turn_meaning_ref).toMatchObject({
asked_domain_family: "documents",
asked_action_family: "list_documents",
explicit_entity_candidates: ["СВК-А"],
explicit_date_scope: "2020"
});
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");
});
});

View File

@ -1,6 +1,7 @@
import { describe, expect, it } from "vitest";
import {
buildAddressMemoryRecapReply,
buildBroadBusinessEvaluationReply,
buildSelectedObjectAnswerInspectionReply,
createAssistantMemoryRecapPolicy,
resolveAssistantLivingChatMemoryContext
@ -133,6 +134,50 @@ describe("assistantMemoryRecapPolicy", () => {
expect(signals.contextualMemoryRecapFollowupDetected).toBe(false);
});
it("detects contextual memory recap over prior grounded MCP discovery answer", () => {
const signals = policy.resolveRouteMemorySignals({
rawUserMessage: "а ты помнишь, что мы уже выяснили по свк?",
repairedRawUserMessage: "",
effectiveAddressUserMessage: "",
repairedEffectiveAddressUserMessage: "",
dataScopeMetaQuery: false,
capabilityMetaQuery: false,
dataRetrievalSignal: false,
strongDataSignal: false,
aggregateBusinessAnalyticsSignal: false,
lastGroundedAddressDebug: null,
hasPriorAddressDebug: true,
sessionItems: [
{
role: "assistant",
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: {
turn_meaning_ref: {
explicit_entity_candidates: ["Группа СВК"],
explicit_date_scope: "2020"
}
},
bridge: {
bridge_status: "answer_draft_ready",
business_fact_answer_allowed: true,
answer_draft: {
answer_mode: "confirmed_with_bounded_inference"
}
}
}
}
}
]
});
expect(signals.contextualMemoryRecapFollowupDetected).toBe(true);
});
it("builds deterministic recap summary from recent selected-object facts", () => {
const context = resolveAssistantLivingChatMemoryContext({
modeDecisionReason: "memory_recap_followup_detected",
@ -278,4 +323,241 @@ describe("assistantMemoryRecapPolicy", () => {
expect(reply).toContain("Рабочая станция");
expect(reply).toContain("Покупатель");
});
it("builds deterministic recap summary from grounded MCP discovery counterparty context", () => {
const sessionItems = [
{
role: "assistant",
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: {
turn_meaning_ref: {
explicit_entity_candidates: ["Группа СВК"],
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020"
}
},
bridge: {
bridge_status: "answer_draft_ready",
business_fact_answer_allowed: true,
answer_draft: {
answer_mode: "confirmed_with_bounded_inference"
},
pilot: {
pilot_scope: "counterparty_bidirectional_value_flow_query_movements_v1",
derived_bidirectional_value_flow: {
net_amount_human_ru: "3 865 501,50 руб.",
incoming_customer_revenue: {
total_amount_human_ru: "47 628 853,03 руб."
},
outgoing_supplier_payout: {
total_amount_human_ru: "43 763 351,53 руб."
}
}
}
}
}
}
}
];
const context = resolveAssistantLivingChatMemoryContext({
modeDecisionReason: "memory_recap_followup_detected",
sessionItems
});
const reply = buildAddressMemoryRecapReply({
organization: null,
addressDebug: context.lastMemoryAddressDebug,
sessionItems,
toNonEmptyString: (value: unknown) => {
const text = String(value ?? "").trim();
return text.length > 0 ? text : null;
}
});
expect(context.contextualMemoryRecapFollowup).toBe(true);
expect(reply).toContain("Группа СВК");
expect(reply).toContain("нетто");
expect(reply).toContain("47 628 853,03 руб.");
expect(reply).toContain("43 763 351,53 руб.");
});
it("builds deterministic recap summary from grounded MCP metadata discovery context", () => {
const sessionItems = [
{
role: "assistant",
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: {
turn_meaning_ref: {
explicit_entity_candidates: ["НДС"]
}
},
bridge: {
bridge_status: "answer_draft_ready",
business_fact_answer_allowed: true,
answer_draft: {
answer_mode: "confirmed_with_bounded_inference"
},
pilot: {
pilot_scope: "metadata_inspection_v1",
derived_metadata_surface: {
metadata_scope: "НДС",
matched_rows: 7,
matched_objects: ["РегистрНакопления.НДСПокупок"],
available_entity_sets: ["accumulation_register", "document"],
available_fields: ["amount", "vat_rate", "organization"]
}
}
}
}
}
}
];
const context = resolveAssistantLivingChatMemoryContext({
modeDecisionReason: "memory_recap_followup_detected",
sessionItems
});
const reply = buildAddressMemoryRecapReply({
organization: null,
addressDebug: context.lastMemoryAddressDebug,
sessionItems,
toNonEmptyString: (value: unknown) => {
const text = String(value ?? "").trim();
return text.length > 0 ? text : null;
}
});
expect(context.contextualMemoryRecapFollowup).toBe(true);
expect(reply).toContain("НДС");
expect(reply).toContain("metadata-поверхность 1С");
expect(reply).toContain("amount");
expect(reply).toContain("accumulation_register");
});
it("builds deterministic broad business evaluation summary from recent grounded organization facts", () => {
const sessionItems = [
{
role: "assistant",
debug: {
execution_lane: "address_query",
answer_grounding_check: {
status: "grounded"
},
detected_intent: "counterparty_activity_lifecycle",
extracted_filters: {
organization: "ООО Альтернатива Плюс"
}
}
},
{
role: "assistant",
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: {
turn_meaning_ref: {
explicit_entity_candidates: ["Группа СВК"],
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020"
}
},
bridge: {
bridge_status: "answer_draft_ready",
business_fact_answer_allowed: true,
answer_draft: {
answer_mode: "confirmed_with_bounded_inference"
},
pilot: {
pilot_scope: "counterparty_bidirectional_value_flow_query_movements_v1",
derived_bidirectional_value_flow: {
net_amount_human_ru: "3 865 501,50 руб.",
incoming_customer_revenue: {
total_amount_human_ru: "47 628 853,03 руб."
},
outgoing_supplier_payout: {
total_amount_human_ru: "43 763 351,53 руб."
}
}
}
}
}
}
}
];
const reply = buildBroadBusinessEvaluationReply({
organization: "ООО Альтернатива Плюс",
addressDebug: sessionItems[1].debug as any,
sessionItems,
toNonEmptyString: (value: unknown) => {
const text = String(value ?? "").trim();
return text.length > 0 ? text : null;
}
});
expect(reply.toLowerCase()).toContain("оценка бизнеса");
expect(reply).toContain("ООО Альтернатива Плюс");
expect(reply).toContain("47 628 853,03");
});
it("builds grounded answer inspection reply for MCP discovery net answer", () => {
const context = resolveAssistantLivingChatMemoryContext({
modeDecisionReason: "answer_inspection_followup_detected",
sessionItems: [
{
role: "assistant",
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: {
turn_meaning_ref: {
explicit_entity_candidates: ["Группа СВК"],
explicit_date_scope: "2020"
}
},
bridge: {
bridge_status: "answer_draft_ready",
business_fact_answer_allowed: true,
answer_draft: {
answer_mode: "confirmed_with_bounded_inference"
},
pilot: {
pilot_scope: "counterparty_bidirectional_value_flow_query_movements_v1"
}
}
}
}
}
]
});
const reply = buildSelectedObjectAnswerInspectionReply({
addressDebug: context.lastAnswerInspectionAddressDebug,
toNonEmptyString: (value: unknown) => {
const text = String(value ?? "").trim();
return text.length > 0 ? text : null;
}
});
expect(context.contextualAnswerInspectionFollowup).toBe(true);
expect(reply).toContain("Группа СВК");
expect(reply).toContain("Нетто");
expect(reply).toContain("проверенному периоду");
});
});

View File

@ -606,7 +606,7 @@ describe("assistantRoutePolicy", () => {
expect(decision.orchestrationContract?.organization_scope_switch_detected).not.toBe(true);
});
it("keeps company activity assessment follow-up in address lane when lifecycle intent is resolved from grounded continuity", () => {
it("routes broad business evaluation follow-up to chat instead of replaying lifecycle address intent", () => {
const policy = buildPolicy({
resolveAddressIntent: () => ({ intent: "counterparty_activity_lifecycle", confidence: "high" }),
findLastGroundedAddressAnswerDebug: () => ({
@ -618,6 +618,15 @@ describe("assistantRoutePolicy", () => {
period_to: "2026-04-18"
}
}),
resolveAssistantTurnMeaning: () => ({
schema_version: "assistant_turn_meaning_v1",
asked_domain_family: "business_summary",
asked_action_family: "broad_evaluation",
explicit_intent_candidate: null,
unsupported_but_understood_family: "broad_business_evaluation",
stale_replay_forbidden: true,
reason_codes: ["broad_business_evaluation_current_turn_signal"]
}),
resolveAddressToolGateDecision: () => ({
runAddressLane: false,
decision: "skip_address_lane",
@ -666,9 +675,12 @@ describe("assistantRoutePolicy", () => {
useMock: false
});
expect(decision.runAddressLane).toBe(true);
expect(decision.toolGateDecision).toBe("run_address_lane");
expect(decision.livingMode).toBe("address_data");
expect(decision.runAddressLane).toBe(false);
expect(decision.toolGateDecision).toBe("skip_address_lane");
expect(decision.toolGateReason).toBe("unsupported_current_turn_meaning_boundary");
expect(decision.livingMode).toBe("chat");
expect(decision.livingReason).toBe("unsupported_current_turn_meaning_boundary");
expect(decision.orchestrationContract?.unsupported_current_turn_family).toBe("broad_business_evaluation");
});
it("recovers an address route from current-turn meaning when L0 resolver is noisy", () => {

View File

@ -796,6 +796,7 @@ describe("assistantTransitionPolicy", () => {
it("retargets selected-object provenance follow-up from inventory root when semantic scope is already detected", () => {
const policy = buildPolicy({
hasAddressFollowupContextSignal: () => true,
findLastAddressAssistantItem: () => ({
text: "На 31.03.2016 на складе подтверждено 2 позиции.",
debug: {
@ -810,8 +811,7 @@ describe("assistantTransitionPolicy", () => {
anchor_type: "organization",
anchor_value_resolved: 'ООО "Альтернатива Плюс"'
}
}),
hasAddressFollowupContextSignal: () => true
})
});
const carryover = policy.resolveAddressFollowupCarryoverContext(
@ -1013,4 +1013,490 @@ describe("assistantTransitionPolicy", () => {
expect(carryover).toBeNull();
});
it("drops carryover for broad business evaluation so lifecycle context does not stick to the new question", () => {
const policy = buildPolicy({
findLastAddressAssistantItem: () => ({
text: "Lifecycle answer",
debug: {
execution_lane: "address_query",
answer_grounding_check: { status: "grounded" },
detected_intent: "counterparty_activity_lifecycle",
extracted_filters: {
organization: 'ООО "Альтернатива Плюс"',
period_to: "2020-12-31"
},
anchor_type: "organization",
anchor_value_resolved: 'ООО "Альтернатива Плюс"'
}
}),
hasAddressFollowupContextSignal: () => true,
resolveAssistantTurnMeaning: () => ({
schema_version: "assistant_turn_meaning_v1",
asked_domain_family: "business_summary",
asked_action_family: "broad_evaluation",
explicit_intent_candidate: null,
unsupported_but_understood_family: "broad_business_evaluation",
explicit_entity_candidates: [],
stale_replay_forbidden: true
})
});
const carryover = policy.resolveAddressFollowupCarryoverContext(
"Как ты оценишь деятельность компании?",
[],
null,
null,
null
);
expect(carryover).toBeNull();
});
it("reuses grounded MCP discovery payout context for a short year-switch follow-up", () => {
const policy = buildPolicy({
findLastAddressAssistantItem: () => null,
hasAddressFollowupContextSignal: () => true
});
const carryover = policy.resolveAddressFollowupCarryoverContext(
"а теперь за 2021?",
[
{
role: "assistant",
text: "Подтверждены исходящие платежи по Группа СВК за 2020 год.",
debug: {
execution_lane: "living_chat",
mcp_discovery_response_applied: true,
assistant_active_organization: "ООО Альтернатива Плюс",
assistant_mcp_discovery_entry_point_v1: {
schema_version: "assistant_mcp_discovery_runtime_entry_point_v1",
entry_status: "bridge_executed",
turn_input: {
turn_meaning_ref: {
asked_action_family: "payout",
explicit_entity_candidates: ["Группа СВК"],
explicit_organization_scope: "ООО Альтернатива Плюс",
explicit_date_scope: "2020"
}
},
bridge: {
bridge_status: "answer_draft_ready",
business_fact_answer_allowed: true,
pilot: {
pilot_scope: "counterparty_supplier_payout_query_movements_v1"
},
answer_draft: {
answer_mode: "confirmed_with_bounded_inference"
}
}
}
}
}
],
null,
null,
null
);
expect(carryover?.followupSelectionMode).toBe("carry_previous_intent");
expect(carryover?.followupContext?.previous_intent).toBe("supplier_payouts_profile");
expect(carryover?.followupContext?.target_intent).toBe("supplier_payouts_profile");
expect(carryover?.followupContext?.previous_discovery_pilot_scope).toBe(
"counterparty_supplier_payout_query_movements_v1"
);
expect(carryover?.followupContext?.previous_anchor_type).toBe("counterparty");
expect(carryover?.followupContext?.previous_anchor_value).toBe("Группа СВК");
expect(carryover?.followupContext?.previous_filters).toMatchObject({
counterparty: "Группа СВК",
organization: "ООО Альтернатива Плюс",
period_from: "2020-01-01",
period_to: "2020-12-31"
});
});
it("carries resolved entity candidates from grounded entity-resolution discovery into followup context", () => {
const policy = buildPolicy({
findLastAddressAssistantItem: () => null,
hasAddressFollowupContextSignal: () => true
});
const carryover = policy.resolveAddressFollowupCarryoverContext(
"по нему документы за 2020 год",
[
{
role: "assistant",
text: "В текущем каталожном срезе 1С по запросу \"СВК\" найден контрагент \"Группа СВК\".",
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: {
turn_meaning_ref: {
asked_domain_family: "entity_resolution",
asked_action_family: "search_business_entity",
explicit_entity_candidates: ["СВК"]
}
},
bridge: {
bridge_status: "answer_draft_ready",
business_fact_answer_allowed: true,
pilot: {
pilot_scope: "entity_resolution_search_v1",
derived_entity_resolution: {
requested_entity: "СВК",
resolution_status: "resolved",
resolved_entity: "Группа СВК",
ambiguity_candidates: []
}
},
answer_draft: {
answer_mode: "confirmed_with_bounded_inference"
}
}
}
}
}
],
null,
null,
null
);
expect(carryover?.followupContext?.previous_discovery_pilot_scope).toBe("entity_resolution_search_v1");
expect(carryover?.followupContext?.previous_discovery_entity_candidates).toEqual(["Группа СВК", "СВК"]);
expect(carryover?.followupContext?.previous_anchor_type).toBe("counterparty");
expect(carryover?.followupContext?.previous_anchor_value).toBe("Группа СВК");
expect(carryover?.followupContext?.previous_filters).toMatchObject({
counterparty: "Группа СВК"
});
});
it("carries ambiguity candidates from entity-resolution discovery into followup context", () => {
const policy = buildPolicy({
findLastAddressAssistantItem: () => null,
hasAddressFollowupContextSignal: () => true
});
const carryover = policy.resolveAddressFollowupCarryoverContext(
"СВК-А",
[
{
role: "assistant",
text: "По каталогу 1С нашлось несколько похожих контрагентов: СВК-А, СВК-Б.",
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: {
turn_meaning_ref: {
asked_domain_family: "entity_resolution",
asked_action_family: "search_business_entity",
explicit_entity_candidates: ["СВК"]
}
},
bridge: {
bridge_status: "answer_draft_ready",
business_fact_answer_allowed: false,
pilot: {
pilot_scope: "entity_resolution_search_v1",
derived_entity_resolution: {
requested_entity: "СВК",
resolution_status: "ambiguous",
resolved_entity: null,
ambiguity_candidates: ["СВК-А", "СВК-Б"]
}
},
answer_draft: {
answer_mode: "needs_clarification"
}
}
}
}
}
],
null,
null,
null
);
expect(carryover?.followupContext?.previous_discovery_pilot_scope).toBe("entity_resolution_search_v1");
expect(carryover?.followupContext?.previous_discovery_entity_resolution_status).toBe("ambiguous");
expect(carryover?.followupContext?.previous_discovery_entity_candidates).toEqual(["СВК", "СВК-А", "СВК-Б"]);
expect(carryover?.followupContext?.previous_discovery_entity_ambiguity_candidates).toEqual([
"СВК-А",
"СВК-Б"
]);
});
it("keeps exact payout carryover for a short net follow-up without restating counterparty or year", () => {
const policy = buildPolicy({
findLastAddressAssistantItem: () => ({
role: "assistant",
text: "Платежи по Группа СВК за 2021",
debug: {
execution_lane: "address_query",
answer_grounding_check: { status: "grounded" },
detected_intent: "customer_revenue_and_payments",
selected_recipe: "address_customer_revenue_and_payments_v1",
extracted_filters: {
counterparty: "Группа СВК",
period_from: "2021-01-01",
period_to: "2021-12-31"
},
anchor_type: "counterparty",
anchor_value_resolved: "Группа СВК"
}
}),
hasAddressFollowupContextSignal: () => true
});
const carryover = policy.resolveAddressFollowupCarryoverContext(
"а какое нетто?",
[],
null,
null,
null
);
expect(carryover?.followupSelectionMode).toBe("carry_previous_intent");
expect(carryover?.followupContext?.previous_intent).toBe("customer_revenue_and_payments");
expect(carryover?.followupContext?.target_intent).toBe("customer_revenue_and_payments");
expect(carryover?.followupContext?.previous_anchor_type).toBe("counterparty");
expect(carryover?.followupContext?.previous_anchor_value).toBe("Группа СВК");
expect(carryover?.followupContext?.previous_filters).toMatchObject({
counterparty: "Группа СВК",
period_from: "2021-01-01",
period_to: "2021-12-31"
});
});
it("carries grounded metadata downstream route hints into followup context", () => {
const policy = buildPolicy({
findLastAddressAssistantItem: () => null,
hasAddressFollowupContextSignal: () => true
});
const carryover = policy.resolveAddressFollowupCarryoverContext(
"then documents",
[
{
role: "assistant",
text: "Metadata surface confirmed.",
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: {
turn_meaning_ref: {
asked_domain_family: "metadata",
asked_action_family: "inspect_documents",
explicit_entity_candidates: ["SVK"],
explicit_date_scope: "2020"
}
},
bridge: {
bridge_status: "answer_draft_ready",
business_fact_answer_allowed: true,
pilot: {
pilot_scope: "metadata_inspection_v1",
derived_metadata_surface: {
selected_entity_set: "Документ",
downstream_route_family: "document_evidence",
ambiguity_detected: false
}
},
answer_draft: {
answer_mode: "confirmed_with_bounded_inference"
}
}
}
}
}
],
null,
null,
null
);
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_selected_entity_set).toBe("Документ");
expect(carryover?.followupContext?.previous_discovery_metadata_ambiguity_detected).toBeUndefined();
});
it("carries metadata ambiguity entity sets into follow-up context for downstream lane arbitration", () => {
const policy = buildPolicy({
findLastAddressAssistantItem: () => ({
text: "metadata ambiguity",
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: {
turn_meaning_ref: {
asked_domain_family: "metadata",
asked_action_family: "inspect_documents",
explicit_entity_candidates: ["SVK"],
explicit_date_scope: "2020"
}
},
bridge: {
bridge_status: "answer_draft_ready",
business_fact_answer_allowed: true,
pilot: {
pilot_scope: "metadata_inspection_v1",
derived_metadata_surface: {
selected_entity_set: null,
downstream_route_family: null,
ambiguity_detected: true,
ambiguity_entity_sets: ["Документ", "РегистрНакопления"]
}
},
answer_draft: {
answer_mode: "confirmed_with_bounded_inference"
}
}
}
}
}),
hasAddressFollowupContextSignal: () => true,
hasReferentialPointer: () => false,
resolveAddressIntent: () => ({ intent: "unknown" }),
resolveAddressIntentFamily: () => null,
resolveAssistantTurnMeaning: () => null
});
const carryover = policy.resolveAddressFollowupCarryoverContext(
"по документам",
[{ kind: "assistant", text: "metadata ambiguity" }],
"по документам",
{ predecomposeContract: { intent: "unknown" } },
null
);
expect(carryover?.followupContext?.previous_discovery_metadata_ambiguity_detected).toBe(true);
expect(carryover?.followupContext?.previous_discovery_metadata_ambiguity_entity_sets).toEqual([
"Документ",
"РегистрНакопления"
]);
});
it("preserves metadata ambiguity choice sets through a clarification assistant turn", () => {
const policy = buildPolicy({
findLastAddressAssistantItem: () => ({
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: {
turn_meaning_ref: {
asked_domain_family: "metadata",
asked_action_family: "resolve_next_lane",
explicit_entity_candidates: ["SVK"],
metadata_ambiguity_entity_sets: ["Документ", "РегистрНакопления"],
explicit_date_scope: "2020",
unsupported_but_understood_family: "metadata_lane_choice_clarification"
}
},
bridge: {
bridge_status: "needs_clarification",
business_fact_answer_allowed: false,
pilot: {
pilot_scope: "metadata_inspection_v1"
},
answer_draft: {
answer_mode: "needs_clarification"
}
}
}
}
}),
hasAddressFollowupContextSignal: () => true,
hasReferentialPointer: () => false,
resolveAddressIntent: () => ({ intent: "unknown" }),
resolveAddressIntentFamily: () => null,
resolveAssistantTurnMeaning: () => null
});
const carryover = policy.resolveAddressFollowupCarryoverContext(
"по движениям",
[{ kind: "assistant", text: "уточните: по документам или по движениям?" }],
"по движениям",
{ predecomposeContract: { intent: "unknown" } },
null
);
expect(carryover?.followupContext?.previous_discovery_pilot_scope).toBe("metadata_inspection_v1");
expect(carryover?.followupContext?.previous_discovery_metadata_ambiguity_detected).toBe(true);
expect(carryover?.followupContext?.previous_discovery_metadata_ambiguity_entity_sets).toEqual([
"Документ",
"РегистрНакопления"
]);
});
it("switches to VAT tax-period intent while preserving carried period filters", () => {
const policy = buildPolicy({
findLastAddressAssistantItem: () => ({
text: "Подтвержденная дебиторская задолженность на 31.05.2017 собрана.",
debug: {
detected_intent: "receivables_confirmed_as_of_date",
extracted_filters: {
organization: 'ООО "Альтернатива Плюс"',
as_of_date: "2017-05-31",
period_from: "2017-05-01",
period_to: "2017-05-31"
},
anchor_type: "organization",
anchor_value_resolved: 'ООО "Альтернатива Плюс"'
}
}),
hasAddressFollowupContextSignal: () => true,
hasReferentialPointer: (value: unknown) => /этот период/i.test(String(value ?? "")),
resolveAddressIntent: () => ({ intent: "unknown" }),
resolveAddressIntentFamily: (intent: unknown) => {
if (String(intent ?? "").startsWith("receivables_")) return "receivables";
if (String(intent ?? "").startsWith("vat_")) return "vat";
return null;
},
resolveAssistantTurnMeaning: () => ({
schema_version: "assistant_turn_meaning_v1",
asked_domain_family: "vat",
asked_action_family: "confirmed_tax_period",
explicit_intent_candidate: "vat_liability_confirmed_for_tax_period",
explicit_entity_candidates: [],
intent_override_strength: "explicit_current_turn_intent",
stale_replay_forbidden: false
})
});
const carryover = policy.resolveAddressFollowupCarryoverContext(
"а какой ндс мы должны примерно заплатить за этот период?",
[],
"Какой НДС должен быть уплачен за текущий период?",
{
predecomposeContract: {
intent: "unknown"
}
},
null
);
expect(carryover?.followupSelectionMode).toBe("carry_previous_intent");
expect(carryover?.followupContext?.previous_intent).toBe("receivables_confirmed_as_of_date");
expect(carryover?.followupContext?.target_intent).toBe("vat_liability_confirmed_for_tax_period");
expect(carryover?.followupContext?.previous_filters).toMatchObject({
organization: 'ООО "Альтернатива Плюс"',
as_of_date: "2017-05-31",
period_from: "2017-05-01",
period_to: "2017-05-31"
});
});
});

View File

@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
import { createAssistantTurnMeaningPolicy } from "../src/services/assistantTurnMeaningPolicy";
import { resolveAddressIntent } from "../src/services/addressIntentResolver";
function buildPolicy() {
function buildPolicy(overrides: Record<string, unknown> = {}) {
return createAssistantTurnMeaningPolicy({
compactWhitespace: (value: string) => String(value ?? "").replace(/\s+/g, " ").trim(),
repairAddressMojibake: (value: string) => value,
@ -13,7 +13,8 @@ function buildPolicy() {
}
const text = String(value).trim();
return text.length > 0 ? text : null;
}
},
...overrides
});
}
@ -55,4 +56,58 @@ describe("assistantTurnMeaningPolicy", () => {
}
]);
});
it("ignores temporal tail words in all-time revenue ranking questions", () => {
const policy = buildPolicy({
resolveAddressIntent: (text: string) =>
text.includes("\u0434\u043e\u0445\u043e\u0434\u043d\u044b\u0439")
? { intent: "customer_revenue_and_payments", confidence: "high" }
: resolveAddressIntent(text)
});
const meaning = policy.resolveAssistantTurnMeaning({
rawUserMessage:
"\u043a\u0442\u043e \u0443 \u043d\u0430\u0441 \u0441\u0430\u043c\u044b\u0439 \u0434\u043e\u0445\u043e\u0434\u043d\u044b\u0439 \u043a\u043b\u0438\u0435\u043d\u0442 \u0437\u0430 \u0432\u0441\u0435 \u0432\u0440\u0435\u043c\u044f"
});
expect(meaning.explicit_intent_candidate).toBe("customer_revenue_and_payments");
expect(meaning.explicit_entity_candidates).toEqual([]);
expect(meaning.stale_replay_forbidden).toBe(false);
});
it("treats VAT period questions as supported current-turn intent", () => {
const policy = buildPolicy({
resolveAddressIntent: (text: string) =>
text.includes("\u043d\u0434\u0441")
? { intent: "vat_liability_confirmed_for_tax_period", confidence: "high" }
: resolveAddressIntent(text)
});
const meaning = policy.resolveAssistantTurnMeaning({
rawUserMessage:
"\u0430 \u043a\u0430\u043a\u043e\u0439 \u043d\u0434\u0441 \u043c\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u0437\u0430\u043f\u043b\u0430\u0442\u0438\u0442\u044c \u0437\u0430 \u044d\u0442\u043e\u0442 \u043f\u0435\u0440\u0438\u043e\u0434"
});
expect(meaning.explicit_intent_candidate).toBe("vat_liability_confirmed_for_tax_period");
expect(meaning.asked_domain_family).toBe("vat");
expect(meaning.asked_action_family).toBe("confirmed_tax_period");
expect(meaning.stale_replay_forbidden).toBe(false);
});
it("marks broad business evaluation as unsupported-but-understood instead of stale lifecycle replay", () => {
const policy = buildPolicy({
resolveAddressIntent: () => ({ intent: "counterparty_activity_lifecycle", confidence: "high" })
});
const meaning = policy.resolveAssistantTurnMeaning({
rawUserMessage: "Как ты оценишь деятельность компании?"
});
expect(meaning.explicit_intent_candidate).toBeNull();
expect(meaning.asked_domain_family).toBe("business_summary");
expect(meaning.asked_action_family).toBe("broad_evaluation");
expect(meaning.unsupported_but_understood_family).toBe("broad_business_evaluation");
expect(meaning.stale_replay_forbidden).toBe(true);
expect(meaning.reason_codes).toContain("broad_business_evaluation_current_turn_signal");
});
});