Архитектура: стабилизировать organization authority после late company switch и закрыть phase16 multi-company replay

This commit is contained in:
dctouch 2026-04-19 15:31:50 +03:00
parent 6e6f94b08c
commit af15e21bf6
25 changed files with 1063 additions and 36 deletions

View File

@ -499,6 +499,13 @@ Still open after the accepted phase12 replay:
- keeping that contract in one helper reduces another major chance that future domain expansion introduces contradictory exact-route vs deep-fallback precedence in nearby branches;
- targeted route/continuity/transition suites remain green after the move, including direct regression coverage that an exact VAT route must stay in the address lane even when only a semantic deep hint is present;
- a fresh live rerun of `address_truth_harness_phase12_wider_saved_session_pool` on `2026-04-19` remains accepted `20/20`, which is the proof that the flagship mixed contour still survives after extracting the route-protection seam.
- the next replay-breadth and continuity-authority pass now closes a non-flagship late-switch seam that was still dangerous before multi-domain expansion:
- `assistantOrganizationScopeRuntimeAdapter` now prefers continuity-backed selected/active organization over stale address navigation scope after a late chat-side company switch, so a fresh company fixation in living-chat can no longer be silently overwritten by the previous address snapshot on the very next exact-data turn;
- `mergeFollowupContextWithOrganizationScopeRuntime(...)` now treats active session organization as the stronger authority over stale `previous_filters.organization` / `root_filters.organization`, which closes the last hot-path drift where follow-up carryover could rehydrate the old company even after the user had already switched contours;
- this matters because the system previously looked “almost fixed” on flagship chains while still failing a real multi-company late-pivot path: `Alternative Plus -> switch to RAYM in chat -> referential inventory/receivables follow-up -> switch back`;
- targeted `assistantOrganizationScopeRuntimeAdapter`, `assistantContinuityPolicy`, `assistantRoutePolicy`, and referential-organization regressions are green after the owner-precedence fix, and backend build stays green;
- 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)

View File

@ -0,0 +1,246 @@
{
"schema_version": "domain_truth_harness_spec_v1",
"scenario_id": "address_truth_harness_phase16_multicompany_late_pivot",
"domain": "address_phase16_multicompany_late_pivot",
"title": "Phase 16 multi-company late-pivot replay for breadth beyond the flagship path",
"description": "Alternative AGENT replay focused on a non-flagship saved-session contour: proactive scope offer, explicit company selection, late company switch, human capability-meta interrupt, return to exact business questions, same-date carryover inside the second company, and a final switch back to the first company. The scenario validates that organization authority survives real session pivots instead of being proven only on one flagship trajectory.",
"bindings": {},
"steps": [
{
"step_id": "step_01_smalltalk_scope_offer",
"title": "Smalltalk entry proactively offers organization scope",
"question": "приветик, как дела?",
"required_answer_patterns_all": [
"(?i)привет|норм|дела",
"(?i)организац|компан|альтернатива плюс|лайсвуд|райм"
],
"forbidden_answer_patterns": [
"(?i)mcp",
"(?i)tool_gate_reason",
"(?i)living_reason",
"(?i)snapshot_items"
],
"criticality": "critical",
"semantic_tags": [
"smalltalk_entry",
"scope_offer"
]
},
{
"step_id": "step_02_choose_alternative_plus",
"title": "Explicitly select Alternative Plus as the first active organization",
"question": "Альтернатива Плюс",
"required_answer_patterns_all": [
"(?i)зафиксир|рабочую организац|работаем по",
"(?i)альтернатива плюс"
],
"criticality": "critical",
"semantic_tags": [
"organization_authority",
"company_selected"
]
},
{
"step_id": "step_03_inventory_today_alt",
"title": "Inventory root uses the first selected organization",
"question": "что у нас сейчас на складе по остаткам?",
"allowed_reply_types": [
"factual",
"partial_coverage"
],
"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}}"
],
"criticality": "critical",
"semantic_tags": [
"inventory_root",
"company_authority"
]
},
{
"step_id": "step_04_switch_to_raym",
"title": "Late company switch changes the active organization instead of asking again from scratch",
"question": "теперь давай по РАЙМ",
"required_answer_patterns_all": [
"(?i)зафиксир|переключ|работаем по|активн",
"(?i)райм"
],
"forbidden_answer_patterns": [
"(?i)mcp",
"(?i)read_only",
"(?i)не могу определить"
],
"criticality": "critical",
"semantic_tags": [
"late_company_switch",
"organization_authority"
]
},
{
"step_id": "step_05_inventory_today_raym",
"title": "Inventory root after the late switch uses RAYM instead of the old company",
"question": "а по этой компании что сейчас на складе?",
"allowed_reply_types": [
"factual",
"partial_coverage"
],
"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)по какой компании"
],
"criticality": "critical",
"semantic_tags": [
"inventory_root",
"late_company_switch"
]
},
{
"step_id": "step_06_capability_meta_after_switch",
"title": "Capability-meta interrupt stays human and does not tear down the second company context",
"question": "а что ты вообще умеешь?",
"allowed_reply_types": [
"factual",
"factual_with_explanation"
],
"required_answer_patterns_any": [
"(?i)могу|умею",
"(?i)ндс|документ|контрагент|склад|остат|долг"
],
"forbidden_answer_patterns": [
"(?i)mcp",
"(?i)read_only",
"(?i)snapshot",
"(?i)assistant_state",
"(?i)tool_gate_reason"
],
"criticality": "important",
"semantic_tags": [
"capability_meta",
"meta_interrupt"
]
},
{
"step_id": "step_07_receivables_march_2020_raym",
"title": "Return to business contour after the meta interrupt keeps RAYM as the active company",
"question": "а по этой компании кто нам должен на март 2020?",
"allowed_reply_types": [
"factual",
"partial_coverage"
],
"expected_intents": [
"receivables_confirmed_as_of_date"
],
"required_filters": {
"as_of_date": "2020-03-31",
"period_from": "2020-03-01",
"period_to": "2020-03-31",
"organization": "РАЙМ"
},
"required_direct_answer_patterns_any": [
"(?i)долг|долж",
"(?:31\\.03\\.2020|31\\s+марта\\s+2020)"
],
"forbidden_direct_answer_patterns": [
"(?i)уточните организацию",
"(?i)по какой компании"
],
"criticality": "critical",
"semantic_tags": [
"receivables_root",
"meta_return_to_business"
]
},
{
"step_id": "step_08_same_date_inventory_raym",
"title": "Same-date inventory follow-up keeps the second company and the restored date",
"question": "а по этой же дате остатки на складе?",
"allowed_reply_types": [
"factual",
"partial_coverage"
],
"expected_intents": [
"inventory_on_hand_as_of_date"
],
"required_filters": {
"as_of_date": "2020-03-31",
"period_from": "2020-03-01",
"period_to": "2020-03-31",
"organization": "РАЙМ"
},
"required_direct_answer_patterns_any": [
"(?i)остат|на складе",
"31\\.03\\.2020"
],
"forbidden_direct_answer_patterns": [
"(?i)уточните организацию",
"(?i)по какой компании"
],
"criticality": "critical",
"semantic_tags": [
"same_date_pivot",
"inventory_root",
"second_company_continuity"
]
},
{
"step_id": "step_09_switch_back_to_alt",
"title": "Switch back to Alternative Plus late in the same session",
"question": "ок, верни обратно Альтернативу Плюс",
"required_answer_patterns_all": [
"(?i)зафиксир|переключ|работаем по|активн",
"(?i)альтернатива плюс"
],
"criticality": "important",
"semantic_tags": [
"late_company_switch_back",
"organization_authority"
]
},
{
"step_id": "step_10_activity_age_after_switch_back",
"title": "Activity-age analytics still work after two company pivots in one session",
"question": "а по Альтернативе Плюс сколько лет активности в базе 1С?",
"allowed_reply_types": [
"factual",
"factual_with_explanation",
"partial_coverage"
],
"expected_intents": [
"counterparty_activity_lifecycle"
],
"required_direct_answer_patterns_any": [
"(?i)активност",
"(?i)первая подтвержденная|последняя подтвержденная|лет"
],
"forbidden_direct_answer_patterns": [
"(?i)не найден контрагент",
"(?i)уточните организацию",
"(?i)по какой компании"
],
"criticality": "critical",
"semantic_tags": [
"activity_age",
"switch_back_integrity"
]
}
]
}

View File

@ -1200,6 +1200,13 @@ function isImplicitSelfScopeWarehouseAnchor(candidate) {
function hasSelectedObjectScopeSignal(text) {
return /(?:по\s+выбранному\s+объекту|selected\s+object)/iu.test(String(text ?? ""));
}
function isReferentialSameDateWarehousePhrase(candidate) {
const normalized = cleanupAnchorValue(candidate)
.toLowerCase()
.replace(/ё/g, "е")
.trim();
return /^(?:по)\s+(?:этой|той)(?:\s+же)?\s+дат(?:е|у|ой)$/iu.test(normalized);
}
function extractInventoryWarehouseAnchor(text) {
const patterns = [
/(?:на|по)\s+склад(?:е|у|ом)?\s+[«"']?([^\r\n,.;:!?]+?)(?:[»"']|(?=\s+(?:на|по|за|с|в)\b|[?]|$))/iu,
@ -1216,6 +1223,7 @@ function extractInventoryWarehouseAnchor(text) {
candidate.includes("->") ||
candidate.includes("=>") ||
isImplicitSelfScopeWarehouseAnchor(candidate) ||
isReferentialSameDateWarehousePhrase(candidate) ||
isLowQualityWarehouseAnchorValue(candidate) ||
normalizedCandidate.startsWith("по состоянию") ||
isTemporalWarehousePhrase(candidate) ||

View File

@ -1485,6 +1485,32 @@ function stripOrganizationLegalForm(value) {
function sameOrganizationEntityReference(left, right) {
return (0, assistantOrganizationMatcher_1.organizationsLikelySameEntity)(left, right);
}
function isReferentialOrganizationScopeValue(value) {
const normalized = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeSearchText)(value ?? "");
if (!normalized) {
return false;
}
return /^(?:эта|этой|эту|этой же|эта же|данная|данной|данную)\s+(?:компания|организация|фирма|контора)$/iu.test(normalized) ||
/^(?:по|у|для)\s+(?:этой|этой же|данной)\s+(?:компании|организации|фирме|конторе)$/iu.test(normalized) ||
/^(?:по|у|для)\s+ней$/iu.test(normalized);
}
function hasReferentialOrganizationScopeSignal(userMessage) {
const normalized = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeSearchText)(userMessage ?? "");
if (!normalized) {
return false;
}
return /(?:^| )(?:по|у|для)\s+(?:этой|этой же|данной)\s+(?:компании|организации|фирме|конторе)(?: |$)/iu.test(normalized) ||
/(?:^| )(?:по|у|для)\s+ней(?: |$)/iu.test(normalized) ||
/(?:^| )(?:эта|этой|эту|эта же|данная|данной)\s+(?:компания|организация|фирма|контора)(?: |$)/iu.test(normalized);
}
function isQuestionFragmentPartyAnchor(value) {
const normalized = normalizeSearchText(String(value ?? ""));
if (!normalized) {
return false;
}
return /(?:^| )(?:что|кто|какие|какой|сколько|сейчас)(?: |$)/iu.test(normalized) ||
/(?:на складе|нам должен|кому мы должны|по этой компании|по этой же компании|по ней)/iu.test(normalized);
}
function applyPreExecutionOrganizationScopeGrounding(input) {
const activeOrganization = (0, assistantOrganizationMatcher_1.normalizeOrganizationScopeValue)(input.activeOrganization ?? null);
const candidateOrganizations = (0, assistantOrganizationMatcher_1.mergeKnownOrganizations)([
@ -1492,6 +1518,7 @@ function applyPreExecutionOrganizationScopeGrounding(input) {
activeOrganization
]);
const resolvedOrganizationFromMessage = (0, assistantOrganizationMatcher_1.resolveOrganizationSelectionFromMessage)(input.userMessage, candidateOrganizations);
const referentialOrganizationScopeDetected = hasReferentialOrganizationScopeSignal(input.userMessage);
if (!input.filters.organization &&
input.semanticFrame?.scope_kind === "implicit_self_scope" &&
activeOrganization) {
@ -1517,6 +1544,35 @@ function applyPreExecutionOrganizationScopeGrounding(input) {
input.semanticFrame.anchor_value = resolvedOrganizationFromMessage;
}
}
if (activeOrganization &&
((typeof input.filters.organization === "string" &&
isReferentialOrganizationScopeValue(input.filters.organization)) ||
referentialOrganizationScopeDetected) &&
!sameNormalizedOrganizationScope(input.filters.organization ?? null, activeOrganization)) {
input.filters.organization = activeOrganization;
if (!input.warnings.includes("organization_grounded_from_referential_scope")) {
input.warnings.push("organization_grounded_from_referential_scope");
}
if (!input.baseReasons.includes("organization_grounded_from_referential_scope")) {
input.baseReasons.push("organization_grounded_from_referential_scope");
}
if (input.semanticFrame?.anchor_kind === "organization") {
input.semanticFrame.anchor_value = activeOrganization;
}
}
if (activeOrganization &&
sameNormalizedOrganizationScope(input.filters.organization ?? null, activeOrganization) &&
typeof input.filters.counterparty === "string" &&
(isLikelyLowQualityPartyAnchor(input.filters.counterparty) ||
isQuestionFragmentPartyAnchor(input.filters.counterparty))) {
delete input.filters.counterparty;
if (!input.warnings.includes("counterparty_cleared_from_referential_organization_scope")) {
input.warnings.push("counterparty_cleared_from_referential_organization_scope");
}
if (!input.baseReasons.includes("counterparty_cleared_from_referential_organization_scope")) {
input.baseReasons.push("counterparty_cleared_from_referential_organization_scope");
}
}
if (!input.filters.organization && !activeOrganization && !resolvedOrganizationFromMessage && candidateOrganizations.length === 1) {
input.filters.organization = candidateOrganizations[0];
if (!input.warnings.includes("organization_auto_selected_from_single_scope_candidate")) {

View File

@ -34,6 +34,9 @@ function hasAllTimeHint(text) {
function hasSameDateHint(text) {
return /(?:на\s+ту\s+же\s+дат[ауеы]|на\s+эту\s+же\s+дат[ауеы]|на\s+эту\s+дат[ауеы]|эту\s+дат[ауеы]|та\s+же\s+дата|дат[ауеы],?\s+котор(?:ую|ая)\s+(?:до\s+этого|раньше|ранее)\s+(?:рассматривали|смотрели)|дат[ауеы],?\s+которая\s+был[ао]?\s+ранее\s+рассмотрен[ао]?|same\s+date|as\s+of\s+same\s+date|the\s+same\s+date|date\s+we\s+looked\s+at\s+before|previously\s+considered\s+date)/iu.test(String(text ?? ""));
}
function hasSameDatePrepositionHint(text) {
return /(?:по\s+(?:этой|той)\s+же\s+дат(?:е|у|ой))/iu.test(String(text ?? ""));
}
function hasSamePeriodHint(text) {
return /(?:на\s+тот\s+же\s+период|за\s+тот\s+же\s+период|тот\s+же\s+период(?:\s+рассмотрения)?|на\s+этот\s+же\s+период|за\s+этот\s+же\s+период|за\s+этот\s+период|на\s+этот\s+период|за\s+тот\s+период|на\s+тот\s+период|этот\s+период|тот\s+период|аналогичн\w+\s+текущ\w+\s+период\w+|same\s+period|same\s+range|same\s+window)/iu.test(String(text ?? ""));
}
@ -628,7 +631,7 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
const relativeMonthFromInventoryRoot = resolveRelativeMonthPeriodFromInventoryRoot(userMessage, followupContext);
const relativeMonthFromFollowupYear = resolveRelativeMonthPeriodFromFollowupYear(userMessage, followupContext);
const allTimeRequested = hasAllTimeHint(userMessage);
const sameDateRequested = hasSameDateHint(userMessage);
const sameDateRequested = hasSameDateHint(userMessage) || hasSameDatePrepositionHint(userMessage);
const samePeriodRequested = hasSamePeriodHint(userMessage);
const explicitQuotedItem = extractSelectedObjectItemFromFollowupText(userMessage);
if (!toNonEmptyString(merged.organization) && previousOrganization) {
@ -789,6 +792,23 @@ function mergeFollowupFilters(current, intent, userMessage, followupContext) {
merged.as_of_date = inheritedAsOfDate;
reasons.push("as_of_date_from_followup_context");
}
if ((intent === "inventory_on_hand_as_of_date" || intent === "inventory_supplier_stock_overlap_as_of_date") &&
previousPeriodFrom &&
merged.period_from !== previousPeriodFrom) {
merged.period_from = previousPeriodFrom;
reasons.push("period_from_from_followup_context");
}
if ((intent === "inventory_on_hand_as_of_date" || intent === "inventory_supplier_stock_overlap_as_of_date") &&
previousPeriodTo &&
merged.period_to !== previousPeriodTo) {
merged.period_to = previousPeriodTo;
reasons.push("period_to_from_followup_context");
}
const currentWarehouse = toNonEmptyString(merged.warehouse);
if (currentWarehouse && hasSameDatePrepositionHint(currentWarehouse)) {
delete merged.warehouse;
reasons.push("warehouse_cleared_from_same_date_followup_noise");
}
}
if (samePeriodRequested &&
(intent === "vat_payable_confirmed_as_of_date" ||

View File

@ -645,15 +645,15 @@ function resolveAssistantOrganizationAuthority(input) {
assistantSignals.lastAssistantActiveOrganization ??
continuityActiveOrganization ??
(knownOrganizations.length === 1 ? knownOrganizations[0] : null);
const organizationClarificationCandidates = Array.isArray(input.lastOrganizationClarificationDebug?.organization_candidates)
? mergeKnownOrganizations([
...input.lastOrganizationClarificationDebug.organization_candidates,
const organizationClarificationCandidates = mergeKnownOrganizations([
...(Array.isArray(input.lastOrganizationClarificationDebug?.organization_candidates)
? input.lastOrganizationClarificationDebug.organization_candidates
: []),
...knownOrganizations,
selectedOrganization,
activeOrganization,
continuityActiveOrganization
])
: [];
]);
const organizationClarificationSelectionFromScope = selectedOrganization ?? activeOrganization;
return {
continuitySnapshot,

View File

@ -35,6 +35,7 @@ function resolveSessionOrganizationScopeContextRuntime(input) {
const continuityKnownOrganizations = Array.isArray(continuityAuthority.knownOrganizations)
? continuityAuthority.knownOrganizations
: [];
const continuitySelectedOrganization = input.normalizeOrganizationScopeValue(continuityAuthority.selectedOrganization);
const continuityActiveOrganization = input.normalizeOrganizationScopeValue(continuityAuthority.activeOrganization);
const knownOrganizations = Array.from(new Map([
...continuityKnownOrganizations,
@ -44,8 +45,9 @@ function resolveSessionOrganizationScopeContextRuntime(input) {
const selectedOrganization = input.resolveOrganizationSelectionFromMessage(input.userMessage, knownOrganizations);
const navigationActiveOrganization = resolveActiveOrganizationFromNavigationState(input.addressNavigationState, input.normalizeOrganizationScopeValue);
const activeOrganization = selectedOrganization ??
navigationActiveOrganization ??
continuitySelectedOrganization ??
continuityActiveOrganization ??
navigationActiveOrganization ??
(knownOrganizations.length === 1 ? knownOrganizations[0] : null);
return {
knownOrganizations,
@ -64,7 +66,8 @@ function mergeFollowupContextWithOrganizationScopeRuntime(input) {
const previousFilters = previousFiltersRaw && typeof previousFiltersRaw === "object"
? { ...previousFiltersRaw }
: {};
if (!input.toNonEmptyString(previousFilters.organization)) {
const previousOrganization = input.toNonEmptyString(previousFilters.organization);
if (!previousOrganization || previousOrganization !== normalizedOrganization) {
previousFilters.organization = normalizedOrganization;
}
base.previous_filters = previousFilters;
@ -72,7 +75,8 @@ function mergeFollowupContextWithOrganizationScopeRuntime(input) {
const rootFilters = rootFiltersRaw && typeof rootFiltersRaw === "object"
? { ...rootFiltersRaw }
: {};
if (!input.toNonEmptyString(rootFilters.organization)) {
const rootOrganization = input.toNonEmptyString(rootFilters.organization);
if (!rootOrganization || rootOrganization !== normalizedOrganization) {
rootFilters.organization = normalizedOrganization;
}
if (Object.keys(rootFilters).length > 0) {

View File

@ -143,7 +143,7 @@ function resolveAddressLaneProtectionArbitration(input) {
};
}
function createAssistantRoutePolicy(deps) {
const { repairAddressMojibake, findLastGroundedAddressAnswerDebug, findLastOrganizationClarificationAddressDebug, mergeKnownOrganizations, normalizeOrganizationScopeValue, resolveOrganizationSelectionFromMessage, resolveMetaSignalSet, resolveHardMetaMode, isMetaFollowupOverGroundedAnswer, isAnswerInspectionFollowupOverGroundedAnswer, hasDataRetrievalRequestSignal, hasOrganizationFactLookupSignal, hasOrganizationFactFollowupSignal, hasAggregateBusinessAnalyticsSignal, hasStandaloneAddressTopicSignal, hasOpenContractsAddressSignal, detectAddressQuestionMode, resolveAddressIntent, toNonEmptyString, hasStrictDeepInvestigationCue, hasStrongDataIntentSignal, hasAccountingSignal, hasDangerOrCoercionSignal, hasAddressFollowupContextSignal, hasShortDebtMirrorFollowupSignal, isInventorySelectedObjectIntent, hasShortInventoryObjectFollowupSignal, isGroundedInventoryContextDebug, resolveRouteMemorySignals, findLastAddressAssistantItem, resolveAddressToolGateDecision: resolveAddressToolGateDecisionOverride, hasAddressLlmPreDecomposeCandidate, hasSameDateAccountFollowupSignalForPredecompose, hasLooseAllTimeAddressLookupSignal, hasDeepAnalysisPreferenceSignal, hasDirectDeepAnalysisSignal, compactWhitespace, hasDeepSessionContinuationSignal, resolveLivingAssistantModeDecision, resolveProviderExecutionState } = deps;
const { repairAddressMojibake, findLastGroundedAddressAnswerDebug, findLastOrganizationClarificationAddressDebug, mergeKnownOrganizations, normalizeOrganizationScopeValue, resolveOrganizationSelectionFromMessage, resolveMetaSignalSet, resolveHardMetaMode, isMetaFollowupOverGroundedAnswer, isAnswerInspectionFollowupOverGroundedAnswer, hasDataRetrievalRequestSignal, hasOrganizationFactLookupSignal, hasOrganizationFactFollowupSignal, hasAggregateBusinessAnalyticsSignal, hasStandaloneAddressTopicSignal, hasOpenContractsAddressSignal, detectAddressQuestionMode, resolveAddressIntent, toNonEmptyString, hasStrictDeepInvestigationCue, hasStrongDataIntentSignal, hasAccountingSignal, hasDangerOrCoercionSignal, hasAddressFollowupContextSignal, hasShortDebtMirrorFollowupSignal, isInventorySelectedObjectIntent, hasShortInventoryObjectFollowupSignal, isGroundedInventoryContextDebug, resolveRouteMemorySignals, findLastAddressAssistantItem, resolveAddressToolGateDecision: resolveAddressToolGateDecisionOverride, hasAddressLlmPreDecomposeCandidate, hasSameDateAccountFollowupSignalForPredecompose, hasLooseAllTimeAddressLookupSignal, hasDeepAnalysisPreferenceSignal, hasDirectDeepAnalysisSignal, shouldEmitOrganizationSelectionReply, compactWhitespace, hasDeepSessionContinuationSignal, resolveLivingAssistantModeDecision, resolveProviderExecutionState } = deps;
function resolveBaseAddressToolGateDecision(addressInputMessage, followupContext, llmPreDecomposeMeta = null, rawUserMessage = null) {
const repairedInputMessage = repairAddressMojibake(String(addressInputMessage ?? ""));
const rawMessageForGate = String(rawUserMessage ?? addressInputMessage ?? "");
@ -447,6 +447,13 @@ function createAssistantRoutePolicy(deps) {
!dataScopeMetaQuery &&
!capabilityMetaQuery &&
!dataRetrievalSignal);
const organizationScopeSwitchDetected = Boolean(organizationClarificationSelection &&
!dataScopeMetaQuery &&
!capabilityMetaQuery &&
(shouldEmitOrganizationSelectionReply(rawUserMessage, organizationClarificationSelection) ||
shouldEmitOrganizationSelectionReply(repairedRawUserMessage, organizationClarificationSelection) ||
shouldEmitOrganizationSelectionReply(effectiveAddressUserMessage, organizationClarificationSelection) ||
shouldEmitOrganizationSelectionReply(repairedEffectiveAddressUserMessage, organizationClarificationSelection)));
const effectiveAddressFollowupSignal = explicitAddressFollowupSignal && !dangerOrCoercionSignal;
const baseToolGate = typeof resolveAddressToolGateDecisionOverride === "function"
? resolveAddressToolGateDecisionOverride(effectiveAddressUserMessage, followupContext, llmPreDecomposeMeta, rawUserMessage)
@ -653,6 +660,37 @@ function createAssistantRoutePolicy(deps) {
}
};
}
if (organizationScopeSwitchDetected) {
return {
runAddressLane: false,
toolGateDecision: "skip_address_lane",
toolGateReason: "organization_scope_switch_detected",
livingMode: "chat",
livingReason: "organization_scope_switch_detected",
orchestrationContract: {
schema_version: "assistant_orchestration_contract_v1",
hard_meta_mode: null,
provider_execution: providerExecution,
address_mode: resolvedModeDetection.mode,
address_mode_confidence: resolvedModeDetection.confidence,
address_intent: resolvedIntentResolution.intent,
address_intent_confidence: resolvedIntentResolution.confidence,
strong_data_signal_detected: strongDataSignal,
data_retrieval_signal_detected: dataRetrievalSignal,
followup_context_detected: Boolean(followupContext || continuitySnapshot.hasGroundedAddressContext),
organization_scope_switch_detected: true,
organization_scope_selection: organizationClarificationSelection,
unsupported_address_intent_fallback_to_deep: false,
final_decision: {
run_address_lane: false,
tool_gate_decision: "skip_address_lane",
tool_gate_reason: "organization_scope_switch_detected",
living_mode: "chat",
living_reason: "organization_scope_switch_detected"
}
}
};
}
const supportedExactInvestigativeAddressBypass = Boolean(llmContractMode === "deep_analysis" &&
semanticApplyCanonicalRecommended &&
strictDeepInvestigationBypassAllowed &&

View File

@ -1,6 +1,6 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.INVENTORY_CAPABILITY_CONTRACTS = exports.ASSISTANT_TRANSITION_CONTRACTS = void 0;
exports.ROOT_EXACT_CAPABILITY_CONTRACTS = exports.INVENTORY_CAPABILITY_CONTRACTS = exports.ASSISTANT_TRANSITION_CONTRACTS = void 0;
exports.listAssistantTransitionContracts = listAssistantTransitionContracts;
exports.getAssistantTransitionContract = getAssistantTransitionContract;
exports.listInventoryCapabilityContracts = listInventoryCapabilityContracts;
@ -139,6 +139,60 @@ const INVENTORY_SELECTED_OBJECT_TESTS = [
"new_explicit_selected_object_overrides_old_focus",
"full_anchor_not_degraded_by_canonical_rewrite"
];
const SHARED_ROOT_EXACT_TESTS = [
"root_context_survives_domain_pivot_without_object_leak",
"limited_mode_remains_truthful"
];
function rootExactCapability(input) {
return {
schema_version: assistantRuntimeContracts_1.ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION,
capability_id: input.capability_id,
domain_id: input.domainId,
runtime_lane: "address_exact",
intent_ids: input.intent_ids,
entry_modes: ["root_entry", "root_followup", "clarification_resume"],
supported_transition_classes: input.transitions,
frame_compatibility: {
root_frame: "optional",
selected_object_frame: "optional",
meta_frame: "forbidden"
},
required_anchors: input.requiredAnchors,
optional_anchors: input.optionalAnchors ?? ["organization", "date_scope", "account", "counterparty", "contract"],
anchor_source_priority: ["explicit_user_anchor", "root_frame", "semantic_hint"],
anchor_admissibility_rules: [
"confirmed_root_scope_beats_semantic_hint",
"no_low_quality_counterparty_rewrite",
"no_conversational_noise_as_entity"
],
organization_scope_behavior: "reuse_or_clarify",
date_scope_behavior: "reuse",
temporal_ceiling_policy: "must_not_expand_without_reason_code",
root_context_compatibility: "required",
requires_focus_object: false,
accepted_focus_object_kinds: [],
focus_object_override_policy: "not_applicable",
bundle_reuse_policy: "none",
resolver_owner: "addressIntentResolver",
recipe_owner: "addressRecipeCatalog",
execution_adapter: "AddressQueryService",
result_shape: input.resultShape,
answer_object_shape: input.answerObjectShape,
minimum_evidence_policy: "route_specific_threshold",
coverage_gate_behavior: "partial_or_blocked_if_evidence_insufficient",
truth_mode_fallbacks: ["limited", "clarification_required", "unsupported"],
blocked_reason_codes: ["missing_anchor", "route_expectation_failure", "execution_error", "insufficient_evidence"],
clarification_triggers: ["ambiguous_organization_scope", "ambiguous_date_scope"],
clarification_questions: ["Уточните организацию, счёт или дату, чтобы не подставлять неподтверждённый контур."],
resume_policy: "resume_original_route_with_resolved_anchors",
empty_match_behavior: "truthful_empty_match",
route_expectation_failure_behavior: "blocked_route_expectation_failure",
execution_error_behavior: "blocked_execution_error",
required_unit_tests: [...SHARED_ROOT_EXACT_TESTS],
required_transition_tests: input.transitions.map((transitionId) => `transition_${transitionId}`),
required_scenario_families: input.scenarioFamilies ?? ["canonical", "colloquial", "followup_date_carryover"]
};
}
function inventoryExactCapability(input) {
return {
schema_version: assistantRuntimeContracts_1.ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION,
@ -269,6 +323,69 @@ exports.INVENTORY_CAPABILITY_CONTRACTS = [
scenarioFamilies: ["canonical", "colloquial", "followup_date_carryover"]
})
];
exports.ROOT_EXACT_CAPABILITY_CONTRACTS = [
rootExactCapability({
capability_id: "confirmed_payables_as_of_date",
domainId: "counterparty_debt",
intent_ids: ["payables_confirmed_as_of_date"],
transitions: ["T1", "T2", "T6", "T7"],
requiredAnchors: [],
resultShape: "counterparty_payables_snapshot",
answerObjectShape: "payables_snapshot"
}),
rootExactCapability({
capability_id: "confirmed_receivables_as_of_date",
domainId: "counterparty_debt",
intent_ids: ["receivables_confirmed_as_of_date"],
transitions: ["T1", "T2", "T6", "T7"],
requiredAnchors: [],
resultShape: "counterparty_receivables_snapshot",
answerObjectShape: "receivables_snapshot"
}),
rootExactCapability({
capability_id: "confirmed_open_contracts_as_of_date",
domainId: "contracts",
intent_ids: ["open_contracts_confirmed_as_of_date"],
transitions: ["T1", "T2", "T6", "T7"],
requiredAnchors: [],
resultShape: "open_contracts_snapshot",
answerObjectShape: "open_contracts_snapshot"
}),
rootExactCapability({
capability_id: "confirmed_vat_payable_as_of_date",
domainId: "vat",
intent_ids: ["vat_payable_confirmed_as_of_date"],
transitions: ["T1", "T2", "T6", "T7"],
requiredAnchors: [],
resultShape: "vat_payable_snapshot",
answerObjectShape: "vat_payable_snapshot"
}),
rootExactCapability({
capability_id: "confirmed_vat_liability_for_tax_period",
domainId: "vat",
intent_ids: ["vat_liability_confirmed_for_tax_period"],
transitions: ["T1", "T2", "T6", "T7"],
requiredAnchors: [],
resultShape: "vat_tax_period_liability_snapshot",
answerObjectShape: "vat_tax_period_liability_snapshot",
scenarioFamilies: ["canonical", "colloquial", "followup_date_carryover", "tax_period_followup"]
}),
rootExactCapability({
capability_id: "account_balance_exact",
domainId: "accounting_balance",
intent_ids: ["account_balance_snapshot", "documents_forming_balance"],
transitions: ["T1", "T2", "T6", "T7"],
requiredAnchors: ["account"],
optionalAnchors: ["organization", "date_scope", "account"],
resultShape: "account_balance_snapshot_or_supporting_documents",
answerObjectShape: "account_balance_context",
scenarioFamilies: ["canonical", "colloquial", "followup_date_carryover", "same_date_account_followup"]
})
];
const ALL_CAPABILITY_CONTRACTS = [
...exports.INVENTORY_CAPABILITY_CONTRACTS,
...exports.ROOT_EXACT_CAPABILITY_CONTRACTS
];
function listAssistantTransitionContracts() {
return exports.ASSISTANT_TRANSITION_CONTRACTS;
}
@ -279,8 +396,8 @@ function listInventoryCapabilityContracts() {
return exports.INVENTORY_CAPABILITY_CONTRACTS;
}
function getAssistantCapabilityContract(capabilityId) {
return exports.INVENTORY_CAPABILITY_CONTRACTS.find((contract) => contract.capability_id === capabilityId) ?? null;
return ALL_CAPABILITY_CONTRACTS.find((contract) => contract.capability_id === capabilityId) ?? null;
}
function getAssistantCapabilityContractByIntent(intent) {
return exports.INVENTORY_CAPABILITY_CONTRACTS.find((contract) => contract.intent_ids.includes(intent)) ?? null;
return ALL_CAPABILITY_CONTRACTS.find((contract) => contract.intent_ids.includes(intent)) ?? null;
}

View File

@ -4092,6 +4092,7 @@ const assistantRoutePolicy = (0, assistantRoutePolicy_1.createAssistantRoutePoli
hasLooseAllTimeAddressLookupSignal,
hasDeepAnalysisPreferenceSignal,
hasDirectDeepAnalysisSignal,
shouldEmitOrganizationSelectionReply: assistantLivingModePolicy.shouldEmitOrganizationSelectionReply,
compactWhitespace,
hasDeepSessionContinuationSignal,
resolveLivingAssistantModeDecision: assistantLivingModePolicy.resolveLivingAssistantModeDecision,

View File

@ -1374,6 +1374,14 @@ function hasSelectedObjectScopeSignal(text: string): boolean {
return /(?:по\s+выбранному\s+объекту|selected\s+object)/iu.test(String(text ?? ""));
}
function isReferentialSameDateWarehousePhrase(candidate: string): boolean {
const normalized = cleanupAnchorValue(candidate)
.toLowerCase()
.replace(/ё/g, "е")
.trim();
return /^(?:по)\s+(?:этой|той)(?:\s+же)?\s+дат(?:е|у|ой)$/iu.test(normalized);
}
function extractInventoryWarehouseAnchor(text: string): string | undefined {
const patterns = [
/(?:на|по)\s+склад(?:е|у|ом)?\s+[«"']?([^\r\n,.;:!?]+?)(?:[»"']|(?=\s+(?:на|по|за|с|в)\b|[?]|$))/iu,
@ -1396,6 +1404,7 @@ function extractInventoryWarehouseAnchor(text: string): string | undefined {
candidate.includes("->") ||
candidate.includes("=>") ||
isImplicitSelfScopeWarehouseAnchor(candidate) ||
isReferentialSameDateWarehousePhrase(candidate) ||
isLowQualityWarehouseAnchorValue(candidate) ||
normalizedCandidate.startsWith("по состоянию") ||
isTemporalWarehousePhrase(candidate) ||

View File

@ -1832,6 +1832,35 @@ function sameOrganizationEntityReference(left: string | null | undefined, right:
return organizationsLikelySameEntity(left, right);
}
function isReferentialOrganizationScopeValue(value: string | null | undefined): boolean {
const normalized = normalizeOrganizationScopeSearchText(value ?? "");
if (!normalized) {
return false;
}
return /^(?:эта|этой|эту|этой же|эта же|данная|данной|данную)\s+(?:компания|организация|фирма|контора)$/iu.test(normalized) ||
/^(?:по|у|для)\s+(?:этой|этой же|данной)\s+(?:компании|организации|фирме|конторе)$/iu.test(normalized) ||
/^(?:по|у|для)\s+ней$/iu.test(normalized);
}
function hasReferentialOrganizationScopeSignal(userMessage: string | null | undefined): boolean {
const normalized = normalizeOrganizationScopeSearchText(userMessage ?? "");
if (!normalized) {
return false;
}
return /(?:^| )(?:по|у|для)\s+(?:этой|этой же|данной)\s+(?:компании|организации|фирме|конторе)(?: |$)/iu.test(normalized) ||
/(?:^| )(?:по|у|для)\s+ней(?: |$)/iu.test(normalized) ||
/(?:^| )(?:эта|этой|эту|эта же|данная|данной)\s+(?:компания|организация|фирма|контора)(?: |$)/iu.test(normalized);
}
function isQuestionFragmentPartyAnchor(value: string | null | undefined): boolean {
const normalized = normalizeSearchText(String(value ?? ""));
if (!normalized) {
return false;
}
return /(?:^| )(?:что|кто|какие|какой|сколько|сейчас)(?: |$)/iu.test(normalized) ||
/(?:на складе|нам должен|кому мы должны|по этой компании|по этой же компании|по ней)/iu.test(normalized);
}
function applyPreExecutionOrganizationScopeGrounding(input: {
userMessage: string;
filters: AddressFilterSet;
@ -1847,6 +1876,7 @@ function applyPreExecutionOrganizationScopeGrounding(input: {
activeOrganization
]);
const resolvedOrganizationFromMessage = resolveOrganizationSelectionFromMessage(input.userMessage, candidateOrganizations);
const referentialOrganizationScopeDetected = hasReferentialOrganizationScopeSignal(input.userMessage);
if (
!input.filters.organization &&
@ -1879,6 +1909,41 @@ function applyPreExecutionOrganizationScopeGrounding(input: {
}
}
if (
activeOrganization &&
((typeof input.filters.organization === "string" &&
isReferentialOrganizationScopeValue(input.filters.organization)) ||
referentialOrganizationScopeDetected) &&
!sameNormalizedOrganizationScope(input.filters.organization ?? null, activeOrganization)
) {
input.filters.organization = activeOrganization;
if (!input.warnings.includes("organization_grounded_from_referential_scope")) {
input.warnings.push("organization_grounded_from_referential_scope");
}
if (!input.baseReasons.includes("organization_grounded_from_referential_scope")) {
input.baseReasons.push("organization_grounded_from_referential_scope");
}
if (input.semanticFrame?.anchor_kind === "organization") {
input.semanticFrame.anchor_value = activeOrganization;
}
}
if (
activeOrganization &&
sameNormalizedOrganizationScope(input.filters.organization ?? null, activeOrganization) &&
typeof input.filters.counterparty === "string" &&
(isLikelyLowQualityPartyAnchor(input.filters.counterparty) ||
isQuestionFragmentPartyAnchor(input.filters.counterparty))
) {
delete input.filters.counterparty;
if (!input.warnings.includes("counterparty_cleared_from_referential_organization_scope")) {
input.warnings.push("counterparty_cleared_from_referential_organization_scope");
}
if (!input.baseReasons.includes("counterparty_cleared_from_referential_organization_scope")) {
input.baseReasons.push("counterparty_cleared_from_referential_organization_scope");
}
}
if (!input.filters.organization && !activeOrganization && !resolvedOrganizationFromMessage && candidateOrganizations.length === 1) {
input.filters.organization = candidateOrganizations[0];
if (!input.warnings.includes("organization_auto_selected_from_single_scope_candidate")) {

View File

@ -98,6 +98,10 @@ function hasSameDateHint(text: string): boolean {
);
}
function hasSameDatePrepositionHint(text: string): boolean {
return /(?:по\s+(?:этой|той)\s+же\s+дат(?:е|у|ой))/iu.test(String(text ?? ""));
}
function hasSamePeriodHint(text: string): boolean {
return /(?:на\s+тот\s+же\s+период|за\s+тот\s+же\s+период|тот\s+же\s+период(?:\s+рассмотрения)?|на\s+этот\s+же\s+период|за\s+этот\s+же\s+период|за\s+этот\s+период|на\s+этот\s+период|за\s+тот\s+период|на\s+тот\s+период|этот\s+период|тот\s+период|аналогичн\w+\s+текущ\w+\s+период\w+|same\s+period|same\s+range|same\s+window)/iu.test(
String(text ?? "")
@ -814,7 +818,7 @@ function mergeFollowupFilters(
const relativeMonthFromInventoryRoot = resolveRelativeMonthPeriodFromInventoryRoot(userMessage, followupContext);
const relativeMonthFromFollowupYear = resolveRelativeMonthPeriodFromFollowupYear(userMessage, followupContext);
const allTimeRequested = hasAllTimeHint(userMessage);
const sameDateRequested = hasSameDateHint(userMessage);
const sameDateRequested = hasSameDateHint(userMessage) || hasSameDatePrepositionHint(userMessage);
const samePeriodRequested = hasSamePeriodHint(userMessage);
const explicitQuotedItem = extractSelectedObjectItemFromFollowupText(userMessage);
if (!toNonEmptyString(merged.organization) && previousOrganization) {
@ -1000,6 +1004,27 @@ function mergeFollowupFilters(
merged.as_of_date = inheritedAsOfDate;
reasons.push("as_of_date_from_followup_context");
}
if (
(intent === "inventory_on_hand_as_of_date" || intent === "inventory_supplier_stock_overlap_as_of_date") &&
previousPeriodFrom &&
merged.period_from !== previousPeriodFrom
) {
merged.period_from = previousPeriodFrom;
reasons.push("period_from_from_followup_context");
}
if (
(intent === "inventory_on_hand_as_of_date" || intent === "inventory_supplier_stock_overlap_as_of_date") &&
previousPeriodTo &&
merged.period_to !== previousPeriodTo
) {
merged.period_to = previousPeriodTo;
reasons.push("period_to_from_followup_context");
}
const currentWarehouse = toNonEmptyString(merged.warehouse);
if (currentWarehouse && hasSameDatePrepositionHint(currentWarehouse)) {
delete merged.warehouse;
reasons.push("warehouse_cleared_from_same_date_followup_noise");
}
}
if (
samePeriodRequested &&

View File

@ -1019,15 +1019,15 @@ export function resolveAssistantOrganizationAuthority(
assistantSignals.lastAssistantActiveOrganization ??
continuityActiveOrganization ??
(knownOrganizations.length === 1 ? knownOrganizations[0] : null);
const organizationClarificationCandidates = Array.isArray(input.lastOrganizationClarificationDebug?.organization_candidates)
? mergeKnownOrganizations([
...input.lastOrganizationClarificationDebug.organization_candidates,
const organizationClarificationCandidates = mergeKnownOrganizations([
...(Array.isArray(input.lastOrganizationClarificationDebug?.organization_candidates)
? input.lastOrganizationClarificationDebug.organization_candidates
: []),
...knownOrganizations,
selectedOrganization,
activeOrganization,
continuityActiveOrganization
])
: [];
]);
const organizationClarificationSelectionFromScope = selectedOrganization ?? activeOrganization;
return {

View File

@ -65,6 +65,9 @@ export function resolveSessionOrganizationScopeContextRuntime<ItemType = unknown
const continuityKnownOrganizations = Array.isArray(continuityAuthority.knownOrganizations)
? continuityAuthority.knownOrganizations
: [];
const continuitySelectedOrganization = input.normalizeOrganizationScopeValue(
continuityAuthority.selectedOrganization
);
const continuityActiveOrganization = input.normalizeOrganizationScopeValue(
continuityAuthority.activeOrganization
);
@ -87,8 +90,9 @@ export function resolveSessionOrganizationScopeContextRuntime<ItemType = unknown
);
const activeOrganization =
selectedOrganization ??
navigationActiveOrganization ??
continuitySelectedOrganization ??
continuityActiveOrganization ??
navigationActiveOrganization ??
(knownOrganizations.length === 1 ? knownOrganizations[0] : null);
return {
@ -113,7 +117,8 @@ export function mergeFollowupContextWithOrganizationScopeRuntime(
previousFiltersRaw && typeof previousFiltersRaw === "object"
? { ...(previousFiltersRaw as Record<string, unknown>) }
: {};
if (!input.toNonEmptyString(previousFilters.organization)) {
const previousOrganization = input.toNonEmptyString(previousFilters.organization);
if (!previousOrganization || previousOrganization !== normalizedOrganization) {
previousFilters.organization = normalizedOrganization;
}
base.previous_filters = previousFilters;
@ -122,7 +127,8 @@ export function mergeFollowupContextWithOrganizationScopeRuntime(
rootFiltersRaw && typeof rootFiltersRaw === "object"
? { ...(rootFiltersRaw as Record<string, unknown>) }
: {};
if (!input.toNonEmptyString(rootFilters.organization)) {
const rootOrganization = input.toNonEmptyString(rootFilters.organization);
if (!rootOrganization || rootOrganization !== normalizedOrganization) {
rootFilters.organization = normalizedOrganization;
}
if (Object.keys(rootFilters).length > 0) {

View File

@ -218,6 +218,7 @@ export function createAssistantRoutePolicy(deps) {
hasLooseAllTimeAddressLookupSignal,
hasDeepAnalysisPreferenceSignal,
hasDirectDeepAnalysisSignal,
shouldEmitOrganizationSelectionReply,
compactWhitespace,
hasDeepSessionContinuationSignal,
resolveLivingAssistantModeDecision,
@ -526,6 +527,13 @@ export function createAssistantRoutePolicy(deps) {
!dataScopeMetaQuery &&
!capabilityMetaQuery &&
!dataRetrievalSignal);
const organizationScopeSwitchDetected = Boolean(organizationClarificationSelection &&
!dataScopeMetaQuery &&
!capabilityMetaQuery &&
(shouldEmitOrganizationSelectionReply(rawUserMessage, organizationClarificationSelection) ||
shouldEmitOrganizationSelectionReply(repairedRawUserMessage, organizationClarificationSelection) ||
shouldEmitOrganizationSelectionReply(effectiveAddressUserMessage, organizationClarificationSelection) ||
shouldEmitOrganizationSelectionReply(repairedEffectiveAddressUserMessage, organizationClarificationSelection)));
const effectiveAddressFollowupSignal = explicitAddressFollowupSignal && !dangerOrCoercionSignal;
const baseToolGate = typeof resolveAddressToolGateDecisionOverride === "function"
? resolveAddressToolGateDecisionOverride(effectiveAddressUserMessage, followupContext, llmPreDecomposeMeta, rawUserMessage)
@ -732,6 +740,37 @@ export function createAssistantRoutePolicy(deps) {
}
};
}
if (organizationScopeSwitchDetected) {
return {
runAddressLane: false,
toolGateDecision: "skip_address_lane",
toolGateReason: "organization_scope_switch_detected",
livingMode: "chat",
livingReason: "organization_scope_switch_detected",
orchestrationContract: {
schema_version: "assistant_orchestration_contract_v1",
hard_meta_mode: null,
provider_execution: providerExecution,
address_mode: resolvedModeDetection.mode,
address_mode_confidence: resolvedModeDetection.confidence,
address_intent: resolvedIntentResolution.intent,
address_intent_confidence: resolvedIntentResolution.confidence,
strong_data_signal_detected: strongDataSignal,
data_retrieval_signal_detected: dataRetrievalSignal,
followup_context_detected: Boolean(followupContext || continuitySnapshot.hasGroundedAddressContext),
organization_scope_switch_detected: true,
organization_scope_selection: organizationClarificationSelection,
unsupported_address_intent_fallback_to_deep: false,
final_decision: {
run_address_lane: false,
tool_gate_decision: "skip_address_lane",
tool_gate_reason: "organization_scope_switch_detected",
living_mode: "chat",
living_reason: "organization_scope_switch_detected"
}
}
};
}
const supportedExactInvestigativeAddressBypass = Boolean(llmContractMode === "deep_analysis" &&
semanticApplyCanonicalRecommended &&
strictDeepInvestigationBypassAllowed &&

View File

@ -142,6 +142,72 @@ const INVENTORY_SELECTED_OBJECT_TESTS = [
"full_anchor_not_degraded_by_canonical_rewrite"
] as const;
const SHARED_ROOT_EXACT_TESTS = [
"root_context_survives_domain_pivot_without_object_leak",
"limited_mode_remains_truthful"
] as const;
function rootExactCapability(input: {
capability_id: string;
domainId: string;
intent_ids: AddressIntent[];
transitions: AssistantTransitionClassId[];
requiredAnchors: string[];
optionalAnchors?: string[];
resultShape: string;
answerObjectShape: string;
scenarioFamilies?: string[];
}): AssistantCapabilityContract {
return {
schema_version: ASSISTANT_RUNTIME_CONTRACTS_SCHEMA_VERSION,
capability_id: input.capability_id,
domain_id: input.domainId,
runtime_lane: "address_exact",
intent_ids: input.intent_ids,
entry_modes: ["root_entry", "root_followup", "clarification_resume"],
supported_transition_classes: input.transitions,
frame_compatibility: {
root_frame: "optional",
selected_object_frame: "optional",
meta_frame: "forbidden"
},
required_anchors: input.requiredAnchors,
optional_anchors: input.optionalAnchors ?? ["organization", "date_scope", "account", "counterparty", "contract"],
anchor_source_priority: ["explicit_user_anchor", "root_frame", "semantic_hint"],
anchor_admissibility_rules: [
"confirmed_root_scope_beats_semantic_hint",
"no_low_quality_counterparty_rewrite",
"no_conversational_noise_as_entity"
],
organization_scope_behavior: "reuse_or_clarify",
date_scope_behavior: "reuse",
temporal_ceiling_policy: "must_not_expand_without_reason_code",
root_context_compatibility: "required",
requires_focus_object: false,
accepted_focus_object_kinds: [],
focus_object_override_policy: "not_applicable",
bundle_reuse_policy: "none",
resolver_owner: "addressIntentResolver",
recipe_owner: "addressRecipeCatalog",
execution_adapter: "AddressQueryService",
result_shape: input.resultShape,
answer_object_shape: input.answerObjectShape,
minimum_evidence_policy: "route_specific_threshold",
coverage_gate_behavior: "partial_or_blocked_if_evidence_insufficient",
truth_mode_fallbacks: ["limited", "clarification_required", "unsupported"],
blocked_reason_codes: ["missing_anchor", "route_expectation_failure", "execution_error", "insufficient_evidence"],
clarification_triggers: ["ambiguous_organization_scope", "ambiguous_date_scope"],
clarification_questions: ["Уточните организацию, счёт или дату, чтобы не подставлять неподтверждённый контур."],
resume_policy: "resume_original_route_with_resolved_anchors",
empty_match_behavior: "truthful_empty_match",
route_expectation_failure_behavior: "blocked_route_expectation_failure",
execution_error_behavior: "blocked_execution_error",
required_unit_tests: [...SHARED_ROOT_EXACT_TESTS],
required_transition_tests: input.transitions.map((transitionId) => `transition_${transitionId}`),
required_scenario_families: input.scenarioFamilies ?? ["canonical", "colloquial", "followup_date_carryover"]
};
}
function inventoryExactCapability(input: {
capability_id: string;
intent_ids: AddressIntent[];
@ -285,6 +351,71 @@ export const INVENTORY_CAPABILITY_CONTRACTS: readonly AssistantCapabilityContrac
})
] as const;
export const ROOT_EXACT_CAPABILITY_CONTRACTS: readonly AssistantCapabilityContract[] = [
rootExactCapability({
capability_id: "confirmed_payables_as_of_date",
domainId: "counterparty_debt",
intent_ids: ["payables_confirmed_as_of_date"],
transitions: ["T1", "T2", "T6", "T7"],
requiredAnchors: [],
resultShape: "counterparty_payables_snapshot",
answerObjectShape: "payables_snapshot"
}),
rootExactCapability({
capability_id: "confirmed_receivables_as_of_date",
domainId: "counterparty_debt",
intent_ids: ["receivables_confirmed_as_of_date"],
transitions: ["T1", "T2", "T6", "T7"],
requiredAnchors: [],
resultShape: "counterparty_receivables_snapshot",
answerObjectShape: "receivables_snapshot"
}),
rootExactCapability({
capability_id: "confirmed_open_contracts_as_of_date",
domainId: "contracts",
intent_ids: ["open_contracts_confirmed_as_of_date"],
transitions: ["T1", "T2", "T6", "T7"],
requiredAnchors: [],
resultShape: "open_contracts_snapshot",
answerObjectShape: "open_contracts_snapshot"
}),
rootExactCapability({
capability_id: "confirmed_vat_payable_as_of_date",
domainId: "vat",
intent_ids: ["vat_payable_confirmed_as_of_date"],
transitions: ["T1", "T2", "T6", "T7"],
requiredAnchors: [],
resultShape: "vat_payable_snapshot",
answerObjectShape: "vat_payable_snapshot"
}),
rootExactCapability({
capability_id: "confirmed_vat_liability_for_tax_period",
domainId: "vat",
intent_ids: ["vat_liability_confirmed_for_tax_period"],
transitions: ["T1", "T2", "T6", "T7"],
requiredAnchors: [],
resultShape: "vat_tax_period_liability_snapshot",
answerObjectShape: "vat_tax_period_liability_snapshot",
scenarioFamilies: ["canonical", "colloquial", "followup_date_carryover", "tax_period_followup"]
}),
rootExactCapability({
capability_id: "account_balance_exact",
domainId: "accounting_balance",
intent_ids: ["account_balance_snapshot", "documents_forming_balance"],
transitions: ["T1", "T2", "T6", "T7"],
requiredAnchors: ["account"],
optionalAnchors: ["organization", "date_scope", "account"],
resultShape: "account_balance_snapshot_or_supporting_documents",
answerObjectShape: "account_balance_context",
scenarioFamilies: ["canonical", "colloquial", "followup_date_carryover", "same_date_account_followup"]
})
] as const;
const ALL_CAPABILITY_CONTRACTS: readonly AssistantCapabilityContract[] = [
...INVENTORY_CAPABILITY_CONTRACTS,
...ROOT_EXACT_CAPABILITY_CONTRACTS
] as const;
export function listAssistantTransitionContracts(): readonly AssistantTransitionContract[] {
return ASSISTANT_TRANSITION_CONTRACTS;
}
@ -298,9 +429,9 @@ export function listInventoryCapabilityContracts(): readonly AssistantCapability
}
export function getAssistantCapabilityContract(capabilityId: string): AssistantCapabilityContract | null {
return INVENTORY_CAPABILITY_CONTRACTS.find((contract) => contract.capability_id === capabilityId) ?? null;
return ALL_CAPABILITY_CONTRACTS.find((contract) => contract.capability_id === capabilityId) ?? null;
}
export function getAssistantCapabilityContractByIntent(intent: AddressIntent): AssistantCapabilityContract | null {
return INVENTORY_CAPABILITY_CONTRACTS.find((contract) => contract.intent_ids.includes(intent)) ?? null;
return ALL_CAPABILITY_CONTRACTS.find((contract) => contract.intent_ids.includes(intent)) ?? null;
}

View File

@ -4049,6 +4049,7 @@ const assistantRoutePolicy = (0, assistantRoutePolicy_1.createAssistantRoutePoli
hasLooseAllTimeAddressLookupSignal,
hasDeepAnalysisPreferenceSignal,
hasDirectDeepAnalysisSignal,
shouldEmitOrganizationSelectionReply: assistantLivingModePolicy.shouldEmitOrganizationSelectionReply,
compactWhitespace,
hasDeepSessionContinuationSignal,
resolveLivingAssistantModeDecision: assistantLivingModePolicy.resolveLivingAssistantModeDecision,

View File

@ -117,6 +117,33 @@ describe("address follow-up temporal regressions", () => {
expect(result?.baseReasons).toContain("as_of_date_from_followup_context");
});
it("keeps period window on inventory same-date follow-up phrased as 'по этой же дате'", () => {
const result = runAddressDecomposeStage(
"\u043f\u043e\u043a\u0430\u0436\u0438 \u043e\u0441\u0442\u0430\u0442\u043a\u0438 \u043d\u0430 \u0441\u043a\u043b\u0430\u0434\u0435 \u043f\u043e \u044d\u0442\u043e\u0439 \u0436\u0435 \u0434\u0430\u0442\u0435",
{
previous_intent: "receivables_confirmed_as_of_date",
previous_filters: {
organization: "\u0420\u0410\u0419\u041c",
period_from: "2020-03-01",
period_to: "2020-03-31",
as_of_date: "2020-03-31"
},
previous_anchor_type: "organization",
previous_anchor_value: "\u0420\u0410\u0419\u041c"
}
);
expect(result).not.toBeNull();
expect(result?.intent.intent).toBe("inventory_on_hand_as_of_date");
expect(result?.filters.extracted_filters.organization).toBe("\u0420\u0410\u0419\u041c");
expect(result?.filters.extracted_filters.as_of_date).toBe("2020-03-31");
expect(result?.filters.extracted_filters.period_from).toBe("2020-03-01");
expect(result?.filters.extracted_filters.period_to).toBe("2020-03-31");
expect(result?.filters.extracted_filters.warehouse).toBeUndefined();
expect(result?.baseReasons).toContain("period_from_from_followup_context");
expect(result?.baseReasons).toContain("period_to_from_followup_context");
});
it("retargets inventory purchase-date VAT bridge into confirmed VAT period with inherited purchase month", () => {
const result = runAddressDecomposeStage("ндс можешь прикинуть на дату покупки рабочей станции?", {
previous_intent: "inventory_purchase_provenance_for_item",

View File

@ -79,6 +79,12 @@ describe("inventory warehouse anchor extraction", () => {
expect(filters.warehouse).toBeUndefined();
});
it("does not materialize 'по той же дате' as warehouse anchor in stock follow-up", () => {
const filters = extractAddressFilters("покажи остатки на складе по той же дате", "inventory_on_hand_as_of_date").extracted_filters;
expect(filters.warehouse).toBeUndefined();
});
it("does not materialize current-date phrasing as warehouse anchor in stock follow-up", () => {
const filters = extractAddressFilters(
"получить остатки по складу на текущую дату",

View File

@ -0,0 +1,70 @@
import { afterEach, describe, expect, it, vi } from "vitest";
const { executeAddressMcpQueryMock } = vi.hoisted(() => ({
executeAddressMcpQueryMock: vi.fn()
}));
vi.mock("../src/services/addressMcpClient", async () => {
const actual = await vi.importActual<typeof import("../src/services/addressMcpClient")>(
"../src/services/addressMcpClient"
);
return {
...actual,
executeAddressMcpQuery: executeAddressMcpQueryMock
};
});
import { AddressQueryService } from "../src/services/addressQueryService";
afterEach(() => {
executeAddressMcpQueryMock.mockReset();
vi.restoreAllMocks();
});
describe("referential organization scope grounding", () => {
it("grounds 'по этой компании' to the active organization and clears bogus counterparty anchor", 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: 9800,
Quantity: 2,
SubcontoDt1: "Рабочая станция универсального специалиста",
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",
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("organization_grounded_from_referential_scope");
expect(result?.debug.reasons).toContain("counterparty_cleared_from_referential_organization_scope");
expect(String(result?.reply_text ?? "")).toContain("Рабочая станция универсального специалиста");
});
});

View File

@ -72,6 +72,28 @@ describe("assistantContinuityPolicy organization authority", () => {
expect(authority.organizationClarificationSelectionFromScope).toBe("Org Selected");
});
it("exposes known organizations as switch candidates even without a prior clarification turn", () => {
const authority = resolveAssistantOrganizationAuthority({
sessionItems: [
{
role: "assistant",
debug: {
execution_lane: "living_chat",
assistant_active_organization: "РАЙМ",
assistant_known_organizations: ['ООО "Альтернатива Плюс"', "РАЙМ"]
}
}
],
sessionKnownOrganizations: ['ООО "Альтернатива Плюс"']
});
expect(authority.activeOrganization).toBe("РАЙМ");
expect(authority.organizationClarificationCandidates).toEqual([
'ООО "Альтернатива Плюс"',
"РАЙМ"
]);
});
it("reads item, organization and scoped date from root-frame fallback when direct filters are missing", () => {
const facts = resolveAddressDebugContextFacts({
anchor_type: "item",

View File

@ -85,6 +85,53 @@ describe("assistant organization scope runtime adapter", () => {
});
});
it("prefers continuity-selected organization over stale navigation scope after late switch", () => {
const normalizeOrganizationScopeValue = vi.fn((value: unknown) =>
typeof value === "string" && value.trim() ? value.trim() : null
);
const context = resolveSessionOrganizationScopeContextRuntime({
userMessage: "а по этой компании что сейчас на складе?",
items: [
{
role: "assistant",
debug: {
assistant_known_organizations: ["ООО Альтернатива Плюс", "РАЙМ"],
assistant_active_organization: "РАЙМ",
living_chat_selected_organization: "РАЙМ"
}
}
] as any[],
addressNavigationState: {
schema_version: "address_navigation_state_v1",
session_id: "asst-nav-stale-org",
updated_at: "2026-04-19T12:04:44.000Z",
session_context: {
active_result_set_id: "rs-1",
active_focus_object: null,
last_confirmed_route: "address_inventory_on_hand_as_of_date_v1",
date_scope: {
as_of_date: "2026-04-19",
period_from: null,
period_to: null
},
organization_scope: "ООО Альтернатива Плюс"
},
result_sets: [],
navigation_history: []
} as any,
extractKnownOrganizationsFromHistory: () => ["ООО Альтернатива Плюс"],
resolveOrganizationSelectionFromMessage: () => null,
normalizeOrganizationScopeValue
});
expect(context).toEqual({
knownOrganizations: ["ООО Альтернатива Плюс", "РАЙМ"],
selectedOrganization: null,
activeOrganization: "РАЙМ"
});
});
it("reuses assistant continuity authority from prior assistant debug when legacy helpers are empty", () => {
const normalizeOrganizationScopeValue = vi.fn((value: unknown) =>
typeof value === "string" && value.trim() ? value.trim() : null
@ -138,11 +185,14 @@ describe("assistant organization scope runtime adapter", () => {
});
});
it("keeps existing organization in followup filters and returns null for empty context without org", () => {
const preserved = mergeFollowupContextWithOrganizationScopeRuntime({
it("overrides stale organization in followup filters and returns null for empty context without org", () => {
const overridden = mergeFollowupContextWithOrganizationScopeRuntime({
followupContext: {
previous_filters: {
organization: "Org Existing"
},
root_filters: {
organization: "Org Existing"
}
},
organization: "Org A",
@ -158,7 +208,8 @@ describe("assistant organization scope runtime adapter", () => {
toNonEmptyString: () => null
});
expect((preserved as any).previous_filters.organization).toBe("Org Existing");
expect((overridden as any).previous_filters.organization).toBe("Org A");
expect((overridden as any).root_filters.organization).toBe("Org A");
expect(empty).toBeNull();
});
});

View File

@ -95,6 +95,7 @@ function buildPolicy(overrides: Record<string, unknown> = {}) {
hasLooseAllTimeAddressLookupSignal: () => false,
hasDeepAnalysisPreferenceSignal: () => false,
hasDirectDeepAnalysisSignal: () => false,
shouldEmitOrganizationSelectionReply: () => false,
compactWhitespace: (text: string) => String(text ?? "").replace(/\s+/g, " ").trim(),
hasDeepSessionContinuationSignal: () => false,
resolveLivingAssistantModeDecision: (input: { addressLaneTriggered?: boolean }) =>
@ -369,6 +370,59 @@ describe("assistantRoutePolicy", () => {
expect(decision.livingReason).toBe("organization_fact_lookup_signal_detected");
});
it("routes a late company switch to chat instead of reusing the old address contour", () => {
const policy = buildPolicy({
detectAddressQuestionMode: () => ({ mode: "address_query", confidence: "high" }),
resolveAddressIntent: () => ({ intent: "inventory_on_hand_as_of_date", confidence: "high" }),
resolveAddressToolGateDecision: () => ({
runAddressLane: true,
decision: "run_address_lane",
reason: "address_mode_classifier_detected"
}),
resolveOrganizationSelectionFromMessage: () => "РАЙМ",
shouldEmitOrganizationSelectionReply: () => true
});
const decision = policy.resolveAssistantOrchestrationDecision({
rawUserMessage: "теперь давай по РАЙМ",
effectiveAddressUserMessage: "теперь давай по РАЙМ",
followupContext: {
previous_intent: "inventory_on_hand_as_of_date",
previous_filters: {
organization: "ООО Альтернатива Плюс",
as_of_date: "2026-04-19"
}
},
sessionItems: [
{
role: "assistant",
debug: {
execution_lane: "address_query",
answer_grounding_check: { status: "grounded" },
extracted_filters: {
organization: "ООО Альтернатива Плюс",
as_of_date: "2026-04-19"
}
}
}
],
sessionOrganizationScope: {
knownOrganizations: ["ООО Альтернатива Плюс", "РАЙМ"],
selectedOrganization: null,
activeOrganization: "ООО Альтернатива Плюс"
},
llmPreDecomposeMeta: null,
useMock: false
});
expect(decision.runAddressLane).toBe(false);
expect(decision.toolGateReason).toBe("organization_scope_switch_detected");
expect(decision.livingMode).toBe("chat");
expect(decision.livingReason).toBe("organization_scope_switch_detected");
expect(decision.orchestrationContract?.organization_scope_switch_detected).toBe(true);
expect(decision.orchestrationContract?.organization_scope_selection).toBe("РАЙМ");
});
it("routes explicit recap wording with selected-object phrasing to chat even when address-like cues exist", () => {
const policy = buildPolicy({
hasStrongDataIntentSignal: () => true,

View File

@ -52,7 +52,7 @@ describe("assistant runtime contract registry", () => {
const contract = getAssistantCapabilityContract("confirmed_inventory_on_hand_as_of_date");
expect(contract).not.toBeNull();
expect(contract?.entry_modes).toEqual(["root_entry", "root_followup", "clarification_resume"]);
expect(contract?.supported_transition_classes).toEqual(["T1", "T2", "T7"]);
expect(contract?.supported_transition_classes).toEqual(["T1", "T2", "T6", "T7"]);
expect(contract?.requires_focus_object).toBe(false);
expect(contract?.result_shape).toBe("item_list_with_quantity_cost_warehouse_organization");
expect(contract?.required_scenario_families).toContain("colloquial");
@ -71,6 +71,30 @@ describe("assistant runtime contract registry", () => {
expect(contract?.required_scenario_families).toContain("pronoun_followup");
});
it("declares root financial exact capabilities for debt and vat snapshots", () => {
const receivables = getAssistantCapabilityContract("confirmed_receivables_as_of_date");
const payables = getAssistantCapabilityContract("confirmed_payables_as_of_date");
const vat = getAssistantCapabilityContract("confirmed_vat_liability_for_tax_period");
expect(receivables?.intent_ids).toEqual(["receivables_confirmed_as_of_date"]);
expect(receivables?.supported_transition_classes).toEqual(["T1", "T2", "T6", "T7"]);
expect(receivables?.requires_focus_object).toBe(false);
expect(payables?.intent_ids).toEqual(["payables_confirmed_as_of_date"]);
expect(payables?.truth_mode_fallbacks).toEqual(["limited", "clarification_required", "unsupported"]);
expect(vat?.intent_ids).toEqual(["vat_liability_confirmed_for_tax_period"]);
expect(vat?.required_scenario_families).toContain("tax_period_followup");
});
it("resolves receivables intent to its exact runtime contract", () => {
const contract = getAssistantCapabilityContractByIntent("receivables_confirmed_as_of_date");
expect(contract?.capability_id).toBe("confirmed_receivables_as_of_date");
expect(contract?.runtime_lane).toBe("address_exact");
expect(contract?.execution_adapter).toBe("AddressQueryService");
});
it("keeps truth semantics outside answer wording for every pilot inventory capability", () => {
for (const contract of listInventoryCapabilityContracts()) {
expect(contract.coverage_gate_behavior).toBe("partial_or_blocked_if_evidence_insufficient");