From 4e1830282bb6248f55c49ee403659d32e900804e Mon Sep 17 00:00:00 2001 From: dctouch Date: Sat, 18 Apr 2026 20:03:11 +0300 Subject: [PATCH] =?UTF-8?q?=D0=90=D0=A0=D0=A7=20=D0=90=D0=9F11=20-=20?= =?UTF-8?q?=D0=90=D1=80=D1=85=D0=B8=D1=82=D0=B5=D0=BA=D1=82=D1=83=D1=80?= =?UTF-8?q?=D0=B0=20=D0=BF=D0=BE=D1=81=D0=BB=D0=B5=20=D1=80=D0=B5=D0=B3?= =?UTF-8?q?=D1=80=D0=B5=D1=81=D1=81=D0=B0:=20=D0=90=D1=80=D1=85=D0=B8?= =?UTF-8?q?=D1=82=D0=B5=D0=BA=D1=82=D1=83=D1=80=D0=B0:=20=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D1=82=D1=8F=D0=BD=D1=83=D1=82=D1=8C=20shared=20organizat?= =?UTF-8?q?ion=20authority=20=D0=B2=20transition=20carryover=20=D0=B8=20?= =?UTF-8?q?=D1=83=D0=B4=D0=B5=D1=80=D0=B6=D0=B0=D1=82=D1=8C=20phase12=20re?= =?UTF-8?q?play=20=D0=B7=D0=B5=D0=BB=D1=91=D0=BD=D1=8B=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ontinuity_stabilization_plan_2026-04-17.md | 14 ++++-- .../services/assistantTransitionPolicy.js | 24 +++++++--- .../src/services/assistantTransitionPolicy.ts | 27 +++++++---- .../tests/assistantTransitionPolicy.test.ts | 48 +++++++++++++++++++ 4 files changed, 94 insertions(+), 19 deletions(-) diff --git a/docs/ARCH/11 - architecture_turnaround/11 - continuity_stabilization_plan_2026-04-17.md b/docs/ARCH/11 - architecture_turnaround/11 - continuity_stabilization_plan_2026-04-17.md index 7c96081..ba0a867 100644 --- a/docs/ARCH/11 - architecture_turnaround/11 - continuity_stabilization_plan_2026-04-17.md +++ b/docs/ARCH/11 - architecture_turnaround/11 - continuity_stabilization_plan_2026-04-17.md @@ -325,10 +325,16 @@ Still open after the accepted phase12 replay: - root supplier tails anomaly questions re-enter `hybrid_store_plus_live` with grounded fragments and non-empty deterministic route summaries; - narrowing follow-up for `2020-06 / account 60` now keeps hybrid/batch routing instead of collapsing into empty clarification; - the broader hybrid investigation contour is therefore back under explicit runtime authority rather than ambient luck. - - the remaining translit root seam is now also closed in the same contour: - - transliterated supplier-tail wording no longer loses the causal tail during predecompose entry handling; - - live replay `address_truth_harness_phase13_hybrid_followup_authority_live_20260418_rerun4` is accepted with the translit root step returning `factual_with_explanation` and staying inside hybrid investigation routing; - - endpoint coverage now explicitly requires the translit account-60 tail question to keep every routed fragment in `hybrid_store_plus_live`, so future refactors cannot silently split the same question back into `hybrid + store_canonical`. +- the remaining translit root seam is now also closed in the same contour: + - transliterated supplier-tail wording no longer loses the causal tail during predecompose entry handling; + - live replay `address_truth_harness_phase13_hybrid_followup_authority_live_20260418_rerun4` is accepted with the translit root step returning `factual_with_explanation` and staying inside hybrid investigation routing; + - endpoint coverage now explicitly requires the translit account-60 tail question to keep every routed fragment in `hybrid_store_plus_live`, so future refactors cannot silently split the same question back into `hybrid + store_canonical`. +- the next authority-convergence pass now removes one more local organization reconstruction seam from the transition hot path: + - `assistantTransitionPolicy` no longer reconstructs clarification/company authority only from ad hoc history scans and raw continuity snapshot pieces; + - follow-up carryover now reads the shared organization authority object first, including assistant-side active organization memory and clarification candidates, before falling back to older local filters; + - this matters because mixed follow-up questions that pivot after assistant-side company fixation no longer depend on whether the previous address debug happened to still carry `organization` in its own extracted filters; + - targeted transition regression now protects the case where grounded history is empty but assistant-side organization authority is already present; + - wide saved-session replay `address_truth_harness_phase12_wider_saved_session_pool_live_20260418_rerun5` remains accepted `20/20`, which is the critical proof that this transition-layer convergence did not reopen the broader continuity path. ## Next Execution Slice (2026-04-18) diff --git a/llm_normalizer/backend/dist/services/assistantTransitionPolicy.js b/llm_normalizer/backend/dist/services/assistantTransitionPolicy.js index 14a1aed..86a0dbe 100644 --- a/llm_normalizer/backend/dist/services/assistantTransitionPolicy.js +++ b/llm_normalizer/backend/dist/services/assistantTransitionPolicy.js @@ -331,18 +331,23 @@ function createAssistantTransitionPolicy(deps) { ? latestAddressItem : findRecentUsableAddressAssistantItem(items)) ?? latestAddressItem; const previousAddressDebug = previousAddressItem?.debug ?? null; - const continuitySnapshot = (0, assistantContinuityPolicy_1.resolveAssistantContinuitySnapshot)({ - sessionItems: items, - toNonEmptyString: deps.toNonEmptyString - }); const lastOrganizationClarificationDebug = deps.findLastOrganizationClarificationAddressDebug(items); - const organizationClarificationCandidates = Array.isArray(lastOrganizationClarificationDebug?.organization_candidates) - ? deps.mergeKnownOrganizations(lastOrganizationClarificationDebug.organization_candidates) + const organizationAuthority = (0, assistantContinuityPolicy_1.resolveAssistantOrganizationAuthority)({ + sessionItems: items, + lastOrganizationClarificationDebug, + toNonEmptyString: deps.toNonEmptyString, + normalizeOrganizationScopeValue: deps.normalizeOrganizationScopeValue, + mergeKnownOrganizations: deps.mergeKnownOrganizations + }); + const continuitySnapshot = organizationAuthority.continuitySnapshot; + const organizationClarificationCandidates = Array.isArray(organizationAuthority.organizationClarificationCandidates) + ? organizationAuthority.organizationClarificationCandidates : []; const organizationClarificationSelection = deps.resolveOrganizationSelectionFromMessage(userMessage, organizationClarificationCandidates) ?? (deps.toNonEmptyString(alternateMessage) ? deps.resolveOrganizationSelectionFromMessage(String(alternateMessage ?? ""), organizationClarificationCandidates) - : null); + : null) ?? + deps.normalizeOrganizationScopeValue(organizationAuthority.organizationClarificationSelectionFromScope); const hasOrganizationClarificationContinuation = Boolean(lastOrganizationClarificationDebug && organizationClarificationSelection); const followupOffer = previousAddressDebug ? deps.buildAddressFollowupOffer(previousAddressDebug) : null; const hasImplicitContinuationSignal = Boolean(previousAddressDebug) && @@ -650,6 +655,11 @@ function createAssistantTransitionPolicy(deps) { previousFilters.organization = historicalOrganization; } } + const authorityActiveOrganization = deps.normalizeOrganizationScopeValue(organizationAuthority.activeOrganization) ?? + deps.normalizeOrganizationScopeValue(organizationAuthority.continuityActiveOrganization); + if (!deps.toNonEmptyString(previousFilters.organization) && authorityActiveOrganization) { + previousFilters.organization = authorityActiveOrganization; + } if (!deps.toNonEmptyString(previousFilters.organization) && continuitySnapshot.activeOrganization) { previousFilters.organization = continuitySnapshot.activeOrganization; } diff --git a/llm_normalizer/backend/src/services/assistantTransitionPolicy.ts b/llm_normalizer/backend/src/services/assistantTransitionPolicy.ts index 4f4e8b1..b2e5cb8 100644 --- a/llm_normalizer/backend/src/services/assistantTransitionPolicy.ts +++ b/llm_normalizer/backend/src/services/assistantTransitionPolicy.ts @@ -3,7 +3,7 @@ import { buildInventoryRootFrameFromAddressDebug, readAddressDebugFilters, readAddressDebugItem, - resolveAssistantContinuitySnapshot + resolveAssistantOrganizationAuthority } from "./assistantContinuityPolicy"; export function createAssistantTransitionPolicy(deps) { @@ -413,19 +413,24 @@ export function createAssistantTransitionPolicy(deps) { ? latestAddressItem : findRecentUsableAddressAssistantItem(items)) ?? latestAddressItem; const previousAddressDebug = previousAddressItem?.debug ?? null; - const continuitySnapshot = resolveAssistantContinuitySnapshot({ - sessionItems: items, - toNonEmptyString: deps.toNonEmptyString - }); const lastOrganizationClarificationDebug = deps.findLastOrganizationClarificationAddressDebug(items); - const organizationClarificationCandidates = Array.isArray(lastOrganizationClarificationDebug?.organization_candidates) - ? deps.mergeKnownOrganizations(lastOrganizationClarificationDebug.organization_candidates) + const organizationAuthority = resolveAssistantOrganizationAuthority({ + sessionItems: items, + lastOrganizationClarificationDebug, + toNonEmptyString: deps.toNonEmptyString, + normalizeOrganizationScopeValue: deps.normalizeOrganizationScopeValue, + mergeKnownOrganizations: deps.mergeKnownOrganizations + }); + const continuitySnapshot = organizationAuthority.continuitySnapshot; + const organizationClarificationCandidates = Array.isArray(organizationAuthority.organizationClarificationCandidates) + ? organizationAuthority.organizationClarificationCandidates : []; const organizationClarificationSelection = deps.resolveOrganizationSelectionFromMessage(userMessage, organizationClarificationCandidates) ?? (deps.toNonEmptyString(alternateMessage) ? deps.resolveOrganizationSelectionFromMessage(String(alternateMessage ?? ""), organizationClarificationCandidates) - : null); + : null) ?? + deps.normalizeOrganizationScopeValue(organizationAuthority.organizationClarificationSelectionFromScope); const hasOrganizationClarificationContinuation = Boolean( lastOrganizationClarificationDebug && organizationClarificationSelection ); @@ -793,6 +798,12 @@ export function createAssistantTransitionPolicy(deps) { previousFilters.organization = historicalOrganization; } } + const authorityActiveOrganization = + deps.normalizeOrganizationScopeValue(organizationAuthority.activeOrganization) ?? + deps.normalizeOrganizationScopeValue(organizationAuthority.continuityActiveOrganization); + if (!deps.toNonEmptyString(previousFilters.organization) && authorityActiveOrganization) { + previousFilters.organization = authorityActiveOrganization; + } if (!deps.toNonEmptyString(previousFilters.organization) && continuitySnapshot.activeOrganization) { previousFilters.organization = continuitySnapshot.activeOrganization; } diff --git a/llm_normalizer/backend/tests/assistantTransitionPolicy.test.ts b/llm_normalizer/backend/tests/assistantTransitionPolicy.test.ts index 2793cc9..de087ca 100644 --- a/llm_normalizer/backend/tests/assistantTransitionPolicy.test.ts +++ b/llm_normalizer/backend/tests/assistantTransitionPolicy.test.ts @@ -256,6 +256,54 @@ describe("assistantTransitionPolicy", () => { expect(carryover?.followupContext?.root_context_only).toBeUndefined(); }); + it("hydrates follow-up organization from shared assistant authority when local history filters are empty", () => { + const policy = buildPolicy({ + findLastAddressAssistantItem: () => ({ + text: "Подтвержденная дебиторская задолженность на 31.03.2020 собрана.", + debug: { + detected_intent: "receivables_confirmed_as_of_date", + extracted_filters: { + as_of_date: "2020-03-31", + period_from: "2020-03-01", + period_to: "2020-03-31" + }, + anchor_type: "organization", + anchor_value_resolved: null + } + }), + hasAddressFollowupContextSignal: () => true, + hasReferentialPointer: () => true, + findRecentInventoryRootFrame: () => null, + findRecentAddressFilterValue: () => null, + resolveAddressIntent: () => ({ intent: "unknown" }) + }); + + const carryover = policy.resolveAddressFollowupCarryoverContext( + "остатки по складу на эту же дату", + [ + { + role: "assistant", + text: "Компания уже выбрана в живом чате.", + debug: { + assistant_active_organization: 'ООО "Альтернатива Плюс"', + assistant_known_organizations: ['ООО "Альтернатива Плюс"', 'ООО "Лайт"'] + } + } + ], + null, + null, + null + ); + + expect(carryover?.followupContext?.previous_filters).toMatchObject({ + organization: 'ООО "Альтернатива Плюс"', + as_of_date: "2020-03-31", + period_from: "2020-03-01", + period_to: "2020-03-31" + }); + expect(carryover?.followupContext?.target_intent).toBe("inventory_on_hand_as_of_date"); + }); + it("bridges selected-item purchase provenance into a VAT period follow-up", () => { const item = "Рабочая станция универсального специалиста"; const policy = buildPolicy({