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 53dc1cd..6bf2bb4 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 @@ -456,6 +456,11 @@ Still open after the accepted phase12 replay: - this matters because `previous_filters.organization` is now aligned with the same continuity authority story that already drives route, living-chat, and data-scope, instead of keeping one more hot-path-only merge order inside transition glue; - targeted `assistantContinuityPolicy` and `assistantTransitionPolicy` suites are green after the move, with direct helper coverage for organization precedence and for preserving an already grounded organization value; - a fresh live rerun of `address_truth_harness_phase12_wider_saved_session_pool` on `2026-04-19` remained semantically stable and again failed only on the already-known date-sensitive `today` expectations, not on the new shared organization carryover authority. +- the next continuity-authority pass now centralizes displayed-entity retargeting and removes another hidden duplicate owner from the transition hot path: + - continuity now owns `resolveDisplayedEntityFollowupRetarget(...)`, so `displayedEntityTargetIntent` and the companion carryover mutation for displayed counterparty / contract mentions are produced by one shared helper instead of being split between a local retarget branch and a separate transition mutation block; + - the shared retarget contract is now explicitly UTF-8-safe, which matters because a broken regex encoding in the first extraction attempt silently collapsed short phrases like `покажи договоры по СВК` back into stale previous-intent carryover; + - `assistantTransitionPolicy` no longer keeps a second local `resolveDisplayedEntityRetargetIntent(...)` owner beside the shared continuity helper, which reduces another chance that future follow-up fixes land in dead code or diverge between route retarget and anchor hydration; + - targeted `assistantContinuityPolicy` and `assistantTransitionPolicy` suites are green after the move, and a fresh live rerun of `address_truth_harness_phase11_manual_followup_meta_quality` on `2026-04-19` is accepted `10/10`, including the short displayed-counterparty retarget step. ## 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 a369a3c..dcf7d5d 100644 --- a/llm_normalizer/backend/dist/services/assistantContinuityPolicy.js +++ b/llm_normalizer/backend/dist/services/assistantContinuityPolicy.js @@ -16,6 +16,8 @@ exports.applyTemporalCarryoverFilters = applyTemporalCarryoverFilters; exports.applyOrganizationCarryoverFilters = applyOrganizationCarryoverFilters; exports.applyHistoricalPartyCarryoverFilters = applyHistoricalPartyCarryoverFilters; exports.applyReferencedEntityCarryover = applyReferencedEntityCarryover; +exports.resolveDisplayedEntityRetargetIntent = resolveDisplayedEntityRetargetIntent; +exports.resolveDisplayedEntityFollowupRetarget = resolveDisplayedEntityFollowupRetarget; exports.applySelectedItemCarryover = applySelectedItemCarryover; exports.buildInventoryRootFrameFromAddressDebug = buildInventoryRootFrameFromAddressDebug; exports.isGroundedAddressDebug = isGroundedAddressDebug; @@ -353,6 +355,47 @@ function applyReferencedEntityCarryover(previousFilters, previousAnchorType, pre resolvedCounterpartyFromDisplay }; } +function resolveDisplayedEntityRetargetIntent(userMessage, entityType, compactWhitespace, repairAddressMojibake) { + return resolveDisplayedEntityRetargetIntentUtf8Safe(userMessage, entityType, compactWhitespace, repairAddressMojibake); +} +function resolveDisplayedEntityRetargetIntentUtf8Safe(userMessage, entityType, compactWhitespace, repairAddressMojibake) { + const normalized = compactWhitespace(repairAddressMojibake(String(userMessage ?? "")).toLowerCase()); + if (!normalized) { + return null; + } + if (String(entityType ?? "") === "counterparty") { + if (/(?:\u0434\u043e\u0433\u043e\u0432\u043e\u0440|\u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442)/iu.test(normalized)) { + return "list_contracts_by_counterparty"; + } + if (/(?:\u0431\u0430\u043d\u043a|\u0432\u044b\u043f\u0438\u0441\u043a|\u043f\u043b\u0430\u0442[\u0435\u0451]\u0436|\u043e\u043f\u043b\u0430\u0442|statement|payment|wire)/iu.test(normalized)) { + return "bank_operations_by_counterparty"; + } + if (/(?:\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442|\u043d\u0430\u043a\u043b\u0430\u0434\u043d|\u0441\u0447\u0435\u0442|\u0441\u0447[\u0435\u0451]\u0442|\u0430\u043a\u0442|\u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446|\u043f\u043e\u0441\u0442\u0443\u043f\u043b)/iu.test(normalized)) { + return "list_documents_by_counterparty"; + } + if (/(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e\s+\u0434\u0435\u043d\u0435\u0433|\u0441\u043a\u043e\u043b\u044c\u043a\u043e\s+\u043f\u0440\u0438\u043d\u0435\u0441|\u0432\u044b\u0440\u0443\u0447\u043a|\u0441\u0443\u043c\u043c[\u0430\u044b]?|\u043e\u043f\u043b\u0430\u0442\u0438\u043b|\u043f\u0440\u043e\u0434\u0430\u0436)/iu.test(normalized)) { + return "customer_revenue_and_payments"; + } + return null; + } + if (String(entityType ?? "") === "contract") { + if (/(?:\u0431\u0430\u043d\u043a|\u0432\u044b\u043f\u0438\u0441\u043a|\u043f\u043b\u0430\u0442[\u0435\u0451]\u0436|\u043e\u043f\u043b\u0430\u0442|statement|payment|wire)/iu.test(normalized)) { + return "bank_operations_by_contract"; + } + if (/(?:\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442|\u043d\u0430\u043a\u043b\u0430\u0434\u043d|\u0441\u0447\u0435\u0442|\u0441\u0447[\u0435\u0451]\u0442|\u0430\u043a\u0442|\u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446|\u043f\u043e\u0441\u0442\u0443\u043f\u043b)/iu.test(normalized)) { + return "list_documents_by_contract"; + } + } + return null; +} +function resolveDisplayedEntityFollowupRetarget(userMessage, resolvedEntityFromFollowup, previousFilters, previousAnchorType, previousAnchorValue, followupSelectionMode, rootScopedPivot, compactWhitespace, repairAddressMojibake, toNonEmptyString = fallbackToNonEmptyString) { + const displayedEntityTargetIntent = resolveDisplayedEntityRetargetIntentUtf8Safe(userMessage, resolvedEntityFromFollowup?.entityType, compactWhitespace, repairAddressMojibake); + const carryover = applyReferencedEntityCarryover(previousFilters, previousAnchorType, previousAnchorValue, followupSelectionMode, resolvedEntityFromFollowup, rootScopedPivot, toNonEmptyString); + return { + displayedEntityTargetIntent, + ...carryover + }; +} function applySelectedItemCarryover(previousFilters, previousAnchorType, previousAnchorValue, rootScopedPivot, shouldApplySelectedItemCarryover, navigationFocusItemLabel, continuityActiveItem, selectedObjectLabelFromUser, selectedObjectLabelFromAlternate, toNonEmptyString = fallbackToNonEmptyString) { const nextFilters = previousFilters && typeof previousFilters === "object" ? { ...previousFilters } : {}; if (rootScopedPivot || !shouldApplySelectedItemCarryover || toNonEmptyString(nextFilters.item)) { diff --git a/llm_normalizer/backend/dist/services/assistantTransitionPolicy.js b/llm_normalizer/backend/dist/services/assistantTransitionPolicy.js index c81011c..4b0d06a 100644 --- a/llm_normalizer/backend/dist/services/assistantTransitionPolicy.js +++ b/llm_normalizer/backend/dist/services/assistantTransitionPolicy.js @@ -284,36 +284,6 @@ function createAssistantTransitionPolicy(deps) { } return null; } - function resolveDisplayedEntityRetargetIntent(userMessage, entityType) { - const normalized = deps.compactWhitespace(deps.repairAddressMojibake(String(userMessage ?? "")).toLowerCase()); - if (!normalized) { - return null; - } - if (entityType === "counterparty") { - if (/(?:договор|контракт)/iu.test(normalized)) { - return "list_contracts_by_counterparty"; - } - if (/(?:банк|выписк|плат[её]ж|оплат|statement|payment|wire)/iu.test(normalized)) { - return "bank_operations_by_counterparty"; - } - if (/(?:документ|накладн|счет|сч[её]т|акт|реализац|поступл)/iu.test(normalized)) { - return "list_documents_by_counterparty"; - } - if (/(?:сколько\s+денег|сколько\s+принес|выручк|сумм[аы]?|оплатил|продаж)/iu.test(normalized)) { - return "customer_revenue_and_payments"; - } - return null; - } - if (entityType === "contract") { - if (/(?:банк|выписк|плат[её]ж|оплат|statement|payment|wire)/iu.test(normalized)) { - return "bank_operations_by_contract"; - } - if (/(?:документ|накладн|счет|сч[её]т|акт|реализац|поступл)/iu.test(normalized)) { - return "list_documents_by_contract"; - } - } - return null; - } function resolveAddressFollowupCarryoverContext(userMessage, items, alternateMessage = null, llmPreDecomposeMeta = null, addressNavigationState = null) { const rawCapabilityMetaQuery = deps.shouldHandleAsAssistantCapabilityMetaQuery(userMessage) || (deps.toNonEmptyString(alternateMessage) @@ -657,16 +627,14 @@ function createAssistantTransitionPolicy(deps) { (deps.toNonEmptyString(alternateMessage) ? deps.resolveDisplayedAddressEntityMention(String(alternateMessage ?? ""), displayedEntities) : null); - if (resolvedEntityFromFollowup && !rootScopedPivot) { - displayedEntityTargetIntent = resolveDisplayedEntityRetargetIntent(userMessage, resolvedEntityFromFollowup.entityType); - } ({ + displayedEntityTargetIntent, previousFilters, previousAnchorType, previousAnchorValue: previousAnchor, followupSelectionMode, resolvedCounterpartyFromDisplay - } = (0, assistantContinuityPolicy_1.applyReferencedEntityCarryover)(previousFilters, previousAnchorType, previousAnchor, followupSelectionMode, resolvedEntityFromFollowup, rootScopedPivot, deps.toNonEmptyString)); + } = (0, assistantContinuityPolicy_1.resolveDisplayedEntityFollowupRetarget)(userMessage, resolvedEntityFromFollowup, previousFilters, previousAnchorType, previousAnchor, followupSelectionMode, rootScopedPivot, deps.compactWhitespace, deps.repairAddressMojibake, deps.toNonEmptyString)); ({ previousFilters, previousAnchorType, diff --git a/llm_normalizer/backend/src/services/assistantContinuityPolicy.ts b/llm_normalizer/backend/src/services/assistantContinuityPolicy.ts index 77049c2..7003104 100644 --- a/llm_normalizer/backend/src/services/assistantContinuityPolicy.ts +++ b/llm_normalizer/backend/src/services/assistantContinuityPolicy.ts @@ -536,6 +536,122 @@ export function applyReferencedEntityCarryover( }; } +export function resolveDisplayedEntityRetargetIntent( + userMessage: unknown, + entityType: unknown, + compactWhitespace: (value: string) => string, + repairAddressMojibake: (value: string) => string +): string | null { + return resolveDisplayedEntityRetargetIntentUtf8Safe( + userMessage, + entityType, + compactWhitespace, + repairAddressMojibake + ); +} + +function resolveDisplayedEntityRetargetIntentUtf8Safe( + userMessage: unknown, + entityType: unknown, + compactWhitespace: (value: string) => string, + repairAddressMojibake: (value: string) => string +): string | null { + const normalized = compactWhitespace(repairAddressMojibake(String(userMessage ?? "")).toLowerCase()); + if (!normalized) { + return null; + } + if (String(entityType ?? "") === "counterparty") { + if (/(?:\u0434\u043e\u0433\u043e\u0432\u043e\u0440|\u043a\u043e\u043d\u0442\u0440\u0430\u043a\u0442)/iu.test(normalized)) { + return "list_contracts_by_counterparty"; + } + if ( + /(?:\u0431\u0430\u043d\u043a|\u0432\u044b\u043f\u0438\u0441\u043a|\u043f\u043b\u0430\u0442[\u0435\u0451]\u0436|\u043e\u043f\u043b\u0430\u0442|statement|payment|wire)/iu.test( + normalized + ) + ) { + return "bank_operations_by_counterparty"; + } + if ( + /(?:\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442|\u043d\u0430\u043a\u043b\u0430\u0434\u043d|\u0441\u0447\u0435\u0442|\u0441\u0447[\u0435\u0451]\u0442|\u0430\u043a\u0442|\u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446|\u043f\u043e\u0441\u0442\u0443\u043f\u043b)/iu.test( + normalized + ) + ) { + return "list_documents_by_counterparty"; + } + if ( + /(?:\u0441\u043a\u043e\u043b\u044c\u043a\u043e\s+\u0434\u0435\u043d\u0435\u0433|\u0441\u043a\u043e\u043b\u044c\u043a\u043e\s+\u043f\u0440\u0438\u043d\u0435\u0441|\u0432\u044b\u0440\u0443\u0447\u043a|\u0441\u0443\u043c\u043c[\u0430\u044b]?|\u043e\u043f\u043b\u0430\u0442\u0438\u043b|\u043f\u0440\u043e\u0434\u0430\u0436)/iu.test( + normalized + ) + ) { + return "customer_revenue_and_payments"; + } + return null; + } + if (String(entityType ?? "") === "contract") { + if ( + /(?:\u0431\u0430\u043d\u043a|\u0432\u044b\u043f\u0438\u0441\u043a|\u043f\u043b\u0430\u0442[\u0435\u0451]\u0436|\u043e\u043f\u043b\u0430\u0442|statement|payment|wire)/iu.test( + normalized + ) + ) { + return "bank_operations_by_contract"; + } + if ( + /(?:\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442|\u043d\u0430\u043a\u043b\u0430\u0434\u043d|\u0441\u0447\u0435\u0442|\u0441\u0447[\u0435\u0451]\u0442|\u0430\u043a\u0442|\u0440\u0435\u0430\u043b\u0438\u0437\u0430\u0446|\u043f\u043e\u0441\u0442\u0443\u043f\u043b)/iu.test( + normalized + ) + ) { + return "list_documents_by_contract"; + } + } + return null; +} + +export function resolveDisplayedEntityFollowupRetarget( + userMessage: unknown, + resolvedEntityFromFollowup: + | { + entityType?: unknown; + value?: unknown; + } + | null + | undefined, + previousFilters: Record | null, + previousAnchorType: string | null, + previousAnchorValue: string | null, + followupSelectionMode: string | null, + rootScopedPivot: boolean, + compactWhitespace: (value: string) => string, + repairAddressMojibake: (value: string) => string, + toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString +): { + displayedEntityTargetIntent: string | null; + previousFilters: Record; + previousAnchorType: string | null; + previousAnchorValue: string | null; + followupSelectionMode: string | null; + resolvedCounterpartyFromDisplay: boolean; +} { + const displayedEntityTargetIntent = resolveDisplayedEntityRetargetIntentUtf8Safe( + userMessage, + resolvedEntityFromFollowup?.entityType, + compactWhitespace, + repairAddressMojibake + ); + const carryover = applyReferencedEntityCarryover( + previousFilters, + previousAnchorType, + previousAnchorValue, + followupSelectionMode, + resolvedEntityFromFollowup, + rootScopedPivot, + toNonEmptyString + ); + return { + displayedEntityTargetIntent, + ...carryover + }; +} + export function applySelectedItemCarryover( previousFilters: Record | null, previousAnchorType: string | null, diff --git a/llm_normalizer/backend/src/services/assistantTransitionPolicy.ts b/llm_normalizer/backend/src/services/assistantTransitionPolicy.ts index 1c02dfa..013e235 100644 --- a/llm_normalizer/backend/src/services/assistantTransitionPolicy.ts +++ b/llm_normalizer/backend/src/services/assistantTransitionPolicy.ts @@ -2,7 +2,6 @@ import { applyHistoricalPartyCarryoverFilters, applyOrganizationCarryoverFilters, - applyReferencedEntityCarryover, applySelectedItemCarryover, applyTemporalCarryoverFilters, buildRootScopedCarryoverFilters, @@ -13,6 +12,7 @@ import { readAddressDebugTemporalScope, resolveAddressDebugCarryoverFilters, resolveAddressDebugAnchorContext, + resolveDisplayedEntityFollowupRetarget, resolveAssistantOrganizationAuthority } from "./assistantContinuityPolicy"; @@ -364,39 +364,6 @@ export function createAssistantTransitionPolicy(deps) { return null; } - function resolveDisplayedEntityRetargetIntent(userMessage, entityType) { - const normalized = deps.compactWhitespace( - deps.repairAddressMojibake(String(userMessage ?? "")).toLowerCase() - ); - if (!normalized) { - return null; - } - if (entityType === "counterparty") { - if (/(?:договор|контракт)/iu.test(normalized)) { - return "list_contracts_by_counterparty"; - } - if (/(?:банк|выписк|плат[её]ж|оплат|statement|payment|wire)/iu.test(normalized)) { - return "bank_operations_by_counterparty"; - } - if (/(?:документ|накладн|счет|сч[её]т|акт|реализац|поступл)/iu.test(normalized)) { - return "list_documents_by_counterparty"; - } - if (/(?:сколько\s+денег|сколько\s+принес|выручк|сумм[аы]?|оплатил|продаж)/iu.test(normalized)) { - return "customer_revenue_and_payments"; - } - return null; - } - if (entityType === "contract") { - if (/(?:банк|выписк|плат[её]ж|оплат|statement|payment|wire)/iu.test(normalized)) { - return "bank_operations_by_contract"; - } - if (/(?:документ|накладн|счет|сч[её]т|акт|реализац|поступл)/iu.test(normalized)) { - return "list_documents_by_contract"; - } - } - return null; - } - function resolveAddressFollowupCarryoverContext( userMessage, items, @@ -854,25 +821,23 @@ export function createAssistantTransitionPolicy(deps) { (deps.toNonEmptyString(alternateMessage) ? deps.resolveDisplayedAddressEntityMention(String(alternateMessage ?? ""), displayedEntities) : null); - if (resolvedEntityFromFollowup && !rootScopedPivot) { - displayedEntityTargetIntent = resolveDisplayedEntityRetargetIntent( - userMessage, - resolvedEntityFromFollowup.entityType - ); - } ({ + displayedEntityTargetIntent, previousFilters, previousAnchorType, previousAnchorValue: previousAnchor, followupSelectionMode, resolvedCounterpartyFromDisplay - } = applyReferencedEntityCarryover( + } = resolveDisplayedEntityFollowupRetarget( + userMessage, + resolvedEntityFromFollowup, previousFilters, previousAnchorType, previousAnchor, followupSelectionMode, - resolvedEntityFromFollowup, rootScopedPivot, + deps.compactWhitespace, + deps.repairAddressMojibake, deps.toNonEmptyString )); ({ diff --git a/llm_normalizer/backend/tests/assistantContinuityPolicy.test.ts b/llm_normalizer/backend/tests/assistantContinuityPolicy.test.ts index f8aed14..afe8dda 100644 --- a/llm_normalizer/backend/tests/assistantContinuityPolicy.test.ts +++ b/llm_normalizer/backend/tests/assistantContinuityPolicy.test.ts @@ -11,6 +11,7 @@ import { resolveAddressDebugCarryoverFilters, resolveAddressDebugContextFacts, resolveAddressDebugAnchorContext, + resolveDisplayedEntityFollowupRetarget, resolveAssistantOrganizationAuthority } from "../src/services/assistantContinuityPolicy"; @@ -350,4 +351,35 @@ describe("assistantContinuityPolicy organization authority", () => { previousAnchorValue: "Workstation Navigation" }); }); + + it("resolves displayed-entity follow-up retarget and carryover through one shared helper", () => { + const carryover = resolveDisplayedEntityFollowupRetarget( + "покажи договоры по СВК", + { + entityType: "counterparty", + value: "SVK Group" + }, + { + organization: "Org Alt" + }, + null, + null, + "carry_previous_intent", + false, + (value) => String(value ?? "").replace(/\s+/g, " ").trim(), + (value) => value + ); + + expect(carryover).toEqual({ + displayedEntityTargetIntent: "list_contracts_by_counterparty", + previousFilters: { + organization: "Org Alt", + counterparty: "SVK Group" + }, + previousAnchorType: "counterparty", + previousAnchorValue: "SVK Group", + followupSelectionMode: "carry_referenced_entity", + resolvedCounterpartyFromDisplay: true + }); + }); });