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 df7a9c9..32d8065 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 @@ -422,6 +422,12 @@ Still open after the accepted phase12 replay: - targeted `assistantContinuityPolicy` and `assistantTransitionPolicy` suites are green after the move, including explicit regression coverage for `inventory_purchase_documents_for_item -> inventory_on_hand_as_of_date` carryover where `root_filters` must override a stale drilldown date; - this pass reduces one more hidden state-reconstruction fork between the continuity layer and transition glue without introducing case-specific routing; - a fresh live rerun of `address_truth_harness_phase12_wider_saved_session_pool` on `2026-04-19` stayed semantically clean on the repaired carryover path and failed only on the already-known time-unstable `today` expectations (`2026-04-18` vs `2026-04-19`) in `inventory_root_today`, `payables_today`, and `receivables_mirror_today`. +- the next continuity-authority pass now centralizes one more shared inventory root-frame seam that used to be split across `assistantService` and `assistantTransitionPolicy`: + - continuity now owns `hydrateInventoryRootFrameState(...)`, which fills missing organization/date scope into `inventoryRootFrame` and computes `currentFrameKind` from the same shared state object instead of rebuilding both pieces locally inside transition glue; + - continuity now also owns `buildRootScopedCarryoverFilters(...)`, so root-scoped filter precedence no longer lives as a separate service-local helper and tests no longer need a legacy re-export from `assistantService`; + - this matters because `inventoryRootFrame`, `current_frame_kind`, and `root-scoped` filter precedence now converge through one authority layer before `root_context_only` pivots are decided, which reduces another hidden chance for state drift when new domains or new follow-up families are added; + - targeted `assistantContinuityPolicy` and `assistantTransitionPolicy` suites are green after the move, with explicit coverage for root-frame hydration from navigation scope and for previous-date precedence over a stale inventory root frame; + - a fresh live rerun of `address_truth_harness_phase12_wider_saved_session_pool` on `2026-04-19` remained semantically stable on all repaired continuity paths and again failed only on the already-known date-sensitive `today` expectations, not on the new shared root-frame state owner. ## Next Execution Slice (2026-04-18) diff --git a/llm_normalizer/backend/dist/services/assistantContinuityPolicy.js b/llm_normalizer/backend/dist/services/assistantContinuityPolicy.js index 11c4a13..64d5ab6 100644 --- a/llm_normalizer/backend/dist/services/assistantContinuityPolicy.js +++ b/llm_normalizer/backend/dist/services/assistantContinuityPolicy.js @@ -9,6 +9,8 @@ exports.readAddressDebugTemporalScope = readAddressDebugTemporalScope; exports.resolveAddressDebugAnchorContext = resolveAddressDebugAnchorContext; exports.resolveAddressDebugContextFacts = resolveAddressDebugContextFacts; exports.resolveAddressDebugCarryoverFilters = resolveAddressDebugCarryoverFilters; +exports.hydrateInventoryRootFrameState = hydrateInventoryRootFrameState; +exports.buildRootScopedCarryoverFilters = buildRootScopedCarryoverFilters; exports.buildInventoryRootFrameFromAddressDebug = buildInventoryRootFrameFromAddressDebug; exports.isGroundedAddressDebug = isGroundedAddressDebug; exports.resolveAssistantContinuitySnapshot = resolveAssistantContinuitySnapshot; @@ -153,6 +155,84 @@ function resolveAddressDebugCarryoverFilters(debug, toNonEmptyString = fallbackT } return nextFilters; } +function hydrateInventoryRootFrameState(inventoryRootFrame, sourceIntent, navigationOrganization, navigationDateScope, toNonEmptyString = fallbackToNonEmptyString, isInventoryDrilldownFrameIntent = () => false, isInventoryRootFrameIntent = () => false) { + if (!inventoryRootFrame) { + return { inventoryRootFrame: null, currentFrameKind: null }; + } + let hydratedRootFrame = { + ...inventoryRootFrame, + filters: { + ...(inventoryRootFrame.filters ?? {}) + }, + currentFrameKind: toNonEmptyString(inventoryRootFrame.currentFrameKind) ?? null + }; + if (navigationOrganization && !toNonEmptyString(hydratedRootFrame.filters?.organization)) { + hydratedRootFrame = { + ...hydratedRootFrame, + filters: { + ...(hydratedRootFrame.filters ?? {}), + organization: navigationOrganization + } + }; + } + if (navigationDateScope) { + hydratedRootFrame = { + ...hydratedRootFrame, + filters: { + ...(hydratedRootFrame.filters ?? {}), + as_of_date: toNonEmptyString(hydratedRootFrame.filters?.as_of_date) ?? + toNonEmptyString(navigationDateScope.as_of_date) ?? + undefined, + period_from: toNonEmptyString(hydratedRootFrame.filters?.period_from) ?? + toNonEmptyString(navigationDateScope.period_from) ?? + undefined, + period_to: toNonEmptyString(hydratedRootFrame.filters?.period_to) ?? + toNonEmptyString(navigationDateScope.period_to) ?? + undefined + } + }; + } + const currentFrameKind = toNonEmptyString(hydratedRootFrame.currentFrameKind) ?? + (isInventoryDrilldownFrameIntent(sourceIntent) + ? "inventory_drilldown" + : isInventoryRootFrameIntent(sourceIntent) + ? "inventory_root" + : "generic"); + return { + inventoryRootFrame: { + ...hydratedRootFrame, + currentFrameKind + }, + currentFrameKind + }; +} +function buildRootScopedCarryoverFilters(previousFilters, inventoryRootFrame, toNonEmptyString = fallbackToNonEmptyString) { + const candidateFilters = inventoryRootFrame?.filters && typeof inventoryRootFrame.filters === "object" + ? inventoryRootFrame.filters + : {}; + const nextFilters = {}; + const organization = toNonEmptyString(candidateFilters?.organization) ?? toNonEmptyString(previousFilters?.organization); + const warehouse = toNonEmptyString(candidateFilters?.warehouse) ?? toNonEmptyString(previousFilters?.warehouse); + const asOfDate = toNonEmptyString(previousFilters?.as_of_date) ?? toNonEmptyString(candidateFilters?.as_of_date); + const periodFrom = toNonEmptyString(previousFilters?.period_from) ?? toNonEmptyString(candidateFilters?.period_from); + const periodTo = toNonEmptyString(previousFilters?.period_to) ?? toNonEmptyString(candidateFilters?.period_to); + if (organization) { + nextFilters.organization = organization; + } + if (warehouse) { + nextFilters.warehouse = warehouse; + } + if (asOfDate) { + nextFilters.as_of_date = asOfDate; + } + if (periodFrom) { + nextFilters.period_from = periodFrom; + } + if (periodTo) { + nextFilters.period_to = periodTo; + } + return nextFilters; +} function buildInventoryRootFrameFromAddressDebug(debug, toNonEmptyString = fallbackToNonEmptyString) { if (!debug || typeof debug !== "object") { return null; diff --git a/llm_normalizer/backend/dist/services/assistantService.js b/llm_normalizer/backend/dist/services/assistantService.js index 42450bc..a6fa93a 100644 --- a/llm_normalizer/backend/dist/services/assistantService.js +++ b/llm_normalizer/backend/dist/services/assistantService.js @@ -41,7 +41,6 @@ exports.evaluateCoverageForTests = evaluateCoverageForTests; exports.extractSubjectTokensForTests = extractSubjectTokensForTests; exports.resolveAssistantOrchestrationDecision = resolveAssistantOrchestrationDecision; exports.resolveSessionOrganizationScopeContextForTests = resolveSessionOrganizationScopeContextForTests; -exports.buildRootScopedCarryoverFiltersForTests = buildRootScopedCarryoverFiltersForTests; exports.extractOrganizationFactsFromRowsForTests = extractOrganizationFactsFromRowsForTests; exports.resolveOrganizationNamesByRefsForTests = resolveOrganizationNamesByRefsForTests; exports.resolveLivingAssistantModeDecision = resolveLivingAssistantModeDecision; @@ -2758,31 +2757,7 @@ function hasForeignAccountingPivotOverInventoryMessage(userMessage, alternateMes /(?:оплат|плат(?:е|ё)ж|аванс|зач(?:е|ё)т|выписк|statement|wire|settlement|payment|\b51(?:\.\d{1,2})?\b|\b60(?:\.\d{1,2})?\b|\b62(?:\.\d{1,2})?\b)/iu.test(sample)); } function buildRootScopedCarryoverFilters(previousFilters, inventoryRootFrame) { - const candidateFilters = inventoryRootFrame?.filters && typeof inventoryRootFrame.filters === "object" - ? inventoryRootFrame.filters - : {}; - const nextFilters = {}; - const organization = toNonEmptyString(candidateFilters?.organization) ?? toNonEmptyString(previousFilters?.organization); - const warehouse = toNonEmptyString(candidateFilters?.warehouse) ?? toNonEmptyString(previousFilters?.warehouse); - const asOfDate = toNonEmptyString(previousFilters?.as_of_date) ?? toNonEmptyString(candidateFilters?.as_of_date); - const periodFrom = toNonEmptyString(previousFilters?.period_from) ?? toNonEmptyString(candidateFilters?.period_from); - const periodTo = toNonEmptyString(previousFilters?.period_to) ?? toNonEmptyString(candidateFilters?.period_to); - if (organization) { - nextFilters.organization = organization; - } - if (warehouse) { - nextFilters.warehouse = warehouse; - } - if (asOfDate) { - nextFilters.as_of_date = asOfDate; - } - if (periodFrom) { - nextFilters.period_from = periodFrom; - } - if (periodTo) { - nextFilters.period_to = periodTo; - } - return nextFilters; + return (0, assistantContinuityPolicy_1.buildRootScopedCarryoverFilters)(previousFilters, inventoryRootFrame, toNonEmptyString); } function resolveDebtRoleSwapFollowupIntent(userMessage, previousIntent) { const normalized = compactWhitespace(String(userMessage ?? "").toLowerCase()); @@ -4573,9 +4548,6 @@ function mergeFollowupContextWithOrganizationScope(followupContext, organization function resolveSessionOrganizationScopeContextForTests(userMessage, items, addressNavigationState = null) { return resolveSessionOrganizationScopeContext(userMessage, items, addressNavigationState); } -function buildRootScopedCarryoverFiltersForTests(previousFilters, inventoryRootFrame) { - return buildRootScopedCarryoverFilters(previousFilters, inventoryRootFrame); -} function normalizeGuidValue(value) { const source = normalizeScopeLabel(value); if (!source) { diff --git a/llm_normalizer/backend/dist/services/assistantTransitionPolicy.js b/llm_normalizer/backend/dist/services/assistantTransitionPolicy.js index 8a1109a..08675f8 100644 --- a/llm_normalizer/backend/dist/services/assistantTransitionPolicy.js +++ b/llm_normalizer/backend/dist/services/assistantTransitionPolicy.js @@ -589,42 +589,10 @@ function createAssistantTransitionPolicy(deps) { const selectedObjectRetargetIntent = hasSelectedObjectInventorySignalPrimary || hasSelectedObjectInventorySignalAlternate ? inferSelectedObjectInventoryFollowupIntent(userMessage, alternateMessage) : null; - let inventoryRootFrame = deps.findRecentInventoryRootFrame(items) ?? + const inventoryRootFrameCandidate = deps.findRecentInventoryRootFrame(items) ?? continuitySnapshot.inventoryRootFrame ?? (0, assistantContinuityPolicy_1.buildInventoryRootFrameFromAddressDebug)(continuitySnapshot.lastGroundedAddressDebug, deps.toNonEmptyString); - if (inventoryRootFrame && navigationOrganization && !deps.toNonEmptyString(inventoryRootFrame.filters?.organization)) { - inventoryRootFrame = { - ...inventoryRootFrame, - filters: { - ...(inventoryRootFrame.filters ?? {}), - organization: navigationOrganization - } - }; - } - if (inventoryRootFrame && navigationDateScope) { - inventoryRootFrame = { - ...inventoryRootFrame, - filters: { - ...(inventoryRootFrame.filters ?? {}), - as_of_date: deps.toNonEmptyString(inventoryRootFrame.filters?.as_of_date) ?? - deps.toNonEmptyString(navigationDateScope.as_of_date) ?? - undefined, - period_from: deps.toNonEmptyString(inventoryRootFrame.filters?.period_from) ?? - deps.toNonEmptyString(navigationDateScope.period_from) ?? - undefined, - period_to: deps.toNonEmptyString(inventoryRootFrame.filters?.period_to) ?? - deps.toNonEmptyString(navigationDateScope.period_to) ?? - undefined - } - }; - } - let currentFrameKind = inventoryRootFrame - ? deps.isInventoryDrilldownFrameIntent(sourceIntent) - ? "inventory_drilldown" - : deps.isInventoryRootFrameIntent(sourceIntent) - ? "inventory_root" - : "generic" - : null; + let { inventoryRootFrame, currentFrameKind } = (0, assistantContinuityPolicy_1.hydrateInventoryRootFrameState)(inventoryRootFrameCandidate, sourceIntent, navigationOrganization, navigationDateScope, deps.toNonEmptyString, deps.isInventoryDrilldownFrameIntent, deps.isInventoryRootFrameIntent); let resolvedCounterpartyFromDisplay = false; let displayedEntityTargetIntent = null; let previousFilters = (0, assistantContinuityPolicy_1.resolveAddressDebugCarryoverFilters)(previousAddressDebug, deps.toNonEmptyString); @@ -745,7 +713,7 @@ function createAssistantTransitionPolicy(deps) { previousIntent = null; previousAnchorType = null; previousAnchor = null; - previousFilters = deps.buildRootScopedCarryoverFilters(previousFilters, inventoryRootFrame); + previousFilters = (0, assistantContinuityPolicy_1.buildRootScopedCarryoverFilters)(previousFilters, inventoryRootFrame, deps.toNonEmptyString); currentFrameKind = inventoryRootFrame ? "inventory_root" : currentFrameKind; followupSelectionMode = "carry_root_context"; } diff --git a/llm_normalizer/backend/src/services/assistantContinuityPolicy.ts b/llm_normalizer/backend/src/services/assistantContinuityPolicy.ts index 57faec6..5a9c4c0 100644 --- a/llm_normalizer/backend/src/services/assistantContinuityPolicy.ts +++ b/llm_normalizer/backend/src/services/assistantContinuityPolicy.ts @@ -39,6 +39,12 @@ export interface AssistantAddressDebugTemporalScope { periodTo: string | null; } +export interface AssistantNavigationDateScope { + as_of_date?: unknown; + period_from?: unknown; + period_to?: unknown; +} + export interface AssistantAddressDebugAnchorContext { anchorType: string | null; anchorValue: string | null; @@ -244,6 +250,125 @@ export function resolveAddressDebugCarryoverFilters( return nextFilters; } +export function hydrateInventoryRootFrameState( + inventoryRootFrame: { + intent: string; + filters: Record; + anchorType: string | null; + anchorValue: string | null; + currentFrameKind?: string | null; + } | null, + sourceIntent: unknown, + navigationOrganization: unknown, + navigationDateScope: AssistantNavigationDateScope | null, + toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString, + isInventoryDrilldownFrameIntent: (intent: unknown) => boolean = () => false, + isInventoryRootFrameIntent: (intent: unknown) => boolean = () => false +): { + inventoryRootFrame: { + intent: string; + filters: Record; + anchorType: string | null; + anchorValue: string | null; + currentFrameKind: string; + } | null; + currentFrameKind: string | null; +} { + if (!inventoryRootFrame) { + return { inventoryRootFrame: null, currentFrameKind: null }; + } + + let hydratedRootFrame = { + ...inventoryRootFrame, + filters: { + ...(inventoryRootFrame.filters ?? {}) + }, + currentFrameKind: toNonEmptyString(inventoryRootFrame.currentFrameKind) ?? null + }; + + if (navigationOrganization && !toNonEmptyString(hydratedRootFrame.filters?.organization)) { + hydratedRootFrame = { + ...hydratedRootFrame, + filters: { + ...(hydratedRootFrame.filters ?? {}), + organization: navigationOrganization + } + }; + } + + if (navigationDateScope) { + hydratedRootFrame = { + ...hydratedRootFrame, + filters: { + ...(hydratedRootFrame.filters ?? {}), + as_of_date: + toNonEmptyString(hydratedRootFrame.filters?.as_of_date) ?? + toNonEmptyString(navigationDateScope.as_of_date) ?? + undefined, + period_from: + toNonEmptyString(hydratedRootFrame.filters?.period_from) ?? + toNonEmptyString(navigationDateScope.period_from) ?? + undefined, + period_to: + toNonEmptyString(hydratedRootFrame.filters?.period_to) ?? + toNonEmptyString(navigationDateScope.period_to) ?? + undefined + } + }; + } + + const currentFrameKind = + toNonEmptyString(hydratedRootFrame.currentFrameKind) ?? + (isInventoryDrilldownFrameIntent(sourceIntent) + ? "inventory_drilldown" + : isInventoryRootFrameIntent(sourceIntent) + ? "inventory_root" + : "generic"); + + return { + inventoryRootFrame: { + ...hydratedRootFrame, + currentFrameKind + }, + currentFrameKind + }; +} + +export function buildRootScopedCarryoverFilters( + previousFilters: Record | null, + inventoryRootFrame: { + filters?: Record | null; + } | null, + toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString +): Record { + const candidateFilters = + inventoryRootFrame?.filters && typeof inventoryRootFrame.filters === "object" + ? inventoryRootFrame.filters + : {}; + const nextFilters: Record = {}; + const organization = toNonEmptyString(candidateFilters?.organization) ?? toNonEmptyString(previousFilters?.organization); + const warehouse = toNonEmptyString(candidateFilters?.warehouse) ?? toNonEmptyString(previousFilters?.warehouse); + const asOfDate = toNonEmptyString(previousFilters?.as_of_date) ?? toNonEmptyString(candidateFilters?.as_of_date); + const periodFrom = toNonEmptyString(previousFilters?.period_from) ?? toNonEmptyString(candidateFilters?.period_from); + const periodTo = toNonEmptyString(previousFilters?.period_to) ?? toNonEmptyString(candidateFilters?.period_to); + if (organization) { + nextFilters.organization = organization; + } + if (warehouse) { + nextFilters.warehouse = warehouse; + } + if (asOfDate) { + nextFilters.as_of_date = asOfDate; + } + if (periodFrom) { + nextFilters.period_from = periodFrom; + } + if (periodTo) { + nextFilters.period_to = periodTo; + } + return nextFilters; +} + export function buildInventoryRootFrameFromAddressDebug( debug: Record | null, toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString diff --git a/llm_normalizer/backend/src/services/assistantService.ts b/llm_normalizer/backend/src/services/assistantService.ts index 5ce928d..caf5bc9 100644 --- a/llm_normalizer/backend/src/services/assistantService.ts +++ b/llm_normalizer/backend/src/services/assistantService.ts @@ -2713,31 +2713,7 @@ function hasForeignAccountingPivotOverInventoryMessage(userMessage, alternateMes /(?:оплат|плат(?:е|ё)ж|аванс|зач(?:е|ё)т|выписк|statement|wire|settlement|payment|\b51(?:\.\d{1,2})?\b|\b60(?:\.\d{1,2})?\b|\b62(?:\.\d{1,2})?\b)/iu.test(sample)); } function buildRootScopedCarryoverFilters(previousFilters, inventoryRootFrame) { - const candidateFilters = inventoryRootFrame?.filters && typeof inventoryRootFrame.filters === "object" - ? inventoryRootFrame.filters - : {}; - const nextFilters = {}; - const organization = toNonEmptyString(candidateFilters?.organization) ?? toNonEmptyString(previousFilters?.organization); - const warehouse = toNonEmptyString(candidateFilters?.warehouse) ?? toNonEmptyString(previousFilters?.warehouse); - const asOfDate = toNonEmptyString(previousFilters?.as_of_date) ?? toNonEmptyString(candidateFilters?.as_of_date); - const periodFrom = toNonEmptyString(previousFilters?.period_from) ?? toNonEmptyString(candidateFilters?.period_from); - const periodTo = toNonEmptyString(previousFilters?.period_to) ?? toNonEmptyString(candidateFilters?.period_to); - if (organization) { - nextFilters.organization = organization; - } - if (warehouse) { - nextFilters.warehouse = warehouse; - } - if (asOfDate) { - nextFilters.as_of_date = asOfDate; - } - if (periodFrom) { - nextFilters.period_from = periodFrom; - } - if (periodTo) { - nextFilters.period_to = periodTo; - } - return nextFilters; + return (0, assistantContinuityPolicy_1.buildRootScopedCarryoverFilters)(previousFilters, inventoryRootFrame, toNonEmptyString); } function resolveDebtRoleSwapFollowupIntent(userMessage, previousIntent) { const normalized = compactWhitespace(String(userMessage ?? "").toLowerCase()); @@ -4529,9 +4505,6 @@ function mergeFollowupContextWithOrganizationScope(followupContext, organization export function resolveSessionOrganizationScopeContextForTests(userMessage, items, addressNavigationState = null) { return resolveSessionOrganizationScopeContext(userMessage, items, addressNavigationState); } -export function buildRootScopedCarryoverFiltersForTests(previousFilters, inventoryRootFrame) { - return buildRootScopedCarryoverFilters(previousFilters, inventoryRootFrame); -} function normalizeGuidValue(value) { const source = normalizeScopeLabel(value); if (!source) { diff --git a/llm_normalizer/backend/src/services/assistantTransitionPolicy.ts b/llm_normalizer/backend/src/services/assistantTransitionPolicy.ts index d1ebae9..adab710 100644 --- a/llm_normalizer/backend/src/services/assistantTransitionPolicy.ts +++ b/llm_normalizer/backend/src/services/assistantTransitionPolicy.ts @@ -1,6 +1,8 @@ // @ts-nocheck import { + buildRootScopedCarryoverFilters, buildInventoryRootFrameFromAddressDebug, + hydrateInventoryRootFrameState, readAddressDebugFilters, readAddressDebugItem, readAddressDebugTemporalScope, @@ -732,46 +734,19 @@ export function createAssistantTransitionPolicy(deps) { hasSelectedObjectInventorySignalPrimary || hasSelectedObjectInventorySignalAlternate ? inferSelectedObjectInventoryFollowupIntent(userMessage, alternateMessage) : null; - let inventoryRootFrame = + const inventoryRootFrameCandidate = deps.findRecentInventoryRootFrame(items) ?? continuitySnapshot.inventoryRootFrame ?? buildInventoryRootFrameFromAddressDebug(continuitySnapshot.lastGroundedAddressDebug, deps.toNonEmptyString); - if (inventoryRootFrame && navigationOrganization && !deps.toNonEmptyString(inventoryRootFrame.filters?.organization)) { - inventoryRootFrame = { - ...inventoryRootFrame, - filters: { - ...(inventoryRootFrame.filters ?? {}), - organization: navigationOrganization - } - }; - } - if (inventoryRootFrame && navigationDateScope) { - inventoryRootFrame = { - ...inventoryRootFrame, - filters: { - ...(inventoryRootFrame.filters ?? {}), - as_of_date: - deps.toNonEmptyString(inventoryRootFrame.filters?.as_of_date) ?? - deps.toNonEmptyString(navigationDateScope.as_of_date) ?? - undefined, - period_from: - deps.toNonEmptyString(inventoryRootFrame.filters?.period_from) ?? - deps.toNonEmptyString(navigationDateScope.period_from) ?? - undefined, - period_to: - deps.toNonEmptyString(inventoryRootFrame.filters?.period_to) ?? - deps.toNonEmptyString(navigationDateScope.period_to) ?? - undefined - } - }; - } - let currentFrameKind = inventoryRootFrame - ? deps.isInventoryDrilldownFrameIntent(sourceIntent) - ? "inventory_drilldown" - : deps.isInventoryRootFrameIntent(sourceIntent) - ? "inventory_root" - : "generic" - : null; + let { inventoryRootFrame, currentFrameKind } = hydrateInventoryRootFrameState( + inventoryRootFrameCandidate, + sourceIntent, + navigationOrganization, + navigationDateScope, + deps.toNonEmptyString, + deps.isInventoryDrilldownFrameIntent, + deps.isInventoryRootFrameIntent + ); let resolvedCounterpartyFromDisplay = false; let displayedEntityTargetIntent = null; let previousFilters = resolveAddressDebugCarryoverFilters(previousAddressDebug, deps.toNonEmptyString); @@ -919,7 +894,7 @@ export function createAssistantTransitionPolicy(deps) { previousIntent = null; previousAnchorType = null; previousAnchor = null; - previousFilters = deps.buildRootScopedCarryoverFilters(previousFilters, inventoryRootFrame); + previousFilters = buildRootScopedCarryoverFilters(previousFilters, inventoryRootFrame, deps.toNonEmptyString); currentFrameKind = inventoryRootFrame ? "inventory_root" : currentFrameKind; followupSelectionMode = "carry_root_context"; } diff --git a/llm_normalizer/backend/tests/assistantContinuityPolicy.test.ts b/llm_normalizer/backend/tests/assistantContinuityPolicy.test.ts index 2e4de62..29fc1f9 100644 --- a/llm_normalizer/backend/tests/assistantContinuityPolicy.test.ts +++ b/llm_normalizer/backend/tests/assistantContinuityPolicy.test.ts @@ -1,5 +1,7 @@ import { describe, expect, it } from "vitest"; import { + buildRootScopedCarryoverFilters, + hydrateInventoryRootFrameState, readAddressDebugTemporalScope, resolveAddressDebugCarryoverFilters, resolveAddressDebugContextFacts, @@ -133,4 +135,68 @@ describe("assistantContinuityPolicy organization authority", () => { period_to: "2021-03-31" }); }); + + it("hydrates inventory root-frame state from navigation scope and preserves derived current frame kind", () => { + const state = hydrateInventoryRootFrameState( + { + intent: "inventory_on_hand_as_of_date", + filters: { + warehouse: "Main Warehouse" + }, + anchorType: "organization", + anchorValue: "Org Alt", + currentFrameKind: null + }, + "inventory_purchase_documents_for_item", + "Org Alt", + { + as_of_date: "2021-03-31", + period_from: "2021-03-01", + period_to: "2021-03-31" + }, + undefined, + (intent) => String(intent ?? "") === "inventory_purchase_documents_for_item", + (intent) => String(intent ?? "") === "inventory_on_hand_as_of_date" + ); + + expect(state.currentFrameKind).toBe("inventory_drilldown"); + expect(state.inventoryRootFrame).toMatchObject({ + currentFrameKind: "inventory_drilldown", + filters: { + organization: "Org Alt", + warehouse: "Main Warehouse", + as_of_date: "2021-03-31", + period_from: "2021-03-01", + period_to: "2021-03-31" + } + }); + }); + + it("builds root-scoped carryover filters with previous date precedence over inventory root frame", () => { + const filters = buildRootScopedCarryoverFilters( + { + organization: "Org Alt", + as_of_date: "2020-03-31", + period_from: "2020-03-01", + period_to: "2020-03-31" + }, + { + filters: { + organization: "Org Alt", + warehouse: "Main Warehouse", + as_of_date: "2021-03-31", + period_from: "2021-03-01", + period_to: "2021-03-31" + } + } + ); + + expect(filters).toEqual({ + organization: "Org Alt", + warehouse: "Main Warehouse", + as_of_date: "2020-03-31", + period_from: "2020-03-01", + period_to: "2020-03-31" + }); + }); }); diff --git a/llm_normalizer/backend/tests/assistantTransitionPolicy.test.ts b/llm_normalizer/backend/tests/assistantTransitionPolicy.test.ts index d59f37f..a7d7868 100644 --- a/llm_normalizer/backend/tests/assistantTransitionPolicy.test.ts +++ b/llm_normalizer/backend/tests/assistantTransitionPolicy.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; import { createAssistantTransitionPolicy } from "../src/services/assistantTransitionPolicy"; -import { buildRootScopedCarryoverFiltersForTests } from "../src/services/assistantService"; +import { buildRootScopedCarryoverFilters } from "../src/services/assistantContinuityPolicy"; function toNonEmptyString(value: unknown): string | null { if (value === null || value === undefined) { @@ -661,7 +661,7 @@ describe("assistantTransitionPolicy", () => { }); it("prefers the freshest previous date scope over a stale inventory root frame during same-date pivot", () => { - const filters = buildRootScopedCarryoverFiltersForTests( + const filters = buildRootScopedCarryoverFilters( { organization: 'ООО "Альтернатива Плюс"', as_of_date: "2020-03-31",