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

This commit is contained in:
2026-04-19 17:17:09 +03:00
parent af15e21bf6
commit 0e098cde38
18 changed files with 842 additions and 87 deletions
@@ -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")) {
@@ -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 ||
(Boolean(inheritedCounterparty) &&
isLowQualityCounterpartyAnchor(currentCounterparty) &&
!isLowQualityCounterpartyAnchor(inheritedCounterparty));
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)));
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) {
@@ -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) ||
@@ -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) {
@@ -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;
@@ -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 ||
(Boolean(inheritedCounterparty) &&
isLowQualityCounterpartyAnchor(currentCounterparty) &&
!isLowQualityCounterpartyAnchor(inheritedCounterparty));
!inheritedCounterpartyLooksLikeOrganization &&
(!currentCounterparty ||
(Boolean(inheritedCounterparty) &&
isLowQualityCounterpartyAnchor(currentCounterparty) &&
!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 };
}
@@ -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) ||
@@ -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);
@@ -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();
});
});
@@ -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";
@@ -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();
@@ -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" }),