Архитектура: закрыть phase17 clarification-resume и обновить readiness-статус перед расширением доменов

This commit is contained in:
dctouch 2026-04-19 17:17:09 +03:00
parent af15e21bf6
commit 0e098cde38
18 changed files with 842 additions and 87 deletions

View File

@ -52,6 +52,21 @@ Rules:
## agent_semantic_runs
- `АГЕНТНЫЙ ПРОГОН` is a targeted full semantic replay for the current architecture fix, not a generic smoke test.
- Use it to validate human user questions, human model answers, technical chats, business logic, and system routing together.
- Semantic meaning analysis is mandatory and has priority over green local tests.
- After every full-system replay or saved-session run, the agent must first analyze:
- what the user actually meant and what business result the user expected;
- what the assistant actually answered in human-readable form;
- whether the answer is semantically correct, context-aware, direct, and useful for the user.
- Only after that semantic review may the agent inspect technical chats, debug payloads, route ids, capability ids, filters, and internal orchestration metadata.
- Unit tests, narrow regressions, and green builds are secondary evidence only. They must never be presented as the primary proof that a replay is healthy when the user-facing semantic answer was not reviewed carefully.
- If a full-system replay contains a business-wrong, context-wrong, misleading, over-broad, or semantically off-target answer, the agent must treat it as a real failure even when route ids, tests, and low-level checks look green.
- The default review order for every substantial replay is:
1. read the user question chain as a human conversation;
2. read the assistant answers as a human user would see them;
3. judge semantic correctness, continuity, directness, and business usefulness;
4. only then inspect technical chats and machine artifacts to explain why the semantic defect happened;
5. only then use unit tests or narrow regressions as supporting verification after the fix.
- Do not hide behind `tests are green` or `route matched` when the semantic answer is still wrong. In this project, the meaning of the user question and the meaning of the assistant answer are the primary acceptance surface.
- Build question lists around the active fix: mix direct domain questions with contextual chains, meta interruptions, cross-domain pivots, and follow-up edges that specifically hit the architecture change under validation.
- Do not run or save an `АГЕНТНЫЙ ПРОГОН` on every turn by default.
- Run it when the user explicitly asks for it, or when a substantial architecture/domain fix needs critical semantic proof beyond unit tests and narrow synthetic checks.

View File

@ -16,14 +16,16 @@ It is the current-state audit that answers:
This snapshot is based on:
- `graphify-out/GRAPH_REPORT.md` rebuilt on `2026-04-18`
- `graphify-out/GRAPH_REPORT.md` rebuilt on `2026-04-19`
- current owner modules in `llm_normalizer/backend/src/services/`
- current scenario acceptance scripts under `scripts/`
- current AGENT semantic source catalog under `docs/orchestration/`
- live replay comparison between:
- `address_truth_harness_phase12_wider_saved_session_pool_live_20260418_rerun10`
- `address_truth_harness_phase12_wider_saved_session_pool_live_20260419_rerun16`
- `address_truth_harness_phase14_counterparty_tail_resume_live_20260418_rerun2`
- `address_truth_harness_phase15_answer_inspection_followup_live_20260418_rerun7`
- `address_truth_harness_phase15_answer_inspection_followup_live_20260418_rerun8`
- `address_truth_harness_phase16_multicompany_late_pivot_live_20260419_rerun10`
- `address_truth_harness_phase17_clarification_resume_and_counterparty_tail_live_20260419_rerun5`
- [10 - regression_breakpoint_analysis_2026-04-17.md](./10%20-%20regression_breakpoint_analysis_2026-04-17.md)
- [11 - continuity_stabilization_plan_2026-04-17.md](./11%20-%20continuity_stabilization_plan_2026-04-17.md)
@ -31,9 +33,9 @@ This snapshot is based on:
Latest graph rebuild:
- `5352 nodes`
- `11506 edges`
- `134 communities`
- `5371 nodes`
- `11523 edges`
- `135 communities`
Most relevant current god nodes for turnaround `11`:
@ -132,9 +134,9 @@ This is enough to build targeted semantic packs that are not single-domain toy s
## Honest Phase Status
Turnaround implementation progress: `~92%`
Turnaround implementation progress: `~96%`
Pre-expansion readiness: `~64%`
Pre-expansion readiness: `~78%`
This split is intentional.
@ -160,7 +162,7 @@ Reason:
### Phase 2. State And Transition Contracts
Status: `84%`
Status: `88%`
Reason:
@ -213,7 +215,7 @@ Remaining debt:
### Phase 5. AssistantService Extraction
Status: `81%`
Status: `84%`
Reason:
@ -242,7 +244,7 @@ Remaining debt:
### Phase 7. Scenario Acceptance As Primary Gate
Status: `79%`
Status: `86%`
Reason:
@ -265,6 +267,8 @@ It is now:
- `phase12_wider_saved_session_pool` is green end-to-end on the broader flagship saved-session family;
- `phase14_counterparty_tail_resume` is green on a different late-session counterparty/inventory/activity contour;
- `phase15_answer_inspection_followup` is green on grounded self-correction plus neighboring VAT bridge continuity;
- `phase16_multicompany_late_pivot` is green on late company switch plus referential inventory/receivables authority;
- `phase17_clarification_resume_and_counterparty_tail` is green on the specific semantic seams exposed by the manual run `assistant-stage1-uWH6xahSDt`: company clarification resumption, historical inventory continuation, short `СВК` retarget, and counterparty tail follow-up;
- therefore the original collapse has been materially repaired, and the main remaining risk has shifted from acute failure to incomplete generalization.
In practical terms, the active breakpoint is now:

View File

@ -507,7 +507,21 @@ Still open after the accepted phase12 replay:
- the phase16 live replay `address_truth_harness_phase16_multicompany_late_pivot` is now accepted on `2026-04-19`, which is the first explicit proof that a non-flagship multi-company late switch keeps truthful company authority across both inventory and receivables exact routes in the same saved session;
- the same phase16 pass also hardened replay-gate honesty: its receivables step now accepts semantically equivalent honest empty-match phrasing (`31.03.2020` or `31 марта 2020`, `долг` / `долж`) instead of overfitting to one single first-line wording, so this pack is now a trustworthy breadth gate rather than a fragile phrasing oracle.
## Next Execution Slice (2026-04-18)
Latest phase17 clarification-resume evidence after the current replay hardening:
- a new live pack `address_truth_harness_phase17_clarification_resume_and_counterparty_tail` validates `smalltalk -> inventory clarification -> explicit company choice -> historical inventory continuation -> selected-item provenance -> short counterparty tail -> current inventory -> activity age` inside one shared session;
- the semantic review of `assistant-stage1-uWH6xahSDt` exposed three distinct architecture defects rather than one generic red score:
- explicit company choice fixed the organization but did not resume the interrupted inventory root;
- short displayed-entity follow-up like `а по свк` lost the previous counterparty thread and fell back into stale organization fixation behavior;
- inventory-root follow-up turns still carried hidden `counterparty=АЛЬТЕРНАТИВА` contamination inside debug-derived filters, which could later poison carryover even when the visible answer looked acceptable;
- the stabilization fix is now explicit in runtime contracts instead of living as prompt luck:
- route/follow-up arbitration resumes the interrupted inventory root after explicit company choice instead of stopping at scope fixation;
- short displayed-entity follow-up keeps counterparty retarget on the validated path instead of losing to stale organization-switch interpretation;
- `decomposeStage` no longer hydrates inventory follow-up filters with the selected organization alias as a fake counterparty anchor, so hidden carryover state stays truthful;
- targeted `assistantRoutePolicy`, `assistantAddressFollowupContext`, `addressImplicitOrganizationScope`, and `addressFollowupTemporalRegression` suites are green after the fix, and backend build stays green;
- live replay `address_truth_harness_phase17_clarification_resume_and_counterparty_tail_live_20260419_rerun5` is accepted `10/10`, which is the current proof that clarification-resume, historical inventory continuation, and short counterparty-tail retarget are now semantically clean on a non-flagship saved-session path.
## Next Execution Slice (2026-04-19)
The project is now moving from:
@ -534,6 +548,15 @@ Current explicit goals for this slice:
- fewer hybrid/deep entry seams that still depend on fragment luck instead of explicit runtime contracts;
- cleaner user-facing business answers on already-correct truth paths;
- lower risk that new domains multiply orchestration chaos faster than capability growth.
- clarification-resume, late company switch, and short entity-tail behavior should stop being "repaired on one chain only" and continue moving toward reusable runtime contracts across replay families.
Current remaining heavy fronts before low-risk domain expansion:
- finish the last convergence work toward one continuity owner for `active organization`, `active date`, `root frame`, `focus object`, and clarification state across every hot runtime path;
- widen saved-session replay breadth beyond the current flagship + phase14 + phase15 + phase16 + phase17 families;
- reduce coordinator pressure still concentrated in `assistantService.ts`, `addressQueryService.ts`, and `resolveAddressIntent()`;
- complete the missing contour for counterparty shipped-goods / service extraction instead of relying on honest-but-limited document-list fallback;
- keep answer shaping as secondary debt only where it materially affects acceptance, not as the primary architecture frontier.
## Ready Signal

View File

@ -23,10 +23,10 @@ Current verdict:
Current confidence snapshot:
- turnaround implementation progress: `~92%`
- exit-from-danger-zone readiness: `~84%`
- pre-multidomain readiness: `~64%`
- latest graph snapshot: `5352 nodes`, `11506 edges`, `134 communities`
- turnaround implementation progress: `~96%`
- exit-from-danger-zone readiness: `~91%`
- pre-multidomain readiness: `~78%`
- latest graph snapshot: `5371 nodes`, `11523 edges`, `135 communities`
## What Is Already True
@ -35,7 +35,11 @@ The following claims are now supported by code plus live replay evidence:
- phase12 flagship wider saved-session replay is accepted end-to-end;
- phase14 counterparty-tail late-session replay is accepted end-to-end;
- phase15 answer-inspection replay is accepted end-to-end;
- phase16 multi-company late-pivot replay is accepted end-to-end;
- phase17 clarification-resume and short counterparty-tail replay is accepted end-to-end;
- continuity on validated inventory / VAT / counterparty / company-authority chains is materially stronger than before;
- clarification-resume after explicit company choice now restores the interrupted business root instead of stopping at organization fixation;
- short displayed-entity follow-up like `а по свк` now wins over stale organization-fixation behavior on the validated replay family;
- user-facing meta answers are significantly cleaner and no longer dominated by technical garbage;
- the assistant no longer depends on the old ambient monolith behavior on the validated seams;
- the team now has a working replay-driven hardening loop instead of blind local patching.
@ -64,7 +68,7 @@ This is much better than the old implicit monolith, but it still means:
The important nuance now is:
- this is no longer causing the original flagship collapse on the validated packs;
- it is still the main architectural reason broad future expansion remains risky.
- it is still the main architectural reason broad future expansion remains risky, especially outside the already repaired replay families.
### 2. Core orchestration remains too concentrated
@ -135,7 +139,7 @@ The system should not be considered ready for the next level until all of the fo
## Recommended Next Execution Sequence
### Pass 16. Continuity authority completion
### Pass 18. Continuity authority completion
Goal:
@ -145,7 +149,7 @@ Target:
- transition / route / clarification should consume one continuity snapshot before making divergent decisions.
### Pass 17. Wider saved-session acceptance pool
### Pass 19. Wider saved-session acceptance pool
Goal:
@ -153,9 +157,9 @@ Goal:
Target:
- several saved sessions covering inventory, VAT, counterparty, payables/receivables, meta interrupts, and cross-domain pivots.
- several saved sessions covering inventory, VAT, counterparty, payables/receivables, meta interrupts, and cross-domain pivots beyond phase12 / phase16 / phase17.
### Pass 18. Human answer shaping cleanup
### Pass 20. Human answer shaping cleanup
Goal:
@ -165,7 +169,7 @@ Target:
- product-quality business answers on already-correct truth paths.
### Pass 19. Coordinator pressure reduction
### Pass 21. Coordinator pressure reduction
Goal:

View File

@ -32,7 +32,7 @@ This package answers the next question:
12. [12 - manual_run_system_analysis_3NilqwT1G2_2026-04-18.md](./12%20-%20manual_run_system_analysis_3NilqwT1G2_2026-04-18.md)
13. [13 - pre_multidomain_readiness_audit_2026-04-18.md](./13%20-%20pre_multidomain_readiness_audit_2026-04-18.md)
## Current Status Snapshot (2026-04-18)
## Current Status Snapshot (2026-04-19)
This package is no longer planning-only.
@ -45,33 +45,35 @@ It now documents a turnaround that is already operational in code, already mater
Current honest status:
- turnaround implementation progress: `~92%`
- exit-from-danger-zone readiness: `~84%`
- pre-multidomain readiness: `~64%`
- graph snapshot after latest rebuild: `5352 nodes`, `11506 edges`, `134 communities`
- turnaround implementation progress: `~96%`
- exit-from-danger-zone readiness: `~91%`
- pre-multidomain readiness: `~78%`
- graph snapshot after latest rebuild: `5371 nodes`, `11523 edges`, `135 communities`
- current breakpoint:
- the validated hot paths are no longer structurally broken;
- flagship continuity collapse is no longer the primary risk;
- the main remaining risk is incomplete convergence toward one true runtime authority plus replay breadth still below the intended multi-domain blast radius;
- the main remaining risk is no longer clarification-resume collapse, but the unfinished final convergence toward one true runtime authority plus replay breadth still below the intended multi-domain blast radius;
- product shaping is now secondary debt, not the primary blocker.
- main remaining architectural pressure:
- no single fully authoritative continuity contract consumed by all hot runtime owners
- no single fully authoritative continuity contract consumed by every hot runtime owner
- residual coordinator/legacy pressure inside `assistantService.ts`
- central domain-intent pressure inside `resolveAddressIntent()`
- replay breadth still narrower than the intended multi-domain rollout surface
- replay breadth still narrower than the intended multi-domain rollout surface beyond the flagship and late-switch families
- remaining answer-semantics pressure inside `composeStage.ts` / `answerComposer.ts`
Latest live proof now includes:
- `address_truth_harness_phase12_wider_saved_session_pool_live_20260418_rerun10` accepted `20/20`
- `address_truth_harness_phase12_wider_saved_session_pool_live_20260419_rerun16` accepted `20/20`
- `address_truth_harness_phase14_counterparty_tail_resume_live_20260418_rerun2` accepted `10/10`
- `address_truth_harness_phase15_answer_inspection_followup_live_20260418_rerun7` accepted `9/9`
- `address_truth_harness_phase15_answer_inspection_followup_live_20260418_rerun8` accepted `9/9`
- `address_truth_harness_phase16_multicompany_late_pivot_live_20260419_rerun10` accepted
- `address_truth_harness_phase17_clarification_resume_and_counterparty_tail_live_20260419_rerun5` accepted `10/10`
Current architectural reading:
- the system is already materially past the dangerous regression breakpoint;
- it is now safe for continued architecture hardening and controlled domain-by-domain enablement under replay gates;
- it is still not safe to declare broad low-risk multi-domain expansion.
- it is now materially closer to pre-multidomain stability, but still not safe to declare broad low-risk multi-domain expansion.
For the detailed audit, current percentages, and remaining debt, read:
@ -131,12 +133,12 @@ and start being described as:
- "a stateful exact-data assistant with explicit transition contracts and isolated truth gating."
As of `2026-04-18`, the project is already materially closer to the target description and is no longer in the same acute collapse state. The remaining blocker is no longer the original continuity failure itself, but the unfinished convergence toward one runtime authority plus still-insufficient replay breadth for low-risk multi-domain expansion.
As of `2026-04-19`, the project is already materially closer to the target description and is no longer in the same acute collapse state. The remaining blocker is no longer the original continuity failure itself, but the unfinished convergence toward one runtime authority plus still-insufficient replay breadth for low-risk multi-domain expansion.
The biggest remaining blockers are:
- split continuity ownership across route / transition / recap / coordinator glue;
- saved-session acceptance still too narrow compared with the intended domain-expansion blast radius;
- saved-session acceptance still too narrow compared with the intended domain-expansion blast radius outside the repaired flagship + late-pivot families;
- clarification precedence is much better than before, but still not yet proven widely enough outside the repaired replay family;
- residual `assistantService` overload;
- central intent pressure in `resolveAddressIntent()`;

View File

@ -0,0 +1,252 @@
{
"schema_version": "domain_truth_harness_spec_v1",
"scenario_id": "address_truth_harness_phase17_clarification_resume_and_counterparty_tail",
"domain": "address_phase17_clarification_resume_and_counterparty_tail",
"title": "Phase 17 clarification-resume and counterparty tail replay",
"description": "Targeted AGENT replay for the semantic defects from assistant-stage1-uWH6xahSDt: company clarification must resume the pending inventory query, short historical date follow-ups must stay in the inventory contour after a capability answer, short counterparty retarget must not collapse into company fixation, and counterparty shipment wording must answer through shipped positions rather than a stale document/date fallback.",
"bindings": {},
"steps": [
{
"step_id": "step_01_smalltalk_entry",
"title": "Smalltalk entry stays human and may offer organization scope without technical garbage",
"question": "приветик - че как там дела",
"required_answer_patterns_all": [
"(?i)привет|дела|норм|помоч"
],
"forbidden_answer_patterns": [
"(?i)mcp",
"(?i)read_only",
"(?i)tool_gate_reason",
"(?i)snapshot_items"
],
"criticality": "important",
"semantic_tags": [
"smalltalk_entry"
]
},
{
"step_id": "step_02_capability_meta_entry",
"title": "Capability-meta answer stays human before the business contour starts",
"question": "расскажи что можешь интересного",
"allowed_reply_types": [
"factual",
"factual_with_explanation"
],
"required_direct_answer_patterns_any": [
"(?i)могу|умею",
"(?i)ндс|документ|контрагент|долг|склад|остат"
],
"forbidden_direct_answer_patterns": [
"(?i)mcp",
"(?i)read_only",
"(?i)tool_gate_reason",
"(?i)snapshot_items"
],
"criticality": "important",
"semantic_tags": [
"capability_meta"
]
},
{
"step_id": "step_03_inventory_root_requires_company",
"title": "Inventory root after meta entry asks to choose the organization",
"question": "кайф - что там на складе по остаткам?",
"required_answer_patterns_all": [
"(?i)уточнит[ье].*организац|выбери.*организац|названи[ея] компании",
"(?i)альтернатива плюс|лайсвуд|райм"
],
"forbidden_answer_patterns": [
"(?i)mcp",
"(?i)read_only",
"(?i)tool_gate_reason",
"(?i)snapshot_items"
],
"criticality": "critical",
"semantic_tags": [
"inventory_root",
"clarification_required"
]
},
{
"step_id": "step_04_company_choice_resumes_inventory_root",
"title": "Bare company selection resumes the pending inventory query instead of only fixing the scope",
"question": "АЛЬТЕРНАТИВА",
"allowed_reply_types": [
"factual"
],
"expected_intents": [
"inventory_on_hand_as_of_date"
],
"required_filters": {
"as_of_date": "{{runtime.today_iso}}",
"organization": "ООО Альтернатива Плюс"
},
"required_direct_answer_patterns_any": [
"(?i)на складе|остат",
"{{runtime.today_dot_regex}}"
],
"forbidden_direct_answer_patterns": [
"(?i)^отлично, фиксирую рабочую организацию",
"(?i)^фиксирую рабочую организацию",
"(?i)уточните организацию"
],
"criticality": "critical",
"semantic_tags": [
"clarification_resume",
"inventory_root"
]
},
{
"step_id": "step_05_historical_inventory_capability",
"title": "Historical inventory capability answer stays human",
"question": "а исторические остатки на другие даты умеешь?",
"allowed_reply_types": [
"factual",
"factual_with_explanation"
],
"required_answer_patterns_any": [
"(?i)историческ",
"(?i)могу|умею"
],
"forbidden_answer_patterns": [
"(?i)mcp",
"(?i)tool_gate_reason",
"(?i)read_only"
],
"criticality": "important",
"semantic_tags": [
"inventory_capability_meta"
]
},
{
"step_id": "step_06_inventory_july_2017_after_capability",
"title": "Date-only follow-up keeps the historical inventory contour after the capability answer",
"question": "давай на июль 2017",
"allowed_reply_types": [
"factual"
],
"expected_intents": [
"inventory_on_hand_as_of_date"
],
"required_filters": {
"as_of_date": "2017-07-31",
"period_from": "2017-07-01",
"period_to": "2017-07-31",
"organization": "ООО Альтернатива Плюс"
},
"required_direct_answer_patterns_any": [
"31\\.07\\.2017",
"(?i)на складе|остат"
],
"forbidden_direct_answer_patterns": [
"(?i)^отлично, фиксирую рабочую организацию",
"(?i)уточните организацию"
],
"criticality": "critical",
"semantic_tags": [
"historical_inventory",
"date_followup"
]
},
{
"step_id": "step_07_inventory_march_2016",
"title": "Another short month follow-up still stays in the same historical inventory contour",
"question": "март 2016",
"allowed_reply_types": [
"factual"
],
"expected_intents": [
"inventory_on_hand_as_of_date"
],
"required_filters": {
"as_of_date": "2016-03-31",
"period_from": "2016-03-01",
"period_to": "2016-03-31",
"organization": "ООО Альтернатива Плюс"
},
"required_direct_answer_patterns_any": [
"31\\.03\\.2016",
"(?i)на складе|остат"
],
"forbidden_direct_answer_patterns": [
"(?i)^отлично, фиксирую рабочую организацию",
"(?i)уточните организацию"
],
"criticality": "critical",
"semantic_tags": [
"historical_inventory",
"date_followup"
]
},
{
"step_id": "step_08_counterparty_documents_chepurnov",
"title": "Counterparty document root stays exact late in the same session",
"question": "по чепурнову покажи все доки",
"allowed_reply_types": [
"factual",
"factual_with_explanation"
],
"expected_intents": [
"list_documents_by_counterparty"
],
"required_direct_answer_patterns_any": [
"(?i)контрагент:.*чепурнов",
"(?i)документ|поступление|счет"
],
"criticality": "critical",
"semantic_tags": [
"counterparty_documents"
]
},
{
"step_id": "step_09_short_counterparty_retarget_svk",
"title": "Short follow-up 'а по свк' retargets the counterparty instead of re-fixing the organization",
"question": "а по свк",
"allowed_reply_types": [
"factual",
"factual_with_explanation"
],
"expected_intents": [
"list_documents_by_counterparty"
],
"required_direct_answer_patterns_any": [
"(?i)контрагент:.*свк",
"(?i)документ|поступление|счет"
],
"forbidden_direct_answer_patterns": [
"(?i)^отлично, фиксирую рабочую организацию",
"(?i)уточните организацию"
],
"criticality": "critical",
"semantic_tags": [
"counterparty_short_retarget",
"display_label_integrity"
]
},
{
"step_id": "step_10_counterparty_item_flow_chepurnov",
"title": "Counterparty shipment wording answers through shipped positions without stale 2017 fallback",
"question": "что нам отгружал чепурнов? какой товар или услугу?",
"allowed_reply_types": [
"factual"
],
"expected_intents": [
"list_documents_by_counterparty"
],
"required_direct_answer_patterns_any": [
"(?i)контрагент:.*чепурнов",
"(?i)позици[ия]|товар|услуг"
],
"forbidden_direct_answer_patterns": [
"(?i)по окну 2017-01-01\\.\\.2017-12-31 строк не найдено",
"(?i)^отлично, фиксирую рабочую организацию",
"(?i)уточните организацию"
],
"criticality": "critical",
"semantic_tags": [
"counterparty_item_flow",
"stale_temporal_carryover"
]
}
]
}

View File

@ -1564,6 +1564,7 @@ function applyPreExecutionOrganizationScopeGrounding(input) {
sameNormalizedOrganizationScope(input.filters.organization ?? null, activeOrganization) &&
typeof input.filters.counterparty === "string" &&
(isLikelyLowQualityPartyAnchor(input.filters.counterparty) ||
sameOrganizationEntityReference(input.filters.counterparty, resolvedOrganizationFromMessage ?? activeOrganization) ||
isQuestionFragmentPartyAnchor(input.filters.counterparty))) {
delete input.filters.counterparty;
if (!input.warnings.includes("counterparty_cleared_from_referential_organization_scope")) {

View File

@ -15,6 +15,7 @@ const addressQueryShapeClassifier_1 = require("../addressQueryShapeClassifier");
const addressIntentResolver_1 = require("../addressIntentResolver");
const addressFilterExtractor_1 = require("../addressFilterExtractor");
const inventoryLifecycleCueHelpers_1 = require("../inventoryLifecycleCueHelpers");
const assistantOrganizationMatcher_1 = require("../assistantOrganizationMatcher");
const semanticHintOverlay_1 = require("./semanticHintOverlay");
function hasExplicitPeriodWindow(filters) {
return ((typeof filters.period_from === "string" && filters.period_from.trim().length > 0) ||
@ -287,6 +288,23 @@ function isInventoryLifecycleHistoryIntent(intent) {
intent === "inventory_sale_trace_for_item" ||
intent === "inventory_purchase_to_sale_chain");
}
function shouldSuppressInventoryCounterpartyAlias(intent, counterparty, organization) {
if (intent !== "inventory_on_hand_as_of_date") {
return false;
}
if (!counterparty || !organization) {
return false;
}
if ((0, assistantOrganizationMatcher_1.organizationsLikelySameEntity)(counterparty, organization)) {
return true;
}
const counterpartyNorm = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeSearchText)(counterparty);
const organizationNorm = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeSearchText)(organization);
if (!counterpartyNorm || !organizationNorm) {
return false;
}
return organizationNorm.includes(counterpartyNorm) || counterpartyNorm.includes(organizationNorm);
}
function buildInventoryRootFollowupContext(followupContext) {
if (!followupContext || !followupContext.root_intent || !followupContext.root_filters) {
return followupContext;
@ -633,21 +651,48 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
const allTimeRequested = hasAllTimeHint(userMessage);
const sameDateRequested = hasSameDateHint(userMessage) || hasSameDatePrepositionHint(userMessage);
const samePeriodRequested = hasSamePeriodHint(userMessage);
const hasFollowupSignal = hasAddressFollowupContextSignal(userMessage);
const hasExplicitPeriodInMessage = hasExplicitPeriodLiteral(userMessage);
const hasExplicitCurrentDateInMessage = hasExplicitCurrentDateHint(userMessage);
const explicitQuotedItem = extractSelectedObjectItemFromFollowupText(userMessage);
if (!toNonEmptyString(merged.organization) && previousOrganization) {
merged.organization = previousOrganization;
reasons.push("organization_from_followup_context");
}
if (intent === "inventory_on_hand_as_of_date" &&
followupContext.previous_intent === "inventory_on_hand_as_of_date" &&
followupContext.target_intent === "inventory_on_hand_as_of_date" &&
followupContext.previous_anchor_type === "organization" &&
!hasFollowupSignal &&
!hasExplicitPeriodInMessage &&
!hasExplicitCurrentDateInMessage &&
toNonEmptyString(merged.counterparty)) {
delete merged.counterparty;
reasons.push("counterparty_cleared_from_organization_clarification_selection");
}
if (intent === "list_documents_by_counterparty" ||
intent === "bank_operations_by_counterparty" ||
intent === "list_contracts_by_counterparty") {
const inheritedCounterparty = previousCounterparty ??
(followupContext.previous_anchor_type === "counterparty" ? previousAnchorValue : null);
const currentCounterparty = toNonEmptyString(merged.counterparty);
const shouldInheritCounterparty = !currentCounterparty ||
const organizationReference = toNonEmptyString(merged.organization) ??
previousOrganization ??
(followupContext.previous_anchor_type === "organization" ? previousAnchorValue : null);
const currentCounterpartyLooksLikeOrganization = shouldSuppressInventoryCounterpartyAlias(intent, currentCounterparty, organizationReference);
const inheritedCounterpartyLooksLikeOrganization = shouldSuppressInventoryCounterpartyAlias(intent, inheritedCounterparty, organizationReference);
if (!currentCounterparty && inheritedCounterpartyLooksLikeOrganization) {
reasons.push("counterparty_cleared_as_organization_scope_alias");
}
if (currentCounterpartyLooksLikeOrganization) {
delete merged.counterparty;
reasons.push("counterparty_cleared_as_organization_scope_alias");
}
const shouldInheritCounterparty = !inheritedCounterpartyLooksLikeOrganization &&
(!currentCounterparty ||
(Boolean(inheritedCounterparty) &&
isLowQualityCounterpartyAnchor(currentCounterparty) &&
!isLowQualityCounterpartyAnchor(inheritedCounterparty));
!isLowQualityCounterpartyAnchor(inheritedCounterparty)));
if (inheritedCounterparty && shouldInheritCounterparty) {
merged.counterparty = inheritedCounterparty;
reasons.push(currentCounterparty ? "counterparty_replaced_from_followup_context" : "counterparty_from_followup_context");
@ -723,7 +768,7 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
intent === "vat_payable_confirmed_as_of_date" ||
intent === "vat_payable_forecast" ||
intent === "vat_liability_confirmed_for_tax_period") {
const hasFollowupSignalForConfirmed = hasAddressFollowupContextSignal(userMessage);
const hasFollowupSignalForConfirmed = hasFollowupSignal;
const inheritedContract = previousContract ?? (followupContext.previous_anchor_type === "contract" ? previousAnchorValue : null);
const currentContract = toNonEmptyString(merged.contract);
const shouldInheritContract = !currentContract ||
@ -948,9 +993,6 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
reasons.push("period_to_cleared_for_lifecycle_followup");
}
}
const hasFollowupSignal = hasAddressFollowupContextSignal(userMessage);
const hasExplicitPeriodInMessage = hasExplicitPeriodLiteral(userMessage);
const hasExplicitCurrentDateInMessage = hasExplicitCurrentDateHint(userMessage);
const inventoryLifecycleHistoryIntent = isInventoryLifecycleHistoryIntent(intent);
const asOfPrimaryIntent = intent === "account_balance_snapshot" ||
intent === "documents_forming_balance" ||
@ -967,6 +1009,18 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
intent === "vat_payable_confirmed_as_of_date";
const currentHasPeriod = hasExplicitPeriodWindow(merged);
const previousHasPeriod = hasExplicitPeriodWindow(previous);
const currentCounterpartyExplicit = toNonEmptyString(merged.counterparty);
const currentContractExplicit = toNonEmptyString(merged.contract);
const currentItemExplicit = toNonEmptyString(merged.item);
const currentAccountExplicit = toNonEmptyString(merged.account);
const shouldSuppressGenericPeriodCarryover = (Boolean(currentCounterpartyExplicit) &&
!isLowQualityCounterpartyAnchor(currentCounterpartyExplicit) &&
currentCounterpartyExplicit !== previousCounterparty) ||
(Boolean(currentContractExplicit) &&
!isLowQualityContractAnchor(currentContractExplicit) &&
currentContractExplicit !== previousContract) ||
(Boolean(currentItemExplicit) && currentItemExplicit !== previousItem) ||
(Boolean(currentAccountExplicit) && currentAccountExplicit !== previousAccount);
const vatRelativeMonthFollowup = relativeMonthFromFollowupYear &&
(intent === "vat_payable_confirmed_as_of_date" ||
intent === "vat_payable_forecast" ||
@ -1006,7 +1060,8 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
hasFollowupSignal &&
!hasExplicitPeriodInMessage &&
!inventoryLifecycleHistoryIntent &&
!vatRelativeMonthFollowup) {
!vatRelativeMonthFollowup &&
!shouldSuppressGenericPeriodCarryover) {
if (previousPeriodFrom) {
merged.period_from = previousPeriodFrom;
}
@ -1038,6 +1093,16 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
: "as_of_date_derived_from_period_for_open_contracts");
}
}
const finalOrganizationReference = toNonEmptyString(merged.organization) ??
previousOrganization ??
(followupContext.previous_anchor_type === "organization" ? previousAnchorValue : null);
const finalCounterparty = toNonEmptyString(merged.counterparty);
if (shouldSuppressInventoryCounterpartyAlias(intent, finalCounterparty, finalOrganizationReference)) {
delete merged.counterparty;
if (!reasons.includes("counterparty_cleared_as_organization_scope_alias")) {
reasons.push("counterparty_cleared_as_organization_scope_alias");
}
}
return { filters: merged, reasons };
}
function resolveMissingRequiredFilters(intent, filters) {

View File

@ -329,10 +329,12 @@ function createAssistantRoutePolicy(deps) {
const lastOrganizationClarificationDebug = findLastOrganizationClarificationAddressDebug(sessionItems);
const organizationClarificationCandidates = organizationAuthority.organizationClarificationCandidates;
const organizationClarificationSelectionFromScope = organizationAuthority.organizationClarificationSelectionFromScope;
const organizationClarificationSelection = resolveOrganizationSelectionFromMessage(rawUserMessage, organizationClarificationCandidates) ??
const explicitOrganizationClarificationSelection = resolveOrganizationSelectionFromMessage(rawUserMessage, organizationClarificationCandidates) ??
resolveOrganizationSelectionFromMessage(repairedRawUserMessage, organizationClarificationCandidates) ??
resolveOrganizationSelectionFromMessage(effectiveAddressUserMessage, organizationClarificationCandidates) ??
resolveOrganizationSelectionFromMessage(repairedEffectiveAddressUserMessage, organizationClarificationCandidates) ??
null;
const organizationClarificationSelection = explicitOrganizationClarificationSelection ??
(organizationClarificationSelectionFromScope &&
organizationClarificationCandidates.some((candidate) => normalizeOrganizationScopeValue(candidate) === organizationClarificationSelectionFromScope)
? organizationClarificationSelectionFromScope
@ -443,11 +445,12 @@ function createAssistantRoutePolicy(deps) {
hasShortInventoryObjectFollowupSignal(repairedEffectiveAddressUserMessage)));
const organizationClarificationContinuationDetected = Boolean((followupContext || continuitySnapshot.hasGroundedAddressContext) &&
lastOrganizationClarificationDebug &&
organizationClarificationSelection &&
explicitOrganizationClarificationSelection &&
!dataScopeMetaQuery &&
!capabilityMetaQuery &&
!dataRetrievalSignal);
const organizationScopeSwitchDetected = Boolean(organizationClarificationSelection &&
const organizationScopeSwitchDetected = Boolean(explicitOrganizationClarificationSelection &&
!organizationClarificationContinuationDetected &&
!dataScopeMetaQuery &&
!capabilityMetaQuery &&
(shouldEmitOrganizationSelectionReply(rawUserMessage, organizationClarificationSelection) ||

View File

@ -321,14 +321,16 @@ function createAssistantTransitionPolicy(deps) {
const organizationClarificationSelection = explicitOrganizationClarificationSelection ??
deps.normalizeOrganizationScopeValue(organizationAuthority.organizationClarificationSelectionFromScope);
const hasOrganizationClarificationContinuation = Boolean(lastOrganizationClarificationDebug && organizationClarificationSelection);
const followupOffer = previousAddressDebug ? deps.buildAddressFollowupOffer(previousAddressDebug) : null;
const carryoverSourceDebug = previousAddressDebug ??
(hasOrganizationClarificationContinuation ? lastOrganizationClarificationDebug : null);
const followupOffer = carryoverSourceDebug ? deps.buildAddressFollowupOffer(carryoverSourceDebug) : null;
const hasImplicitContinuationSignal = Boolean(previousAddressDebug) &&
Boolean(followupOffer?.enabled) &&
(deps.isImplicitAddressContinuationByLlm(userMessage, llmPreDecomposeMeta) ||
(deps.toNonEmptyString(alternateMessage)
? deps.isImplicitAddressContinuationByLlm(alternateMessage, llmPreDecomposeMeta)
: false));
const sourceIntentHint = deps.toNonEmptyString(previousAddressDebug?.detected_intent);
const sourceIntentHint = deps.toNonEmptyString(carryoverSourceDebug?.detected_intent);
const navigationFocusObjectHint = addressNavigationState &&
typeof addressNavigationState === "object" &&
addressNavigationState.session_context &&
@ -427,10 +429,10 @@ function createAssistantTransitionPolicy(deps) {
!hasIndexReferenceSignal) {
return null;
}
if (!previousAddressDebug) {
if (!carryoverSourceDebug) {
return null;
}
const sourceIntent = deps.toNonEmptyString(previousAddressDebug.detected_intent);
const sourceIntent = deps.toNonEmptyString(carryoverSourceDebug.detected_intent);
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;
@ -494,7 +496,7 @@ function createAssistantTransitionPolicy(deps) {
followupSelectionMode = "switch_to_suggested_intent";
}
}
const previousAnchorContext = (0, assistantContinuityPolicy_1.resolveAddressDebugAnchorContext)(previousAddressDebug, deps.toNonEmptyString);
const previousAnchorContext = (0, assistantContinuityPolicy_1.resolveAddressDebugAnchorContext)(carryoverSourceDebug, deps.toNonEmptyString);
let previousAnchorType = previousAnchorContext.anchorType;
let previousAnchor = previousAnchorContext.anchorValue;
const navigationSessionContext = addressNavigationState && typeof addressNavigationState === "object"
@ -565,7 +567,7 @@ function createAssistantTransitionPolicy(deps) {
let { inventoryRootFrame, currentFrameKind } = (0, assistantContinuityPolicy_1.hydrateInventoryRootFrameState)(inventoryRootFrameCandidate, sourceIntent, navigationOrganization, navigationDateScope, deps.toNonEmptyString, deps.isInventoryDrilldownFrameIntent, deps.isInventoryRootFrameIntent);
let resolvedCounterpartyFromDisplay = false;
let displayedEntityTargetIntent = null;
let previousFilters = (0, assistantContinuityPolicy_1.resolveAddressDebugCarryoverFilters)(previousAddressDebug, deps.toNonEmptyString);
let previousFilters = (0, assistantContinuityPolicy_1.resolveAddressDebugCarryoverFilters)(carryoverSourceDebug, deps.toNonEmptyString);
const shouldBackfillHistoricalPartyAnchors = sourceIntentHint === "list_contracts_by_counterparty" ||
sourceIntentHint === "list_documents_by_counterparty" ||
sourceIntentHint === "bank_operations_by_counterparty" ||
@ -579,10 +581,10 @@ function createAssistantTransitionPolicy(deps) {
previousFilters = (0, assistantContinuityPolicy_1.applyOrganizationCarryoverFilters)(previousFilters, historicalOrganization, authorityActiveOrganization, continuitySnapshot.activeOrganization, navigationOrganization, organizationClarificationSelection, deps.toNonEmptyString);
if (inventoryPurchaseDateVatBridge) {
const purchaseBridgeItem = previousAddressItem &&
deps.toNonEmptyString(previousAddressDebug?.detected_intent) === "inventory_purchase_provenance_for_item"
deps.toNonEmptyString(carryoverSourceDebug?.detected_intent) === "inventory_purchase_provenance_for_item"
? previousAddressItem
: findRecentInventoryPurchaseProvenanceItem(items, deps.toNonEmptyString(navigationFocusObjectLabel) ??
(0, assistantContinuityPolicy_1.readAddressDebugItem)(previousAddressDebug, deps.toNonEmptyString) ??
(0, assistantContinuityPolicy_1.readAddressDebugItem)(carryoverSourceDebug, deps.toNonEmptyString) ??
deps.toNonEmptyString(previousFilters.item)) ?? previousAddressItem;
const purchaseBridgeWindow = extractPurchaseDateBridgeWindow(purchaseBridgeItem, addressNavigationState);
if (purchaseBridgeWindow) {

View File

@ -1933,6 +1933,7 @@ function applyPreExecutionOrganizationScopeGrounding(input: {
sameNormalizedOrganizationScope(input.filters.organization ?? null, activeOrganization) &&
typeof input.filters.counterparty === "string" &&
(isLikelyLowQualityPartyAnchor(input.filters.counterparty) ||
sameOrganizationEntityReference(input.filters.counterparty, resolvedOrganizationFromMessage ?? activeOrganization) ||
isQuestionFragmentPartyAnchor(input.filters.counterparty))
) {
delete input.filters.counterparty;

View File

@ -21,6 +21,7 @@ import {
hasInventorySaleCue,
hasInventorySupplierCue
} from "../inventoryLifecycleCueHelpers";
import { normalizeOrganizationScopeSearchText, organizationsLikelySameEntity } from "../assistantOrganizationMatcher";
import { applyAddressLlmSemanticHintsToExtraction } from "./semanticHintOverlay";
import type { AddressLlmSemanticHints } from "../../types/addressQuery";
@ -393,6 +394,28 @@ function isInventoryLifecycleHistoryIntent(intent: AddressIntent | undefined): b
);
}
function shouldSuppressInventoryCounterpartyAlias(
intent: AddressIntent,
counterparty: string | null,
organization: string | null
): boolean {
if (intent !== "inventory_on_hand_as_of_date") {
return false;
}
if (!counterparty || !organization) {
return false;
}
if (organizationsLikelySameEntity(counterparty, organization)) {
return true;
}
const counterpartyNorm = normalizeOrganizationScopeSearchText(counterparty);
const organizationNorm = normalizeOrganizationScopeSearchText(organization);
if (!counterpartyNorm || !organizationNorm) {
return false;
}
return organizationNorm.includes(counterpartyNorm) || counterpartyNorm.includes(organizationNorm);
}
function buildInventoryRootFollowupContext(
followupContext: AddressFollowupContext | null
): AddressFollowupContext | null {
@ -820,12 +843,29 @@ function mergeFollowupFilters(
const allTimeRequested = hasAllTimeHint(userMessage);
const sameDateRequested = hasSameDateHint(userMessage) || hasSameDatePrepositionHint(userMessage);
const samePeriodRequested = hasSamePeriodHint(userMessage);
const hasFollowupSignal = hasAddressFollowupContextSignal(userMessage);
const hasExplicitPeriodInMessage = hasExplicitPeriodLiteral(userMessage);
const hasExplicitCurrentDateInMessage = hasExplicitCurrentDateHint(userMessage);
const explicitQuotedItem = extractSelectedObjectItemFromFollowupText(userMessage);
if (!toNonEmptyString(merged.organization) && previousOrganization) {
merged.organization = previousOrganization;
reasons.push("organization_from_followup_context");
}
if (
intent === "inventory_on_hand_as_of_date" &&
followupContext.previous_intent === "inventory_on_hand_as_of_date" &&
followupContext.target_intent === "inventory_on_hand_as_of_date" &&
followupContext.previous_anchor_type === "organization" &&
!hasFollowupSignal &&
!hasExplicitPeriodInMessage &&
!hasExplicitCurrentDateInMessage &&
toNonEmptyString(merged.counterparty)
) {
delete merged.counterparty;
reasons.push("counterparty_cleared_from_organization_clarification_selection");
}
if (
intent === "list_documents_by_counterparty" ||
intent === "bank_operations_by_counterparty" ||
@ -835,11 +875,33 @@ function mergeFollowupFilters(
previousCounterparty ??
(followupContext.previous_anchor_type === "counterparty" ? previousAnchorValue : null);
const currentCounterparty = toNonEmptyString(merged.counterparty);
const organizationReference =
toNonEmptyString(merged.organization) ??
previousOrganization ??
(followupContext.previous_anchor_type === "organization" ? previousAnchorValue : null);
const currentCounterpartyLooksLikeOrganization = shouldSuppressInventoryCounterpartyAlias(
intent,
currentCounterparty,
organizationReference
);
const inheritedCounterpartyLooksLikeOrganization = shouldSuppressInventoryCounterpartyAlias(
intent,
inheritedCounterparty,
organizationReference
);
if (!currentCounterparty && inheritedCounterpartyLooksLikeOrganization) {
reasons.push("counterparty_cleared_as_organization_scope_alias");
}
if (currentCounterpartyLooksLikeOrganization) {
delete merged.counterparty;
reasons.push("counterparty_cleared_as_organization_scope_alias");
}
const shouldInheritCounterparty =
!currentCounterparty ||
!inheritedCounterpartyLooksLikeOrganization &&
(!currentCounterparty ||
(Boolean(inheritedCounterparty) &&
isLowQualityCounterpartyAnchor(currentCounterparty) &&
!isLowQualityCounterpartyAnchor(inheritedCounterparty));
!isLowQualityCounterpartyAnchor(inheritedCounterparty)));
if (inheritedCounterparty && shouldInheritCounterparty) {
merged.counterparty = inheritedCounterparty;
reasons.push(currentCounterparty ? "counterparty_replaced_from_followup_context" : "counterparty_from_followup_context");
@ -925,7 +987,7 @@ function mergeFollowupFilters(
intent === "vat_payable_forecast" ||
intent === "vat_liability_confirmed_for_tax_period"
) {
const hasFollowupSignalForConfirmed = hasAddressFollowupContextSignal(userMessage);
const hasFollowupSignalForConfirmed = hasFollowupSignal;
const inheritedContract = previousContract ?? (followupContext.previous_anchor_type === "contract" ? previousAnchorValue : null);
const currentContract = toNonEmptyString(merged.contract);
const shouldInheritContract =
@ -1189,9 +1251,6 @@ function mergeFollowupFilters(
}
}
const hasFollowupSignal = hasAddressFollowupContextSignal(userMessage);
const hasExplicitPeriodInMessage = hasExplicitPeriodLiteral(userMessage);
const hasExplicitCurrentDateInMessage = hasExplicitCurrentDateHint(userMessage);
const inventoryLifecycleHistoryIntent = isInventoryLifecycleHistoryIntent(intent);
const asOfPrimaryIntent =
intent === "account_balance_snapshot" ||
@ -1209,6 +1268,19 @@ function mergeFollowupFilters(
intent === "vat_payable_confirmed_as_of_date";
const currentHasPeriod = hasExplicitPeriodWindow(merged);
const previousHasPeriod = hasExplicitPeriodWindow(previous);
const currentCounterpartyExplicit = toNonEmptyString(merged.counterparty);
const currentContractExplicit = toNonEmptyString(merged.contract);
const currentItemExplicit = toNonEmptyString(merged.item);
const currentAccountExplicit = toNonEmptyString(merged.account);
const shouldSuppressGenericPeriodCarryover =
(Boolean(currentCounterpartyExplicit) &&
!isLowQualityCounterpartyAnchor(currentCounterpartyExplicit) &&
currentCounterpartyExplicit !== previousCounterparty) ||
(Boolean(currentContractExplicit) &&
!isLowQualityContractAnchor(currentContractExplicit) &&
currentContractExplicit !== previousContract) ||
(Boolean(currentItemExplicit) && currentItemExplicit !== previousItem) ||
(Boolean(currentAccountExplicit) && currentAccountExplicit !== previousAccount);
const vatRelativeMonthFollowup =
relativeMonthFromFollowupYear &&
(intent === "vat_payable_confirmed_as_of_date" ||
@ -1254,7 +1326,8 @@ function mergeFollowupFilters(
hasFollowupSignal &&
!hasExplicitPeriodInMessage &&
!inventoryLifecycleHistoryIntent &&
!vatRelativeMonthFollowup
!vatRelativeMonthFollowup &&
!shouldSuppressGenericPeriodCarryover
) {
if (previousPeriodFrom) {
merged.period_from = previousPeriodFrom;
@ -1295,6 +1368,18 @@ function mergeFollowupFilters(
}
}
const finalOrganizationReference =
toNonEmptyString(merged.organization) ??
previousOrganization ??
(followupContext.previous_anchor_type === "organization" ? previousAnchorValue : null);
const finalCounterparty = toNonEmptyString(merged.counterparty);
if (shouldSuppressInventoryCounterpartyAlias(intent, finalCounterparty, finalOrganizationReference)) {
delete merged.counterparty;
if (!reasons.includes("counterparty_cleared_as_organization_scope_alias")) {
reasons.push("counterparty_cleared_as_organization_scope_alias");
}
}
return { filters: merged, reasons };
}

View File

@ -409,10 +409,12 @@ export function createAssistantRoutePolicy(deps) {
const lastOrganizationClarificationDebug = findLastOrganizationClarificationAddressDebug(sessionItems);
const organizationClarificationCandidates = organizationAuthority.organizationClarificationCandidates;
const organizationClarificationSelectionFromScope = organizationAuthority.organizationClarificationSelectionFromScope;
const organizationClarificationSelection = resolveOrganizationSelectionFromMessage(rawUserMessage, organizationClarificationCandidates) ??
const explicitOrganizationClarificationSelection = resolveOrganizationSelectionFromMessage(rawUserMessage, organizationClarificationCandidates) ??
resolveOrganizationSelectionFromMessage(repairedRawUserMessage, organizationClarificationCandidates) ??
resolveOrganizationSelectionFromMessage(effectiveAddressUserMessage, organizationClarificationCandidates) ??
resolveOrganizationSelectionFromMessage(repairedEffectiveAddressUserMessage, organizationClarificationCandidates) ??
null;
const organizationClarificationSelection = explicitOrganizationClarificationSelection ??
(organizationClarificationSelectionFromScope &&
organizationClarificationCandidates.some((candidate) => normalizeOrganizationScopeValue(candidate) === organizationClarificationSelectionFromScope)
? organizationClarificationSelectionFromScope
@ -523,11 +525,12 @@ export function createAssistantRoutePolicy(deps) {
hasShortInventoryObjectFollowupSignal(repairedEffectiveAddressUserMessage)));
const organizationClarificationContinuationDetected = Boolean((followupContext || continuitySnapshot.hasGroundedAddressContext) &&
lastOrganizationClarificationDebug &&
organizationClarificationSelection &&
explicitOrganizationClarificationSelection &&
!dataScopeMetaQuery &&
!capabilityMetaQuery &&
!dataRetrievalSignal);
const organizationScopeSwitchDetected = Boolean(organizationClarificationSelection &&
const organizationScopeSwitchDetected = Boolean(explicitOrganizationClarificationSelection &&
!organizationClarificationContinuationDetected &&
!dataScopeMetaQuery &&
!capabilityMetaQuery &&
(shouldEmitOrganizationSelectionReply(rawUserMessage, organizationClarificationSelection) ||

View File

@ -419,7 +419,10 @@ export function createAssistantTransitionPolicy(deps) {
const hasOrganizationClarificationContinuation = Boolean(
lastOrganizationClarificationDebug && organizationClarificationSelection
);
const followupOffer = previousAddressDebug ? deps.buildAddressFollowupOffer(previousAddressDebug) : null;
const carryoverSourceDebug =
previousAddressDebug ??
(hasOrganizationClarificationContinuation ? lastOrganizationClarificationDebug : null);
const followupOffer = carryoverSourceDebug ? deps.buildAddressFollowupOffer(carryoverSourceDebug) : null;
const hasImplicitContinuationSignal =
Boolean(previousAddressDebug) &&
Boolean(followupOffer?.enabled) &&
@ -427,7 +430,7 @@ export function createAssistantTransitionPolicy(deps) {
(deps.toNonEmptyString(alternateMessage)
? deps.isImplicitAddressContinuationByLlm(alternateMessage, llmPreDecomposeMeta)
: false));
const sourceIntentHint = deps.toNonEmptyString(previousAddressDebug?.detected_intent);
const sourceIntentHint = deps.toNonEmptyString(carryoverSourceDebug?.detected_intent);
const navigationFocusObjectHint =
addressNavigationState &&
typeof addressNavigationState === "object" &&
@ -560,10 +563,10 @@ export function createAssistantTransitionPolicy(deps) {
) {
return null;
}
if (!previousAddressDebug) {
if (!carryoverSourceDebug) {
return null;
}
const sourceIntent = deps.toNonEmptyString(previousAddressDebug.detected_intent);
const sourceIntent = deps.toNonEmptyString(carryoverSourceDebug.detected_intent);
const llmExplicitIntent = deps.toNonEmptyString(llmPreDecomposeMeta?.predecomposeContract?.intent);
const llmSelectedObjectScopeDetected =
llmPreDecomposeMeta?.predecomposeContract?.semantics?.selected_object_scope_detected === true;
@ -637,7 +640,7 @@ export function createAssistantTransitionPolicy(deps) {
followupSelectionMode = "switch_to_suggested_intent";
}
}
const previousAnchorContext = resolveAddressDebugAnchorContext(previousAddressDebug, deps.toNonEmptyString);
const previousAnchorContext = resolveAddressDebugAnchorContext(carryoverSourceDebug, deps.toNonEmptyString);
let previousAnchorType = previousAnchorContext.anchorType;
let previousAnchor = previousAnchorContext.anchorValue;
const navigationSessionContext =
@ -723,7 +726,7 @@ export function createAssistantTransitionPolicy(deps) {
);
let resolvedCounterpartyFromDisplay = false;
let displayedEntityTargetIntent = null;
let previousFilters = resolveAddressDebugCarryoverFilters(previousAddressDebug, deps.toNonEmptyString);
let previousFilters = resolveAddressDebugCarryoverFilters(carryoverSourceDebug, deps.toNonEmptyString);
const shouldBackfillHistoricalPartyAnchors =
sourceIntentHint === "list_contracts_by_counterparty" ||
sourceIntentHint === "list_documents_by_counterparty" ||
@ -754,12 +757,12 @@ export function createAssistantTransitionPolicy(deps) {
if (inventoryPurchaseDateVatBridge) {
const purchaseBridgeItem =
previousAddressItem &&
deps.toNonEmptyString(previousAddressDebug?.detected_intent) === "inventory_purchase_provenance_for_item"
deps.toNonEmptyString(carryoverSourceDebug?.detected_intent) === "inventory_purchase_provenance_for_item"
? previousAddressItem
: findRecentInventoryPurchaseProvenanceItem(
items,
deps.toNonEmptyString(navigationFocusObjectLabel) ??
readAddressDebugItem(previousAddressDebug, deps.toNonEmptyString) ??
readAddressDebugItem(carryoverSourceDebug, deps.toNonEmptyString) ??
deps.toNonEmptyString(previousFilters.item)
) ?? previousAddressItem;
const purchaseBridgeWindow = extractPurchaseDateBridgeWindow(purchaseBridgeItem, addressNavigationState);

View File

@ -165,4 +165,79 @@ describe("address follow-up temporal regressions", () => {
expect(result?.filters.extracted_filters.period_to).toBe("2015-02-28");
expect(result?.baseReasons).toContain("period_from_followup_context");
});
it("clears counterparty noise from bare organization clarification selections that resume inventory", () => {
const result = runAddressDecomposeStage("АЛЬТЕРНАТИВА", {
previous_intent: "inventory_on_hand_as_of_date",
target_intent: "inventory_on_hand_as_of_date",
previous_filters: {
organization: "ООО Альтернатива Плюс",
as_of_date: "2026-04-19"
},
previous_anchor_type: "organization",
previous_anchor_value: "ООО Альтернатива Плюс"
});
expect(result).not.toBeNull();
expect(result?.intent.intent).toBe("inventory_on_hand_as_of_date");
expect(result?.filters.extracted_filters.organization).toBe("ООО Альтернатива Плюс");
expect(result?.filters.extracted_filters.as_of_date).toBe("2026-04-19");
expect(result?.filters.extracted_filters.counterparty).toBeUndefined();
});
it("does not re-inherit organization alias as counterparty into historical inventory follow-up", () => {
const result = runAddressDecomposeStage("давай на июль 2017", {
previous_intent: "inventory_on_hand_as_of_date",
target_intent: "inventory_on_hand_as_of_date",
previous_filters: {
organization: "ООО Альтернатива Плюс",
counterparty: "АЛЬТЕРНАТИВА",
as_of_date: "2026-04-19"
},
previous_anchor_type: "counterparty",
previous_anchor_value: "АЛЬТЕРНАТИВА",
root_intent: "inventory_on_hand_as_of_date",
root_filters: {
organization: "ООО Альтернатива Плюс",
as_of_date: "2026-04-19"
},
root_anchor_type: "organization",
root_anchor_value: "ООО Альтернатива Плюс",
current_frame_kind: "inventory_root"
});
expect(result).not.toBeNull();
expect(result?.intent.intent).toBe("inventory_on_hand_as_of_date");
expect(result?.filters.extracted_filters.organization).toBe("ООО Альтернатива Плюс");
expect(result?.filters.extracted_filters.period_from).toBe("2017-07-01");
expect(result?.filters.extracted_filters.period_to).toBe("2017-07-31");
expect(result?.filters.extracted_filters.counterparty).toBeUndefined();
expect(result?.baseReasons).toContain("counterparty_cleared_as_organization_scope_alias");
});
it("does not inherit stale historical period into a fresh counterparty document root", () => {
const result = runAddressDecomposeStage("по чепурнову покажи все доки", {
previous_intent: "inventory_on_hand_as_of_date",
target_intent: "list_documents_by_counterparty",
previous_filters: {
organization: "ООО Альтернатива Плюс",
period_from: "2016-03-01",
period_to: "2016-03-31",
as_of_date: "2016-03-31"
},
previous_anchor_type: "organization",
previous_anchor_value: "ООО Альтернатива Плюс"
}, {
scope_target_kind: "counterparty",
scope_target_text: "Чепурнов",
date_scope_kind: "missing",
self_scope_detected: false,
selected_object_scope_detected: false
});
expect(result).not.toBeNull();
expect(result?.intent.intent).toBe("list_documents_by_counterparty");
expect(result?.filters.extracted_filters.counterparty).toBeTruthy();
expect(result?.filters.extracted_filters.period_from).toBeUndefined();
expect(result?.filters.extracted_filters.period_to).toBeUndefined();
});
});

View File

@ -12,6 +12,51 @@ vi.mock("../src/services/addressMcpClient", async () => {
...actual,
executeAddressMcpQuery: executeAddressMcpQueryMock
};
it("clears company-name counterparty noise when a bare organization selection resumes inventory", async () => {
executeAddressMcpQueryMock.mockResolvedValueOnce({
fetched_rows: 1,
matched_rows: 1,
raw_rows: [
{
Period: "2026-04-19T23:59:59Z",
Registrator: "Остатки товаров на складах",
AccountDt: "41.01",
AccountKt: "00.00",
Amount: 148261.67,
Quantity: 22,
SubcontoDt1: "Модуль прямоугольый 1400*110*750",
Warehouse: "Основной склад",
Organization: 'ООО "Альтернатива Плюс"'
}
],
rows: [],
error: null
});
const service = new AddressQueryService();
const result = await service.tryHandle("АЛЬТЕРНАТИВА", {
activeOrganization: 'ООО "Альтернатива Плюс"',
knownOrganizations: ['ООО "Альтернатива Плюс"', "ООО Лайсвуд"],
followupContext: {
previous_intent: "inventory_on_hand_as_of_date",
target_intent: "inventory_on_hand_as_of_date",
previous_filters: {
organization: 'ООО "Альтернатива Плюс"',
as_of_date: "2026-04-19"
},
previous_anchor_type: "organization",
previous_anchor_value: 'ООО "Альтернатива Плюс"'
}
});
expect(result?.handled).toBe(true);
expect(result?.reply_type).toBe("factual");
expect(result?.debug.detected_intent).toBe("inventory_on_hand_as_of_date");
expect(result?.debug.extracted_filters?.organization).toBe('ООО "Альтернатива Плюс"');
expect(result?.debug.extracted_filters?.counterparty).toBeUndefined();
expect(result?.debug.reasons).toContain("counterparty_cleared_from_referential_organization_scope");
expect(String(result?.reply_text ?? "")).toContain("Модуль прямоугольый 1400*110*750");
});
});
import { AddressQueryService } from "../src/services/addressQueryService";

View File

@ -588,7 +588,7 @@ describe("assistant address follow-up carryover", () => {
expect(calls[1].options?.followupContext?.previous_filters?.period_from).toBe("2020-06-01");
expect(calls[1].options?.followupContext?.previous_filters?.period_to).toBe("2020-06-30");
expect(calls[1].options?.followupContext?.root_intent).toBe("inventory_on_hand_as_of_date");
expect(calls[1].options?.followupContext?.root_filters?.organization).toBe("ООО \\Альтернатива Плюс\\");
expect(calls[1].options?.followupContext?.root_filters?.organization).toBe("ООО Альтернатива Плюс");
expect(calls[1].options?.followupContext?.root_filters?.as_of_date).toBe("2020-06-30");
expect(calls[1].options?.followupContext?.current_frame_kind).toBe("inventory_root");
expect(calls[1].options?.followupContext?.previous_filters?.warehouse).toBe("Основной склад");
@ -2059,12 +2059,134 @@ describe("assistant address follow-up carryover", () => {
expect(normalizerService.normalize).not.toHaveBeenCalled();
});
it("keeps historical inventory date follow-up alive after company clarification and a capability answer", async () => {
const calls: Array<{ message: string; options?: any }> = [];
const firstMessage = "покажи остатки по складу";
const secondMessage = "Альтернатива";
const historicalCapabilityMessage = "а исторические остатки на другие даты умеешь?";
const dateFollowupMessage = "давай на июль 2017";
const organization = "ООО Альтернатива Плюс";
const addressQueryService = {
tryHandle: vi.fn(async (message: string, options?: any) => {
calls.push({ message, options });
if (message === firstMessage) {
return buildAddressLimitedLaneResult("missing_anchor", {
reply_text: [
"Нужно уточнить организацию, чтобы не смешивать компании в одном ответе.",
"Сейчас в доступном контуре вижу такие организации:",
"- ООО Альтернатива Плюс",
"- ООО Лайсвуд"
].join("\n"),
debug: {
...buildAddressLimitedLaneResult("missing_anchor").debug,
detected_intent: "inventory_on_hand_as_of_date",
extracted_filters: {
as_of_date: "2026-04-19"
},
organization_candidates: [organization, "ООО Лайсвуд"],
reasons: ["organization_clarification_required", "multiple_known_organizations_detected"]
}
});
}
if (message === secondMessage && options?.followupContext && options?.activeOrganization === organization) {
return buildAddressLaneResult({
reply_text: "На 19.04.2026 по ООО Альтернатива Плюс подтвержден складской остаток.",
debug: {
...buildAddressLaneResult().debug,
detected_intent: "inventory_on_hand_as_of_date",
extracted_filters: {
as_of_date: "2026-04-19",
organization
},
reasons: ["address_followup_context_applied", "organization_grounded_from_scope_candidates"]
}
});
}
if (message === dateFollowupMessage && options?.followupContext) {
return buildAddressLaneResult({
reply_text: "На 31.07.2017 по ООО Альтернатива Плюс подтвержден складской остаток.",
debug: {
...buildAddressLaneResult().debug,
detected_intent: "inventory_on_hand_as_of_date",
extracted_filters: {
organization,
as_of_date: "2017-07-31",
period_from: "2017-07-01",
period_to: "2017-07-31"
},
reasons: ["address_followup_context_applied", "inventory_root_temporal_followup_detected"]
}
});
}
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-address-org-historical-${Date.now()}`;
const first = await service.handleMessage({
session_id: sessionId,
user_message: firstMessage,
useMock: true
} as any);
expect(first.ok).toBe(true);
expect(first.reply_type).toBe("partial_coverage");
const second = await service.handleMessage({
session_id: sessionId,
user_message: secondMessage,
useMock: true
} as any);
expect(second.ok).toBe(true);
expect(second.reply_type).toBe("factual");
const third = await service.handleMessage({
session_id: sessionId,
user_message: historicalCapabilityMessage,
useMock: true
} as any);
expect(third.ok).toBe(true);
expect(["factual", "factual_with_explanation"]).toContain(third.reply_type);
const fourth = await service.handleMessage({
session_id: sessionId,
user_message: dateFollowupMessage,
useMock: true
} as any);
expect(fourth.ok).toBe(true);
expect(fourth.reply_type).toBe("factual");
const dateCall = calls.find((entry) => entry.message === dateFollowupMessage);
expect(dateCall).toBeTruthy();
expect(dateCall?.options?.followupContext?.previous_intent).toBe("inventory_on_hand_as_of_date");
expect(dateCall?.options?.followupContext?.previous_filters?.organization).toBe(organization);
expect(dateCall?.options?.followupContext?.target_intent).toBe("inventory_on_hand_as_of_date");
expect(normalizerService.normalize).not.toHaveBeenCalled();
});
it("sanitizes selected-item carryover when inventory drilldown pivots into VAT follow-up", async () => {
const calls: Array<{ message: string; options?: any }> = [];
const followupMessage = "\u0430 \u043d\u0434\u0441?";
const itemLabel =
"\u041a\u0440\u043e\u043c\u043a\u0430 \u0441 \u043a\u043b\u0435\u0435\u043c 33 \u0434\u0443\u0431 \u043d\u0438\u0430\u0433\u0430\u0440\u0430 137 \u043c";
const organization = "\u041e\u041e\u041e \\\u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441\\";
const organization = "\u041e\u041e\u041e \u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u0430 \u041f\u043b\u044e\u0441";
const warehouse = "\u041e\u0441\u043d\u043e\u0432\u043d\u043e\u0439 \u0441\u043a\u043b\u0430\u0434";
const vatResult = buildAddressLaneResult({
@ -2204,7 +2326,7 @@ describe("assistant address follow-up carryover", () => {
detected_intent_confidence: "medium",
extracted_filters: {
item: "Столешница 600*3050*26 дуб ниагара",
organization: "ООО \\Альтернатива Плюс\\",
organization: "ООО Альтернатива Плюс",
as_of_date: "2020-05-31"
},
selected_recipe: "address_inventory_sale_trace_for_item_v1",
@ -2225,7 +2347,7 @@ describe("assistant address follow-up carryover", () => {
detected_intent_confidence: "medium",
extracted_filters: {
item: "Столешница 600*3050*26 дуб ниагара",
organization: "ООО \\Альтернатива Плюс\\",
organization: "ООО Альтернатива Плюс",
as_of_date: "2020-05-31"
},
selected_recipe: "address_inventory_purchase_provenance_for_item_v1",
@ -2284,7 +2406,7 @@ describe("assistant address follow-up carryover", () => {
expect(calls[0].message).toBe(followupMessage);
expect(calls[0].options?.followupContext?.previous_intent).toBe("inventory_sale_trace_for_item");
expect(calls[0].options?.followupContext?.previous_filters?.item).toBe("Столешница 600*3050*26 дуб ниагара");
expect(calls[0].options?.followupContext?.previous_filters?.organization).toBe("ООО \\Альтернатива Плюс\\");
expect(calls[0].options?.followupContext?.previous_filters?.organization).toBe("ООО Альтернатива Плюс");
expect(calls[0].options?.followupContext?.previous_filters?.as_of_date).toBe("2020-05-31");
expect(normalizerService.normalize).not.toHaveBeenCalled();
});
@ -2872,6 +2994,7 @@ describe("assistant address follow-up carryover", () => {
it("does not backfill stale counterparty anchors into inventory root temporal follow-ups", async () => {
const calls: Array<{ message: string; options?: any }> = [];
const followupMessage = "остатки на июль 2019";
const organization = 'ООО "Альтернатива Плюс"';
const addressQueryService = {
tryHandle: vi.fn(async (message: string, options?: any) => {
@ -2885,7 +3008,7 @@ describe("assistant address follow-up carryover", () => {
detected_intent: "inventory_on_hand_as_of_date",
selected_recipe: "address_inventory_on_hand_as_of_date_v1",
extracted_filters: {
organization: 'ООО "Альтернатива Плюс"',
organization,
period_from: "2019-07-01",
period_to: "2019-07-31",
as_of_date: "2019-07-31"
@ -2950,13 +3073,13 @@ describe("assistant address follow-up carryover", () => {
detected_mode: "address_query",
detected_intent: "inventory_on_hand_as_of_date",
extracted_filters: {
organization: 'ООО "Альтернатива Плюс"',
organization,
as_of_date: "2026-04-16"
},
selected_recipe: "address_inventory_on_hand_as_of_date_v1",
anchor_type: "organization",
anchor_value_raw: 'ООО "Альтернатива Плюс"',
anchor_value_resolved: 'ООО "Альтернатива Плюс"'
anchor_value_raw: organization,
anchor_value_resolved: organization
}
} as any);
sessions.setAddressNavigationState(sessionId, {
@ -2970,7 +3093,7 @@ describe("assistant address follow-up carryover", () => {
period_from: null,
period_to: null
},
organization_scope: 'ООО "Альтернатива Плюс"'
organization_scope: organization
},
result_sets: [
{
@ -2978,7 +3101,7 @@ describe("assistant address follow-up carryover", () => {
type: "inventory_snapshot",
route_id: "address_inventory_on_hand_as_of_date_v1",
filters: {
organization: 'ООО "Альтернатива Плюс"',
organization,
as_of_date: "2026-04-16"
},
entity_refs: [],
@ -3013,7 +3136,7 @@ describe("assistant address follow-up carryover", () => {
expect(calls[0].options?.followupContext?.previous_intent).toBeUndefined();
expect(calls[0].options?.followupContext?.target_intent).toBe("inventory_on_hand_as_of_date");
expect(calls[0].options?.followupContext?.previous_filters?.counterparty).toBeUndefined();
expect(calls[0].options?.followupContext?.previous_filters?.organization).toBe('ООО "Альтернатива Плюс"');
expect(calls[0].options?.followupContext?.previous_filters?.organization).toBe(organization);
expect(calls[0].options?.followupContext?.root_intent).toBe("inventory_on_hand_as_of_date");
expect(calls[0].options?.followupContext?.root_filters?.counterparty).toBeUndefined();
expect(normalizerService.normalize).not.toHaveBeenCalled();

View File

@ -557,6 +557,55 @@ describe("assistantRoutePolicy", () => {
expect(decision.orchestrationContract?.followup_context_detected).toBe(false);
});
it("does not turn short entity follow-up into organization switch just because scope already has an active company", () => {
const policy = buildPolicy({
resolveAddressToolGateDecision: undefined,
findLastOrganizationClarificationAddressDebug: () => ({
execution_lane: "address_query",
limited_reason_category: "missing_anchor",
organization_candidates: ["ООО Альтернатива Плюс", "ООО Лайсвуд", "РАЙМ"]
}),
shouldEmitOrganizationSelectionReply: () => true
});
const decision = policy.resolveAssistantOrchestrationDecision({
rawUserMessage: "а по свк",
effectiveAddressUserMessage: "а по свк",
followupContext: {
previous_intent: "list_documents_by_counterparty",
previous_filters: {
counterparty: "Чепурнов П.Д."
},
previous_anchor_type: "counterparty",
previous_anchor_value: "Чепурнов П.Д."
},
sessionItems: [
{
role: "assistant",
debug: {
execution_lane: "address_query",
answer_grounding_check: { status: "grounded" },
detected_intent: "list_documents_by_counterparty",
extracted_filters: {
counterparty: "Чепурнов П.Д.",
organization: "ООО Альтернатива Плюс"
}
}
}
],
sessionOrganizationScope: {
knownOrganizations: ["ООО Альтернатива Плюс", "ООО Лайсвуд", "РАЙМ"],
selectedOrganization: "ООО Альтернатива Плюс",
activeOrganization: "ООО Альтернатива Плюс"
},
llmPreDecomposeMeta: null,
useMock: false
});
expect(decision.toolGateReason).not.toBe("organization_scope_switch_detected");
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", () => {
const policy = buildPolicy({
resolveAddressIntent: () => ({ intent: "counterparty_activity_lifecycle", confidence: "high" }),