Архитектура: вернуть company activity assessment в address lane и убрать ложный organization anchor из root-scoped carryover

This commit is contained in:
dctouch 2026-04-18 22:24:41 +03:00
parent 8111586d8d
commit c605fae3a3
12 changed files with 400 additions and 28 deletions

View File

@ -362,6 +362,20 @@ Still open after the accepted phase12 replay:
- active organization bootstrap now flows through selected organization, navigation organization, and shared continuity authority in one place instead of keeping a second callback-shaped fallback branch beside the authority object;
- `assistantService.resolveSessionOrganizationScopeContext(...)` no longer passes that legacy callback into the runtime adapter, which reduces one more orchestration seam where old and new organization owners could drift;
- targeted organization-scope, data-scope, and route regressions remain green after the change, and wide saved-session replay `address_truth_harness_phase12_wider_saved_session_pool_live_20260418_rerun9` remains accepted `20/20`, which is the critical proof that this bootstrap convergence did not reopen the flagship continuity path.
- the next replay-breadth pass now proves a different late-session contour instead of replaying only the flagship chain:
- a new live pack `address_truth_harness_phase14_counterparty_tail_resume` validates `data-scope meta -> explicit company selection -> counterparty docs -> short-name follow-up -> inventory today -> account 60 -> inventory aging -> historical inventory -> organization activity analytics` inside one shared session;
- the first draft of this pack exposed one real architecture seam rather than another continuity collapse: `Как ты оценишь деятельность компании?` after grounded organization activity-age was still falling into `living_chat` because the route depended too much on the L0 gate and not enough on the resolved supported intent;
- `addressCounterpartyIntentSignals` now treats company-activity assessment wording as the same `counterparty_activity_lifecycle` contour instead of leaving it as unsupported meta chat;
- `assistantRoutePolicy` now recovers the address lane from a supported resolved intent even when the initial L0 gate stays negative, so the system no longer loses a real business contour just because the low-level shape classifier stayed `unsupported`;
- targeted counterparty UTF-8 and route-policy regressions now explicitly protect this seam, including the exact late-tail wording `Как ты оценишь деятельность компании?`;
- live replay `address_truth_harness_phase14_counterparty_tail_resume_live_20260418_rerun2` is accepted `10/10`, which is the critical proof that replay breadth is now broader than the original flagship chain and that late-session organization analytics no longer depend on ambient chat luck.
- the next transition-authority pass now closes a subtler root-scoped carryover seam inside the shared follow-up path:
- `assistantService.buildAddressFollowupOffer(...)` now reads follow-up anchor metadata through the shared continuity helper instead of reconstructing it from yet another local `addressDebug` parser;
- `assistantTransitionPolicy` no longer promotes assistant-side `organization authority` into `previous_anchor_type=organization` when a root-scoped inventory pivot intentionally sanitizes the selected-item carryover and keeps only the restored root frame;
- this matters because `root_context_only` VAT pivots from inventory drilldown should preserve restored organization/date filters without pretending that restored scope is itself a user-selected follow-up anchor;
- targeted `assistantAddressFollowupContext` and `assistantTransitionPolicy` suites are now green after the fix, explicitly protecting the `inventory drilldown -> VAT pivot` regression where selected-item carryover must be removed while the inventory root company/date window remains intact;
- live replay `address_truth_harness_phase12_wider_saved_session_pool_live_20260418_rerun10` remains accepted `20/20`, which is the critical proof that this anchor-sanitization convergence did not reopen the flagship saved-session continuity path.
## Next Execution Slice (2026-04-18)

View File

@ -0,0 +1,250 @@
{
"schema_version": "domain_truth_harness_spec_v1",
"scenario_id": "address_truth_harness_phase14_counterparty_tail_resume",
"domain": "address_phase14_counterparty_tail_resume",
"title": "Phase 14 counterparty-tail replay for late-session authority breadth",
"description": "Alternative AGENT replay built from a different saved-session tail. The scenario validates explicit organization selection, exact counterparty documents, a short-name counterparty follow-up, inventory and account-60 pivots without repeated company loss, old-purchase inventory aging, historical inventory on May 2020, and organization activity analytics at the tail of the same live session.",
"bindings": {},
"steps": [
{
"step_id": "step_01_data_scope_meta",
"title": "Session starts with a human company-scope answer",
"question": "по какой компании мы сейчас работаем?",
"required_answer_patterns_all": [
"(?i)компан|организац|контур",
"(?i)альтернатива плюс|лайсвуд|райм"
],
"forbidden_answer_patterns": [
"(?i)mcp",
"(?i)read_only",
"(?i)snapshot_items",
"(?i)tool_gate_reason",
"(?i)living_reason"
],
"criticality": "critical",
"semantic_tags": [
"data_scope_meta",
"multi_company_entry"
]
},
{
"step_id": "step_02_choose_organization_after_clarification",
"title": "Explicit organization selection fixes the contour for the tail session",
"question": "Альтернатива Плюс",
"required_answer_patterns_all": [
"(?i)зафиксир|работаем по|рабочую организац",
"(?i)Альтернатива Плюс"
],
"forbidden_answer_patterns": [
"(?i)mcp",
"(?i)read_only",
"(?i)не могу определить"
],
"criticality": "critical",
"semantic_tags": [
"organization_authority",
"company_selected"
]
},
{
"step_id": "step_03_counterparty_docs_after_choice",
"title": "Counterparty-documents root becomes exact after company choice",
"question": "по чепурнову покажи все доки",
"allowed_reply_types": [
"factual"
],
"expected_intents": [
"list_documents_by_counterparty"
],
"expected_recipe": "address_documents_by_counterparty_v1",
"required_direct_answer_patterns_any": [
"(?i)чепурнов",
"(?i)документ|поступление"
],
"criticality": "critical",
"semantic_tags": [
"counterparty_root",
"company_selected"
]
},
{
"step_id": "step_04_short_counterparty_followup",
"title": "Short-name counterparty follow-up keeps the correct target and name",
"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)контрагент: группа\\s+найдено",
"(?i)mcp"
],
"criticality": "important",
"semantic_tags": [
"counterparty_followup",
"display_label_integrity"
]
},
{
"step_id": "step_05_inventory_today_after_counterparty_tail",
"title": "Inventory pivot after counterparty docs keeps the selected organization",
"question": "а сейчас у нас есть что на складе?",
"allowed_reply_types": [
"factual"
],
"expected_intents": [
"inventory_on_hand_as_of_date"
],
"required_filters": {
"as_of_date": "2026-04-18",
"organization": "ООО Альтернатива Плюс"
},
"required_direct_answer_patterns_any": [
"(?i)на складе|остат",
"18\\.04\\.2026"
],
"forbidden_direct_answer_patterns": [
"(?i)уточните организац",
"(?i)по какой компании"
],
"criticality": "critical",
"semantic_tags": [
"inventory_root",
"cross_domain_pivot"
]
},
{
"step_id": "step_06_open_items_account_60_august_2022",
"title": "Account 60 tails stay exact after the counterparty-to-inventory pivot",
"question": "хвосты покажи по счету 60 на август 2022",
"allowed_reply_types": [
"factual",
"factual_with_explanation"
],
"expected_intents": [
"open_items_by_counterparty_or_contract"
],
"required_filters": {
"account": "60",
"period_from": "2022-08-01",
"period_to": "2022-08-31",
"as_of_date": "2022-08-31"
},
"required_direct_answer_patterns_any": [
"(?i)счету 60|счёту 60",
"(?i)хвост|открыт|не найдено"
],
"criticality": "critical",
"semantic_tags": [
"settlements_account_60",
"late_session_tail"
]
},
{
"step_id": "step_07_inventory_aging_old_purchases",
"title": "Old-purchase inventory aging remains reachable late in the same session",
"question": "Есть ли остатки товара, которые закупались очень давно",
"allowed_reply_types": [
"factual",
"factual_with_explanation"
],
"expected_intents": [
"inventory_aging_by_purchase_date"
],
"required_direct_answer_patterns_any": [
"(?i)стар|давно|ранней первой закупк",
"(?i)остат|закуп"
],
"criticality": "critical",
"semantic_tags": [
"inventory_aging",
"late_session_stability"
]
},
{
"step_id": "step_08_inventory_may_2020_after_aging",
"title": "Historical inventory root restores a concrete month after the aging branch",
"question": "Какие конкретно номенклатуры формируют остаток по складу на май 2020",
"allowed_reply_types": [
"factual"
],
"expected_intents": [
"inventory_on_hand_as_of_date"
],
"required_filters": {
"as_of_date": "2020-05-31",
"period_from": "2020-05-01",
"period_to": "2020-05-31",
"organization": "ООО Альтернатива Плюс"
},
"required_direct_answer_patterns_any": [
"31\\.05\\.2020",
"(?i)позици|остат"
],
"criticality": "critical",
"semantic_tags": [
"inventory_root",
"historical_restore"
]
},
{
"step_id": "step_09_company_activity_age_tail",
"title": "Organization activity age still works at the tail of the mixed session",
"question": "а по Альтернативе Плюс сколько лет активности в базе 1С?",
"allowed_reply_types": [
"factual",
"factual_with_explanation",
"partial_coverage"
],
"expected_intents": [
"counterparty_activity_lifecycle"
],
"expected_recipe": "address_counterparty_activity_lifecycle_v1",
"required_direct_answer_patterns_any": [
"(?i)активност",
"(?i)первая подтвержденная|последняя подтвержденная|лет"
],
"forbidden_direct_answer_patterns": [
"(?i)не найден контрагент",
"(?i)уточните точное наименование организации"
],
"criticality": "critical",
"semantic_tags": [
"organization_activity_age",
"tail_authority_proof"
]
},
{
"step_id": "step_10_company_activity_assessment_tail",
"title": "Tail activity assessment stays business-first instead of collapsing to garbage",
"question": "Как ты оценишь деятельность компании?",
"allowed_reply_types": [
"factual",
"factual_with_explanation",
"partial_coverage"
],
"required_direct_answer_patterns_any": [
"(?i)коротко|активн",
"(?i)операц|заказчик|активност|последняя активность"
],
"forbidden_direct_answer_patterns": [
"(?i)mcp",
"(?i)read_only",
"(?i)tool_gate_reason"
],
"criticality": "important",
"semantic_tags": [
"activity_assessment",
"tail_quality"
]
}
]
}

View File

@ -65,8 +65,10 @@ function hasUnicodeCounterpartyActivityLifecycleSignal(text) {
if (!normalized) {
return false;
}
const hasActivityAssessmentCue = /(?:\u043a\u0430\u043a\s+[\p{L}\d_-]+\s+\u043e\u0446\u0435\u043d(?:\u0438\u0448\u044c|\u0438\u0442\u044c|\u0438\u0432\u0430\u0435\u0448\u044c)|\u043e\u0446\u0435\u043d(?:\u0438\u0442\u044c|\u043a\u0430)|\u043e\u0445\u0430\u0440\u0430\u043a\u0442\u0435\u0440\u0438\u0437(?:\u0443\u0435\u0448\u044c|\u043e\u0432\u0430\u0442\u044c)|\u0447\u0442\u043e\s+\u043c\u043e\u0436\u043d\u043e\s+\u0441\u043a\u0430\u0437\u0430\u0442\u044c\s+\u043e)/iu.test(normalized) &&
/(?:\u0434\u0435\u044f\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442|\u0430\u043a\u0442\u0438\u0432\u043d\u043e\u0441\u0442|\u0440\u0430\u0431\u043e\u0442)/iu.test(normalized);
const hasActivityAgeCue = /(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e\s+\u043b\u0435\u0442\s+\u0430\u043a\u0442\u0438\u0432\u043d\u043e\u0441\u0442\u0438|\u0441\u043a\u043e\u043b\u044c\u043a\u043e\s+\u043b\u0435\u0442\s+\u0432\s+\u0431\u0430\u0437\u0435|\u0432\u043e\u0437\u0440\u0430\u0441\u0442\s+\u0430\u043a\u0442\u0438\u0432\u043d\u043e\u0441\u0442\u0438|\u043f\u0435\u0440\u0432(?:\u0430\u044f|\u044b\u0439|\u043e\u0435)\s+(?:\u0430\u043a\u0442\u0438\u0432\u043d\u043e\u0441\u0442\u044c|\u043f\u043b\u0430\u0442\u0435\u0436|\u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d\u0438\u0435|\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442)|\u043f\u043e\u0441\u043b\u0435\u0434\u043d(?:\u044f\u044f|\u0438\u0439|\u0435\u0435)\s+\u0430\u043a\u0442\u0438\u0432\u043d\u043e\u0441\u0442\u044c|\u0441\s+\u043a\u0430\u043a\u043e\u0433\u043e\s+\u0433\u043e\u0434\u0430\s+\u0430\u043a\u0442\u0438\u0432)/iu.test(normalized);
if (!hasActivityAgeCue) {
if (!hasActivityAgeCue && !hasActivityAssessmentCue) {
return false;
}
const hasOneCLexeme = /(?:\u0432\s+\u0431\u0430\u0437\u0435\s+1[\u0441c]|\u0432\s+1[\u0441c]\s+\u0431\u0430\u0437\u0435|\u0438\u0437\s+1[\u0441c])/iu.test(normalized);

View File

@ -721,6 +721,20 @@ function createAssistantRoutePolicy(deps) {
let runAddressLane = Boolean(baseToolGate?.runAddressLane);
let toolGateDecision = String(baseToolGate?.decision ?? "skip_address_lane");
let toolGateReason = String(baseToolGate?.reason ?? "no_address_signal_after_l0");
const semanticAddressLaneRecovery = Boolean(!runAddressLane &&
supportedAddressRouteCandidateDetected &&
!deepAnalysisPreferenceDetected &&
!unsupportedAddressIntentFallbackToDeep &&
!deepAnalysisSignalFallbackToDeep &&
!aggregateAnalyticsFallbackToDeep &&
!deepSessionContinuationFallbackToDeep);
if (semanticAddressLaneRecovery) {
runAddressLane = true;
toolGateDecision = "run_address_lane";
toolGateReason = resolvedIntentResolution.intent !== "unknown" || llmContractIntent
? "address_intent_resolver_detected"
: "address_signal_detected";
}
if (unsupportedAddressIntentFallbackToDeep) {
runAddressLane = false;
toolGateDecision = "skip_address_lane";

View File

@ -73,6 +73,7 @@ const assistantBoundaryPolicy_1 = __importStar(require("./assistantBoundaryPolic
const assistantLivingModePolicy_1 = __importStar(require("./assistantLivingModePolicy"));
const assistantMetaFollowupPolicy_1 = __importStar(require("./assistantMetaFollowupPolicy"));
const assistantMemoryRecapPolicy_1 = __importStar(require("./assistantMemoryRecapPolicy"));
const assistantContinuityPolicy_1 = __importStar(require("./assistantContinuityPolicy"));
const assistantProviderExecutionPolicy_1 = __importStar(require("./assistantProviderExecutionPolicy"));
const assistantRoutePolicy_1 = __importStar(require("./assistantRoutePolicy"));
const assistantTransitionPolicy_1 = __importStar(require("./assistantTransitionPolicy"));
@ -2512,18 +2513,12 @@ function buildAddressFollowupOffer(addressDebug) {
if (!Array.isArray(suggestedIntents) || suggestedIntents.length === 0) {
return null;
}
const anchorType = toNonEmptyString(addressDebug.anchor_type);
const anchorValue = toNonEmptyString(addressDebug.anchor_value_resolved) ??
toNonEmptyString(addressDebug.anchor_value_raw) ??
readAddressInventoryItemFilter(addressDebug) ??
readAddressFilterString(addressDebug, "counterparty") ??
readAddressFilterString(addressDebug, "contract") ??
readAddressFilterString(addressDebug, "account");
const anchorContext = (0, assistantContinuityPolicy_1.resolveAddressDebugAnchorContext)(addressDebug, toNonEmptyString);
return {
enabled: true,
source_intent: intent,
anchor_type: anchorType ?? "unknown",
anchor_value: anchorValue,
anchor_type: anchorContext.anchorType ?? "unknown",
anchor_value: anchorContext.anchorValue,
suggested_intents: suggestedIntents
};
}

View File

@ -344,10 +344,11 @@ function createAssistantTransitionPolicy(deps) {
const organizationClarificationCandidates = Array.isArray(organizationAuthority.organizationClarificationCandidates)
? organizationAuthority.organizationClarificationCandidates
: [];
const organizationClarificationSelection = deps.resolveOrganizationSelectionFromMessage(userMessage, organizationClarificationCandidates) ??
const explicitOrganizationClarificationSelection = deps.resolveOrganizationSelectionFromMessage(userMessage, organizationClarificationCandidates) ??
(deps.toNonEmptyString(alternateMessage)
? deps.resolveOrganizationSelectionFromMessage(String(alternateMessage ?? ""), organizationClarificationCandidates)
: null) ??
: null);
const organizationClarificationSelection = explicitOrganizationClarificationSelection ??
deps.normalizeOrganizationScopeValue(organizationAuthority.organizationClarificationSelectionFromScope);
const hasOrganizationClarificationContinuation = Boolean(lastOrganizationClarificationDebug && organizationClarificationSelection);
const followupOffer = previousAddressDebug ? deps.buildAddressFollowupOffer(previousAddressDebug) : null;
@ -798,9 +799,9 @@ function createAssistantTransitionPolicy(deps) {
previousAnchor = selectedObjectLabel;
}
}
if (organizationClarificationSelection && !previousAnchor) {
if (explicitOrganizationClarificationSelection && !previousAnchor) {
previousAnchorType = "organization";
previousAnchor = organizationClarificationSelection;
previousAnchor = explicitOrganizationClarificationSelection;
}
if (inventoryRootFrame &&
organizationClarificationSelection &&

View File

@ -114,11 +114,19 @@ function hasUnicodeCounterpartyActivityLifecycleSignal(text: string): boolean {
return false;
}
const hasActivityAssessmentCue =
/(?:\u043a\u0430\u043a\s+[\p{L}\d_-]+\s+\u043e\u0446\u0435\u043d(?:\u0438\u0448\u044c|\u0438\u0442\u044c|\u0438\u0432\u0430\u0435\u0448\u044c)|\u043e\u0446\u0435\u043d(?:\u0438\u0442\u044c|\u043a\u0430)|\u043e\u0445\u0430\u0440\u0430\u043a\u0442\u0435\u0440\u0438\u0437(?:\u0443\u0435\u0448\u044c|\u043e\u0432\u0430\u0442\u044c)|\u0447\u0442\u043e\s+\u043c\u043e\u0436\u043d\u043e\s+\u0441\u043a\u0430\u0437\u0430\u0442\u044c\s+\u043e)/iu.test(
normalized
) &&
/(?:\u0434\u0435\u044f\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442|\u0430\u043a\u0442\u0438\u0432\u043d\u043e\u0441\u0442|\u0440\u0430\u0431\u043e\u0442)/iu.test(
normalized
);
const hasActivityAgeCue =
/(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e\s+\u043b\u0435\u0442\s+\u0430\u043a\u0442\u0438\u0432\u043d\u043e\u0441\u0442\u0438|\u0441\u043a\u043e\u043b\u044c\u043a\u043e\s+\u043b\u0435\u0442\s+\u0432\s+\u0431\u0430\u0437\u0435|\u0432\u043e\u0437\u0440\u0430\u0441\u0442\s+\u0430\u043a\u0442\u0438\u0432\u043d\u043e\u0441\u0442\u0438|\u043f\u0435\u0440\u0432(?:\u0430\u044f|\u044b\u0439|\u043e\u0435)\s+(?:\u0430\u043a\u0442\u0438\u0432\u043d\u043e\u0441\u0442\u044c|\u043f\u043b\u0430\u0442\u0435\u0436|\u043f\u043e\u0441\u0442\u0443\u043f\u043b\u0435\u043d\u0438\u0435|\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442)|\u043f\u043e\u0441\u043b\u0435\u0434\u043d(?:\u044f\u044f|\u0438\u0439|\u0435\u0435)\s+\u0430\u043a\u0442\u0438\u0432\u043d\u043e\u0441\u0442\u044c|\u0441\s+\u043a\u0430\u043a\u043e\u0433\u043e\s+\u0433\u043e\u0434\u0430\s+\u0430\u043a\u0442\u0438\u0432)/iu.test(
normalized
);
if (!hasActivityAgeCue) {
if (!hasActivityAgeCue && !hasActivityAssessmentCue) {
return false;
}

View File

@ -760,6 +760,20 @@ export function createAssistantRoutePolicy(deps) {
let runAddressLane = Boolean(baseToolGate?.runAddressLane);
let toolGateDecision = String(baseToolGate?.decision ?? "skip_address_lane");
let toolGateReason = String(baseToolGate?.reason ?? "no_address_signal_after_l0");
const semanticAddressLaneRecovery = Boolean(!runAddressLane &&
supportedAddressRouteCandidateDetected &&
!deepAnalysisPreferenceDetected &&
!unsupportedAddressIntentFallbackToDeep &&
!deepAnalysisSignalFallbackToDeep &&
!aggregateAnalyticsFallbackToDeep &&
!deepSessionContinuationFallbackToDeep);
if (semanticAddressLaneRecovery) {
runAddressLane = true;
toolGateDecision = "run_address_lane";
toolGateReason = resolvedIntentResolution.intent !== "unknown" || llmContractIntent
? "address_intent_resolver_detected"
: "address_signal_detected";
}
if (unsupportedAddressIntentFallbackToDeep) {
runAddressLane = false;
toolGateDecision = "skip_address_lane";

View File

@ -26,6 +26,7 @@ import * as assistantBoundaryPolicy_1 from "./assistantBoundaryPolicy";
import * as assistantLivingModePolicy_1 from "./assistantLivingModePolicy";
import * as assistantMetaFollowupPolicy_1 from "./assistantMetaFollowupPolicy";
import * as assistantMemoryRecapPolicy_1 from "./assistantMemoryRecapPolicy";
import * as assistantContinuityPolicy_1 from "./assistantContinuityPolicy";
import * as assistantProviderExecutionPolicy_1 from "./assistantProviderExecutionPolicy";
import * as assistantRoutePolicy_1 from "./assistantRoutePolicy";
import * as assistantTransitionPolicy_1 from "./assistantTransitionPolicy";
@ -2467,18 +2468,12 @@ function buildAddressFollowupOffer(addressDebug) {
if (!Array.isArray(suggestedIntents) || suggestedIntents.length === 0) {
return null;
}
const anchorType = toNonEmptyString(addressDebug.anchor_type);
const anchorValue = toNonEmptyString(addressDebug.anchor_value_resolved) ??
toNonEmptyString(addressDebug.anchor_value_raw) ??
readAddressInventoryItemFilter(addressDebug) ??
readAddressFilterString(addressDebug, "counterparty") ??
readAddressFilterString(addressDebug, "contract") ??
readAddressFilterString(addressDebug, "account");
const anchorContext = (0, assistantContinuityPolicy_1.resolveAddressDebugAnchorContext)(addressDebug, toNonEmptyString);
return {
enabled: true,
source_intent: intent,
anchor_type: anchorType ?? "unknown",
anchor_value: anchorValue,
anchor_type: anchorContext.anchorType ?? "unknown",
anchor_value: anchorContext.anchorValue,
suggested_intents: suggestedIntents
};
}

View File

@ -431,11 +431,13 @@ export function createAssistantTransitionPolicy(deps) {
const organizationClarificationCandidates = Array.isArray(organizationAuthority.organizationClarificationCandidates)
? organizationAuthority.organizationClarificationCandidates
: [];
const organizationClarificationSelection =
const explicitOrganizationClarificationSelection =
deps.resolveOrganizationSelectionFromMessage(userMessage, organizationClarificationCandidates) ??
(deps.toNonEmptyString(alternateMessage)
? deps.resolveOrganizationSelectionFromMessage(String(alternateMessage ?? ""), organizationClarificationCandidates)
: null) ??
: null);
const organizationClarificationSelection =
explicitOrganizationClarificationSelection ??
deps.normalizeOrganizationScopeValue(organizationAuthority.organizationClarificationSelectionFromScope);
const hasOrganizationClarificationContinuation = Boolean(
lastOrganizationClarificationDebug && organizationClarificationSelection
@ -979,9 +981,9 @@ export function createAssistantTransitionPolicy(deps) {
previousAnchor = selectedObjectLabel;
}
}
if (organizationClarificationSelection && !previousAnchor) {
if (explicitOrganizationClarificationSelection && !previousAnchor) {
previousAnchorType = "organization";
previousAnchor = organizationClarificationSelection;
previousAnchor = explicitOrganizationClarificationSelection;
}
if (
inventoryRootFrame &&

View File

@ -66,6 +66,18 @@ describe("address counterparty utf8 regression", () => {
it("keeps the main resolver in the supported contour for direct company activity-age wording", () => {
const result = resolveAddressIntent("а по Альтернативе Плюс сколько лет активности в базе 1С?");
expect(result.intent).toBe("counterparty_activity_lifecycle");
});
it("classifies company activity assessment wording into lifecycle intent", () => {
const result = resolveCounterpartyAddressIntent("Как ты оценишь деятельность компании?", utf8Deps);
expect(result?.intent).toBe("counterparty_activity_lifecycle");
expect(result?.reasons).toContain("counterparty_activity_lifecycle_signal_detected");
});
it("keeps the main resolver in the supported contour for company activity assessment wording", () => {
const result = resolveAddressIntent("Как ты оценишь деятельность компании?");
expect(result.intent).toBe("counterparty_activity_lifecycle");
});
});

View File

@ -406,4 +406,69 @@ describe("assistantRoutePolicy", () => {
expect(decision.orchestrationContract?.hard_meta_mode).toBeNull();
expect(decision.orchestrationContract?.followup_context_detected).toBe(false);
});
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" }),
findLastGroundedAddressAnswerDebug: () => ({
execution_lane: "address_query",
answer_grounding_check: { status: "grounded" },
detected_intent: "counterparty_activity_lifecycle",
extracted_filters: {
organization: 'ООО "Альтернатива Плюс"',
period_to: "2026-04-18"
}
}),
resolveAddressToolGateDecision: () => ({
runAddressLane: false,
decision: "skip_address_lane",
reason: "no_address_signal_after_l0"
})
});
const decision = policy.resolveAssistantOrchestrationDecision({
rawUserMessage: "Как ты оценишь деятельность компании?",
effectiveAddressUserMessage: "Как ты оценишь деятельность компании?",
followupContext: null,
llmPreDecomposeMeta: {
applied: true,
predecomposeContract: {
mode: "unsupported",
mode_confidence: "low",
intent: "unknown",
intent_confidence: "low"
},
semanticExtractionContract: {
valid: true,
apply_canonical_recommended: true,
extraction: {
query_shape: "UNKNOWN",
aggregation_profile: "unknown"
},
guard_hints: {
deep_investigation_signal_detected: false
}
}
},
sessionItems: [
{
role: "assistant",
debug: {
execution_lane: "address_query",
answer_grounding_check: { status: "grounded" },
detected_intent: "counterparty_activity_lifecycle",
extracted_filters: {
organization: 'ООО "Альтернатива Плюс"',
period_to: "2026-04-18"
}
}
}
],
useMock: false
});
expect(decision.runAddressLane).toBe(true);
expect(decision.toolGateDecision).toBe("run_address_lane");
expect(decision.livingMode).toBe("address_data");
});
});