Архитектура: централизовать selected-item carryover в continuity policy и transition glue

This commit is contained in:
dctouch 2026-04-19 12:13:43 +03:00
parent 4f56aa9b2a
commit 851289b9a6
7 changed files with 179 additions and 38 deletions

View File

@ -434,6 +434,11 @@ Still open after the accepted phase12 replay:
- this matters because counterparty / contract / selected-entity follow-ups are one of the heaviest remaining sources of local carryover reconstruction, and moving them under the shared continuity layer reduces another chance that route retargeting and anchor state drift apart when new domains are added;
- targeted `assistantContinuityPolicy` and `assistantTransitionPolicy` regressions now protect both the helper layer and a real `displayed counterparty -> contracts` follow-up path;
- the next proof after this pass should still come from a live replay, but the expected verdict should now only move if a real counterparty carryover path regresses rather than because the state mutation lived in an inline transition branch.
- the next continuity-authority pass now removes another selected-item state owner from the transition hot path:
- continuity now owns `applySelectedItemCarryover(...)`, so `previous_filters.item` plus `previous_anchor_*` for selected-object inventory follow-ups no longer mutate inline inside `assistantTransitionPolicy`;
- item carryover precedence is now explicit in one helper: navigation focus item -> continuity active item -> explicit selected-object label from the current message;
- this matters because selected-object follow-ups are one of the most fragile continuity seams in the product, and keeping their anchor mutation in the shared continuity layer reduces another chance that future inventory/domain expansion splits `focus_object` truth from follow-up carryover truth;
- targeted continuity and transition regressions now protect both the helper layer and a real short selected-item follow-up path that must keep the `item` anchor without reopening company/date drift.
- the next continuity-authority pass now centralizes temporal backfill precedence for follow-up filters:
- transition no longer holds a service-local block of `shouldBackfillPreviousDateScopeFromNavigation + six field-level ifs` for `as_of_date / period_from / period_to`;
- shared continuity now owns that merge via `applyTemporalCarryoverFilters(...)`, while `shouldUseNavigationTemporalCarryover(...)` keeps the intent-family boundary explicit in one place;

View File

@ -16,6 +16,7 @@ exports.applyTemporalCarryoverFilters = applyTemporalCarryoverFilters;
exports.applyOrganizationCarryoverFilters = applyOrganizationCarryoverFilters;
exports.applyHistoricalPartyCarryoverFilters = applyHistoricalPartyCarryoverFilters;
exports.applyReferencedEntityCarryover = applyReferencedEntityCarryover;
exports.applySelectedItemCarryover = applySelectedItemCarryover;
exports.buildInventoryRootFrameFromAddressDebug = buildInventoryRootFrameFromAddressDebug;
exports.isGroundedAddressDebug = isGroundedAddressDebug;
exports.resolveAssistantContinuitySnapshot = resolveAssistantContinuitySnapshot;
@ -352,6 +353,33 @@ function applyReferencedEntityCarryover(previousFilters, previousAnchorType, pre
resolvedCounterpartyFromDisplay
};
}
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)) {
return {
previousFilters: nextFilters,
previousAnchorType,
previousAnchorValue
};
}
const selectedObjectLabel = toNonEmptyString(navigationFocusItemLabel) ??
toNonEmptyString(continuityActiveItem) ??
toNonEmptyString(selectedObjectLabelFromUser) ??
toNonEmptyString(selectedObjectLabelFromAlternate);
if (!selectedObjectLabel) {
return {
previousFilters: nextFilters,
previousAnchorType,
previousAnchorValue
};
}
nextFilters.item = selectedObjectLabel;
return {
previousFilters: nextFilters,
previousAnchorType: "item",
previousAnchorValue: selectedObjectLabel
};
}
function buildInventoryRootFrameFromAddressDebug(debug, toNonEmptyString = fallbackToNonEmptyString) {
if (!debug || typeof debug !== "object") {
return null;

View File

@ -667,27 +667,19 @@ function createAssistantTransitionPolicy(deps) {
followupSelectionMode,
resolvedCounterpartyFromDisplay
} = (0, assistantContinuityPolicy_1.applyReferencedEntityCarryover)(previousFilters, previousAnchorType, previousAnchor, followupSelectionMode, resolvedEntityFromFollowup, rootScopedPivot, deps.toNonEmptyString));
if (!rootScopedPivot &&
!deps.toNonEmptyString(previousFilters.item) &&
(sourceIntentHint === "inventory_on_hand_as_of_date" ||
sourceIntentHint === "inventory_purchase_provenance_for_item" ||
sourceIntentHint === "inventory_purchase_documents_for_item" ||
sourceIntentHint === "inventory_sale_trace_for_item" ||
sourceIntentHint === "inventory_profitability_for_item" ||
sourceIntentHint === "inventory_purchase_to_sale_chain" ||
sourceIntentHint === "inventory_aging_by_purchase_date" ||
hasSelectedObjectInventorySignalPrimary ||
hasSelectedObjectInventorySignalAlternate)) {
const selectedObjectLabel = (navigationFocusObjectType === "item" ? navigationFocusObjectLabel : null) ??
continuitySnapshot.activeItem ??
extractSelectedObjectLabel(userMessage) ??
(deps.toNonEmptyString(alternateMessage) ? extractSelectedObjectLabel(String(alternateMessage ?? "")) : null);
if (selectedObjectLabel) {
previousFilters.item = selectedObjectLabel;
previousAnchorType = "item";
previousAnchor = selectedObjectLabel;
}
}
({
previousFilters,
previousAnchorType,
previousAnchorValue: previousAnchor
} = (0, assistantContinuityPolicy_1.applySelectedItemCarryover)(previousFilters, previousAnchorType, previousAnchor, rootScopedPivot, sourceIntentHint === "inventory_on_hand_as_of_date" ||
sourceIntentHint === "inventory_purchase_provenance_for_item" ||
sourceIntentHint === "inventory_purchase_documents_for_item" ||
sourceIntentHint === "inventory_sale_trace_for_item" ||
sourceIntentHint === "inventory_profitability_for_item" ||
sourceIntentHint === "inventory_purchase_to_sale_chain" ||
sourceIntentHint === "inventory_aging_by_purchase_date" ||
hasSelectedObjectInventorySignalPrimary ||
hasSelectedObjectInventorySignalAlternate, navigationFocusObjectType === "item" ? navigationFocusObjectLabel : null, continuitySnapshot.activeItem, extractSelectedObjectLabel(userMessage), deps.toNonEmptyString(alternateMessage) ? extractSelectedObjectLabel(String(alternateMessage ?? "")) : null, deps.toNonEmptyString));
if (explicitOrganizationClarificationSelection && !previousAnchor) {
previousAnchorType = "organization";
previousAnchor = explicitOrganizationClarificationSelection;

View File

@ -536,6 +536,53 @@ export function applyReferencedEntityCarryover(
};
}
export function applySelectedItemCarryover(
previousFilters: Record<string, unknown> | null,
previousAnchorType: string | null,
previousAnchorValue: string | null,
rootScopedPivot: boolean,
shouldApplySelectedItemCarryover: boolean,
navigationFocusItemLabel: unknown,
continuityActiveItem: unknown,
selectedObjectLabelFromUser: unknown,
selectedObjectLabelFromAlternate: unknown,
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString
): {
previousFilters: Record<string, unknown>;
previousAnchorType: string | null;
previousAnchorValue: string | null;
} {
const nextFilters =
previousFilters && typeof previousFilters === "object" ? { ...previousFilters } : {};
if (rootScopedPivot || !shouldApplySelectedItemCarryover || toNonEmptyString(nextFilters.item)) {
return {
previousFilters: nextFilters,
previousAnchorType,
previousAnchorValue
};
}
const selectedObjectLabel =
toNonEmptyString(navigationFocusItemLabel) ??
toNonEmptyString(continuityActiveItem) ??
toNonEmptyString(selectedObjectLabelFromUser) ??
toNonEmptyString(selectedObjectLabelFromAlternate);
if (!selectedObjectLabel) {
return {
previousFilters: nextFilters,
previousAnchorType,
previousAnchorValue
};
}
nextFilters.item = selectedObjectLabel;
return {
previousFilters: nextFilters,
previousAnchorType: "item",
previousAnchorValue: selectedObjectLabel
};
}
export function buildInventoryRootFrameFromAddressDebug(
debug: Record<string, unknown> | null,
toNonEmptyString: (value: unknown) => string | null = fallbackToNonEmptyString

View File

@ -3,6 +3,7 @@ import {
applyHistoricalPartyCarryoverFilters,
applyOrganizationCarryoverFilters,
applyReferencedEntityCarryover,
applySelectedItemCarryover,
applyTemporalCarryoverFilters,
buildRootScopedCarryoverFilters,
buildInventoryRootFrameFromAddressDebug,
@ -874,10 +875,16 @@ export function createAssistantTransitionPolicy(deps) {
rootScopedPivot,
deps.toNonEmptyString
));
if (
!rootScopedPivot &&
!deps.toNonEmptyString(previousFilters.item) &&
(sourceIntentHint === "inventory_on_hand_as_of_date" ||
({
previousFilters,
previousAnchorType,
previousAnchorValue: previousAnchor
} = applySelectedItemCarryover(
previousFilters,
previousAnchorType,
previousAnchor,
rootScopedPivot,
sourceIntentHint === "inventory_on_hand_as_of_date" ||
sourceIntentHint === "inventory_purchase_provenance_for_item" ||
sourceIntentHint === "inventory_purchase_documents_for_item" ||
sourceIntentHint === "inventory_sale_trace_for_item" ||
@ -885,19 +892,13 @@ export function createAssistantTransitionPolicy(deps) {
sourceIntentHint === "inventory_purchase_to_sale_chain" ||
sourceIntentHint === "inventory_aging_by_purchase_date" ||
hasSelectedObjectInventorySignalPrimary ||
hasSelectedObjectInventorySignalAlternate)
) {
const selectedObjectLabel =
(navigationFocusObjectType === "item" ? navigationFocusObjectLabel : null) ??
continuitySnapshot.activeItem ??
extractSelectedObjectLabel(userMessage) ??
(deps.toNonEmptyString(alternateMessage) ? extractSelectedObjectLabel(String(alternateMessage ?? "")) : null);
if (selectedObjectLabel) {
previousFilters.item = selectedObjectLabel;
previousAnchorType = "item";
previousAnchor = selectedObjectLabel;
}
}
hasSelectedObjectInventorySignalAlternate,
navigationFocusObjectType === "item" ? navigationFocusObjectLabel : null,
continuitySnapshot.activeItem,
extractSelectedObjectLabel(userMessage),
deps.toNonEmptyString(alternateMessage) ? extractSelectedObjectLabel(String(alternateMessage ?? "")) : null,
deps.toNonEmptyString
));
if (explicitOrganizationClarificationSelection && !previousAnchor) {
previousAnchorType = "organization";
previousAnchor = explicitOrganizationClarificationSelection;

View File

@ -3,6 +3,7 @@ import {
applyHistoricalPartyCarryoverFilters,
applyOrganizationCarryoverFilters,
applyReferencedEntityCarryover,
applySelectedItemCarryover,
applyTemporalCarryoverFilters,
buildRootScopedCarryoverFilters,
hydrateInventoryRootFrameState,
@ -324,4 +325,29 @@ describe("assistantContinuityPolicy organization authority", () => {
resolvedCounterpartyFromDisplay: true
});
});
it("applies selected-item carryover from navigation focus before continuity and explicit labels", () => {
const carryover = applySelectedItemCarryover(
{
organization: "Org Alt"
},
null,
null,
false,
true,
"Workstation Navigation",
"Workstation Continuity",
"Workstation User",
"Workstation Alternate"
);
expect(carryover).toEqual({
previousFilters: {
organization: "Org Alt",
item: "Workstation Navigation"
},
previousAnchorType: "item",
previousAnchorValue: "Workstation Navigation"
});
});
});

View File

@ -297,6 +297,48 @@ describe("assistantTransitionPolicy", () => {
expect(carryover?.followupContext?.root_context_only).toBeUndefined();
});
it("hydrates selected-item carryover through shared continuity helper for short object follow-up", () => {
const policy = buildPolicy({
findLastAddressAssistantItem: () => ({
text: "Подтвержден складской срез по выбранной позиции.",
debug: {
detected_intent: "inventory_on_hand_as_of_date",
extracted_filters: {
organization: 'ООО "Альтернатива Плюс"',
as_of_date: "2020-03-31"
}
}
}),
hasAddressFollowupContextSignal: () => true,
hasShortInventoryObjectFollowupSignal: () => true,
findRecentInventoryRootFrame: () => null,
resolveAddressIntent: () => ({ intent: "unknown" })
});
const carryover = policy.resolveAddressFollowupCarryoverContext(
"по этой позиции покажи документы",
[],
null,
null,
{
session_context: {
active_focus_object: {
object_type: "item",
label: "Workstation Focus"
}
}
}
);
expect(carryover?.followupContext?.previous_filters).toMatchObject({
organization: 'ООО "Альтернатива Плюс"',
as_of_date: "2020-03-31",
item: "Workstation Focus"
});
expect(carryover?.followupContext?.previous_anchor_type).toBe("item");
expect(carryover?.followupContext?.previous_anchor_value).toBe("Workstation Focus");
});
it("hydrates follow-up organization from shared assistant authority when local history filters are empty", () => {
const policy = buildPolicy({
findLastAddressAssistantItem: () => ({