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 8ee4a83..df7a9c9 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 @@ -415,6 +415,13 @@ Still open after the accepted phase12 replay: - this matters because follow-up carryover in the top-level service now reads the same root-frame authority that already owns `root_filters / root_anchor / current_frame_kind`, instead of keeping a service-local fallback that could silently prefer drilldown `extracted_filters` over the real `address_root_frame_context`; - targeted `assistantAddressFollowupContext` and `addressInventoryRootFrameRegression` suites are green after the move, including a new regression that explicitly proves `root_filters` come from `address_root_frame_context.root_filters` rather than from stale drilldown `extracted_filters`; - this pass strengthens continuity convergence in the top-level orchestration glue without introducing a new case-specific branch. +- the next continuity-authority pass now removes one more duplicate carryover owner from `assistantTransitionPolicy`: + - transition no longer seeds `previous_filters` from raw `previousAddressDebug.extracted_filters` as an isolated local truth source; + - shared continuity now owns that merge through `resolveAddressDebugCarryoverFilters(...)`, which overlays inventory `address_root_frame_context.root_filters` onto stale drilldown filters before the follow-up policy starts composing pivots; + - this matters because the top-level transition glue can now inherit the same root-frame date and warehouse authority that already exists in continuity, instead of silently carrying a stale drilldown `as_of_date` into `root_context_only` pivots; + - 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`. ## 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 091e849..11c4a13 100644 --- a/llm_normalizer/backend/dist/services/assistantContinuityPolicy.js +++ b/llm_normalizer/backend/dist/services/assistantContinuityPolicy.js @@ -8,6 +8,7 @@ exports.readAddressDebugScopedDate = readAddressDebugScopedDate; exports.readAddressDebugTemporalScope = readAddressDebugTemporalScope; exports.resolveAddressDebugAnchorContext = resolveAddressDebugAnchorContext; exports.resolveAddressDebugContextFacts = resolveAddressDebugContextFacts; +exports.resolveAddressDebugCarryoverFilters = resolveAddressDebugCarryoverFilters; exports.buildInventoryRootFrameFromAddressDebug = buildInventoryRootFrameFromAddressDebug; exports.isGroundedAddressDebug = isGroundedAddressDebug; exports.resolveAssistantContinuitySnapshot = resolveAssistantContinuitySnapshot; @@ -121,6 +122,37 @@ function resolveAddressDebugContextFacts(debug, toNonEmptyString = fallbackToNon scopedDate: readAddressDebugScopedDate(debug) }; } +function resolveAddressDebugCarryoverFilters(debug, toNonEmptyString = fallbackToNonEmptyString) { + const extractedFilters = readAddressDebugFilters(debug); + const nextFilters = extractedFilters ? { ...extractedFilters } : {}; + const inventoryRootFrame = buildInventoryRootFrameFromAddressDebug(debug, toNonEmptyString); + const rootFilters = inventoryRootFrame?.filters && typeof inventoryRootFrame.filters === "object" + ? inventoryRootFrame.filters + : null; + if (rootFilters) { + const organization = toNonEmptyString(rootFilters.organization); + const warehouse = toNonEmptyString(rootFilters.warehouse); + const asOfDate = toNonEmptyString(rootFilters.as_of_date); + const periodFrom = toNonEmptyString(rootFilters.period_from); + const periodTo = toNonEmptyString(rootFilters.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/assistantTransitionPolicy.js b/llm_normalizer/backend/dist/services/assistantTransitionPolicy.js index 0caaf8e..8a1109a 100644 --- a/llm_normalizer/backend/dist/services/assistantTransitionPolicy.js +++ b/llm_normalizer/backend/dist/services/assistantTransitionPolicy.js @@ -627,8 +627,7 @@ function createAssistantTransitionPolicy(deps) { : null; let resolvedCounterpartyFromDisplay = false; let displayedEntityTargetIntent = null; - const previousFiltersRaw = previousAddressDebug.extracted_filters; - let previousFilters = previousFiltersRaw && typeof previousFiltersRaw === "object" ? { ...previousFiltersRaw } : {}; + let previousFilters = (0, assistantContinuityPolicy_1.resolveAddressDebugCarryoverFilters)(previousAddressDebug, deps.toNonEmptyString); const shouldBackfillHistoricalPartyAnchors = sourceIntentHint === "list_contracts_by_counterparty" || sourceIntentHint === "list_documents_by_counterparty" || sourceIntentHint === "bank_operations_by_counterparty" || diff --git a/llm_normalizer/backend/src/services/assistantContinuityPolicy.ts b/llm_normalizer/backend/src/services/assistantContinuityPolicy.ts index fea9f61..57faec6 100644 --- a/llm_normalizer/backend/src/services/assistantContinuityPolicy.ts +++ b/llm_normalizer/backend/src/services/assistantContinuityPolicy.ts @@ -208,6 +208,42 @@ export function resolveAddressDebugContextFacts( }; } +export function resolveAddressDebugCarryoverFilters( + debug: Record | null, + toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString +): Record { + const extractedFilters = readAddressDebugFilters(debug); + const nextFilters = extractedFilters ? { ...extractedFilters } : {}; + const inventoryRootFrame = buildInventoryRootFrameFromAddressDebug(debug, toNonEmptyString); + const rootFilters = + inventoryRootFrame?.filters && typeof inventoryRootFrame.filters === "object" + ? inventoryRootFrame.filters + : null; + if (rootFilters) { + const organization = toNonEmptyString(rootFilters.organization); + const warehouse = toNonEmptyString(rootFilters.warehouse); + const asOfDate = toNonEmptyString(rootFilters.as_of_date); + const periodFrom = toNonEmptyString(rootFilters.period_from); + const periodTo = toNonEmptyString(rootFilters.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/assistantTransitionPolicy.ts b/llm_normalizer/backend/src/services/assistantTransitionPolicy.ts index 2ca0cbe..d1ebae9 100644 --- a/llm_normalizer/backend/src/services/assistantTransitionPolicy.ts +++ b/llm_normalizer/backend/src/services/assistantTransitionPolicy.ts @@ -4,6 +4,7 @@ import { readAddressDebugFilters, readAddressDebugItem, readAddressDebugTemporalScope, + resolveAddressDebugCarryoverFilters, resolveAddressDebugAnchorContext, resolveAssistantOrganizationAuthority } from "./assistantContinuityPolicy"; @@ -773,9 +774,7 @@ export function createAssistantTransitionPolicy(deps) { : null; let resolvedCounterpartyFromDisplay = false; let displayedEntityTargetIntent = null; - const previousFiltersRaw = previousAddressDebug.extracted_filters; - let previousFilters = - previousFiltersRaw && typeof previousFiltersRaw === "object" ? { ...previousFiltersRaw } : {}; + let previousFilters = resolveAddressDebugCarryoverFilters(previousAddressDebug, deps.toNonEmptyString); const shouldBackfillHistoricalPartyAnchors = sourceIntentHint === "list_contracts_by_counterparty" || sourceIntentHint === "list_documents_by_counterparty" || diff --git a/llm_normalizer/backend/tests/assistantContinuityPolicy.test.ts b/llm_normalizer/backend/tests/assistantContinuityPolicy.test.ts index b7e3d88..2e4de62 100644 --- a/llm_normalizer/backend/tests/assistantContinuityPolicy.test.ts +++ b/llm_normalizer/backend/tests/assistantContinuityPolicy.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "vitest"; import { readAddressDebugTemporalScope, + resolveAddressDebugCarryoverFilters, resolveAddressDebugContextFacts, resolveAddressDebugAnchorContext, resolveAssistantOrganizationAuthority @@ -100,4 +101,36 @@ describe("assistantContinuityPolicy organization authority", () => { anchorValue: "Рабочая станция" }); }); + it("prefers inventory root-frame filters over stale drilldown date scope in carryover filters", () => { + const filters = resolveAddressDebugCarryoverFilters({ + detected_intent: "inventory_purchase_documents_for_item", + extracted_filters: { + item: "Workstation", + organization: "Org Alt", + as_of_date: "2021-04-15" + }, + address_root_frame_context: { + root_intent: "inventory_on_hand_as_of_date", + root_filters: { + organization: "Org Alt", + warehouse: "Main Warehouse", + as_of_date: "2021-03-31", + period_from: "2021-03-01", + period_to: "2021-03-31" + }, + root_anchor_type: "organization", + root_anchor_value: "Org Alt", + current_frame_kind: "inventory_drilldown" + } + }); + + expect(filters).toEqual({ + item: "Workstation", + organization: "Org Alt", + warehouse: "Main Warehouse", + as_of_date: "2021-03-31", + period_from: "2021-03-01", + period_to: "2021-03-31" + }); + }); }); diff --git a/llm_normalizer/backend/tests/assistantTransitionPolicy.test.ts b/llm_normalizer/backend/tests/assistantTransitionPolicy.test.ts index 7eeb3b8..d59f37f 100644 --- a/llm_normalizer/backend/tests/assistantTransitionPolicy.test.ts +++ b/llm_normalizer/backend/tests/assistantTransitionPolicy.test.ts @@ -754,4 +754,88 @@ describe("assistantTransitionPolicy", () => { }); expect(carryover?.followupContext?.previous_anchor_type).toBe("item"); }); + + it("prefers root-frame dates over stale drilldown filters when hydrating previous filters", () => { + const organization = "Org Alt"; + const policy = buildPolicy({ + findLastAddressAssistantItem: (_items: unknown[]) => ({ + text: "Workstation drilldown", + debug: { + detected_intent: "inventory_purchase_documents_for_item", + extracted_filters: { + item: "Workstation", + organization, + as_of_date: "2021-04-15" + }, + anchor_type: "item", + anchor_value_resolved: "Workstation", + address_root_frame_context: { + root_intent: "inventory_on_hand_as_of_date", + root_filters: { + organization, + warehouse: "Main Warehouse", + as_of_date: "2021-03-31", + period_from: "2021-03-01", + period_to: "2021-03-31" + }, + root_anchor_type: "organization", + root_anchor_value: organization, + current_frame_kind: "inventory_drilldown" + } + } + }), + findRecentInventoryRootFrame: () => null, + hasInventoryRootTemporalFollowupSignal: (message: string) => /эту же дату/i.test(message) + }); + + const items = [ + { + role: "assistant", + text: "Workstation drilldown", + debug: { + execution_lane: "address_query", + answer_grounding_check: { status: "grounded" }, + detected_intent: "inventory_purchase_documents_for_item", + extracted_filters: { + item: "Workstation", + organization, + as_of_date: "2021-04-15" + }, + anchor_type: "item", + anchor_value_resolved: "Workstation", + address_root_frame_context: { + root_intent: "inventory_on_hand_as_of_date", + root_filters: { + organization, + warehouse: "Main Warehouse", + as_of_date: "2021-03-31", + period_from: "2021-03-01", + period_to: "2021-03-31" + }, + root_anchor_type: "organization", + root_anchor_value: organization, + current_frame_kind: "inventory_drilldown" + } + } + } + ]; + + const carryover = policy.resolveAddressFollowupCarryoverContext( + "остатки на эту же дату", + items as any, + null, + null, + null + ); + + expect(carryover?.followupContext?.root_context_only).toBe(true); + expect(carryover?.followupContext?.previous_filters).toMatchObject({ + organization, + warehouse: "Main Warehouse", + as_of_date: "2021-03-31", + period_from: "2021-03-01", + period_to: "2021-03-31" + }); + expect(carryover?.followupContext?.previous_filters?.as_of_date).not.toBe("2021-04-15"); + }); });